State: 개념, 상태 관리 및 작동 원리

저자: IT Sectr 게시일: 2026-07-01 읽는 시간: 9 분

State는 Flutter의 중앙 데이터 관리 객체로, StatefulWidget과 연결되어 변경 가능한 정보를 저장하고 인터페이스를 구축하는 역할을 합니다. 공식 Flutter 문서(Flutter.dev, 2026)에 따르면, State는 위젯의 전체 수명 주기 동안 존재하며 위젯 재구축 이후에도 유지되어 UI 업데이트 간 데이터 일관성을 보장합니다. 위젯 자체와 달리 State는 필드를 수정하고 setState 호출을 통해 재구축을 트리거할 수 있습니다.

주요 내용

  • State — StatefulWidget의 변경 가능한 데이터를 저장하고 setState를 통해 재구축을 관리하는 객체
  • 수명 주기 — State는 initState, didChangeDependencies, build, didUpdateWidget 및 dispose를 거치며, 각 단계는 명확한 목적을 가집니다
  • mounted — State가 여전히 위젯 트리에 있으며 안전하게 setState를 호출할 수 있음을 나타내는 플래그
  • widget — 연결된 StatefulWidget에 대한 참조로, State 속성을 통해 부모 매개변수를 읽을 수 있습니다
  • 격리 — State는 다른 State로부터 격리됩니다. 데이터 교환을 위해 InheritedWidget 또는 외부 상태 관리 도구가 사용됩니다

Flutter에서 State란?

State는 Flutter 아키텍처의 객체로, StatefulWidget의 변경 가능한 데이터를 저장하고 이 데이터가 인터페이스에 표시되는 방식을 결정합니다. 각 StatefulWidget은 트리에 삽입될 때 createState 메서드를 통해 정확히 하나의 State 객체를 생성합니다. State는 위젯과 독립적으로 존재합니다. 부모가 새 매개변수로 StatefulWidget을 재구축해도 State는 동일하게 유지되며 widget 속성을 통해 업데이트된 위젯을 받습니다.

Flutter 아키텍처 개요(Google, 2026)에 따르면, Widget과 State의 분리는 프레임워크가 트리 요소를 재사용할 수 있도록 하는 의도적인 아키텍처 결정입니다. 위젯(가벼운 설명)은 여러 번 생성되고 파괴될 수 있지만, State(데이터를 가진 무거운 객체)는 요소가 트리에 있는 한 메모리에 남아 있습니다. 이는 부모 위젯의 빈번한 재구축 중 데이터 손실을 방지합니다.

State는 제네릭을 통해 StatefulWidget 인터페이스를 구현합니다: class _MyState extends State<MyWidget>. 제네릭은 State를 특정 StatefulWidget 유형에 바인딩하여 widget 속성을 통해 해당 필드에 대한 타입 안전 접근을 제공합니다.

State는 어디에 저장되나요?

State 객체는 StatefulElement에 저장됩니다 — Widget과 RenderObject 사이의 중간 계층입니다. StatefulElement는 createState를 통해 State를 생성하고, 이에 대한 참조를 유지하며 State를 소유자로 전달합니다. Element는 위젯이 트리에서 제거될 때만 파괴됩니다 — 그때까지 State는 메모리에 유지됩니다.

State의 수명 주기

State의 수명 주기는 결정론적이며 엄격한 호출 순서로 구성됩니다. 이 순서를 이해하는 것은 올바른 리소스 관리와 메모리 누수 방지의 기초입니다.

initState — 초기화

initState는 State가 생성될 때 가장 먼저 호출됩니다. 이 메서드에서 컨트롤러, 스트림 구독, 타이머 및 필드의 초기 값이 초기화됩니다. 첫 번째 줄에서 super.initState()를 호출하는 것은 필수입니다. initState 단계에서는 위젯 트리가 아직 완전히 마운트되지 않았으므로 MediaQuery.of(context)와 같은 메서드가 올바르게 작동하지 않을 수 있습니다.

didChangeDependencies

didChangeDependencies는 initState 이후와 InheritedWidget 종속성이 변경될 때마다 호출됩니다. MediaQuery.of(context) 또는 Theme.of(context)는 initState가 아닌 여기서 호출해야 합니다. 이 시점에는 트리가 이미 마운트되어 있기 때문입니다. 이 메서드는 위젯이 InheritedWidget이 다른 값을 제공하는 다른 컨텍스트로 이동할 때도 호출됩니다.

build — UI 구축

build는 State의 주요 메서드로, 위젯 트리를 반환합니다. initState 이후, didChangeDependencies 이후 및 각 setState 이후에 호출됩니다. build 메서드는 부작용이 없어야 합니다 — 현재 State 필드 값을 기반으로 인터페이스를 설명만 합니다.

didUpdateWidget

didUpdateWidget은 부모가 새 매개변수로 StatefulWidget을 재구축할 때 호출됩니다. State는 oldWidget을 통해 이전 위젯에 접근하여 새 위젯과 비교할 수 있습니다. 매개변수가 변경된 경우 상태를 업데이트하거나, 새 데이터를 로드하거나, 애니메이션을 다시 시작할 수 있습니다.

dispose — 리소스 해제

dispose는 모든 리소스(컨트롤러, 구독, 타이머)가 해제되는 최종 메서드입니다. dispose 후 State는 죽은 것으로 표시됩니다: mounted는 false를 반환하고, setState를 호출하면 예외가 발생합니다. 메서드의 마지막 줄에서 super.dispose()를 호출하는 것은 필수입니다.

메서드호출 시점필수 super
initStateState 생성 시예, 첫 번째 줄
didChangeDependenciesinitState 후 및 InheritedWidget 변경 시
buildinitState, didChangeDependencies, setState 후아니요
didUpdateWidget부모로부터 새 위젯 수신 시
setState개발자 호출 시아니요
dispose트리에서 제거 시예, 마지막 줄

State의 작동 방식

State의 작동 메커니즘은 Element와의 연결, setState를 통한 반응성, widget 속성을 통한 부모 접근의 세 가지 핵심 원칙을 기반으로 합니다. Flutter가 요소 트리를 구축하고 StatefulElement를 만나면 연결된 위젯의 createState를 호출합니다. 생성된 State는 요소에 저장되고 요소가 제거될 때까지 존재합니다.

setState가 호출되면 State는 자신을 더티(dirty)로 표시하고 다음 프레임의 재구축을 예약합니다. 중요한 점: setState는 build를 즉시 호출하지 않습니다 — 재구축 필요성을 등록만 합니다. Flutter는 현재 프레임의 모든 더티 요소를 수집하여 일괄 재구축하여 성능을 최적화합니다. build 호출 후 State는 클린(clean) 상태로 돌아갑니다.

widget 속성을 통해 State는 StatefulWidget 생성자에 전달된 매개변수를 읽을 수 있습니다. StatefulWidget은 불변(StatelessWidget과 마찬가지로)이므로 해당 필드는 변경되지 않습니다 — 매개변수가 변경되면 부모가 새 위젯을 생성하고 State는 didUpdateWidget을 통해 이를 수신합니다. 이는 State가 항상 최신 부모 데이터로 작업함을 보장합니다.

Dart 코드 예제

타이머에 의해 수정되는 필드가 있는 기본 State 예제. initState, setState 및 dispose를 보여줍니다:

dart
class _TimerWidgetState extends State<TimerWidget> {
  int _seconds = 0;
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(
      const Duration(seconds: 1),
      (_) => setState(() => _seconds++),
    );
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text('$_seconds seconds elapsed');
  }
}

widget 속성을 사용하여 부모 매개변수에 접근하고 didUpdateWidget을 통해 변경 사항에 반응하는 예제:

dart
class _GreetingState extends State<GreetingWidget> {
  String _displayName = '';

  @override
  void initState() {
    super.initState();
    _displayName = _formatName(widget.name);
  }

  @override
  void didUpdateWidget(GreetingWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.name != oldWidget.name) {
      setState(() {
        _displayName = _formatName(widget.name);
      });
    }
  }

  String _formatName(String name) => name.trim().isEmpty ? 'Guest' : name;

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $_displayName!');
  }
}

두 번째 예제에서 State는 입력 매개변수 name의 변경을 추적하고 실제 변경이 있을 때만 표시를 다시 포맷합니다. widget.name != oldWidget.name 확인이 없으면 이름이 변경되지 않았더라도 부모가 재구축될 때마다 메서드가 호출되어 프레임워크에 불필요한 작업이 발생합니다.

State vs StatefulWidget

State와 StatefulWidget은 Flutter 아키텍처에서 서로 다른 역할을 수행하는 두 개의 별도 클래스입니다. StatefulWidget은 위젯 구성을 설명하고 State를 생성하는 가벼운 불변 래퍼입니다. State는 변경 가능한 데이터를 저장하고, 구독을 관리하며 UI를 구축하는 무거운 객체입니다. 이 분리를 통해 Flutter는 상태를 잃지 않고 위젯을 파괴하고 생성할 수 있습니다.

StatefulWidget의 모든 필드는 final이어야 하며 생성자에서 설정되어야 합니다 — 생성 후 변경되지 않습니다. 반면 State는 언제든지 필드를 수정할 수 있지만, 모든 변경 전에 setState 호출이 선행되어 Flutter가 재구축 필요성을 알 수 있도록 해야 합니다. 이것이 주요 차이점입니다: StatefulWidget은 “무엇을 보여줄지”이고, State는 “어떻게 보여줄지와 어떤 데이터를 사용할지”입니다.

Flutter 소스 코드 분석(Flutter SDK, 2026)에 따르면, StatefulWidget에는 하나의 필수 필드만 있습니다 — createState, 반면 State는 BuildContext에 접근할 수 있고, 스트림을 구독하고, 애니메이션과 컨트롤러를 관리할 수 있습니다. StatefulWidget을 가능한 한 간단하게 유지하고 모든 로직을 State로 이동하는 것이 권장됩니다.

왜 StatefulWidget이 State가 될 수 없나요?

Widget과 State의 분리는 구성의 불변성을 보장하는 아키텍처 결정입니다. StatefulWidget 자체가 상태를 저장했다면 부모가 재구축될 때마다 상태가 손실됩니다. 상태를 별도의 객체로 이동함으로써 Flutter는 데이터가 재구축 후에도 유지되도록 보장하며, 위젯은 가볍고 비교 가능한 상태로 유지됩니다.

위젯 간 상태 관리

State 객체는 격리되어 있습니다 — 다른 위젯의 State에 직접 접근할 수 없습니다. 위젯 간 데이터 교환을 위해 InheritedWidget 또는 외부 상태 관리 도구(Provider, Riverpod, Bloc, Redux)가 사용됩니다. 각 접근 방식은 문제를 다르게 해결합니다: InheritedWidget은 위젯 트리를 통해 작동하고, Provider는 DI 컨테이너를 통해, Bloc은 이벤트 스트림을 통해 작동합니다.

도구 선택은 프로젝트 규모에 따라 달라집니다. 소규모 애플리케이션에는 InheritedWidget과 로컬 State로 충분합니다. 중대형 프로젝트에는 Riverpod 또는 Bloc이 권장됩니다 — 이들은 테스트 가능성, 예측 가능성 및 UI로부터 로직 분리를 보장합니다. State는 로컬 위젯 데이터(포커스, 스크롤, 애니메이션)에만 사용됩니다.

Flutter 커뮤니티 설문조사 2025(Flutter Foundation, 2025년 12월)에 따르면, Riverpod은 새 프로젝트에서 가장 인기 있는 상태 관리 솔루션(38%)이며, Bloc(31%)과 Provider(22%)가 그 뒤를 잇습니다. 세 도구 모두 State와 호환되며 표준 수명 주기를 포기할 필요가 없습니다.

로컬 상태 vs 전역 상태

  • 로컬 상태 — 특정 위젯의 State 내(스크롤 위치, 포커스 상태)
  • 전역 상태 — 외부 저장소(사용자 데이터, 설정, 캐시)
  • 규칙: 데이터가 하나의 위젯에서만 사용되는 경우 — State에 저장
  • 데이터가 2개 이상의 위젯에서 사용되는 경우 — Riverpod/Bloc/Provider로 이동

일반적인 실수

첫 번째 실수는 비동기 콜백에서 setState 전에 mounted를 확인하는 것을 잊는 것입니다. 위젯이 트리에서 제거되고(예: 사용자가 화면을 떠남) 비동기 작업(HTTP 요청)이 아직 실행 중인 경우, 완료 후 State는 이미 죽어 있습니다. 죽은 State에서 setState를 호출하면 예외가 발생합니다. if (mounted) setState(...) 확인으로 문제를 해결합니다.

두 번째 실수는 didChangeDependencies 대신 initState에서 InheritedWidget 종속성을 초기화하는 것입니다. initState에서는 컨텍스트가 아직 마운트되지 않았으므로 MediaQuery.of(context)가 예외를 발생시킵니다. 모든 InheritedWidget 종속성은 didChangeDependencies 또는 build에서 설정해야 합니다.

세 번째 실수는 setState를 호출하지 않고 필드를 변경하는 것입니다. 개발자가 setState 없이 State 필드를 변경하면 Flutter가 변경을 인식하지 못하고 UI가 업데이트되지 않습니다. 예: 후속 setState((){}) 없이 _list.add(item)을 실행하면 목록은 변경되지만 화면은 그대로 유지됩니다.

setState 전에 mounted 확인

State에서 비동기 작업을 위한 안전 패턴:

dart
Future<void> _fetchData() async {
  final data = await ApiService.fetch();
  if (mounted) {
    setState(() => _data = data);
  }
}

mounted 확인은 setState가 살아있는 State에서만 호출되도록 보장하여 “setState called after dispose” 예외를 방지합니다.

자주 묻는 질문

State와 StatefulWidget의 차이점은 무엇인가요?

StatefulWidget은 불변의 위젯 구성이며, State는 데이터를 저장하고 수명 주기를 관리하는 변경 가능한 객체입니다. 위젯은 다시 생성될 수 있지만 State는 그렇지 않습니다. StatefulWidget은 createState를 통해 State를 생성합니다.

하나의 StatefulWidget에 대해 몇 개의 State 객체가 생성되나요?

정확히 하나입니다. createState 메서드는 StatefulWidget이 처음 트리에 삽입될 때 한 번 호출됩니다. 부모가 여러 번 재구축해도 위젯 유형이나 Key가 변경되지 않는 한 State 객체는 동일하게 유지됩니다.

State에서 mounted란 무엇인가요?

mounted는 State가 위젯 트리에 있는지 여부를 나타내는 부울 플래그입니다. dispose 호출 후 mounted는 false가 됩니다. 예외를 피하기 위해 비동기 콜백에서 setState 전 확인에 사용됩니다.

State를 StatefulWidget 없이 사용할 수 있나요?

아니요. State는 항상 제네릭을 통해 특정 StatefulWidget에 바인딩됩니다: State<T extends StatefulWidget>. 위젯과의 연결 없이 State를 직접 생성하는 것은 아키텍처상 불가능합니다.

dispose에서 setState를 호출하면 어떻게 되나요?

예외가 발생합니다: “setState called after dispose”. dispose 후 State는 죽은 것으로 간주되며, setState를 통해 UI를 재구축하려는 모든 시도는 금지됩니다. 해결책은 각 setState 전에 mounted를 확인하는 것입니다.

요약

  • State — StatefulWidget 데이터 관리 객체로, 변경 가능한 필드를 저장하고 setState를 통해 UI 재구축을 트리거합니다
  • 수명 주기에는 필수 메서드 initState, didChangeDependencies, build, didUpdateWidget 및 dispose가 포함되며, 각각 고유한 목적이 있습니다
  • mounted — 위젯이 트리에서 제거된 후 setState 호출을 방지하는 중요한 안전 플래그
  • widget — 연결된 StatefulWidget 매개변수에 접근하기 위한 State 속성, didUpdateWidget을 통해 업데이트
  • 격리 — State는 다른 State에 접근할 수 없습니다. 위젯 간 통신은 InheritedWidget 또는 외부 도구를 통해 구현됩니다
  • setState — build를 즉시 호출하지 않고, 다음 프레임에서 재구축하기 위해 State를 더티로 표시만 합니다
  • 규칙 — 로컬 위젯 데이터에는 State를 사용하고, 전역 상태는 외부 계층(Riverpod, Bloc)으로 이동

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기