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