Weekly Flutter

[Flutter 주석 파헤치기] 01. StatefulWidget과 State

marsg6 2023. 1. 31. 01:01

.../flutter/lib/src/widgets/framework.dart 에는 각종 Key 클래스들과, BuildContext, Widget, Element 등이 선언돼 있습니다.

 

Flutter 위젯의 두 축을 이루는 StatelessWidget과 StatefulWidget도 해당 파일에 선언돼 있는데, 항상 사용하기에 앞서 고민을 하게 만드는 StatefulWidget에 대해 조금 더 알아보고자 합니다.

 

StatefulWidget은 Widget을 상속하는데, Widget은 다시 DiagnosticableTree라는 디버깅용 클래스를 상속합니다. 실질적으로는 Widget이 최상위 추상 클래스인 거죠.

 

Flutter 팀에서는 Widget 클래스 상단에 다음과 같은 주석으로 설명을 시작합니다.

Describes the configuration for an [Element].

 

실상 본체는 Element 클래스라는 것을 유추할 수 있습니다. Element와 Widget의 관계에 대해서는 다음에 기회가 되면 알아보도록 하겠습니다. 간단하게 표현하면 Flutter Framework는 Tree 구조의 Element들로 이루어져 있고, 각 Element들은 대응되는 Widget과 Render Object, 그리고 필요하다면 State를 갖고 있습니다.

아무튼, Widget은 Flutter Framework의 핵심 축이고('the central class hierarchy in the Flutter framework'), 변할 수 있는 상태를 갖지 않습니다('has no mutable state'). 여기가 핵심입니다. Widget은 변하지 않습니다.

 

더보기

여기서 잠깐 다음 주석에 주목해봅시다.
'all their fields must be final'
Flutter 팀은 Widget에 대해 설명하는 주석에 위와 같은 문구를 넣어 놨습니다.
Widget의 모든 필드는 final로 선언하는 습관을 들이도록 합시다.

 

그렇다면 Widget의 상태가 변하도록 하고 싶을 때는 어떻게 해야 할까요? 이 때 사용하는 것이 StatefulWidget입니다. StatefulWidget은 스스로 변할 수는 없지만, 변할 수 있는 상태를 갖습니다. 상태는 State 클래스로 표현됩니다.

 

State는 (1) 연결된 위젯이 빌드되는 것과 동시에 접근 가능해야 하고('can be read synchronously when the widget is built'), (2) 위젯의 생명주기 동안에 바뀔 수도 있습니다('might change during the lifetime of the widget'). State의 상태가 바뀌는 것은 State.setState를 통해 전파할 수 있습니다. 

 

State의 라이프사이클은 다음과 같습니다.

  1. StatefulWidget.createState 호출로 인한 State 인스턴스 생성.
  2. State 인스턴스와 BuildContext 인스턴스의 연결. State에서 BuildContext로의 일방적인 연결이며, 이 때 State가 mounted 값이 true가 됨.
  3. State.initState의 호출. 일회성 초기화 작업이 여기서 이루어져야 하며, context와 widget에 접근이 가능해지는 시점.
  4. State.didChangeDependencies의 호출. 사용할 일은 거의 없음. 초기화가 완료되는 시점.
  5. 트리 내에서 같은 위치에, 동일한 타입과 동일한 key를 갖는 위젯이 보여지도록 업데이트되면, State.didUpdateWidget이 호출됨. 이 호출은 State.build 호출로 이어짐. 즉, didUpdateWidget 안에서의 setState 호출은 불필요함.
  6. 디버그 모드에서는 hot reload 시에 State.reassemble이 호출되기 때문에, 필요하다면 초기화 기능을 넣어줄 수 있음.
  7. 만약 State가 포함된 서브 트리가 제거되면(예를 들어, 다른 타입의 위젯이나, 다른 키를 가진 동일 타입의 위젯이 빌드되면) State.deactivate가 호출됨.
  8. 애니메이션 프레임이 끝나기 전까지 State는 트리로 복귀할 수 있음(예를 들어, 해당 State가 트리의 다른 부분에서 재사용될 수 있음).
  9. 애니메이션 프레임이 끝나기 전까지 복귀하지 못하면 State.dispose가 호출되고, mounted 값이 false가 됨. 이 때부터는 State.setState를 호출할 수 없음(에러가 발생함).

State의 라이프사이클에서 중요한 부분은 5번과 7번입니다. State.build가 호출되면, 기존 위젯을 사라지고 새로운 위젯이 생성됩니다. 이 때, 기존 위젯의 타입과 key가 이전 위젯의 그것들과 동일하다면 State의 바뀐 정보들만을 가지고 화면을 다시 그립니다. 만약 타입이나 key가 다르다면 State가 폐기(deactivate)되고 새로운 State가 만들어지는 거죠. 리소스가 더 들 수 있습니다. 그래서 색이 다른 Container 두 개의 위치를 바꿔야 할 때는 ValueKey 등을 사용해줘야 합니다.

 

마지막으로, StatefulWidget이 부하를 발생시키는 상황과, 이러한 상황에서 효율적으로 StatefulWidget을 사용하기 위한 대한 Flutter 팀의 권고사항을 살펴보며 마무리하겠습니다.

 

StatefulWidget은 다음 두 유형으로 분류할 수 있다고 합니다.

  • State.setState에 의존하지 않는 유형.
  • State.setState에 의존하는 유형.

첫 번째 유형은 State.initState와 State.dispose 등의 기능만을 사용하기 때문에 상대적으로 가볍고 빠르지만, 두 번째 유형은 런타임에 상태를 변화시키기에 부하가 심하다고 합니다. 이에 Flutter 팀은 다음과 같은 사용법을 권고합니다.

  • State를 말단에 둘 것. 예를 들어, 시계가 있다면 시계를 포함하는 페이지가 아니라, 시계 위젯 자체만을 StatefulWidget으로 만들 것.
  • build 시에 생성되는 노드의 수를 최소화 할 것. 1번과 같은 의미이다.
  • 서브 트리가 바뀌지 않는다면 해당 서브 트리를 캐싱해두고 재사용할 것. 즉, StatefulWidget에서 사용하는, 상태가 변하지 않는 서브 위젯을 final로 선언해두고 사용할 것.
  • 가능하면 const Widget을 사용할 것.
  • 서브 트리의 depth를 바꾸거나 타입을 바꾸지 말 것. 예를 들어, isContainer == true ? Container(...) : SizedBox(...) 따위나, isIgnoring ? IgnorePointer(... child: child) : child 따위는 사용하지 말 것.
  • 서브 트리의 depth가 모종의 이유로 바뀌어야 한다면, 공통 부분을 KeyedSubtree 등으로 감싸고 GlobalKey를 할당할 것.