mirror of
https://github.com/ChangJoo-Park/learn-flutter.git
synced 2025-06-08 21:53:09 +00:00
755 lines
22 KiB
Markdown
755 lines
22 KiB
Markdown
# Widget Tree 이해
|
|
|
|
Flutter의 UI는 위젯 트리(Widget Tree)라고 불리는 계층 구조로 구성됩니다. 이 장에서는 위젯 트리의 개념, 작동 방식, 그리고 Flutter가 위젯 트리를 통해 효율적으로 UI를 렌더링하는 방법에 대해 알아보겠습니다.
|
|
|
|
## 위젯 트리란?
|
|
|
|
위젯 트리는 Flutter 애플리케이션의 UI를 구성하는 위젯들의 계층적 구조입니다. 모든 Flutter 앱은 루트 위젯에서 시작하여 중첩된 자식 위젯들로 이루어진 트리 형태를 가집니다.
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[MaterialApp] --> B[Scaffold]
|
|
B --> C[AppBar]
|
|
B --> D[Body: Container]
|
|
D --> E[Column]
|
|
E --> F[Text]
|
|
E --> G[Button]
|
|
E --> H[Image]
|
|
B --> I[Drawer]
|
|
B --> J[BottomNavigationBar]
|
|
```
|
|
|
|
## 위젯 트리의 중요성
|
|
|
|
위젯 트리는 다음과 같은 이유로 Flutter에서 중요한 개념입니다:
|
|
|
|
1. **UI 구조화**: 복잡한 UI를 명확하고 체계적으로 구성할 수 있습니다.
|
|
2. **렌더링 최적화**: Flutter는 위젯 트리를 사용하여 변경된 부분만 효율적으로 다시 렌더링합니다.
|
|
3. **상태 관리**: 위젯 트리는 상태 관리 및 데이터 흐름의 기반을 제공합니다.
|
|
4. **컨텍스트 제공**: 위젯 트리는 `BuildContext`를 통해 상위 위젯과 테마, 미디어 쿼리 등에 접근할 수 있게 해줍니다.
|
|
|
|
## 세 가지 트리
|
|
|
|
Flutter의 렌더링 과정은 세 가지 트리로 이루어집니다:
|
|
|
|
1. **위젯 트리(Widget Tree)**: 애플리케이션의 UI를 설명하는 불변(immutable) 객체의 트리
|
|
2. **요소 트리(Element Tree)**: 위젯 트리의 런타임 표현으로, 위젯과 렌더 객체를 연결하는 가변(mutable) 트리
|
|
3. **렌더 트리(Render Tree)**: 실제 화면에 그리기를 담당하는 객체들의 트리
|
|
|
|
```mermaid
|
|
graph TD
|
|
subgraph "위젯 계층"
|
|
A1[MyApp Widget] --> B1[HomePage Widget]
|
|
B1 --> C1[Container Widget]
|
|
C1 --> D1[Text Widget]
|
|
end
|
|
|
|
subgraph "요소 계층"
|
|
A2[MyApp Element] --> B2[HomePage Element]
|
|
B2 --> C2[Container Element]
|
|
C2 --> D2[Text Element]
|
|
end
|
|
|
|
subgraph "렌더 계층"
|
|
C3[RenderBox] --> D3[RenderParagraph]
|
|
end
|
|
|
|
A1 -.-> A2
|
|
B1 -.-> B2
|
|
C1 -.-> C2
|
|
D1 -.-> D2
|
|
|
|
C2 -.-> C3
|
|
D2 -.-> D3
|
|
```
|
|
|
|
### 1. 위젯 트리 (Widget Tree)
|
|
|
|
위젯 트리는 개발자가 작성한 코드로, UI를 구성하는 위젯들의 설계도입니다. 위젯은 불변 객체이므로 상태가 변경되면 새로운 위젯 트리가 생성됩니다.
|
|
|
|
```dart
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('위젯 트리 예제'),
|
|
),
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text('Hello, Flutter!'),
|
|
ElevatedButton(
|
|
onPressed: () {},
|
|
child: Text('버튼'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
```
|
|
|
|
### 2. 요소 트리 (Element Tree)
|
|
|
|
요소 트리는 위젯 트리의 인스턴스로, 위젯의 수명 주기를 관리하고 위젯과 렌더 객체 사이의 연결을 유지합니다. 요소는 위젯이 변경될 때 업데이트되거나 재사용됩니다.
|
|
|
|
요소의 주요 유형:
|
|
|
|
- **ComponentElement**: 다른 위젯을 빌드하는 위젯에 대응 (예: StatelessWidget, StatefulWidget)
|
|
- **RenderObjectElement**: 화면에 무언가를 그리는 위젯에 대응 (예: RenderObjectWidget)
|
|
|
|
### 3. 렌더 트리 (Render Tree)
|
|
|
|
렌더 트리는 화면에 실제로 그리기를 담당하는 객체들의 트리입니다. 레이아웃 계산, 그리기, 히트 테스트(터치 이벤트 처리) 등을 수행합니다.
|
|
|
|
렌더 객체의 주요 유형:
|
|
|
|
- **RenderBox**: 사각형 영역을 차지하는 렌더 객체
|
|
- **RenderSliver**: 스크롤 가능한 영역의 일부를 렌더링하는 객체
|
|
- **RenderParagraph**: 텍스트를 렌더링하는 객체
|
|
|
|
## 위젯 트리의 빌드 과정
|
|
|
|
Flutter가 위젯 트리를 화면에 렌더링하는 과정은 다음과 같습니다:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant App
|
|
participant WidgetTree
|
|
participant ElementTree
|
|
participant RenderTree
|
|
participant Screen
|
|
|
|
App->>WidgetTree: 위젯 생성
|
|
WidgetTree->>ElementTree: 요소 생성/업데이트
|
|
ElementTree->>RenderTree: 렌더 객체 생성/업데이트
|
|
RenderTree->>RenderTree: 레이아웃 계산
|
|
RenderTree->>RenderTree: 페인팅
|
|
RenderTree->>Screen: 화면에 표시
|
|
```
|
|
|
|
1. **위젯 생성**: 개발자가 작성한 코드에 따라 위젯 트리가 생성됩니다.
|
|
2. **요소 생성/업데이트**: 각 위젯에 대응하는 요소가 생성되거나 업데이트됩니다.
|
|
3. **렌더 객체 생성/업데이트**: 요소와 연결된 렌더 객체가 생성되거나 업데이트됩니다.
|
|
4. **레이아웃 계산**: 렌더 객체는 부모로부터 제약 조건을 받아 자신의 크기를 결정합니다.
|
|
5. **페인팅**: 렌더 객체가 자신의 모양을 그립니다.
|
|
6. **화면에 표시**: 최종 결과가 화면에 표시됩니다.
|
|
|
|
## BuildContext
|
|
|
|
`BuildContext`는 위젯 트리에서 위젯의 위치를 나타내는 객체입니다. 실제로는 요소 트리의 요소(Element)를 참조합니다.
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[MaterialApp] --> B[Scaffold]
|
|
B --> C[AppBar]
|
|
B --> D[Container]
|
|
D --> E[Text]
|
|
|
|
D -.->|"BuildContext of Container"| D
|
|
E -.->|"BuildContext of Text"| E
|
|
```
|
|
|
|
BuildContext의 주요 용도:
|
|
|
|
1. **상위 위젯 탐색**: `dependOnInheritedWidgetOfExactType()`를 사용하여 상위 위젯에 접근
|
|
2. **테마 및 미디어 쿼리 접근**: `Theme.of(context)`, `MediaQuery.of(context)`
|
|
3. **네비게이션**: `Navigator.of(context).push()`
|
|
4. **기타 서비스 접근**: `ScaffoldMessenger.of(context)`, `Form.of(context)` 등
|
|
|
|
```dart
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
// BuildContext를 사용하여 스낵바 표시
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('안녕하세요!')),
|
|
);
|
|
|
|
// BuildContext를 사용하여 다른 화면으로 이동
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(builder: (context) => SecondScreen()),
|
|
);
|
|
},
|
|
child: Text('버튼'),
|
|
)
|
|
```
|
|
|
|
## 위젯 트리 업데이트
|
|
|
|
Flutter는 위젯 트리가 변경될 때 효율적으로 UI를 업데이트하기 위해 "재조정(reconciliation)" 과정을 수행합니다:
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[상태 변경] --> B[새 위젯 트리 생성]
|
|
B --> C[기존 요소 트리와 비교]
|
|
C --> D{위젯 타입 동일?}
|
|
D -->|Yes| E[요소 유지, 속성 업데이트]
|
|
D -->|No| F[이전 요소 폐기, 새 요소 생성]
|
|
E --> G[자식 위젯 재조정]
|
|
F --> G
|
|
G --> H[렌더 트리 업데이트]
|
|
H --> I[화면 업데이트]
|
|
```
|
|
|
|
### 키워드: 동일성과 동등성
|
|
|
|
Flutter의 재조정 알고리즘은 위젯의 "동일성"(identity)이 아닌 "동등성"(equality)에 기반합니다:
|
|
|
|
1. **동일성(identity)**: 두 객체가 메모리에서 같은 인스턴스인지 (`identical(a, b)` 또는 `a === b`)
|
|
2. **동등성(equality)**: 두 객체가 같은 타입과 속성을 가지는지 (`a == b`)
|
|
|
|
Flutter는 다음 규칙을 사용하여 위젯을 비교합니다:
|
|
|
|
1. **다른 runtimeType**: 위젯이 다른 타입이면 이전 요소를 폐기하고 새 요소를 생성합니다.
|
|
2. **같은 runtimeType, 다른 key**: 이전 요소를 폐기하고 새 요소를 생성합니다.
|
|
3. **같은 runtimeType, 같은 key**: 요소를 유지하고 속성을 업데이트합니다.
|
|
|
|
## 위젯 키(Keys)
|
|
|
|
키는 Flutter가 위젯을 식별하는 데 사용되는 식별자입니다. 특히 동적 위젯(리스트, 그리드 등)에서 중요합니다.
|
|
|
|
```mermaid
|
|
graph TD
|
|
subgraph "키가 없는 경우"
|
|
A1[List: A, B, C] --> A2[List: B, C]
|
|
A2 -->|"위젯 비교: A → B, B → C"| A3[요소 재사용]
|
|
end
|
|
|
|
subgraph "키가 있는 경우"
|
|
B1[List: A(key:1), B(key:2), C(key:3)] --> B2[List: B(key:2), C(key:3)]
|
|
B2 -->|"키 비교: key:1 삭제"| B3[정확한 요소 제거]
|
|
end
|
|
```
|
|
|
|
키가 중요한 상황:
|
|
|
|
1. **리스트 항목의 순서가 변경될 때**
|
|
2. **위젯이 추가/제거될 때**
|
|
3. **상태를 유지해야 할 때**
|
|
|
|
### 키 유형
|
|
|
|
1. **ValueKey**: 단일 값을 기반으로 한 키
|
|
|
|
```dart
|
|
ListView.builder(
|
|
itemCount: items.length,
|
|
itemBuilder: (context, index) {
|
|
return ListTile(
|
|
key: ValueKey(items[index].id),
|
|
title: Text(items[index].title),
|
|
);
|
|
},
|
|
)
|
|
```
|
|
|
|
2. **ObjectKey**: 객체 전체를 기반으로 한 키
|
|
|
|
```dart
|
|
ListTile(
|
|
key: ObjectKey(item), // 'item' 객체 전체를 키로 사용
|
|
title: Text(item.title),
|
|
)
|
|
```
|
|
|
|
3. **UniqueKey**: 매번 고유한 키 생성
|
|
|
|
```dart
|
|
// 애니메이션 중에 위젯을 강제로 재생성할 때 유용
|
|
Container(
|
|
key: UniqueKey(),
|
|
color: Colors.blue,
|
|
child: Text('새로운 인스턴스'),
|
|
)
|
|
```
|
|
|
|
4. **GlobalKey**: 위젯의 상태에 접근하거나 위젯의 크기/위치를 파악하는 데 사용
|
|
|
|
```dart
|
|
final formKey = GlobalKey<FormState>();
|
|
|
|
Form(
|
|
key: formKey,
|
|
child: Column(
|
|
children: [
|
|
TextFormField(/* ... */),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
if (formKey.currentState!.validate()) {
|
|
// 폼 처리
|
|
}
|
|
},
|
|
child: Text('제출'),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
```
|
|
|
|
## 리스트 내부의 위젯 트리
|
|
|
|
리스트 위젯(`ListView`, `GridView` 등)은 많은 자식 위젯을 포함할 수 있습니다. 이러한 리스트에서 항목을 추가, 제거, 재정렬할 때 키를 사용하면 Flutter가 효율적으로 요소 트리를 업데이트할 수 있습니다.
|
|
|
|
### 키 없이 리스트 항목 제거
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[원래 리스트: A, B, C] --> B["항목 제거 후: A, C"]
|
|
B --> C["위젯 비교: A → A, B → C (상태 혼동)"]
|
|
```
|
|
|
|
키가 없으면 Flutter는 위치 기반으로 위젯을 비교합니다. 첫 번째 위젯 A는 그대로 유지되고, 두 번째 위치에 있던 B는 C로 업데이트됩니다. 이로 인해 상태가 예상치 않게 섞일 수 있습니다.
|
|
|
|
### 키를 사용한 리스트 항목 제거
|
|
|
|
```mermaid
|
|
graph TD
|
|
A["원래 리스트: A(key:1), B(key:2), C(key:3)"] --> B["항목 제거 후: A(key:1), C(key:3)"]
|
|
B --> C["키 비교: key:1 유지, key:2 제거, key:3 유지 (상태 일관성)"]
|
|
```
|
|
|
|
키를 사용하면 Flutter는 키를 기반으로 위젯을 식별합니다. B(key:2)가 제거되고 A와 C는 키를 통해 정확히 식별되어 상태가 올바르게 유지됩니다.
|
|
|
|
## 실제 예제: 위젯 트리 구성
|
|
|
|
아래 예제는 복잡한 위젯 트리를 보여줍니다:
|
|
|
|
```dart
|
|
class ProfileScreen extends StatelessWidget {
|
|
final User user;
|
|
|
|
const ProfileScreen({
|
|
Key? key,
|
|
required this.user,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('프로필'),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(Icons.settings),
|
|
onPressed: () { /* ... */ },
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 사용자 헤더 섹션
|
|
UserHeaderWidget(user: user),
|
|
|
|
// 카운터 섹션
|
|
StatsSection(
|
|
followers: user.followers,
|
|
following: user.following,
|
|
posts: user.posts,
|
|
),
|
|
|
|
// 포스트 그리드
|
|
PostGridWidget(
|
|
posts: user.recentPosts,
|
|
onPostTap: (post) { /* ... */ },
|
|
),
|
|
],
|
|
),
|
|
),
|
|
bottomNavigationBar: BottomNavigationBar(
|
|
currentIndex: 0,
|
|
items: [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.home),
|
|
label: '홈',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.search),
|
|
label: '검색',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.person),
|
|
label: '프로필',
|
|
),
|
|
],
|
|
onTap: (index) { /* ... */ },
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 중첩된 자식 위젯의 예
|
|
class UserHeaderWidget extends StatelessWidget {
|
|
final User user;
|
|
|
|
const UserHeaderWidget({
|
|
Key? key,
|
|
required this.user,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 40,
|
|
backgroundImage: NetworkImage(user.avatarUrl),
|
|
),
|
|
SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user.name,
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 4),
|
|
Text(user.bio),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
위 코드의 위젯 트리 구조:
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[ProfileScreen] --> B[Scaffold]
|
|
B --> C[AppBar]
|
|
C --> C1[Text: '프로필']
|
|
C --> C2[IconButton]
|
|
B --> D[SingleChildScrollView]
|
|
D --> E[Column]
|
|
E --> F[UserHeaderWidget]
|
|
F --> F1[Container]
|
|
F1 --> F2[Row]
|
|
F2 --> F3[CircleAvatar]
|
|
F2 --> F4[SizedBox]
|
|
F2 --> F5[Expanded]
|
|
F5 --> F6[Column]
|
|
F6 --> F7[Text: user.name]
|
|
F6 --> F8[SizedBox]
|
|
F6 --> F9[Text: user.bio]
|
|
E --> G[StatsSection]
|
|
E --> H[PostGridWidget]
|
|
B --> I[BottomNavigationBar]
|
|
I --> I1[BottomNavigationBarItem: 홈]
|
|
I --> I2[BottomNavigationBarItem: 검색]
|
|
I --> I3[BottomNavigationBarItem: 프로필]
|
|
```
|
|
|
|
## 위젯 트리 디버깅
|
|
|
|
Flutter는 위젯 트리를 디버깅하기 위한 다양한 도구를 제공합니다:
|
|
|
|
### 1. Flutter DevTools
|
|
|
|
Flutter DevTools의 위젯 인스펙터를 사용하면 위젯 트리를 시각적으로 탐색하고 속성을 검사할 수 있습니다.
|
|
|
|
### 2. debugDumpApp() 메서드
|
|
|
|
```dart
|
|
// 위젯 트리를 콘솔에 출력
|
|
void _printWidgetTree() {
|
|
debugDumpApp();
|
|
}
|
|
|
|
// 사용 예
|
|
ElevatedButton(
|
|
onPressed: _printWidgetTree,
|
|
child: Text('위젯 트리 출력'),
|
|
)
|
|
```
|
|
|
|
### 3. Widget Inspector 서비스
|
|
|
|
```dart
|
|
// 위젯 인스펙터 활성화
|
|
void main() {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
if (kDebugMode) {
|
|
WidgetInspectorService.instance.selection.addListener(() {
|
|
// 선택한 위젯이 변경될 때 호출
|
|
print('선택된 위젯: ${WidgetInspectorService.instance.selection.current}');
|
|
});
|
|
}
|
|
runApp(MyApp());
|
|
}
|
|
```
|
|
|
|
## 위젯 트리의 최적화
|
|
|
|
위젯 트리를 효율적으로 구성하면 앱의 성능을 향상시킬 수 있습니다:
|
|
|
|
### 1. 트리 깊이 최소화
|
|
|
|
과도하게 깊은 위젯 트리는 빌드 시간을 늘리고 메모리를 더 많이 사용합니다.
|
|
|
|
```dart
|
|
// 좋지 않은 예: 불필요하게 깊은 트리
|
|
Container(
|
|
child: Container(
|
|
child: Container(
|
|
child: Text('깊은 트리'),
|
|
),
|
|
),
|
|
)
|
|
|
|
// 좋은 예: 간결한 트리
|
|
Container(
|
|
padding: EdgeInsets.all(16),
|
|
margin: EdgeInsets.all(8),
|
|
decoration: BoxDecoration(/* ... */),
|
|
child: Text('간결한 트리'),
|
|
)
|
|
```
|
|
|
|
### 2. const 생성자 사용
|
|
|
|
`const` 생성자로 만든 위젯은 빌드 시간에 한 번만 생성되어 메모리와 성능을 개선합니다.
|
|
|
|
```dart
|
|
// 좋지 않은 예: 매번 새로운 위젯 인스턴스 생성
|
|
Container(
|
|
padding: EdgeInsets.all(16),
|
|
child: Text('Hello'),
|
|
)
|
|
|
|
// 좋은 예: 불변 위젯 재사용
|
|
const SizedBox(height: 16)
|
|
```
|
|
|
|
### 3. 위젯 분리 및 캐싱
|
|
|
|
자주 변경되지 않는 위젯을 분리하여 불필요한 재빌드를 방지합니다.
|
|
|
|
```dart
|
|
// 좋지 않은 예: 전체 화면이 다시 빌드됨
|
|
class MyScreen extends StatefulWidget {
|
|
@override
|
|
_MyScreenState createState() => _MyScreenState();
|
|
}
|
|
|
|
class _MyScreenState extends State<MyScreen> {
|
|
int _count = 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text('앱')), // 매번 재빌드됨
|
|
body: Center(
|
|
child: Column(
|
|
children: [
|
|
ComplexWidget(), // 매번 재빌드됨
|
|
Text('카운트: $_count'),
|
|
ElevatedButton(
|
|
onPressed: () => setState(() => _count++),
|
|
child: Text('증가'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 좋은 예: 변경되지 않는 위젯 분리
|
|
class MyScreen extends StatefulWidget {
|
|
@override
|
|
_MyScreenState createState() => _MyScreenState();
|
|
}
|
|
|
|
class _MyScreenState extends State<MyScreen> {
|
|
int _count = 0;
|
|
|
|
// 클래스 필드로 선언하여 재사용
|
|
final _appBar = AppBar(title: Text('앱'));
|
|
final _complexWidget = ComplexWidget();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: _appBar, // 재사용됨
|
|
body: Center(
|
|
child: Column(
|
|
children: [
|
|
_complexWidget, // 재사용됨
|
|
Text('카운트: $_count'), // 변경됨
|
|
ElevatedButton(
|
|
onPressed: () => setState(() => _count++),
|
|
child: const Text('증가'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. RepaintBoundary 사용
|
|
|
|
`RepaintBoundary`는 자식 위젯이 다시 그려질 때 부모 위젯까지 다시 그려지는 것을 방지합니다.
|
|
|
|
```dart
|
|
class MyAnimatedWidget extends StatefulWidget {
|
|
@override
|
|
_MyAnimatedWidgetState createState() => _MyAnimatedWidgetState();
|
|
}
|
|
|
|
class _MyAnimatedWidgetState extends State<MyAnimatedWidget>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(seconds: 1),
|
|
)..repeat();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
Text('이 텍스트는 다시 그려지지 않습니다'),
|
|
|
|
// RepaintBoundary로 애니메이션 위젯 격리
|
|
RepaintBoundary(
|
|
child: AnimatedBuilder(
|
|
animation: _controller,
|
|
builder: (context, child) {
|
|
return Transform.rotate(
|
|
angle: _controller.value * 2 * pi,
|
|
child: Container(
|
|
width: 100,
|
|
height: 100,
|
|
color: Colors.blue,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
Text('이 텍스트도 다시 그려지지 않습니다'),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 상속된 위젯(InheritedWidget)과 위젯 트리
|
|
|
|
`InheritedWidget`은 위젯 트리를 통해 데이터를 효율적으로 전달하는 방법을 제공합니다. 이는 테마, 사용자 데이터 등을 하위 위젯에 전달하는 데 유용합니다.
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[InheritedWidget] --> B[Child 1]
|
|
A --> C[Child 2]
|
|
B --> D[Grandchild 1]
|
|
B --> E[Grandchild 2]
|
|
C --> F[Grandchild 3]
|
|
|
|
D -.->|"of(context)"| A
|
|
F -.->|"of(context)"| A
|
|
```
|
|
|
|
### InheritedWidget 예제
|
|
|
|
```dart
|
|
// 데이터 모델
|
|
class UserData {
|
|
final String name;
|
|
final String email;
|
|
|
|
UserData({required this.name, required this.email});
|
|
}
|
|
|
|
// InheritedWidget 정의
|
|
class UserProvider extends InheritedWidget {
|
|
final UserData userData;
|
|
|
|
const UserProvider({
|
|
Key? key,
|
|
required this.userData,
|
|
required Widget child,
|
|
}) : super(key: key, child: child);
|
|
|
|
// of 메서드로 위젯 트리에서 UserProvider 인스턴스 찾기
|
|
static UserProvider of(BuildContext context) {
|
|
final provider = context.dependOnInheritedWidgetOfExactType<UserProvider>();
|
|
assert(provider != null, 'UserProvider가 위젯 트리에 없습니다');
|
|
return provider!;
|
|
}
|
|
|
|
@override
|
|
bool updateShouldNotify(UserProvider oldWidget) {
|
|
return userData.name != oldWidget.userData.name ||
|
|
userData.email != oldWidget.userData.email;
|
|
}
|
|
}
|
|
|
|
// 사용 예시
|
|
class MyApp extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
home: UserProvider(
|
|
userData: UserData(
|
|
name: '홍길동',
|
|
email: 'hong@example.com',
|
|
),
|
|
child: HomeScreen(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 하위 위젯에서 데이터 접근
|
|
class ProfileSection extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// UserProvider.of(context)로 데이터 접근
|
|
final userData = UserProvider.of(context).userData;
|
|
|
|
return Card(
|
|
child: Column(
|
|
children: [
|
|
Text('이름: ${userData.name}'),
|
|
Text('이메일: ${userData.email}'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 결론
|
|
|
|
위젯 트리는 Flutter UI의 핵심 구성 요소입니다. 위젯 트리, 요소 트리, 렌더 트리의 개념을 이해하면 Flutter가 어떻게 효율적으로 UI를 구성하고 업데이트하는지 파악할 수 있습니다.
|
|
|
|
효율적인 위젯 트리 구성은 Flutter 앱의 성능과 유지 관리성에 큰 영향을 미칩니다. 적절한 위젯 키 사용, 위젯 구조 최적화, `const` 생성자 활용 등의 기법으로 더 효율적인 UI를 구축할 수 있습니다.
|
|
|
|
`InheritedWidget`과 같은 상속 메커니즘을 활용하면 위젯 트리를 통해 데이터를 효율적으로 공유하여 앱 아키텍처를 개선할 수 있습니다. 이러한 개념들은 Provider, Riverpod 등 Flutter의 상태 관리 솔루션의 기반이 됩니다.
|
|
|
|
다음 장에서는 Flutter의 기본 위젯들에 대해 더 자세히 알아보겠습니다.
|