Flutter에서는 Stateless 위젯과 Stateful 위젯이 있습니다. 각각에 대해 소개한 뒤, 어떤 상황에서 stateless위젯을 써야 하고 stateful 위젯을 써야 하는지 정리하고자 합니다.
Stateless 위젯과 Stateful 위젯
Stateless 위젯
Stateless 위젯은 변경 가능한 상태나 데이터가 없는 위젯입니다. Stateless 위젯의 형태은 자체 configuration과 상위 위젯(부모)로부터 받는 데이터에만 의존합니다. Stateless 위젯은 아이콘, 이미지, 텍스트 레이블 등 같은 간단한 UI 컴포넌트로 주로 사용됩니다.
Stateless 위젯이 생성되면 초기에 한 번 렌더링된 다음 변경되지 않은 상태로 유지됩니다. Stateless라는 이름에서도 알 수 있듯, Stateless 위젯에는 형태에 영향을 주는 자체 내부 상태가 없습니다. Stateless 위젯의 configuration이 변경되면 위젯의 인스턴스가 새로 생성되고, 새 configuration으로 렌더링 됩니다.
Stateless 위젯은 변경 가능한 상태가 없으므로, 앱의 여러 위치에서 재사용하는 것이 가능합니다. 위젯을 재사용하면 코드 중복을 줄이고 앱 성능을 개선하는 데 도움이 될 수 있습니다.
Stateful 위젯
Stateful 위젯에는 변경 가능한 상태와 시간이 지남에 따라 변경될 수 있는 데이터를 가질 수 있는 위젯입니다. 즉, Stateful 위젯의 형태는 위젯이 살아있는 동안 내부 상태의 변화에 따라 달라질 수 있습니다. Stateful 위젯은 양식, 애니메이션 및 대화형 컴포넌트와 같은, 보다 복잡한 UI를 구현할 때 활용됩니다.
Stateful 위젯이 생성되면 초기 구성 및 상태로 렌더링됩니다. Stateful 위젯의 상태는 이벤트, 사용자 입력 또는 기타 요인에 따라 시간이 지남에 따라 변경될 수 있습니다. Stateful 위젯의 상태가 변경되면 위젯이 새 상태로 다시 빌드되고 그에 따라 형태가 업데이트됩니다.
Stateful 위젯의 상태는 setState() 함수를 호출하여 수정할 수 있습니다. 이 함수는 위젯을 새 상태로 다시 빌드하도록 트리거합니다. 즉, 여러 번 렌더링 될 수 있습니다. 새 상태로 빌드된 위젯은 이벤트, 사용자 입력에 응답하여 업데이트 된 UI를 사용자에게 보여줄 수 있습니다.
Stateful 위젯은 여러 번 다시 빌드될 수 있으므로, 앱의 성능을 위해 Stateful 위젯이 수행해야 하는 작업량을 최소로 하는 것이 좋습니다.
선택하기: Stateless vs Stateful
Flutter 개발을 하다보면 이걸 stateless 위젯으로 만들어야 할지 stateful 위젯으로 만들어야 할지 고민하게 됩니다. 실제 개발할 때는 stateful 위젯으로 개발하는 빈도가 높습니다. 사용자 인터렉션에 따라 화면을 업데이트해야 하는 경우가 많기 때문입니다.
Stateless:
- 위젯이 위젯 수명 동안 상태를 변경하지 않을 때
- 정적이며 사용자 입력이나 데이터 변경에 따라 업데이트할 필요가 없을 때
- 텍스트, 아이콘, 이미지 등 단순 UI component
Stateful:
- 위젯이 위젯 수명 동안 상태를 변경해야 할 때
- 사용자 입력 또는 데이터 변경에 따라 업데이트해야 하는 UI component
- 양식, 버튼 및 목록 보기와 같은 복잡한 UI component
Stateless와 Stateful 위젯 비교 예제 코드
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stateful vs Stateless Widget Example',
home: Scaffold(
appBar: AppBar(
title: Text('Stateful vs Stateless Widget Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StatefulExample(),
SizedBox(height: 50),
StatelessExample(),
],
),
),
),
);
}
}
class StatefulExample extends StatefulWidget {
@override
_StatefulExampleState createState() => _StatefulExampleState();
}
class _StatefulExampleState extends State<StatefulExample> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Stateful Example',
),
SizedBox(height: 20),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
],
);
}
}
class StatelessExample extends StatelessWidget {
final int _counter = 0;
void _incrementCounter() {
// This method does nothing because we cannot change the value of _counter
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Stateless Example',
),
SizedBox(height: 20),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
],
);
}
}
StatefulExample 위젯과 StatelessExample 위젯을 만들었습니다. StatefulExample 위젯의 Increment Counter 버튼을 누르면 숫자가 증가하는 것을 확인할 수 있습니다. 반면 StatelessExample 위젯에서는 아무런 변화가 없습니다.
예제 결과
Stateful 위젯의 수명 주기(Lifecycle)
Flutter에서 Stateful 위젯의 수명 주기는 프레임워크에서 호출하는 자체 메서드가 있는 여러 단계로 나눌 수 있습니다. Stateful 위젯의 수명 주기 단계에 대하여 소개합니다.
Lifecycle에 따른 메서드
1. createState()
createState()는 Stateful 위젯이 생성될 때 호출되는 첫 번째 메서드입니다. 위젯의 변경 가능한 상태를 관리할 인스턴스를 생성합니다. 생성된 상태 인스턴스에는 buildContext가 할당되는데요, buildContext는 위젯 트리에서 해당 위젯이 배치되는 위치입니다. buildContext가 할당되면 해당 위젯이 갖고 있는 bool this.mounted 속성이 true로 변경됩니다. 만약 위젯이 마운트되지 않은 상태에서 setState() 메서드가 호출되면 오류가 발생하게 됩니다.
2. initState()
initState() 메서드는 상태 객체가 생성된 후 위젯이 위젯 트리에 마운트되기 전에 호출됩니다. 위젯이 생성되었을 때 가장 먼저, 딱 한 번, 생성자 다음으로 호출되는 메서드이며 위젯의 가변 상태를 초기화하는 데 사용됩니다.
- 특정 BuildContext에 의존하는 데이터를 초기화합니다.
- 위젯 트리에서 해당 위젯 부모에 의존하는 속성을 초기화합니다.
- 스트림, ChangeNotifier 또는 이 위젯의 데이터를 변경할 수 있는 다른 객체를 subscribe합니다.
주의할 점은 initState() 메서드의 호출이 끝나기 전에 build() 메서드가 호출 될 수 있습니다. 변수 초기화를 late 키워드를 써서 미뤄놓았고 initState() 내에서 초기화를 하려고 했는데, 초기화 전에 build() 메서드가 호출되면 LateInitializationError가 발생할 수 있습니다.
3. didChangeDependencies()
didChangeDependencies() 메서드는 위젯이 처음 빌드될 때 initState() 메서드 직후에 호출됩니다. 또는 위젯의 종속성이 변경될 때 호출됩니다. 이는 위젯이 위젯 트리에 삽입되거나 위젯 트리에서 제거될 때 또는 위젯의 부모가 다시 빌드될 때 발생합니다. 이 메서드는 위젯 종속성 상태의 변경 사항을 처리하는 데 사용됩니다.
4. build()
build() 메서드는 위젯을 빌드해야 할 때마다 호출됩니다. 위젯에 대한 위젯 트리를 빌드하는 데 사용됩니다. 필수 override 되어야 하는 메서드이며 위젯을 반환해야 합니다.
5. setState()
setState() 메서드는 위젯의 가변 상태를 업데이트하는 데 사용됩니다. 프레임워크에 데이터가 변경되었음을 알리고, 새로운 상태로 빌드 컨텍스트의 위젯을 다시 빌드해야 함을 알리는 데 사용됩니다. repainting을 위해 자주 사용됩니다.
6. didUpdateWidget()
didUpdateWidget()은 위젯의 구성 또는 속성이 변경될 때마다 호출되는 메서드입니다. 프레임워크가 이전 위젯을 동일한 runtimeType을 가진 새 위젯으로 교체한 후에 호출됩니다.
이 메서드는 구성 또는 속성의 변경에 대한 응답으로 위젯의 상태를 업데이트할 수 있는 기회를 제공합니다. 예를 들어 위젯의 속성이 변경되고, 이러한 변경 사항이 위젯의 상태에 영향을 미치는 경우 이 메서드에서 상태를 업데이트할 수 있습니다.
didUpdateWidget() 메서드는 이전 위젯을 매개변수로 사용하여 이전 위젯의 속성을 현재 위젯의 속성과 비교하고 그에 따라 변경할 수 있습니다.
7. deactivate()
이 메서드는 위젯 트리에서 위젯이 제거될 때 호출됩니다. initState()에서 할당된 리소스를 정리하는 데 사용됩니다.
8. dispose()
이 메서드는 위젯이 위젯 트리에서 영구적으로 제거될 때 호출됩니다. initState() 또는 다른 수명 주기 메서드에서 할당된 리소스를 해제하는 데 사용됩니다.
Lifecycle 예제 코드
Lifecycle에 따라 호출되는 메서드를 프린트해 보았습니다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: Scaffold(
appBar: AppBar(
title: Text('My Widget'),
),
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int _counter = 0;
@override
void initState() {
super.initState();
print('initState()');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies()');
}
@override
Widget build(BuildContext context) {
print('build()');
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Counter: $_counter',
style: const TextStyle(
fontSize: 24,
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(
fontSize: 24,
),
),
onPressed: () {
setState(() {
_counter++;
print("ElevatedButton is pressed: ${_counter}");
});
},
child: Text('Increment'),
),
],
);
}
@override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget()');
}
@override
void deactivate() {
super.deactivate();
print('deactivate()');
}
@override
void dispose() {
super.dispose();
print('dispose()');
}
}
예제 결과