Flutter 위젯 생명주기와 조건부 렌더링 트러블슈팅

허준호·2025년 6월 11일
0
post-thumbnail

문제 상황

Flutter 앱에서 게임 타입에 따라 NumberPadColorPad 위젯을 조건부로 렌더링하는 과정에서 특이한 문제가 발생했다. 카운트다운이 끝난 후 게임 패드가 뒤집히는 기능이 NumberPad에서는 didUpdateWidget만으로 정상 작동했지만, ColorPad에서는 initState에 추가 코드를 넣어야만 작동하는 것이다...

// GameScreen에서의 조건부 렌더링
isNumberType
    ? NumberPad(
        state: state,
        decreaseCount: (int value) => onAction(GameAction.decreaseCount(value)),
        gameSuccess: () => onAction(const GameAction.gameSuccess()),
        gameFail: () => onAction(const GameAction.gameFail()),
      )
    : ColorPad(
        state: state,
        decreaseCount: (int value) => onAction(GameAction.decreaseCount(value)),
        gameSuccess: () => onAction(const GameAction.gameSuccess()),
        gameFail: () => onAction(const GameAction.gameFail()),
      )

원인 분석

위젯 생명주기와 Element 재사용

Flutter는 위젯 트리를 효율적으로 관리하기 위해 Element 재사용 전략을 사용한다고 한다. 조건부 렌더링에서 위젯이 교체될 때, Flutter는 다음과 같은 과정으로 판단:

  1. Key 비교: 동일한 Key를 가진 위젯이 있는지 확인
  2. 타입 비교: 동일한 타입(runtimeType)의 위젯이 있는지 확인
  3. 재사용 결정: 위 조건을 만족하면 기존 Element를 재사용, 아니면 새로 생성

문제는 Key가 없는 조건부 렌더링에서 Flutter의 Element 재사용 전략이 예측 불가능하게 동작할 수 있다는 점이다. 이로 인해 ColorPad가 새로 생성되는 경우가 발생했고, 이때는 didUpdateWidget이 호출되지 않았다.

didUpdateWidget vs initState

  • didUpdateWidget: 위젯이 업데이트될 때 호출 (Element가 재사용될 때)
  • initState: 위젯이 처음 생성될 때 호출 (새 Element가 생성될 때)

NumberPad는 Element가 재사용되어 didUpdateWidget이 호출되었지만, ColorPad는 새 Element가 생성되어 didUpdateWidget 대신 initState만 호출되는 상황이 발생.

해결 방법

1. ValueKey 사용으로 위젯 정체성 보장

가장 효과적인 해결책은 각 위젯에 고유한 Key를 부여하는 것:

isNumberType
    ? NumberPad(
        key: const ValueKey('number_pad'),
        state: state,
        // ... 기타 매개변수
      )
    : ColorPad(
        key: const ValueKey('color_pad'),
        state: state,
        // ... 기타 매개변수
      )

ValueKey를 지정하면 Flutter가 위젯의 정체성을 명확히 인식하여 Element 재사용 패턴이 예측 가능해지고, didUpdateWidget 호출이 안정적으로 보장된다.

2. 방어적 코딩 패턴 적용

두 위젯 모두에 동일한 방어적 코드를 적용하여 어떤 상황에서도 일관된 동작을 보장할 수 있음:


void initState() {
  super.initState();
  // 기본 초기화...
  
  // 생성 시점에 이미 카운트다운이 끝났다면 처리
  if (widget.state.isCountDownFinished) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _flipAll();
    });
  }
}


void didUpdateWidget(covariant GamePad oldWidget) {
  super.didUpdateWidget(oldWidget);
  // 정상적인 카운트다운 완료 시 처리
  if (!oldWidget.state.isCountDownFinished &&
      widget.state.isCountDownFinished) {
    _flipAll();
  }
}

느낀점

조건부 렌더링에서 Key의 중요성

조건부 렌더링에서 서로 다른 위젯 타입을 교체할 때는 항상 명시적인 Key를 사용해야 한다. 이는 다음과 같은 이점을 제공한다:

  • 위젯 정체성 명확화: Flutter가 위젯을 일관되게 식별
  • 생명주기 메서드 호출 안정화: didUpdateWidget 등의 호출 보장
  • 상태 보존: 위젯의 상태가 예측 가능하게 유지

방어적 코딩의 필요성

Flutter 앱 개발에서는 다양한 엣지 케이스를 고려한 방어적 코딩이 중요하다:

  • initState와 didUpdateWidget 모두 활용: 위젯 생성과 업데이트 모두 처리
  • WidgetsBinding.instance.addPostFrameCallback 활용: 렌더링 완료 후 작업 예약
  • Hot Reload 고려: 개발 환경에서의 특수한 상황 대비

결론

Flutter의 조건부 렌더링에서 발생하는 위젯 생명주기 문제는 명시적인 Key 사용과 방어적 코딩 패턴을 통해 해결할 수 있다. 특히 ValueKey를 사용하여 위젯의 정체성을 명확히 하는 것이 가장 효과적인 해결책이다.

이번 트러블 슈팅을 통해 Flutter의 위젯 생명주기와 Element 재사용 메커니즘에 대해 깊이 이해할 수 있었고, 더 안정적이고 예측 가능한 앱을 개발하는 데 도움이 되었다.

profile
후추랑 소금 좋아하는 개발자

0개의 댓글