Widget 트리 — StatelessWidget vs StatefulWidget

Flutter에서는 모든 것이 위젯 입니다. 텍스트, 버튼, 패딩, 정렬까지 전부 위젯입니다. 이 위젯들이 트리 구조로 조합되어 화면을 구성합니다.

면접에서 "Widget, Element, RenderObject 트리의 차이를 설명해주세요"라는 질문이 나올 수 있습니다. 이번 글에서 기초를 잡아보겠습니다.


Widget 트리란?

PLAINTEXT
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

상태가 없는 위젯입니다. 한 번 만들어지면 외부에서 주어진 데이터만으로 화면을 그립니다.

DART
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

내부 상태를 가질 수 있는 위젯입니다. 사용자 입력, 애니메이션, 네트워크 응답 등으로 상태가 변할 때 사용합니다.

DART
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의 동작 원리

  1. setState() 호출
  2. 해당 Element를 "dirty"로 표시
  3. 다음 프레임에서 build() 재실행
  4. 새 Widget 트리와 기존 Element 트리를 비교
  5. 변경된 부분만 RenderObject 트리에 반영

State의 생명주기

DART
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 생성자의 중요성

DART
// const를 쓰면 같은 위젯을 재사용 (리빌드 방지)
const Text('변하지 않는 텍스트');

// const가 아니면 매 빌드마다 새 인스턴스 생성
Text('카운트: $_count');  // 상태에 따라 변하므로 const 불가

const 위젯은 리빌드 시에도 재생성되지 않으므로 성능에 유리합니다. 가능한 곳에 const를 붙이는 습관을 들이세요.


정리

  • Flutter의 모든 UI는 Widget 트리로 구성됩니다
  • Widget(설계도) → Element(중재자) → RenderObject(실제 렌더링) 3단 구조
  • StatelessWidget: 상태 없음, StatefulWidget: 내부 상태 보유
  • setState()를 호출하면 build()가 다시 실행됩니다
  • dispose()에서 리소스를 꼭 정리하세요
  • const 생성자를 적극 활용하면 불필요한 리빌드를 줄일 수 있습니다
댓글 로딩 중...