[Flutter] Provider를 이용한 상태관리

2025. 4. 24. 23:03·Dart/Flutter

 

Flutter에서의 UI는 철저하게 상태(State)에 기반하여 동작합니다.
사용자가 버튼을 누르거나, 데이터를 입력하거나, 서버에서 값을 받아오면 UI는 달라져야 하죠.
그렇다면 “이 상태를 어떻게 관리할 것인가?”는 앱의 복잡도가 올라갈수록 더 중요해지고, 제대로 설계하지 않으면 유지보수는 지옥이 됩니다.

그래서 오늘은 Flutter의 상태 관리 개념을 근본부터 설명하고,
그 중에서도 공식적으로 권장되고 가장 많이 사용되는 Provider를 중심으로 구조화, 원리, 예제, 설계 방식까지 모두 정리해봅니다.

상태 관리란 무엇인가?

상태(state)란 UI의 현재 상태를 표현하는 값입니다. 예를 들어,

  • 로그인 여부 (true/false)
  • 사용자 이름
  • 현재 탭 index
  • 서버에서 받아온 리스트 데이터
  • 버튼 클릭 여부 등

이 상태가 바뀌면 Flutter는 해당 상태를 사용하는 위젯을 다시 빌드(build)하여 화면을 갱신합니다.
즉, 상태 관리란 다음의 흐름을 관리하는 것입니다:

[상태의 변경] → [UI 업데이트]

setState()로는 부족한 이유

Flutter 초심자 대부분은 처음에 setState()만으로 모든 걸 처리합니다.
물론 작은 앱에서는 충분하지만, 앱이 커지면 문제가 생깁니다:

  • 같은 상태를 여러 위젯에서 공유하기 어려움
  • 위젯 트리 중간에 데이터 전달이 복잡해짐
  • 유지보수가 힘들고, 테스트도 어렵다

상태를 전역에서 관리할 수 있는 구조적인 방식이 필요하게 됩니다.
그때 등장하는 것이 바로 Provider입니다.

Provider란 무엇인가?

Provider는 Flutter 공식 문서에서 권장하는 상태 관리 도구입니다.
기본적으로는 InheritedWidget이라는 Flutter의 내장 메커니즘 위에 구축된 라이브러리로, 상태를 효율적으로 공유하고 관리할 수 있게 도와줍니다.

핵심 개념

  • 상태 객체를 Provider를 통해 앱 전반에 공유
  • 상태가 변경되면 notifyListeners()를 통해 UI 갱신
  • 구독 중인 위젯만 rebuild 되므로 성능에도 효율적
  • ChangeNotifier 기반으로 사용이 간단

설치 방법

dependencies:
  provider: ^6.1.1
flutter pub get

ChangeNotifier와 Provider의 관계

Provider는 여러 타입이 있지만, 가장 기본적인 형태는 다음과 같은 조합입니다

클래스/도구 역할
ChangeNotifier 상태를 가지는 객체. 변화가 생기면 notifyListeners() 호출
ChangeNotifierProvider 해당 상태 객체를 위젯 트리에 공급
context.watch() 상태 객체를 읽고 변경을 감지 (rebuild 발생)
context.read() 상태 객체를 읽지만 변경을 감지하지 않음 (이벤트 처리용)
Consumer<T> 특정 위젯만 rebuild 하고 싶을 때 사용

 

예제: Counter 앱

상태 클래스 정의

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

앱 시작점에 Provider 연결

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => Counter(),
      child: MyApp(),
    ),
  );
}

상태를 사용하여 UI 표시

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<Counter>();

    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(child: Text('${counter.count}', style: TextStyle(fontSize: 48))),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<Counter>().increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

Provider의 작동 원리

Provider는 기본적으로 위젯 트리 상위에서 상태 객체를 등록하고,
하위 위젯이 해당 상태를 구독하면서 변경될 때 자동으로 build()를 재실행합니다.

notifyListeners()는 어떻게 동작하나?

  • ChangeNotifier는 상태 변경 시 notifyListeners()를 호출
  • 이 메서드는 해당 객체를 구독 중인 모든 위젯에게 알림을 보내 rebuild하게 함
  • 단, Consumer를 쓰거나 watch()로 접근한 위젯만 영향을 받음

Consumer vs context.watch() vs context.read()

방식 설명 재빌드 발생 여부
context.watch<T>() 상태를 구독하고 자동 재빌드 O
context.read<T>() 상태를 읽기만 하고 구독하지 않음 X
Consumer<T>() 특정 위젯만 rebuild 하도록 분리 O (해당 위젯만)

 

예시

Consumer<Counter>(
  builder: (context, counter, _) {
    return Text('${counter.count}');
  },
)

실전: Todo 앱에서 Provider 적용 예시

모델 클래스 정의

class Todo {
  final String id;
  final String title;
  bool isDone;

  Todo({required this.id, required this.title, this.isDone = false});
}

상태 클래스 정의

class TodoListModel extends ChangeNotifier {
  final List<Todo> _todos = [];

  List<Todo> get todos => List.unmodifiable(_todos);

  void addTodo(String title) {
    _todos.add(Todo(id: DateTime.now().toString(), title: title));
    notifyListeners();
  }

  void toggleTodo(String id) {
    final todo = _todos.firstWhere((t) => t.id == id);
    todo.isDone = !todo.isDone;
    notifyListeners();
  }

  void removeTodo(String id) {
    _todos.removeWhere((t) => t.id == id);
    notifyListeners();
  }
}

앱에 Provider 연결

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => TodoListModel(),
      child: MyApp(),
    ),
  );
}

UI에서 사용

class TodoListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final todos = context.watch<TodoListModel>().todos;

    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        final todo = todos[index];
        return ListTile(
          title: Text(todo.title),
          leading: Checkbox(
            value: todo.isDone,
            onChanged: (_) => context.read<TodoListModel>().toggleTodo(todo.id),
          ),
          trailing: IconButton(
            icon: Icon(Icons.delete),
            onPressed: () => context.read<TodoListModel>().removeTodo(todo.id),
          ),
        );
      },
    );
  }
}

실전 앱에서 Provider 구조화 방법

lib/
 ┣ main.dart
 ┣ models/
 ┃ ┗ todo.dart
 ┣ providers/
 ┃ ┗ todo_list_model.dart
 ┣ screens/
 ┃ ┗ home_screen.dart
 ┣ widgets/
 ┃ ┗ todo_list_view.dart
 ┗ utils/
    ┗ logger.dart
  • 비즈니스 로직은 providers/
  • 데이터 모델은 models/
  • 화면은 screens/
  • 재사용 UI는 widgets/

이런 구조는 테스트, 유지보수, 협업에 모두 좋습니다.

고급 Provider: MultiProvider, ProxyProvider, FutureProvider

  • MultiProvider: 여러 Provider를 한 번에 제공
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => Counter()),
    ChangeNotifierProvider(create: (_) => ThemeModel()),
  ],
  child: MyApp(),
)
  • ProxyProvider: Provider 간의 의존성 연결
  • FutureProvider: 비동기 데이터 제공
  • StreamProvider: 스트리밍 데이터 처리 (ex: Firebase)

마무리: Provider는 설계 철학이다

Provider는 단순히 상태를 전달하는 도구가 아닙니다.
Flutter 앱을 구조적으로 설계하는 철학이자 패턴입니다.
상태를 어떻게 공유하고, 변경을 어떻게 관리하며, UI에 어떻게 반영할지?
이 모든 것을 Provider를 통해 명확히 할 수 있습니다.

728x90
'Dart/Flutter' 카테고리의 다른 글
  • [Flutter] deprecated 경고 발생 시 해결법
  • [Flutter] 안드로이드 스튜디오, 왜 껐다 켜야 하나요?
츄핑
츄핑
    250x250
  • 츄핑
    개발로그
    츄핑
  • 전체
    오늘
    어제
    • 분류 전체보기
      • CS
        • 자료구조
        • 알고리즘
        • 운영체제
        • 네트워크
        • 데이터베이스
        • 인프라
        • Web
      • PS
        • 백준
      • JavaScript
        • React
        • Express
        • NestJS
        • TypeScript
        • Node.js
        • Electron
      • Java
        • Spring
      • Dart
        • Flutter
      • PHP
        • CodeIgniter
      • etc
        • 이산수학
        • 선형대수학
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    티스토리챌린지
    오블완
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
츄핑
[Flutter] Provider를 이용한 상태관리
상단으로

티스토리툴바