Widget 트리 — StatelessWidget vs StatefulWidget
Widget 트리 — StatelessWidget vs StatefulWidget
Flutter에서는 모든 것이 위젯 입니다. 텍스트, 버튼, 패딩, 정렬까지 전부 위젯입니다. 이 위젯들이 트리 구조로 조합되어 화면을 구성합니다.
면접에서 "Widget, Element, RenderObject 트리의 차이를 설명해주세요"라는 질문이 나올 수 있습니다. 이번 글에서 기초를 잡아보겠습니다.
Widget 트리란?
MaterialApp
└─ Scaffold
├─ AppBar
│ └─ Text('제목')
└─ Center
└─ Column
├─ Text('안녕하세요')
└─ ElevatedButton
└─ Text('클릭')
위젯은 불변(immutable) 객체입니다. 상태가 바뀌면 기존 위젯을 수정하는 것이 아니라, ** 새 위젯을 만들어서 교체 **합니다.
3개의 트리
Flutter는 내부적으로 3개의 트리를 관리합니다.
| 트리 | 역할 | 특징 |
|---|---|---|
| Widget 트리 | UI 설계도 (blueprint) | 불변, 매 빌드마다 새로 생성 |
| Element 트리 | Widget과 RenderObject의 중재자 | 재사용 가능, 생명주기 관리 |
| RenderObject 트리 | 실제 레이아웃과 페인팅 | 크기 계산, 화면에 그리기 |
Widget은 가볍고 자주 생성/폐기되지만, Element와 RenderObject는 가능한 한 재사용됩니다. 이것이 Flutter의 성능 비결입니다.
StatelessWidget
상태가 없는 위젯입니다. 한 번 만들어지면 외부에서 주어진 데이터만으로 화면을 그립니다.
class GreetingCard extends StatelessWidget {
final String name;
final String message;
const GreetingCard({
super.key,
required this.name,
required this.message,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(message),
],
),
),
);
}
}
사용 시점:
- 부모로부터 받은 데이터만 표시할 때
- 사용자 인터랙션이 없는 정적 UI
StatefulWidget
내부 상태를 가질 수 있는 위젯입니다. 사용자 입력, 애니메이션, 네트워크 응답 등으로 상태가 변할 때 사용합니다.
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0; // 내부 상태
void _increment() {
setState(() {
_count++; // setState 안에서 상태 변경
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$_count',
style: const TextStyle(fontSize: 48),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _increment,
child: const Text('증가'),
),
],
);
}
}
setState의 동작 원리
setState()호출- 해당 Element를 "dirty"로 표시
- 다음 프레임에서
build()재실행 - 새 Widget 트리와 기존 Element 트리를 비교
- 변경된 부분만 RenderObject 트리에 반영
State의 생명주기
class LifecycleWidget extends StatefulWidget {
const LifecycleWidget({super.key});
@override
State<LifecycleWidget> createState() => _LifecycleWidgetState();
}
class _LifecycleWidgetState extends State<LifecycleWidget> {
@override
void initState() {
super.initState();
// 1. 최초 1회 호출 — 초기화 작업
print('initState');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 2. initState 직후 + InheritedWidget 변경 시
print('didChangeDependencies');
}
@override
void didUpdateWidget(covariant LifecycleWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// 부모가 같은 타입의 새 위젯을 전달했을 때
print('didUpdateWidget');
}
@override
Widget build(BuildContext context) {
// 3. UI 빌드 (여러 번 호출될 수 있음)
print('build');
return const Text('생명주기 테스트');
}
@override
void deactivate() {
// 4. 트리에서 제거될 때 (다시 삽입될 수도 있음)
print('deactivate');
super.deactivate();
}
@override
void dispose() {
// 5. 영구 제거 — 리소스 정리
print('dispose');
super.dispose();
}
}
면접 포인트: dispose()에서 Controller, StreamSubscription, Timer 등을 반드시 정리해야 메모리 누수가 발생하지 않습니다.
언제 어떤 걸 쓸까?
| 상황 | 선택 |
|---|---|
| 정적 텍스트, 아이콘 표시 | StatelessWidget |
| 카운터, 토글 같은 내부 상태 | StatefulWidget |
| API 데이터 로딩 | StatefulWidget (또는 상태관리 라이브러리) |
| 애니메이션 | StatefulWidget + AnimationController |
실무에서는 StatefulWidget을 직접 쓰기보다 Provider, Riverpod, Bloc 같은 상태 관리 라이브러리를 쓰는 경우가 많습니다. 하지만 기본 원리를 이해하는 것이 중요합니다.
const 생성자의 중요성
// const를 쓰면 같은 위젯을 재사용 (리빌드 방지)
const Text('변하지 않는 텍스트');
// const가 아니면 매 빌드마다 새 인스턴스 생성
Text('카운트: $_count'); // 상태에 따라 변하므로 const 불가
const 위젯은 리빌드 시에도 재생성되지 않으므로 성능에 유리합니다. 가능한 곳에 const를 붙이는 습관을 들이세요.
정리
- Flutter의 모든 UI는 Widget 트리로 구성됩니다
- Widget(설계도) → Element(중재자) → RenderObject(실제 렌더링) 3단 구조
- StatelessWidget: 상태 없음, StatefulWidget: 내부 상태 보유
setState()를 호출하면build()가 다시 실행됩니다dispose()에서 리소스를 꼭 정리하세요const생성자를 적극 활용하면 불필요한 리빌드를 줄일 수 있습니다
댓글 로딩 중...