Flutter (앱 개발)/Flutter 트랙

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

Ize𓆜 2025. 4. 16. 11:15
728x90
728x90

 


Ⅰ. 들어가며

 

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으로 적용하니

전체 구조가 단단해졌고,

앱의 유지보수성과

확장성이 달라졌습니다.

728x90
728x90