StatefulWidget, StatelessWidget에 대하여

서론

Flutter를 처음 시작했을 때가 내 기억으로는 2022년쯤이었던 것으로 기억하는데 그때 당시에 가장 이해할 수 없었던 것은 StatefulWidget과 StatelessWidget이었다. 이건 왜 상태관리를 하는 위젯과 상태관리를 하지 않는 위젯이 따로 있는 것인가에 대한 짜증인지 귀찮음인지 그 중간 어딘가의 부정적인 생각들이 가득했었다.
첫째로 왜 상태관리 위젯과 상태관리를 하지 않는 위젯이 필요한가. 그냥 전부 상태관리가 가능한 위젯으로 설계하면 될 일이 아닌가? 라는 질문이 들었고 온갖 뇌피셜로 모바일 디바이스에서 상태를 관리하는 위젯은 배터리 소모가 많아서 그런가 하는 생각을 했다. 그래서 매뉴얼을 읽어보기로 했다.

왜 StatelessWidget과 StatefulWidget을 나눠서 만들까?

매뉴얼에는 이렇다할 설명을 찾기 힘들었지만 이런 내용을 찾을 수 있었다.

Stateless와 Stateful 생성 라이플 사이클

위 이미지는 StatelessWidget과 StatefulWidget이 생성할 때 수행하는 Life Cycle을 다이어그램으로 표현한 것이다.
기본적으로 StatelessWidget은 상태가 변하지 않기 때문에 빌드 과정이 간단하고 성능 최적화가 용이하다. 상태 관리가 필요하지 않은 위젯에서 StatefulWidget을 사용하게 되면 불필요한 상태 관리 로직이 추가되어 복잡도가 증가하고 성능이 저하될 수 있다.
반대로, 상태가 변하는 UI를 StatelessWidget으로 구현하려고 하면 상태를 외부에서 관리해야 하기 때문에 코드가 복잡해지고, 유지보수가 어려워지게 된다. 따라서 각 위젯의 특성과 생명주기를 잘 이해하고, 상황에 맞게 StatelessWidget과 StatefulWidget을 구분하여 사용하는 것이 Flutter 애플리케이션의 효율성을 높이는 데 매우 중요하다.

StatelessWidget 만드는 방법

StatelessWidget은 상태가 변하지 않는 위젯을 정의할 때 사용된다. 즉, 위젯이 생성된 이후에는 상태가 고정되어 변경되지 않는 경우에 사용한다는 뜻이다. 주로 UI 요소가 사용자 상호작용이나 내부 데이터 변경에 영향을 받지 않는 경우에 적합한데 예를 들어, 단순히 텍스트를 화면에 표시하거나 버튼을 보여주는 등 단순한 기능을 할 때 StatelessWidget을 사용하게 된다.

StatelessWidget은 불변성을 가지며, 위젯이 다시 그려질 필요가 있을 때마다 완전히 새로운 위젯이 생성된다. 이 때문에, 성능적으로 간단한 작업에 적합하고 코드가 상대적으로 간결하다.

1
2
3
4
5
6
7
8
import 'package:flutter/material.dart';

class SampleWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('뿡빵뽕!');
}
}

StatefulWidget 만드는 방법

StatefulWidget은 상태가 변할 수 있는 위젯을 정의할 때 사용된다. 이 위젯은 내부적으로 상태(State)를 유지하고, 상태가 변경될 때마다 UI를 갱신할 수 있는데 이것은 나중에 설명할 Life Cycle에서 확인할 수 있다. 암튼, 이런 SatefulWidget은 사용자가 버튼을 눌렀을 때 화면의 일부가 변경되거나, 사용자가 입력한 내용을 기억해야 할 때 StatefulWidget을 사용한다.

StatefulWidget은 두 가지 클래스로 구성한다. 위젯 자체를 나타내는 클래스와 그 위젯의 상태를 관리하는 클래스인데 State 클래스는 상태를 저장하고, 상태가 변경될 때 내부적으로 setState() 메서드를 호출하여 UI를 다시 빌드하게 한다. 이러한 구조는 React Life Cycle과 매우 유사하여 본인이 React를 했다면 이해하기 용이하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import 'package:flutter/material.dart';

class SampleWidget extends StatefulWidget {
const SampleWidget({super.key});

@override
State<SampleWidget> createState() => _SampleState();
}

class _SampleState extends State<SampleWidget> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}

StatefulWidget의 Life Cycle

StatelessWidget은 매우 간단한 구조인데 반해 StatefuleWidget은 좀 복잡한 구조를 갖고 있다. 아래의 이미지가 이해를 도울 것 같다.

StatefulWidget 라이플 사이클

위 이미지의 순서대로 컴포넌트가 생성되고 앱이 구동하는 Life Cycle을 확인할 수 있다. 이는 상태를 관리하고 상태가 변경 될 때마다 build 메서드가 실행되어 화면을 재구성한다. 각 메서드의 역할은 아래와 같다.

  • createState(): StatefulWidget의 상태를 정의하는 State 객체를 생성
  • initState(): State 객체가 처음 생성될 때 호출 및 초기화 작업을 할 때 사용된다. 예를 들어, 애니메이션 컨트롤러나 스트림 구독을 초기화할 때 사용된다.
  • build(): State가 변경될 때마다 호출되어 화면을 랜더링한다. 상태가 바뀌면 setState()를 호출하고, 그에 따라 build() 메서드가 다시 실행되어 화면이 업데이트된다.
  • didUpdateWidget(): 부모 위젯이 변경되었을 때 호출된다. 새로운 속성에 따라 위젯을 갱신해야 할 때 사용한다.
  • dispose(): 위젯이 더 이상 필요하지 않을 때 호출되며, 리소스를 해제하는 데 사용된다. 예를 들어, 화면이동이나 라우팅으로 인해 컴포넌트가 제거될 때 호출된다.

결론

Flutter의 위젯은 리소스의 사용, 성능, 코드의 효율을 잘 파악하여 개발하는 것이 중요하다. 그래서 각 위젯의 Life Cycle을 파악해야 하며 여기에 맞춰서 코딩해야한다.
덕분에 아 귀찮다. 공부할게 두배로 늘어난 기분이다. 슬프다.