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를 통해 명확히 할 수 있습니다.