상태 관리 입문 — setState의 한계와 Provider 소개

Flutter 면접에서 가장 많이 나오는 주제 중 하나가 상태 관리입니다. setState부터 시작해서 왜 상태 관리 라이브러리가 필요한지, Provider는 어떤 문제를 해결하는지 정리해보겠습니다.


상태(State)란?

앱에서 변할 수 있는 모든 데이터를 상태라고 합니다.

종류예시관리 위치
Ephemeral State (로컬)현재 탭 인덱스, 애니메이션 진행률해당 위젯 내부
App State (전역)로그인 정보, 장바구니, 설정앱 전체에서 공유

면접 포인트: "Ephemeral state와 App state의 차이를 설명해주세요"라는 질문이 자주 나옵니다.


setState의 한계

문제 1: 상태 끌어올리기 (Lifting State Up)

자식 위젯에서 상태를 변경하려면, 상태를 공통 조상으로 끌어올려야 합니다.

DART
// 상태를 최상위로 끌어올려야 하는 구조
class ParentWidget extends StatefulWidget {
  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  int _count = 0;

  void _increment() {
    setState(() => _count++);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 상태를 표시하는 위젯
        DisplayWidget(count: _count),
        // 상태를 변경하는 위젯 — 콜백을 전달해야 함
        ButtonWidget(onPressed: _increment),
      ],
    );
  }
}

문제 2: Prop Drilling

깊은 위젯 트리에서 상태를 전달하려면 중간 위젯들이 모두 해당 데이터를 전달해야 합니다.

PLAINTEXT
App
 └─ HomePage (user 전달)
     └─ Content (user 전달, 사용 안 함)
         └─ Sidebar (user 전달, 사용 안 함)
             └─ ProfileCard (user 실제 사용)

중간 위젯들은 user 데이터를 사용하지 않지만 전달만 해야 합니다. 위젯 트리가 깊어질수록 이 문제가 심각해집니다.

문제 3: 불필요한 리빌드

DART
// setState를 호출하면 해당 위젯의 build() 전체가 다시 실행됨
// 변경된 부분만 리빌드하고 싶어도 전체가 리빌드됨
setState(() {
  _count++;  // count만 바꿨는데 전체 build가 다시 실행
});

InheritedWidget — Flutter의 기본 해결책

Flutter에 내장된 상태 전파 메커니즘입니다. Provider의 기반 기술이기도 합니다.

DART
class CounterInherited extends InheritedWidget {
  final int count;
  final VoidCallback increment;

  const CounterInherited({
    super.key,
    required this.count,
    required this.increment,
    required super.child,
  });

  // 하위 위젯에서 접근하는 헬퍼 메서드
  static CounterInherited of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterInherited>()!;
  }

  @override
  bool updateShouldNotify(CounterInherited oldWidget) {
    return count != oldWidget.count;
  }
}

InheritedWidget은 보일러플레이트가 많고 사용하기 번거롭습니다. 이를 쉽게 만든 것이 Provider입니다.


Provider 소개

Provider는 InheritedWidget을 감싼 래퍼로, 상태 관리를 간편하게 만들어줍니다.

YAML
# pubspec.yaml
dependencies:
  provider: ^6.1.0

기본 사용법

DART
// 1. 상태 클래스 정의
class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();  // 변경 알림
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

// 2. Provider로 상태 제공
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: const MyApp(),
    ),
  );
}

// 3. 하위 위젯에서 상태 사용
class CounterScreen extends StatelessWidget {
  const CounterScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // 상태 읽기 (변경 시 리빌드)
    final count = context.watch<CounterModel>().count;

    return Scaffold(
      body: Center(
        child: Text('$count', style: const TextStyle(fontSize: 48)),
      ),
      floatingActionButton: FloatingActionButton(
        // 상태 변경 (리빌드 없이 접근)
        onPressed: () => context.read<CounterModel>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

watch vs read

메서드용도리빌드
context.watch<T>()상태 읽기 + 변경 감지변경 시 리빌드
context.read<T>()상태 읽기 (1회)리빌드 안 함
context.select<T, R>()특정 값만 감시해당 값 변경 시만 리빌드
DART
// build 메서드 안에서 상태를 읽을 때 → watch
final count = context.watch<CounterModel>().count;

// 콜백(onPressed 등)에서 메서드를 호출할 때 → read
onPressed: () => context.read<CounterModel>().increment(),

// 특정 속성만 감시할 때 → select
final count = context.select<CounterModel, int>((m) => m.count);

면접 포인트: watchbuild 안에서, read는 콜백 안에서 사용합니다. 이 규칙을 지키지 않으면 불필요한 리빌드가 발생하거나 상태 변경을 감지하지 못합니다.


여러 Provider 제공하기

DART
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => AuthModel()),
        ChangeNotifierProvider(create: (_) => CartModel()),
        ChangeNotifierProvider(create: (_) => ThemeModel()),
      ],
      child: const MyApp(),
    ),
  );
}

상태 관리 라이브러리 비교

라이브러리특징난이도
setState내장, 로컬 상태용쉬움
ProviderInheritedWidget 래퍼, 가장 기본적쉬움
RiverpodProvider의 진화, 컴파일타임 안전중간
Bloc이벤트 기반, 엔터프라이즈중간~어려움
GetX간편하지만 규모가 커지면 관리 어려움쉬움

정리

  • setState는 로컬 상태에만 적합합니다
  • Prop Drilling, 불필요한 리빌드가 setState의 주요 한계입니다
  • Provider는 InheritedWidget을 쉽게 사용할 수 있게 한 래퍼입니다
  • watch는 build에서, read는 콜백에서 사용하세요
  • 프로젝트 규모와 팀 상황에 맞는 상태 관리 라이브러리를 선택하세요
댓글 로딩 중...