본문 바로가기
비전공자를 위한 Flutter/Flutter 심화과정

Flutter StatefulWidget 란? 코드 뜯어보기 (한글 주석 번역)

by 밍잔 2022. 5. 2.

Flutter에 있는 2가지 위젯 중 오늘은 StatefulWidget에 대해 알아보겠습니다. 한 줄로 요약하자면 화면에 계속 변화를 줘야 할 때 쓰는 위젯입니다. 아래 동영상은 2018년에 나온 StatefulWidget을 설명하는 동영상이니 한 번 시청해보시죠.

 

 

[설정 > 자동번역 > 한국어]로 설정하면 자막을 볼 수 있어요.

 

한글 자막 제공

 

Widget은 element를 화면에 그리기 위한 청사진입니다. Widget 트리에 1:1 매칭된 Element 트리의 element들이 바로 우리의 휴대폰 화면에 보이는 UI입니다. StatefulWidget을 만드는 건 화면에 변화가 계속 일어나는 UI 설계도를 만드는 작업이라고 생각하시면 됩니다.

 


 

 

앞서 StatelessWidget이라는 변하지 않는 위젯에 대해 알아봤습니다. 그렇다면 변경되는 데이터, 그리고 그걸 나타내는 UI를 그리려면 어떻게 해야할까요? 우리는 가만히 있는 화면을 보기 위해 앱을 만드는 게 아닌데 말이죠. 화면에 변화를 주기 위해 사용하는 게 바로 StatefulWidget입니다.

 

StatefulWidget과 StatelessWidget은 UI로 렌더링되는 과정에 차이가 있습니다. StatelessWidget은 데이터가 변하지 않으니, Widget tree가 Element tree에 element를 요청해서 1:1 마운트하는 걸로 끝났었는데요.

 

Stateless의 렌더링 과정

 

 

StatefulWidget은 여기서 한 가지 단계가 더 있습니다. Widget tree는 Element tree에게 데이터 변화를 감지하기 위해 State Object를 하나 요청합니다. 이때 State가 생성되는데 이때 호출하는 메소드가 createState입니다. createState로 새로운 State 객체를 만들고 Element tree가 이 객체를 유지하죠. 

 

 

아래의 Text 위젯에 있는 텍스트를 제대로 보여주려면 widget이 가진 name과 state이 가진 count 프로퍼티가 필요하죠.

 

class ItemCounter extends StatefulWidget {
  const ItemCounter({Key? key, required this.name}) : super(key: key);

  final String name;

  @override
  State<ItemCounter> createState() => _ItemCounterState();
}

class _ItemCounterState extends State<ItemCounter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return const Text(
      '${widget.name}: $count',
    );
  }
}

 

Text위젯은 StatelessWidget이니 Element tree에 element를 요청하여 렌더링하면 됩니다. 여기까지가 데이터 상태 변화를 감지할 준비가 완료된 겁니다.

 

 

setState는 state 객체의 프로퍼티를 재설정하고 UI를 업데이트할 수 있는 트리거 메소드입니다. 

class _ItemCounterState extends State<ItemCounter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          count++;
        });
      },
      child: Text(
        '${widget.name}: $count',
      ),
    );
  }
}

 

화면을 한 번 클릭하면 count가 1 증가하며 트리가 아래와 같은 상태가 됩니다. State 객체의 count 프로퍼티가 1로 증가했죠. 이때 트리는 변경된 프로퍼티를 더티로 표시합니다. 그리고 다음 프레임에 UI를 다시 빌드하죠. 

 

StatelessWidget을 빌드할 때와 같이 동일하지만 값이 다른 Text 위젯 하나를 새로 생성합니다. 이 Text 위젯은 업데이트 된 내용으로 새로 빌드하고, Widget 트리는 새로 빌드한 Text 위젯을 마운트하게 됩니다. 교체되는 거죠.

 

 

State 객체의 장점은 생명주기가 길다는 겁니다. 지금은 ItemCounter의 name이 Tom인 객체로 되어있지만, Tom이 아니라 Dan이라는 이름으로 객체를 변경해도, 동일한 유형의 위젯이기 때문에 State는 그대로 유지됩니다. 그리고 State가 스스로를 더티로 표시하여, 트리 아래에 있는 Text 위젯 내용을 바꿔 새로 빌드하고 마운트합니다.

 

 

위젯이 새로 빌드되는 시기를 알아야 할 때 state가 가진 didupdateWidget이라는 메소드를 이용해 알아볼 수 있습니다.

@override
void didUpdateWidget(ItemCounter oldWidget) {
  super.didUpdateWidget(oldWidget);
  // ...
}

 

StatefulWidget의 rebuild 영향을 최소화할 수 있는 몇 가지 기술이 있습니다.

1. StatefulWidget를 트리의 가장 하위 위젯에 넣습니다. 

2. 빌드 메소드와 생성하는 위젯에 의해 전이적으로 생성되는 노드의 수를 최소화합니다.

 

3. 가능하면 `const` 위젯을 사용합니다.


4. state가 변경될 때 생성된 하위 트리의 깊이를 변경하거나 하위 트리의 위젯 유형을 변경하지 않습니다. 


5. 어떤 이유로 깊이를 변경해야 하는 경우 상태 저장 위젯의 수명 동안 일관되게 유지되는 [GlobalKey]가 있는 위젯에서 하위 트리의 공통 부분을 래핑하세요.

 


 

2가지 케이스의 StatelessWidget의 코딩 컨벤션을 보여드릴게요

 

첫번째 케이스

 

두번째 케이스

 


 

플러터는 잘 쓰면 잘 쓸 수록 StatefulWidget을 안 쓰게 됩니다.

 

 

위와 같은 StreamBuilder를 이용할 수도 있고, 트리에 StatefulWidget이 여러개 중첩된 경우 생성자를 통해 계속 데이터를 전달하는 게 번거롭기 때문입니다. 트리 100개 하위 위젯이어도 상위 위젯의 데이터에 접근할 수 있는 위젯이 바로 InheritedWidget 입니다.

다음 강의에서는 InheritedWidget을 알아봅시다. 아래 링크를 누르세요.

 

 

https://mingzan.tistory.com/263

 

 

State에 대해 자세히 알고 싶다면 아래 링크를 눌러주세요.

 

 

https://mingzan.tistory.com/264


StatefulWidget 관련 번역 전문을 남깁니다. 오역이 있다면 댓글로 알려주세요!

 

/// 위젯은 변경 가능한 state입니다.
///
/// State는 동기적으로 읽을 수 있는 정보입니다:
/// (1) 위젯이 빌드될 때 
/// (2) 위젯의 라이프타임 동안의 변경될 때
/// 
/// State가 변경되면 State.setState를 통해 변경 사항을 위젯 생성자가 반드시 알리도록 되어있습니다.
///
/// StatefulWidget은 다른 위젯의 집합을 빌드하여 구체적으로 UI를 묘사합니다.  
/// 
/// 빌드 프로세스는 사용자 UI가 [RenderObject]를 설명하는 [RenderObjectWidget]으로 
/// 완전히 구성될 때까지 재귀적으로 계속됩니다.
/// 
/// StatefulWidget은 시간이 지남에 따라 또는 시스템에 따라서UI가 변경되는 걸 그려야 할 때 유용합니다. 
/// 객체 자체의 구성 정보와 위젯이 [BuildContext]에만 의존하는 구성의 경우 [StatelessWidget] 사용을 고려하세요.
/// 
/// [StatefulWidget] 인스턴스 자체는 변경할 수 없으며 [createState] 메서드에 의해 생성된 별도의 
/// [State] 객체 또는 해당 [State]가 구독하는 객체(예: [Stream] 또는 [ChangeNotifier] 객체)에 
/// 변경 가능한 상태를 저장합니다. [StatefulWidget] 자체의 최종 필드에 참조가 저장됩니다.
/// 
/// 플러터 프레임워크는 [StatefulWidget]을 확장할 때마다 [createState]를 호출합니다. 
/// 즉, 해당 위젯이 여러 위치의 트리에 삽입된 경우 여러 [State] 객체가 
/// 해당 [StatefulWidget]과 연관될 수 있음을 의미합니다. 
/// 유사하게, [StatefulWidget]이 트리에서 제거되고 나중에 트리에 다시 
/// 삽입되면 프레임워크는 [createState]를 다시 호출하여 
/// 새로운 [State] 객체를 생성하여 [State] 객체의 수명 주기를 단순화합니다.
///
/// [StatefulWidget]은 작성자가 [key]에 대해 [GlobalKey]를 사용한 경우 트리의 한 위치에서 
/// 다른 위치로 이동할 때 동일한 [State] 객체를 유지합니다. [GlobalKey]가 있는 위젯은 트리의 
/// 탑레벨 위치에서 사용할 수 있으므로 [GlobalKey]를 사용하는 위젯은 연결된 요소가 최대 하나입니다. 
/// 이런 점을 이용해 이전 위치의 서브 트리와 관련된 위젯을 새로운 위치로 옮길 때
/// 서브트리를 새 위치에 재생성하지 않고, 글로벌 키를 이용하여 [State] 객체를 유지한 채로 이동합니다. 
/// [StatefulWidget]과 연결된 [State] 개체는 나머지 하위 트리와 함께 접목됩니다. 
/// 즉, [State] 개체가 새 위치에서 다시 생성되는 대신 재사용됩니다. 
/// 그러나 접목할 수 있으려면 위젯이 이전 위치에서 제거된 
/// 동일한 애니메이션 프레임의 새 위치에 위젯을 삽입해야 합니다.
/// 
///
///
/// ## 성능 최적화 고려사항
/// 
/// [StatefulWidget]에는 두 가지 기본 범주가 있습니다.
///
/// 첫 번째는 [State.initState]에 리소스를 할당하고 [State.dispose]에 
/// 폐기하지만 [InheritedWidget]에 의존하거나 [State.setState]를 호출하지 않는 것입니다. 
/// 이러한 위젯은 일반적으로 애플리케이션이나 페이지의 루트에서 사용되며 
/// [ChangeNotifier], [Stream] 또는 기타 개체를 통해 하위 위젯과 통신합니다. 
/// 이러한 패턴을 따르는 StatefulWidget은 한 번 빌드된 후 업데이트되지 않기 때문에 
/// CPU 및 GPU 측면에서 상대적으로 저렴합니다. 
/// 따라서 이 위젯은 다소 복잡하고 깊은 빌드 방법을 가질 수 있습니다.
/// 
/// 두 번째 범주는 [State.setState]를 사용하거나 [InheritedWidget]에 의존하는 위젯입니다. 
/// 이들은 일반적으로 애플리케이션의 수명 동안 여러 번 다시 작성하므로 이러한 위젯을 다시 
/// 작성하는 데 따른 영향을 최소화하는 것이 중요합니다. 
/// ([State.initState] 또는 [State.didChangeDependencies]를 사용하여 
/// 리소스를 할당할 수도 있지만 중요한 부분은 다시 빌드한다는 것입니다.)
///
/// StatefulWidget을 다시 빌드할 때의 영향을 최소화하는 데 사용할 수 있는 몇 가지 기술이 있습니다:
///
///  * state를 가장 하위 위젯에 넣습니다. 
///    예를 들어 페이지에 똑딱거리는 시계가 있는 경우, 
///    상태를 페이지 맨 위에 놓고 시계가 똑딱할 때마다 전체 페이지를 다시 작성하는 대신 
///    자체 업데이트만 하는 전용 시계 위젯을 만드세요.
/// 
///  * 빌드 메소드와 생성하는 위젯에 의해 전이적으로 생성되는 노드의 수를 최소화합니다. 
///    이상적으로는 상태 저장 위젯은 단일 위젯만 생성하고 해당 위젯은 [RenderObjectWidget]이 됩니다. 
///    (물론 이게 항상 실용적인 건 아니지만, 위젯이 이 이상에 가까울수록 더 효율적입니다.)
///
///  * 하위 트리가 변경되지 않으면 해당 하위 트리를 나타내는 위젯을 캐시하고 사용할 수 있을 때마다 다시 사용합니다. 
///    새(그러나 동일하게 구성된) 위젯을 생성하는 것보다 위젯을 재사용하는 것이 훨씬 더 효율적입니다. 
///    자식 인수를 취하는 위젯으로 stateful 부분을 빼내는 것은 이것을 하는 일반적인 방법입니다.
///    (1번째와 동일한 내용)
///
///  * 가능하면 `const` 위젯을 사용하세요. (위젯을 캐싱하여 재사용하는 것과 같습니다.)
///
///  * 생성된 하위 트리의 깊이를 변경하거나 하위 트리의 위젯 유형을 변경하지 마십시오. 
///    예를 들어 자식이나 [IgnorePointer]로 래핑된 자식을 반환하는 대신 항상 자식 위젯을 
///    [IgnorePointer]로 래핑하고 [IgnorePointer.ignoring] 속성을 제어합니다. 
///    이는 하위 트리의 깊이를 변경하면 전체 하위 트리를 다시 작성, 
///    레이아웃 및 페인팅해야 하는 반면 속성을 변경하는 것만으로는 렌더링 트리에 대한 가능한 
///    최소한의 변경이 필요하기 때문입니다
///    (예: [IgnorePointer]의 경우 no layout 또는 repaint가 필요합니다).
/// 
///  * 어떤 이유로 깊이를 변경해야 하는 경우 상태 저장 위젯의 수명 동안 
///    일관되게 유지되는 [GlobalKey]가 있는 위젯에서 하위 트리의 공통 부분을 래핑하는 것을 고려하십시오. 
///    ([KeyedSubtree] 위젯은 편리하게 키를 할당할 수 있는 다른 위젯이 없는 경우 이 용도로 유용할 수 있습니다.)
///
/// See also:
///
///  * [State], where the logic behind a [StatefulWidget] is hosted.
///  * [StatelessWidget], for widgets that always build the same way given a
///    particular configuration and ambient state.
///  * [InheritedWidget], for widgets that introduce ambient state that can
///    be read by descendant widgets.
/// 
/// 참고해보세요:
///
///  * [State]의 로직은 [StatefulWidget] 로직 뒤에 있습니다.
///  * 특정 구성 및 주변 상태가 주어지면 항상 동일한 방식으로 빌드하는 위젯의 경우 [StatelessWidget]를 사용하세요.
///  * 하위 위젯이 읽을 수 있는 주변 상태를 도입해야 하는 경우 [InheritedWidget]를 사용하세요.
abstract class StatefulWidget extends Widget {
  /// 서브클래스를 위해 [key] 초기화합니다.
  const StatefulWidget({ Key? key }) : super(key: key);

  /// 트리의 위치를 관리하기 위해 [StatefulElement]를 생성합니다.
  ///
  /// 이 메소드를 오버라이드 하지 마세요.
  @override
  StatefulElement createElement() => StatefulElement(this);

  /// 트리에 주어진 위치에 변경 가능한 state를 만듭니다.
  ///
  /// 서브클래스는 이 메소드를 새로운 [State] 인스턴스를 리턴하도록 오버라이드 합니다:
  ///
  /// ```dart
  /// @override
  /// State<MyWidget> createState() => _MyWidgetState();
  /// ```
  ///
  /// [StatefulWidget]의 생명주기동안 이 메소드는 여러번 실행됩니다.
  /// 예를 들면, 이 위젯이 트리의 여러 군데에 삽입되면, 그 삽입된 여러 곳에 [State] 객체를 각 위치에 생성합니다.
  /// 비슷하게, 위젯이 트리에서 지워지거나 나중에 트리에 다시 삽입되면, 
  /// [State] 객체를 최신화하고 생명주기를 단순하기 관리하기 위해 [createState]를 다시 실행합니다.
  @protected
  @factory
  State createState(); // createState 안에 아무 로직도 넣지 마세요.
}

 

 

댓글