StatefulWidget은 변경 가능한 상태를 가진 Flutter 위젯으로, UI가 사용자 작업, 비동기 이벤트 및 데이터 스트림에 반응할 수 있도록 합니다. 공식 Flutter 문서(Flutter.dev, 2026)에 따르면 StatefulWidget은 애플리케이션의 모든 대화형 요소(입력 양식, 애니메이션, 체크박스, 스위치, 네트워크에서 데이터를 로드하는 화면)에 사용됩니다. StatelessWidget과 달리 전체 생명주기 동안 지속되는 별도의 State 객체를 생성하며, 위젯 자체를 다시 만들지 않고도 재구성할 수 있습니다.
핵심 요점
StatefulWidget은 사용자 작업, 시스템 이벤트 또는 비동기 작업에 응답하여 상태를 변경할 수 있는 Flutter 클래스입니다. StatelessWidget과 달리 StatefulWidget은 직접 렌더링되지 않고 렌더링을 담당하는 State 객체를 생성합니다. Widget과 State의 두 클래스로 분리하면 Flutter가 위젯 자체를 다시 만들지 않고 UI를 재구성할 수 있어 빈번한 업데이트 시 상당한 성능 이점을 제공합니다.
StatefulWidget의 아키텍처는 “변경 가능과 변경 불가능의 분리” 패턴을 따릅니다. 위젯 자체는 (StatelessWidget처럼) 변경 불가능하게 유지되는 반면, 모든 변경 가능한 상태는 별도의 State 객체에 저장됩니다. 이를 통해 Flutter는 유형과 Key로 비교하여 위젯을 재사용할 수 있으며, 재구성 간에 실제 상태를 보존할 수 있습니다.
Google(Flutter Architectural Overview, 2026)에 따르면 StatefulWidget은 위젯의 수명 동안 상태가 두 번 이상 변경되는 시나리오(텍스트 필드, 애니메이션, 타이머, 데이터 스트림, 비동기 로드)에 최적입니다. 일회성 초기화에는 StatelessWidget으로 충분합니다.
StatefulWidget은 위젯이 외부 이벤트(버튼 클릭, HTTP 요청 완료, 데이터베이스 업데이트, WebSocket 구독)에 응답해야 할 때 필수적입니다. 또한 애니메이션, 컨트롤러가 있는 텍스트 필드 및 포커스를 관리하는 컴포넌트에도 필요합니다. 위젯이 단순히 데이터를 표시하고 이벤트를 생성하지 않는 경우 StatelessWidget을 사용하세요.
StatefulWidget은 StatefulWidget 자체(가벼움, 변경 불가능)와 State(무거움, 변경 가능)의 두 클래스로 구성됩니다. 프레임워크는 트리에 삽입될 때 한 번 호출되는 createState() 메서드를 통해 State를 생성합니다. State는 widget 속성을 통해 위젯에 대한 참조를 얻고 생명주기의任何시점에서 해당 필드에 액세스할 수 있습니다.
생명주기는 6가지 주요 단계로 구성되며, 각 단계는 특정 작업을 수행하기 위한 재정의 가능한 메서드를 제공합니다. 이러한 단계를 이해하는 것은 적절한 리소스 관리와 메모리 누수 방지에 중요합니다.
createState는 StatefulWidget이 트리에 삽입될 때 호출되는 첫 번째 생명주기 메서드입니다. 이 위젯과 연결된 새 State 인스턴스를 반환해야 합니다. 이 메서드는 요소의 전체 수명 동안 정확히 한 번 호출됩니다. 여기서 무거운 작업을 수행하지 않는 것이 중요합니다 — createState는 가능한 한 가벼워야 합니다.
initState는 State 생성 직후, 첫 번째 UI 구축 전에 호출됩니다. 여기서 컨트롤러(TextEditingController, AnimationController) 초기화, 데이터 스트림(StreamSubscription) 구독, 타이머 설정, 필드 초기화를 수행합니다. Flutter 문서(Flutter.dev, 2026)에 따르면 initState에서는 BuildContext.of()를 호출할 수 없습니다 — 트리가 아직 완전히 마운트되지 않았습니다.
didChangeDependencies는 initState 이후와 InheritedWidget 종속성이 변경될 때마다 호출됩니다. MediaQuery.of(context)를 호출하거나 Theme을 구독하기에 적합한 위치입니다 — 애플리케이션 실행 중에 변경될 수 있는 값입니다. 위젯이 InheritedWidget을 사용하는 경우 초기화 로직은 initState가 아닌 여기에 있어야 합니다.
build는 위젯 트리를 반환하는 주요 메서드입니다. initState 후, didChangeDependencies 후 및 각 setState 후에 호출됩니다. didUpdateWidget은 부모가 재구성되고 새 매개변수로 StatefulWidget을 전달할 때 호출됩니다. 여기서 이전 및 새 위젯 필드를 비교하고 필요에 따라 상태를 업데이트할 수 있습니다.
dispose는 생명주기의 마지막 단계입니다. 여기서 모든 리소스가 해제됩니다: 스트림 구독 취소, 컨트롤러 제거, 타이머 취소. dispose를 호출하지 않으면 메모리 누수가 발생합니다. dispose 후 State는 죽은 것으로 간주되며, 내부에서 setState를 호출하면 예외가 발생합니다.
StatefulWidget의 작동 메커니즘은 Widget(가벼운 설명), Element(중간 계층), State(데이터 저장소)의 세 가지 엔터티의 조정된 작업을 기반으로 합니다. Flutter가 설명에서 StatefulWidget을 발견하면 StatefulElement를 생성하고, createState를 호출하여 State 객체에 대한 참조를 저장합니다. 부모가 재구성되면 Flutter는 새 위젯을 현재 Element와 비교합니다 — 유형과 Key가 일치하면 Element가 업데이트되고 State는 동일하게 유지됩니다.
상태는 setState 호출을 통해서만 변경되며, 프레임워크에 재구성 필요성을 알립니다. 중요한 점은 setState가 자동으로 상태를 변경하지 않는다는 것입니다 — 위젯을 “더티”로 표시만 합니다. 개발자는 setState에 전달된 콜백에서 State 필드를 독립적으로 업데이트합니다. 콜백 완료 후 Flutter는 build를 호출하고 UI를 업데이트합니다.
Dart/Flutter 팀(Dart Language Specification, 2026)에 따르면, 이 분리는 build가 호출되기 전에 모든 상태 변경이 동기적으로 발생하도록 보장하여 UI가 부분적으로 업데이트된 데이터를 표시하는 상황을 제거합니다. 이것은 Flutter에서 인터페이스 일관성의 핵심 메커니즘입니다.
간단한 StatefulWidget — 버튼 클릭 카운터를 살펴보겠습니다. 기본 패턴(State 생성, initState에서 필드 초기화, setState를 통한 변경)을 보여줍니다.
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}
비동기 데이터 로드 및 생명주기 관리 예제입니다. StatefulWidget이 네트워크에서 데이터를 로드하고 로딩 상태를 표시합니다.
class UserProfilePage extends StatefulWidget {
final String userId;
const UserProfilePage({super.key, required this.userId});
@override
State<UserProfilePage> createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
UserModel? _user;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadUser();
}
Future<void> _loadUser() async {
final user = await UserService.fetchUser(widget.userId);
setState(() {
_user = user;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) return const CircularProgressIndicator();
return Text('Hello, ${_user!.name}');
}
}
두 번째 예제에서 중요한 점: initState는 비동기 작업을 시작하지만 메서드 자체는 비동기가 아닙니다. 비동기성은 별도의 메서드 _loadUser 내에서 async/await를 통해 구현되며, 요청 완료 후 setState를 통해 상태를 업데이트합니다. 이 접근 방식은 데이터를 수신하기 전에 위젯이 로딩 표시기를 올바르게 표시하도록 보장합니다.
StatefulWidget과 StatelessWidget의 선택은 단순히 상태 유무의 문제가 아닙니다. StatefulWidget은 컨트롤러, 애니메이션 및 스트림 작업에 필요한 initState, didChangeDependencies, didUpdateWidget 및 dispose 메서드가 있는 완전한 생명주기를 제공합니다. 반면 StatelessWidget에는 이러한 메서드가 없으며 프레임워크에 항상 가볍습니다.
Flutter 팀의 권장 사항(Flutter docs, 2026)은 상태를 트리 위로 올리거나(State Hoisting) 상태 관리 솔루션(Riverpod, Bloc, Provider)을 사용하여 애플리케이션에서 StatefulWidget의 수를 최소화하는 것입니다. 각 StatefulWidget은 요소가 제거될 때까지 유지되는 State 객체를 생성합니다 — 이러한 위젯이 많을수록 메모리 부하가 높아집니다.
| 기준 | StatefulWidget | StatelessWidget |
|---|---|---|
| 상태 | 변경 가능 | 변경 불가능 |
| 생명주기 | 6단계 | build만 |
| State 객체 | 별도 생성 | 필요 없음 |
| setState | 사용 가능 | 사용 불가능 |
| 구독 | initState/dispose | 지원 안 함 |
| const 생성자 | 제한적 | 완전 지원 |
| 메모리 소비 | 높음 | 낮음 |
StatefulWidget은 State 객체를 생성하고 유지해야 하므로 StatelessWidget보다 더 많은 리소스가 필요합니다. 그러나 몇 가지 규칙을 따르면 StatefulWidget의 올바른 사용은 성능 문제를 일으키지 않습니다. 첫째, StatefulWidget의 깊은 중첩을 피하세요 — 각 수준이 트리 탐색에 오버헤드를 추가합니다. 둘째, 복잡한 StatefulWidget을 여러 개의 간단한 위젯으로 분할하고 각각이 상태의 자체 부분을 담당하게 하세요.
Flutter 성능 조사(Flutter.dev, 2026년 2월)에 따르면 FPS 저하의 가장 일반적인 원인은 표시가 변경되지 않은 StatelessWidget을 포함한 모든 하위 항목을 재구성하는 부모 위젯에서 setState를 호출하는 것입니다. 해결책은 UI의 변경 가능한 부분을 별도의 StatefulWidget으로 추출하여 setState가 최소한의 필요한 위젯만 재구성하도록 하는 것입니다.
State 내에서 const를 사용하는 것도 중요한 기술입니다. 자식 위젯이 const로 선언된 경우 부모에서 setState가 호출되어도 Flutter가 이를 재구성하지 않습니다. 이는 프레임워크의 부하를 줄이고 프레임 렌더링 시간을 단축합니다.
각 setState 호출은 완전한 위젯 재구성을 트리거합니다. 상태가 높은 빈도로 변경되는 경우(예: 애니메이션 또는 데이터 스트림) 수동으로 setState를 호출하는 대신 AnimatedBuilder, ValueListenableBuilder 또는 StreamBuilder 사용을 고려하세요. 이러한 위젯은 재구성을 최적화하여 실제로 변경된 UI 부분만 업데이트합니다.
StatefulWidget 관련 첫 번째 일반적인 실수는 dispose 후 setState를 호출하는 것입니다. 위젯이 트리에서 제거되면 State는 죽은 것으로 간주되며 setState를 호출하면 “setState called after dispose” 예외가 발생합니다. 이는 위젯이 제거된 후 비동기 작업이 완료될 때 가장 자주 발생합니다. 해결책은 setState를 호출하기 전에 mounted 플래그를 확인하거나 dispose에서 비동기 작업을 취소하는 것입니다.
두 번째 실수는 build 메서드에서 무거운 계산을 수행하는 것입니다. build는 모든 setState 및 모든 부모 재구성 시 호출되므로 모든 계산은 가능한 한 가벼워야 합니다. 리소스 집약적 작업이 필요한 경우 별도의 Isolate로 이동하거나 결과를 State 필드에 캐시하세요.
세 번째 실수는 super.initState()와 super.dispose()를 호출하지 않는 것입니다. 이러한 메서드를 재정의할 때 개발자는 부모 구현을 호출해야 합니다. 그렇지 않으면 프레임워크가 Element 상태를 올바르게 관리할 수 없어 추적하기 어려운 버그가 발생합니다.
mounted 확인super.initState() 및 super.dispose() 호출 잊지 말기자주 묻는 질문
StatefulWidget은 setState를 통해 상태를 변경할 수 있고, 생명주기(initState, dispose)가 있으며 별도의 State 객체를 생성합니다. StatelessWidget은 상태를 변경할 수 없고 생명주기 메서드가 없으며 단순히 전달된 데이터를 표시합니다.
createState는 각 StatefulElement 인스턴스에 대해 정확히 한 번 호출됩니다. 부모가 여러 번 재구성되어도 위젯의 유형과 Key가 변경되지 않는 한 createState는 호출되지 않으며 기존 State 객체가 사용됩니다.
리소스가 해제되지 않습니다: 컨트롤러가 백그라운드에서 계속 작동하고, 스트림 구독이 활성 상태로 유지되며, 타이머가 취소되지 않습니다. 이로 인해 메모리 누수가 발생하고 dispose 후 setState 호출이 예외를 발생시킬 수 있습니다.
네, StatefulWidget의 생성자는 const일 수 있습니다. 그러나 StatelessWidget만큼의 이점은 없습니다 — State 객체는 첫 번째 삽입 시 여전히 생성됩니다. const는 위젯 자체(가벼운 래퍼)에만 영향을 미치고 State에는 영향을 미치지 않습니다.
didUpdateWidget은 부모가 새 매개변수로 StatefulWidget을 전달할 때 호출됩니다. 상태를 새 데이터와 동기화하는 데 필요합니다 — 예를 들어 매개변수의 userId가 변경된 경우 새 사용자의 프로필을 로드해야 합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.