1. 들어가며
1) 상태관리란?
상태라는 것은 앱에서 변할 수 있는 데이터를 의미합니다.
사용자가 버튼을 누르면 숫자가 1 증가하는 앱이 있다면
여기서 증가하는 숫자가 상태가 되는 것이고,
로그인 기능이 있는 앱이 있으면
로그인 여부(로그인/로그아웃)가 상태가 되는 것입니다.
Flutter는 화면을 그릴 때 상태에 따라 위젯을 구성합니다.
즉, 상태가 바뀌면 화면도 다시 그려야 하며,
이런 상태 데이터를 효과적으로 다루는 방식이 상태 관리입니다.
2) setState의 한계점
setState()는 Flutter에서 기본적으로 제공하는 가장 간단한 상태 관리 방법입니다.
① setState는 상태 변경과 UI 업데이트를 같은 위젯 안에서 처리하기 때문에
상태와 UI 코드가 섞여 복잡해지고, 재사용성이 떨어진다는 단점이 있습니다.
② 또한, 여러 위젯 간 상태 공유가 어렵다는 점도 있습니다.
공유하려면 상태를 상대 위젯으로 끌어올리고, 콜백으로 전달하거나 상태를 저장하는 전역 변수를 만들어야하기 때문에 매우 번거롭습니다.
③ 마지막으로 setState는 BuildContext에 의존하기 때문에 로직을 테스트하려면 항상 위젯 트리 안에서 실행해야 합니다
BuildContext란 위젯 트리 내에서 위젯의 위치를 참조하는 객체입니다.
즉, setState는 위젯이 다시 그려져야 한다는 신호를 주기 위해 BuildContext를 사용하므로 위젯 트리 안에서만 동작 가능합니다.
이로 인해 setState는 UI와 강하게 결합되어 있으며, 상태를 변경하는 로직만 독립적으로 테스트할 수 없습니다.
테스트를 하기 위해서는 반드시 로직이 들어있는 위젯을 화면에 띄우고, setState를 호출해야만 결과를 확인할 수 있습니다.
이러한 한계점 때문에 상태 관리 로직을 UI와 분리하고 보다 효율적으로 관리하기 위해 Provider, Riverpod, BLoc, GetX 등 다양한 상태 관리 패키지가 나오게 되었습니다.
2. Riverpod
1) Riverpod
Riverpod은 Provider에서 느꼈던 한계점들을 보완해 만든 최신 상태 관리 라이브러리입니다.
공식 문서에 보면 Riverpod은 Provider의 anagram이라고 합니다.
2) 왜 Riverpod인가?
Riverpod은 BuildContext에 의존하지 않습니다.
기존 Provider는 context.read, context.watch를 사용해야 하므로, 위젯 안에서만 상태 접근이 가능했습니다.
하지만 Riverpod은 ref.read, ref.watch를 사용하기 때문에 위젯이 아닌 일반 클래스나 비즈니스 로직에서도 자유롭게 상태 접근을 할 수 있습니다.
두 번째로 Flutter 위젯 트리에 의존하지 않습니다.
기존 Provider는 위젯 트리를 따라 Provider를 전달해야해서 위치에 민감하고 복잡한 트리에서 실수하기 쉬웠습니다.
하지만 Riverpod은 Provider 트리와 위젯 트리를 완전히 분리해서 동작하기 때문에
더 안전하고, 직관적인 구조를 만들 수 있습니다.
세 번째로 자동으로 의존성을 추적할 수 있습니다.
Provier 안에서 다른 Provider를 watch / read 하면,
Riverpod은 두 Provider 간의 관계를 자동으로 기억합니다.
때문에 aProvider가 변경되면, aProvider를 사용하는 모든 UI가 자동으로 다시 빌드됩니다.
즉, 상태가 바뀌면 필요한 곳만 자동으로 다시 빌드되기 때문에 직접 신경 쓸 필요가 없습니다.
네 번째로 컴파일 타임에 오류를 발견할 수 있습니다.
Riverpod은 null safety, 타입 추론, 오타 방지 등 이 잘 적용된 구조이기 때문에
잘못된 상태 접근, 타이핑 오류 등은 컴파일 단계에서 오류를 잡을 수 있습니다.
마지막으로 테스트 친화적입니다.
위젯 없이도 상태를 만들고 테스트할 수 있습니다.
ProviderContainer를 사용해 상태를 독립적으로 만들 수 있어서 단위 테스트, 통합 테스트가 쉬워집니다.
3. Riverpod 용어 정리
아마 위 장점을 보고 아직 이해가 안 되는 부분이 있을 것이라 생각합니다.
이제부터 Riverpod에서 사용하는 용어에 대해 알아보겠습니다.
예제는 타이머 앱을 실행하는 코드입니다.
1) 준비 - ProvierScope
ProviderScope는 Flutter 앱에서 Riverpod을 사용하기 위해 반드시 필요한 최상위 위젯입니다.
Riverpod은 내부적으로 상태를 저장하고 추적하기 위해 ProviderContainer라는 객체를 사용하는데,
이 ProviderContainer는 ProviderScope 안에서 생성되고 관리됩니다.
즉, 모든 Provider는 ProviderScope 범위 안에서만 작동합니다.
여기서 ProvideContainer는 모든 Provider 상태와 로직을 실제로 담고 관리하는 핵심 객체로,
Provider의 인스턴스를 저장하고 관리하는 객체입니다.
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
2) 상태 클래스 정의 - StateNotifier
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0); // 초기값 설정
void increment() => state++;
}
StateNotifier는 상태를 가지고, 상태를 변경하는 로직을 담은 클래스입니다.
StateNotifier<int>는 int 타입의 상태를 관리하겠다는 뜻이며,
시작할 때의 상태는 0이고,
increment가 호출될 때마다 이 값이 1씩 증가합니다.
즉, 카운터 값을 저장하고, 증가시키는 행위인 로직만 담당합니다.
3) 상태를 UI에 연결하는 Provider 생성
Provider는 읽기 전용 데이터를 제공하며,
내부에 상태를 직접 가지고 있지 않으며, 고정된 값을 제공합니다.
StateNotifierProvider는 StateNotifier 클래스를 Provider로 등록해서 UI와 연결합니다.
두 개의 타입 <Notifier, State>로 구성되어 있으며,
첫 번째 제네릭은 CounterNotifier로 상태 로직 클래스이고,
두 번째 제네릭은 int로 상태 값의 타입을 의미합니다.
UI는 StateNotifier를 감시하고 있다가 값이 바뀌면 자동으로 다시 그려집니다.
ref는 Provider안에서 다른 Provider를 사용하거나 접근할 때 필요합니다.
이는 4번에서 자세히 다루겠습니다.
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
즉, 이 코드는 CounterNotifier라는 클래스에서 상태(int)를 관리하고,
counterProvider라는 이름으로 상태 + 로직을 UI에 연결하는 Provider를 생성한 것입니다.
4) UI에서 Provider 사용 - ConsumerWidget
Riverpod에서 Provider를 UI에서 사용하려면, ref.watch()나 ref.read()를 사용해야합니다.
ref는 Provider에 접근할 수 있는 도구입니다.
하지만 일반적인 StatelessWidget이나 StatefulWidget에서는 ref를 사용할 수 없기 때문에
Provider를 사용하기 위해 반드시 ConsumerWidget 혹은 Consumer를 사용해야합니다.
ConsumerWidget은 Riverpod의 위젯으로 Provider의 상태를 읽고 사용할 수 있게 해주는 위젯입니다.
build() 함수의 두 번째 인자인 ref를 통해 Provider에 접근할 수 있습니다.
ref.watch()는 상태를 구독해서 값이 바뀌면 자동으로 UI가 리빌드되는 것이고,
ref.read()는 상태를 한 번만 읽기 때문에 값이 바뀌어도 UI가 리빌드되지 않습니다.
class HomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // 상태 구독
return Scaffold(
appBar: AppBar(title: Text("Riverpod Counter")),
body: Center(child: Text('$count')), // 상태 출력
floatingActionButton: FloatingActionButton(
onPressed: () {
// 상태 변경 (UI는 변경된 count로 자동 리빌드)
ref.read(counterProvider.notifier).increment();
},
child: Icon(Icons.add),
),
);
}
}
상태 값(카운트 값)을 구독해서 화면에 출력하고,
버튼을 클릭하면 ref.read(counterProvider.notifier).increment()를 호출합니다.
그 후, 상태값이 증가되고,
상태가 변경되는 것을 감지하면,
화면이 자동으로 리빌드 되어 새로운 값이 출력되는 것입니다.
4. Riverpod 상태 관리 흐름
Riverpod의 상태 관리의 흐름은 다음과 같습니다.
1. 상태와 로직을 Provider로 만든다.
StateNotifier로 감싸고, StateNotifierProvider로 등록한다.
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());
2. 위젯에서 상태를 읽거나 구독한다.
ref.watch()는 상태를 자동으로 구독하고, 값이 바뀌면 자동으로 리빌드된다.
ref.read()는 구독 없이 상태를 한 번만 가져올 때 사용한다.
final count = ref.watch(counterProvider); // 값이 바뀌면 자동 리빌드
3. notifier를 통해 상태를 변경한다.
.notifier로 상태를 가지고 있는 객체에 접근해서 메서드를 호출해 상태를 변경한다.
ref.read(counterProvider.notifier).increment(); // 상태 변경
MVVM에 대해 더 알고 싶다면?
[Flutter] MVVM 패턴, 앱 아키텍처 가이드
1. MVVM 패턴MVVM은 앱의 기능을 Model - View - ViewModel 로 구분하는 아키텍처 패턴입니다. Model : 데이터 구조와 비즈니스 로직 담당View : 사용자의 입력을 받고 UI를 구성ViewModel : 상태를 관리하고, View
sfida.tistory.com
MVVM과 Riverpod을 이용하여 Counter 앱 만들기
[Flutter] Riverpod으로 상태관리하고, MVVM패턴 적용해서 Counter앱 만들기
1. MVVMhttps://sfida.tistory.com/115 2. Riverpodhttps://sfida.tistory.com/112 3. Riverpod 적용 이전 코드 Flutter 프로젝트를 생성하면 자동으로 생성되는 기본 예제 코드입니다. 이 코드는 StatefulWidget 내에서 상태를
sfida.tistory.com
5. 참고 자료
https://docs.flutter.dev/data-and-backend/state-mgmt/intro
https://riverpod.dev/docs/introduction/why_riverpod
https://medium.com/stackademic/flutter-riverpod-tutorial-530bceca0ffd
https://github.com/rrousselGit/riverpod/tree/master/examples/counter
https://github.com/suragch/riverpod_timer_demo
https://github.com/funwithflutter/riverpod_tutorials
'Flutter' 카테고리의 다른 글
| [Flutter] 지역 검색 앱 프로젝트 소개 (0) | 2025.04.22 |
|---|---|
| [Flutter] MVVM 패턴, 앱 아키텍처 가이드 (0) | 2025.04.14 |
| [Flutter] StatelessWidget, StatefulWidget, build() (0) | 2025.03.26 |
| [Dart] 조건문 (if, if-else, switch) (0) | 2025.03.10 |
| [Dart] 연산자 (Operators) (0) | 2025.03.10 |