[Flutter 숙련 TIL] 상태관리 패키지 Riverpod 사용법과 MVVM 구조 적용하기

Ⅰ. 들어가며
📌 Flutter 상태관리, 왜 Riverpod인가?
Ⅱ. 본론
📌 1. Riverpod이란?
📌 2. 기본 사용법 요약
📌 3. 실전 예제 – "유저 정보를 가져와 화면에 표시하기"
📌 4. 상태유지 전략 – Notifier, AutoDispose, Family (+GitHub)
Ⅲ. 정리하며 – 내가 겪은 시행착오와 배운 점
📌 시작하며
📌 Riverpod을 쓰고 나서 달라진 점
Ⅰ. 들어가며
Flutter 상태관리, 왜 Riverpod인가?
Flutter를 공부하면서
가장 큰 고민 중 하나는
바로 “상태 관리”였습니다.
기초적인 예제에서는
setState()로 해결할 수 있었지만,
앱이 커지면 커질수록
위젯과 상태가 뒤섞이고
유지보수가 어려워졌죠.
이 때 등장하는 것이
바로 Riverpod입니다.
이번 포스트에서는
Riverpod의 기본적인 사용법부터,
MVVM 구조를 적용하는 예제까지
하나씩 정리해보겠습니다.
Ⅱ. 본론
Riverpod이란?
Riverpod은 Flutter의
상태관리를 위한 강력한 라이브러리입니다.
- View와 상태(ViewModel)를 분리하여 깔끔한 아키텍처 구성 가능
- 코드 테스트와 유지보수에 유리
- ProviderScope로 앱 전체를 감싸면 전역 상태관리 가능
쉽게 말해, 상태관리 로직을 별도 클래스로 정리하고, Widget은 필요한 상태만 구독하여 자동으로 재빌드되도록 하는 구조를 쉽게 구현할 수 있게 해주는 도구입니다.
기본 사용법 요약
✅ 상태 클래스 만들기
class HomeState {
int counter;
HomeState(this.counter);
}
✅ ViewModel 만들기 (Notifier 상속)
class HomeViewModel extends Notifier<HomeState> {
@override
HomeState build() {
return HomeState(1);
}
void updateState() {
state = HomeState(state.counter + 1);
}
}
✅ Provider 정의하기
final homeViewModelProvider = NotifierProvider<HomeViewModel, HomeState>(
() => HomeViewModel(),
);
✅ Widget에서 사용하기 (Consumer 사용)
Consumer(
builder: (context, ref, child) {
final state = ref.watch(homeViewModelProvider);
return Text('카운트: ${state.counter}');
},
)
✅ main 함수에서 ProviderScope로 감싸기
void main() {
runApp(ProviderScope(child: MyApp()));
}
실전 예제 – "유저 정보를 가져와 화면에 표시하기"
가상의 서버에서
유저 정보를 받아서
화면에 표시하는 실습을 진행합니다.
✅ User 모델
class User {
final String name;
final int age;
User({required this.name, required this.age});
factory User.fromJson(Map<String, dynamic> map) =>
User(name: map['name'], age: map['age']);
}
✅ Repository (데이터 요청 시뮬레이션)
class UserRepository {
Future<User> getUser() async {
await Future.delayed(Duration(seconds: 1));
String json = '{"name": "이지원", "age": 20}';
return User.fromJson(jsonDecode(json));
}
}
✅ 상태 클래스
class HomeState {
final User? user;
final DateTime? fetchTime;
HomeState({this.user, this.fetchTime});
}
✅ ViewModel
class HomeViewModel extends Notifier<HomeState> {
@override
HomeState build() => HomeState();
void getUserInfo() async {
final repo = UserRepository();
final user = await repo.getUser();
state = HomeState(user: user, fetchTime: DateTime.now());
}
}
✅ Provider 등록
final homeViewModelProvider = NotifierProvider<HomeViewModel, HomeState>(
() => HomeViewModel(),
);
✅ View에서 사용하기
Consumer(
builder: (context, ref, child) {
final state = ref.watch(homeViewModelProvider);
final viewModel = ref.read(homeViewModelProvider.notifier);
return Column(
children: [
Text('이름: ${state.user?.name ?? ''}'),
Text('나이: ${state.user?.age ?? ''}'),
Text('가져온 시간: ${state.fetchTime ?? ''}'),
ElevatedButton(
onPressed: () => viewModel.getUserInfo(),
child: Text('정보 가져오기'),
),
],
);
},
)
상태유지 전략 – Notifier, AutoDispose, Family
종류 | 설명 | 사용 시점 |
Notifier | 앱이 꺼질 때까지 유지됨 | 로그인 상태, 전역 데이터 |
AutoDisposeNotifier | 위젯이 사라지면 상태도 사라짐 | 입력폼, 일회성 결과 |
AutoDisposeFamilyNotifier | 값에 따라 ViewModel 분기 | 상세 페이지 등에서 ID를 넘겨줘야 할 때 |
이제 제가 연습한 자료는
아래 GitHub링크에서 확인할 수 있습니다.
Linayoo01/-250416-flutter_user_app
GitHub - Linayoo01/-250416-flutter_user_app
Contribute to Linayoo01/-250416-flutter_user_app development by creating an account on GitHub.
github.com
Ⅲ. 정리하며 – 내가 겪은 시행착오와 배운 점
처음에는 왜 이렇게 복잡한가 싶었다
처음 Notifier, NotifierProvider,
WidgetRef, ref.watch, ref.read…
이런 용어들이 나올 때
솔직히 머릿속이 복잡했습니다.
“setState() 쓰면 되는 거 아닌가?” 하는
생각도 들었고요.
특히 상태 업데이트 시에
state 자체를 새로 만들어서
할당해야 한다는 개념은 낯설었습니다.
기존의 객체 값을 바꾸면
재빌드가 안 된다는 걸 알고 나서야
“아, 이게 불변성의 개념이구나!” 하고
이해하게 됐습니다.
가장 헷갈렸던 포인트
- ref.read()는 왜 상태 변화를 감지 못할까?
- ViewModel 접근은 .notifier, 상태 접근은 그냥 ref.watch()로 가능하다는 구조
이런 시행착오를 겪으며
점점 코드가 깔끔해지고,
구조가 눈에 들어오기 시작했어요.
Riverpod을 쓰고 나서 달라진 점
가장 먼저 느낀 건
코드가 훨씬 명확해진다는 점이었어요.
- ViewModel에서 상태 로직을 전부 처리하고,
- View에서는 그저 필요한 상태를 ref.watch()로 받아오기만 하면 되니,
- UI 코드는 훨씬 읽기 쉬워졌고,
- 테스트 코드 작성도 쉬워질 것 같다는 생각이 들었습니다.
특히 AutoDisposeFamilyNotifier는
앞으로 페이지별로
고유 데이터를 처리할 때
엄청 유용하게 쓸 수 있을 것 같아요.
MVVM 구조를
Riverpod으로 적용하니
전체 구조가 단단해졌고,
앱의 유지보수성과
확장성이 달라졌습니다.