guricode

[Flutter] StatefulWidget의 생명주기(Lifecycle) 본문

앱/Flutter&Dart

[Flutter] StatefulWidget의 생명주기(Lifecycle)

agentrakugaki 2025. 10. 19. 23:18

StatefulWidget은 State를 가지는 위젯으로 상태를 관리할수있다.

StatefulWidget에는 라이프사이클이 존재하는데 이 라이프사이클을 알고있어야 앱에서 상태관리에 유용하게 사용할수있다.

1.createState() - State 객체 생성

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
  // StatefulWidget이 생성될 때 단 한 번만 호출
  // State 객체를 생성하여 반환
}

state객체를 생성해준다. 처음 위젯이 삽입될때만 호출된다.

2.initState()-초기화

@override
void initState() {
  super.initState();  // 반드시 가장 먼저 호출!

  // 사용 예시:
  _controller = AnimationController(vsync: this);
  _scrollController = ScrollController();
  _fetchData();  // API 호출
  _timer = Timer.periodic(Duration(seconds: 1), (timer) {
    // 주기적 작업
  });
}

State객체가 생성된 직후 한번 호출된다.

build()보다 먼저 실행 된다.

주요역할은 컨트롤러 초기화,리스너등록, 초기데이터 로드, Stream 구독시작, 타이머설정 등이다.

context를 사용할 수 있지만, Theme.of(context) 같은 InheritedWidget은 아직 사용 불가하다. InheritedWidget는 didChangeDependencies단계에서 의존성 설정이 완료되기 때문이다. didChangeDependencies 여기서 Theme, MediaQuery 등 사용 가능하다.

따라서 아래와 같이 시도하면 오류가 난다.

// ❌ 잘못된 예
@override
void initState() {
  super.initState();
  final size = MediaQuery.of(context).size;  // 에러 발생 가능
}

// ✅ 올바른 예 - didChangeDependencies 사용
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final size = MediaQuery.of(context).size;
}
1. createState() 
   → State 객체 생성
   → context 생성 ✅
   → 하지만 위젯 트리에 아직 안 붙음

2. initState()
   → context는 있지만 ⚠️
   → 위젯 트리와의 "의존성 관계" 설정 전
   → InheritedWidget을 찾을 수 없음 ❌

3. didChangeDependencies()
   → 위젯 트리에 완전히 연결됨 ✅
   → InheritedWidget 의존성 설정 완료 ✅
   → 이제 Theme, MediaQuery 등 사용 가능!

4. build()
   → 모든 것 사용 가능 ✅

3.didChageDependencies()-의존성 변경

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  // 사용 예시:
  final theme = Theme.of(context);  // 이제 사용 가능!
  final mediaQuery = MediaQuery.of(context);
  final locale = Localizations.localeOf(context);

  // 의존성이 변경될 때마다 실행할 코드
  _updateThemeBasedData(theme);
}

initState() 직후 처음 한번 실행된다.

이후 InheritedWidget이 변경될때마다 실행된다.

주로 InheritedWidget(Theme, MediaQuery 등)에 의존하는 초기화를 한다.

자주호출될수있으니 무거운 작업은 피해야한다.

그리고 initState()와 달리 여러번 호출 가능하다.

4.Build() - UI구성

@override
Widget build(BuildContext context) {
  print('build 호출됨: ${DateTime.now()}');

  return Scaffold(
    appBar: AppBar(title: Text('Life Cycle')),
    body: Column(
      children: [
        Text('카운터: $_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: Text('증가'),
        ),
      ],
    ),
  );
}

didChangeDependencies() 이후 처음 한 번, didUpdateWidget() 이후 ,부모위젯이 재빌드될때 실행된다.

그리고 상태 setState가 호출된때마다 리빌드된다.

build안에서는 상태변경(setState) 금지다.

빌드안에서 또 setState가 실행되면 중복으로 실행되게된다.

5.didUpdatewidget()-위젯 업데이트

@override
void didUpdateWidget(MyWidget oldWidget) {
  super.didUpdateWidget(oldWidget);

  // 사용 예시:
  if (widget.title != oldWidget.title) {
    print('제목이 변경됨: ${oldWidget.title} -> ${widget.title}');
    _updateTitle();
  }

  if (widget.url != oldWidget.url) {
    _fetchNewData(widget.url);
  }
}

부모위젯이 재빌드되어 같은 타입의 새로운 위젯으로 교체될때 실행된다.

이후 자동으로 build()가 실행된다.

이 부분이 이해가 잘 안갔는데 아래 예시코드로 쉽게 확인할수있었다.

예시 코드

class NetworkImageWidget extends StatefulWidget {
  final String imageUrl;

  NetworkImageWidget({required this.imageUrl});

  @override
  _NetworkImageWidgetState createState() => _NetworkImageWidgetState();
}

class _NetworkImageWidgetState extends State<NetworkImageWidget> {
  late Future<Uint8List> _imageFuture;

  @override
  void initState() {
    super.initState();
    _imageFuture = _loadImage(widget.imageUrl);
  }

  @override
  void didUpdateWidget(NetworkImageWidget oldWidget) {
    super.didUpdateWidget(oldWidget);

    // URL이 변경되었을 때만 새로 로드
    if (oldWidget.imageUrl != widget.imageUrl) {
      print('이미지 URL 변경: ${oldWidget.imageUrl} → ${widget.imageUrl}');
      _imageFuture = _loadImage(widget.imageUrl);
    }
  }

  Future<Uint8List> _loadImage(String url) async {
    // 이미지 로드 로직
    return Uint8List(0);
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List>(
      future: _imageFuture,
      builder: (context, snapshot) {
        // UI 구성
        return Container();
      },
    );
  }
}

6.setState() - 상태변경 알림

void _incrementCounter() {
  setState(() {
    _counter++;  // 상태 변경
    _message = '카운터가 증가했습니다';
  });
  // setState가 완료된 후에 실행됨
  print('setState 완료');
}

// 비동기 작업 후 상태 변경
Future<void> _loadData() async {
  final data = await fetchDataFromServer();

  // mounted 체크 필수!
  if (mounted) {
    setState(() {
      _data = data;
    });
  }
}

생명주기 메서드는 아니지만 중요해서 넣었다.

다음 프레임에 build()재호출을 예약한다.

initState(), dispose() 내에서 호출 불가능하다.

비동기 작업 후에는 mounted체크를 습관적으로 해줘야한다.

7.deactivate() - 위젯트리에서 제거

@override
void deactivate() {
  print('deactivate 호출 - 위젯이 트리에서 제거됨');
  super.deactivate();

  // 임시 제거 시 정리할 작업
}

위젯이 트리에서 제거될떄 실행된다.

dispose바로 전에 실행된다.

드물게 사용되며 위젯이 다른위치로 이동할때 처리한다.

8.dispose() - 정리 및 해제

@override
void dispose() {
  // 리소스 정리 (반드시 super.dispose() 전에!)
  _controller?.dispose();
  _scrollController?.dispose();
  _focusNode?.dispose();
  _timer?.cancel();
  _subscription?.cancel();

  print('dispose 호출 - State 객체 영구 제거');
  super.dispose();  // 반드시 마지막에 호출!
}

State가 영구적으로 제거될때, deactivate() 이후 실행된다.

주로 컨트롤러 dispose,리스너제거,Stream 구독취소,메모리 누수 방지로 사용한다.

 

메서드명 호출시점 호출 횟수 BuildContext 접근 setState 사용 주요 역할 Build 호출 여부
createState() Widget 생성 시 1회 State 객체 생성
initState() State 생성 직후 1회 ⚠️ 제한적 초기화, 리스너 등록, API 호출
didChangeDependencies() initState 직후, InheritedWidget 변경 시 1회 이상 InheritedWidget 의존성 처리
build() 위 단계 후, setState() 호출 시 매우 많음 UI 위젯 트리 반환
didUpdateWidget() 부모 재빌드로 새 위젯 받을 때 0회 이상 속성 변경 감지 및 처리
deactivate() 위젯 트리에서 제거 시 0-1회 임시 제거 처리 (드물게 사용) ⚠️
dispose() State 영구 제거 시 1회 리소스 정리, 메모리 해제

 

 

 

 

항목  StatelessWidget  StatefulWidget
상태 보관 없음(불변). 생성자 값만 표시 있음(가변). State 객체에 저장
재빌드 트리거 부모가 새 값 전달 시만 setState() 호출 시 해당 위젯 subtree 리빌드
라이프사이클 build 한 번 중심 initState → build → didUpdateWidget → dispose 등
사용 예 아이콘, 고정 텍스트, 단순 레이아웃 토글, 폼 입력, 애니메이션, 스크롤 위치 등