setState() — 본질, 작동 메커니즘 및 응용

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

setState() 는 Flutter에서 State의 핵심 메서드로, 프레임워크에 데이터 변경을 알리고 인터페이스 재구성을 트리거합니다. 공식 Flutter 문서(Flutter.dev, 2026)에 따르면, setState는 StatefulWidget의 주요 반응성 메커니즘입니다. 이를 호출하지 않으면 UI는 State 필드의 변경을 알지 못하며 이전 상태로 유지됩니다. 이 메서드는 VoidCallback을 허용하며, 개발자는 그 내부에서 변경 가능한 필드를 수정한 후 Flutter가 자동으로 build를 호출하여 위젯을 재구성합니다.

핵심 요점

  • setState() — 위젯을 dirty로 표시하고 다음 프레임에서 UI 재구성을 예약하는 State 메서드
  • 콜백 — setState는 VoidCallback을 허용하며, 그 내부에서 UI에 영향을 미치는 모든 State 필드 변경이 이루어져야 합니다
  • 비동기성 — setState 내부의 setTimeout 또는 Future는 동기성을 보장하지 않습니다. await 이후의 변경은 다른 setState 내부에서 이루어져야 합니다
  • 성능 — setState 호출 시마다 전체 위젯이 재구성됩니다. 최소화하려면 const 자식 위젯을 사용하세요
  • mounted — 비동기 콜백에서 setState 호출 전 항상 mounted를 확인하세요. 그렇지 않으면 예외가 발생합니다

setState()란?

setState() 는 Flutter에서 State 클래스의 내장 메서드로, 위젯의 내부 상태가 변경되어 UI를 재구성해야 함을 프레임워크에 알리기 위해 설계되었습니다. setState를 호출하지 않으면 Flutter는 변경 사항을 알지 못합니다. State 필드가 수정되어도 부모에 의한 다음 강제 재구성까지 인터페이스는 변경되지 않습니다.

메서드 시그니처: void setState(VoidCallback fn). 콜백은 setState 내부에서 동기적으로 실행되며, 완료된 후에만 State가 dirty로 표시됩니다. 이는 재구성 전에 모든 변경이 원자적으로 적용됨을 보장합니다. Dart 언어 사양(Dart Team, 2026)에 따르면, setState의 원자성은 build가 부분적으로 업데이트된 상태를 볼 수 있는 경합 조건을 방지합니다.

setState는 인수를 받지 않고, 값을 반환하지 않으며, 재정의할 수 없습니다. State 클래스의 final(봉인) 메서드입니다. 개발자는 그 동작을 변경할 수 없으며 의도된 대로만 사용할 수 있습니다. State 외부(예: 다른 클래스)에서 setState를 호출하려는 시도는 메서드가 State 클래스에서 선언되었기 때문에 불가능합니다.

setState는 상태를 변경하지 않습니다 — 여러분이 변경합니다

setState 자체가 상태를 변경한다고 생각하는 것은 일반적인 오해입니다. 이는 사실이 아닙니다. setState는 전달된 콜백만 호출하고(개발자가 그 안에서 필드를 수정함) 그런 다음 프레임워크에 build의 필요성을 알립니다. 콜백은 필수입니다. null 또는 빈 콜백을 전달하면 오류가 발생합니다.

setState()는 어떻게 작동하나요?

setState() 의 작동 메커니즘은 네 단계로 나눌 수 있습니다. 첫째 — 콜백과 함께 메서드 호출. 둘째 — 콜백의 동기적 실행, 그 내부에서 State 필드가 수정됩니다. 셋째 — 특수 필드 _dirty에서 State가 dirty로 표시됩니다. 넷째 — 현재 마이크로태스크가 끝나면 Flutter는 모든 dirty 요소를 반복하고 트리에서 나타나는 순서대로 build를 호출합니다.

중요한 세부 사항: setState는 즉시 build를 호출하지 않습니다. Flutter는 배치 업데이트 전략을 사용합니다. 모든 dirty 요소가 수집되어 단일 프레임에서 재구성됩니다. 즉, 하나의 동기 블록 내에서 setState가 여러 번 호출되면 모든 변경이 완료된 후에 build가 한 번만 실행됩니다. 이 최적화는 프레임당 여러 번의 재구성을 방지합니다.

Flutter Engine Team(Google, 2025)에 따르면, dirty 플래그 메커니즘은 BuildOwner._dirtyElements 패스를 기반으로 합니다. 각 dirty StatefulElement는 목록에 추가되고 프레임 업데이트 단계에서 처리됩니다. 처리 전에 위젯이 트리에서 제거된 경우 dirty 요소 목록에서 자동으로 제외됩니다.

setState 보장 사항

  • 콜백은 dirty 표시 전에 동기적으로 실행됩니다
  • build는 프레임당 최대 한 번 호출됩니다(여러 setState 호출에도)
  • UI 업데이트는 다음 프레임에서 발생합니다(일반적으로 60 FPS에서 ~16ms)
  • dispose 후 setState 호출은 금지됩니다 — 예외가 발생합니다
  • build 중 setState 호출은 금지됩니다 — 무한 루프

Dart 코드 예제

카운터 증가를 사용한 setState() 기본 예제. 올바른 사용법을 보여줍니다: 콜백 내에서 필드 수정:

dart
class _CounterState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++; // 콜백 내에서 필드 변경
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _increment,
      child: Text('$_count'),
    );
  }
}

텍스트 필드와 컨트롤러를 사용한 예제 — 비밀번호 표시 관리를 위한 setState():

dart
class _PasswordFieldState extends State<PasswordField> {
  bool _obscured = true;
  final _controller = TextEditingController();

  void _toggleVisibility() {
    setState(() {
      _obscured = !_obscured;
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      obscureText: _obscured,
      decoration: InputDecoration(
        suffixIcon: IconButton(
          icon: Icon(_obscured ? Icons.visibility : Icons.visibility_off),
          onPressed: _toggleVisibility,
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

이 예제에서 setState() 는 부울 필드 _obscured만 변경하여 새 아이콘과 표시 모드로 TextField 재구성을 트리거합니다. 텍스트 컨트롤러는 다시 생성되지 않으며 initState에서 한 번 초기화되고 dispose에서 해제됩니다.

하나의 setState에서 여러 변경

여러 필드를 변경해야 하는 경우 모든 변경은 하나의 setState 내에서 이루어져야 합니다. 이렇게 하면 build가 일관된 상태를 볼 수 있습니다:

dart
setState(() {
  _isLoading = false;
  _items = newItems;
  _error = null;
});

하나의 콜백에서 세 개의 필드가 변경됩니다. build는 한 번 실행되고 모든 변경을 동시에 볼 수 있습니다. 각 호출이 별도의 setState여도 dirty 요소의 배치 처리 덕분에 build는 한 번만 실행됩니다.

비동기성과 setState

setState() 의 가장 중요한 미묘한 점 중 하나는 비동기 작업에서의 동작입니다. setState 콜백은 동기적으로 실행되지만, 그 내부에서 await가 호출되면 await 이후의 코드는 setState가 이미 완료된 후에 실행됩니다. 즉, await 이후의 필드 변경은 현재 setState에 의해 캡처되지 않습니다.

올바른 접근 방식: 비동기 작업은 setState 외부에서 수행되고 setState는 완료 후에 호출됩니다. 결과 수신과 setState 호출 사이의 모든 코드는 await 후 동기 컨텍스트에서 실행됩니다:

dart
// 올바름: setState 외부에서 await
Future<void> _loadData() async {
  final result = await ApiService.fetchData();
  setState(() {
    _data = result;
    _isLoading = false;
  });
}

// 틀림: setState 내부에서 await — 업데이트 보장 없음
void _loadDataWrong() {
  setState(() async {
    _data = await ApiService.fetchData(); // setState가 await 완료 전에 반환됨
    _isLoading = false; // 이 코드는 setState에 의해 캡처되지 않음
  });
}

Flutter 문서(Dart async patterns, 2026)에 따르면 setState에 비동기 콜백을 전달하는 것은 안티패턴입니다. setState는 VoidCallback(동기 함수)을 기대하는 반면, 비동기 함수는 무시되는 Future를 반환하기 때문입니다. 이러한 콜백에서 첫 번째 await 이후의 변경은 프레임워크에 의해 올바르게 처리되지 않습니다.

비동기 시나리오에서 mounted 확인

비동기 작업 후 setState() 를 호출하기 전에 항상 mounted를 확인하세요:

dart
if (mounted) {
  setState(() => _data = data);
}

비동기 작업 중에 위젯이 트리에서 제거된 경우 mounted는 false가 되고 setState가 호출되지 않습니다. 이는 예외와 리소스 누수를 방지합니다.

성능 및 최적화

setState() 는 편리하지만 생각 없이 사용하면 잠재적으로 비용이 많이 드는 메커니즘입니다. setState를 호출할 때마다 전체 위젯과 모든 하위 항목(const가 아닌 경우)이 재구성됩니다. 깊은 트리나 빈번한 호출에서는 FPS 저하로 이어질 수 있습니다.

주요 최적화 전략: 재구성 영역 최소화(변경 가능한 UI 부분을 별도의 StatefulWidget으로 추출), 변경 불가능한 자식에는 const 사용, 작은 UX 세부 사항만 변경된 경우 부모 위젯에서 setState 호출 방지. 상태가 높은 빈도로 업데이트되는 경우(애니메이션, 데이터 스트림) AnimatedBuilder 또는 ValueListenableBuilder를 고려하세요.

Flutter 성능 모범 사례(Flutter.dev, 2026년 2월)에 따르면 실제 애플리케이션 프로파일링 결과 모든 setState 호출의 최대 40%를 const 자식 위젯 또는 반응형 빌더(StreamBuilder, FutureBuilder)로 대체할 수 있습니다. 이는 평균 프레임 빌드 시간을 15-25% 줄입니다.

setState가 불필요한 경우

시나리오대안장점
애니메이션AnimatedBuilder애니메이션 위젯만 재구성
데이터 스트림StreamBuilder각 스트림 요소에 반응
미래 결과FutureBuilder로딩/오류 상태 관리
로컬 값ValueListenableBuilder단일 값 변경에 반응

setState의 대안

setState() 의 다용성에도 불구하고 대규모 프로젝트에서는 주로 로컬 상태에 사용됩니다. 전역 또는 공유 상태에는 각각 setState를 대체하거나 래핑하는 특화된 솔루션이 사용됩니다.

Provider는 setState의 유사체로 ChangeNotifier + notifyListeners를 사용하지만 여러 위젯을 구독할 수 있는 기능이 있습니다. Bloc은 Streams를 사용합니다 — StreamController에 이벤트를 추가하여 상태가 변경됩니다. Riverpod는 접근 방식을 결합하여 StatefulWidget에 바인딩 없이 로컬(StateProvider) 및 비동기(AsyncNotifier) 관리를 모두 제공합니다. 세 가지 접근 방식 모두 수동으로 setState를 호출할 필요가 없습니다 — 데이터가 변경되면 UI 업데이트가 자동으로 발생합니다.

Flutter 커뮤니티 설문조사 2025(Flutter Foundation, 2025년 12월)에 따르면, 74%의 개발자가 setState 외에 최소한 하나의 상태 관리 도구를 사용합니다. 동시에 92%는 텍스트 필드, 체크박스 또는 단순 카운터의 로컬 데이터에 setState를 계속 사용합니다 — 이는 모범 사례로 간주됩니다.

setState를 유지해야 하는 경우

  • 상태가 하나의 위젯에서만 사용되는 경우
  • 단순한 부울 또는 숫자 값(포커스, 가시성, 카운터)
  • 프로토타이핑 및 빠른 실험
  • 컨트롤러(TextEditingController, PageController)는 여전히 StatefulWidget이 필요

일반적인 실수

첫 번째이자 가장 위험한 실수는 dispose 후 setState를 호출하는 것입니다. initState에서 시작된 비동기 작업, 사용자가 화면을 떠나고 위젯이 제거되었으며 비동기 콜백이 setState를 호출하면 앱이 예외와 함께 충돌합니다. 해결책 — 호출 전에 항상 mounted를 확인하세요.

두 번째 실수는 build 내에서 setState를 호출하는 것입니다. 이는 무한 루프로 이어집니다: build → setState → dirty → build → setState → ... Flutter는 이러한 호출을 차단하지 않으며(StackOverflowError 발생) setState는 이벤트(버튼 누름, Future 완료, 스트림 데이터)에 응답해서만 호출할 수 있습니다.

세 번째 실수는 setState를 호출하지 않고 State 필드를 수정하는 것입니다. 개발자가 _count++를 작성하고 UI가 업데이트되기를 기대합니다. Flutter는 필드 변경을 자동으로 추적할 수 없으며 setState를 통한 명시적 신호가 필요합니다. 이는 데이터 변경이 자동으로 업데이트를 트리거하는 Vue.js와 같은 반응형 프레임워크와의 근본적인 차이점입니다.

네 번째 실수는 비동기 콜백(async lambda)으로 setState를 호출하는 것입니다. 비동기성 섹션에서 설명한 대로 await 이후의 변경은 캡처되지 않아 재현하기 어려운 버그로 이어집니다. 동기 콜백을 사용하고 await 후에 setState를 호출하세요.

안전한 setState 체크리스트

  • 비동기 콜백에서 항상 mounted 확인
  • build 내에서 setState 호출하지 않기
  • async lambda를 setState에 전달하지 않기
  • setState 외부에서 State 필드 수정하지 않기
  • 여러 필드를 변경하는 경우 — 하나의 setState에서 처리

자주 묻는 질문

Flutter에서 setState()는 무엇을 하나요?

setState() 는 StatefulWidget의 내부 데이터가 변경되어 UI를 재구성해야 함을 Flutter에 알립니다. 이 메서드는 콜백을 받아 동기적으로 실행하고 위젯을 dirty로 표시한 후 다음 프레임에서 build를 호출하도록 예약합니다.

필드를 변경한 후 setState를 호출하지 않으면 어떻게 되나요?

UI가 업데이트되지 않습니다. Flutter는 필드 변경을 자동으로 추적하지 않습니다. 필드 값은 메모리에서 변경되지만 부모에 의한 다음 강제 재구성까지 위젯은 이전 상태로 유지됩니다.

build 내에서 setState를 호출할 수 있나요?

아니요. 이는 무한 루프로 이어집니다: build가 setState를 호출하고, setState가 위젯을 dirty로 표시한 후 다시 build를 호출합니다. Flutter는 이 상황을 차단하지 않으며 앱이 StackOverflowError로 충돌합니다.

두 번 연속 setState를 호출하면 build가 몇 번 실행되나요?

build는 한 번 실행됩니다. Flutter는 모든 dirty 요소를 수집하여 프레임 끝에서 배치로 재구성합니다. 처리 전 두 번째 setState는 단순히 동일한 dirty 요소 목록에 요소를 추가할 뿐이며 반복 재구성은 발생하지 않습니다.

mounted란 무엇이며 setState에 왜 중요한가요?

mounted 는 위젯이 아직 트리에 있음을 나타내는 부울 플래그입니다. mounted를 확인하지 않고 비동기 작업 후에 setState를 호출하고 위젯이 이미 제거된 경우 앱이 “setState called after dispose” 예외와 함께 충돌합니다.

요약

  • setState() — 데이터 변경을 Flutter에 알리고 다음 프레임에서 UI 재구성을 트리거하는 State 메서드
  • 작동 메커니즘 — 콜백 동기 실행, State를 dirty로 표시, 프레임 끝에서 모든 dirty 요소의 배치 재구성
  • 비동기성 — setState 내 async 콜백은 작동하지 않음; await는 외부에 있어야 하며 결과 수집 후 setState 호출
  • mounted — 예외 방지를 위해 비동기 작업에서 setState 전 필수 확인
  • 최적화 — const 자식 위젯으로 재구성 영역 최소화, 애니메이션을 AnimatedBuilder로 이동
  • 대안 — 전역 상태에는 Riverpod, Bloc 또는 Provider 사용; 로컬 데이터에는 setState 유지
  • 규칙 — build 내에서 setState 호출 금지, async lambda 전달 금지, 항상 mounted 확인

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

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

프로젝트 논의

더 읽어보기