learn-flutter/content/docs/part3/widget-tree.md
ChangJoo Park(박창주) 900b0ab68b update markdown
2025-05-14 09:34:13 +09:00

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의 기본 위젯들에 대해 더 자세히 알아보겠습니다.