[Flutter] Riverpod - 상태 관리

2025. 5. 8. 14:58·Flutter/Basic Knowledge

TL;DR

Riverpod은 Flutter의 강력한 상태 관리 프레임워크로, Provider의 여러 한계를 극복한 프레임워크입니다.
Provider에서 글자 배열만 바꿔서 Riverpod이라고 만들었을 정도로 같은 사람이 만든 티가 나죠?
이 글에서는 Riverpod의 기본 상태 관리 방식과 MVVM 아키텍처에서 View, ViewModel, Provider를 효과적으로 연결하는 방법을 실제 코드 예제와 함께 알아봅니다.

목차

  1. Riverpod 소개
  2. 기본적인 상태 관리 방식
  3. MVVM 패턴과 Riverpod 통합
  4. 실제 예제 구현
  5. 성능 최적화 팁
  6. 결론

1. Riverpod 소개

Flutter 애플리케이션을 개발하다 보면 상태 관리는 피할 수 없는 과제입니다.
Provider 패키지가 인기를 얻었지만, 타입 안전성과 컴파일 타임 검증에 한계가 있었습니다.
Riverpod는 이름 그대로 'Provider'를 뒤집은 단어로, Provider의 창시자가 개발한 Provider의 완전한 재구상(Rethinking) 버전입니다.

// Provider vs Riverpod
// Provider
final counterProvider = Provider((ref) => 0);
// 사용: Provider.of<int>(context)

// Riverpod
final counterProvider = Provider<int>((ref) => 0);
// 사용: ref.watch(counterProvider)

Riverpod(v2.3.6 기준)는 몇 가지 핵심 이점을 제공합니다:

  • 컴파일 타임 안전성
  • Provider 의존성 오버라이드 용이
  • 전역 접근 가능성(context 불필요)
  • 중복 Provider 선언 감지


2. 기본적인 상태 관리 방식

Riverpod는 다양한 유형의 Provider를 제공하며, 각각 특정 사용 사례에 최적화되어 있습니다.

주요 Provider 유형

Provider

가장 기본적인 형태로, 변경되지 않는 값을 제공합니다.

final nameProvider = Provider<String>((ref) {
  return 'Flutter Developer';
});

StateProvider

간단한 상태를 관리할 때 사용하며,. state로 직접 값을 변경할 수 있습니다.

final counterProvider = StateProvider<int>((ref) {
  return 0;
});

// 사용
ref.read(counterProvider.notifier).state++;

StateNotifierProvider

복잡한 상태를 관리할 때 사용하며, 상태 변경 로직을 캡슐화합니다.

class Counter extends StateNotifier<int> {
  Counter() : super(0);
  
  void increment() => state++;
  void decrement() => state--;
}

final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

// 사용
ref.read(counterProvider.notifier).increment();


NotifierProvider

Riverpod v2.0에서 새롭게 도입된 Provider로, StateNotifier의 대체재입니다. Notifier 클래스를 사용하며 상태 관리 로직을 더 직관적으로 작성할 수 있습니다.

class CounterNotifier extends Notifier<int> {
  @override
  int build() {
    return 0; // 초기 상태
  }
  
  void increment() {
    state++; // 상태 업데이트
  }
  
  void decrement() {
    state--;
  }
}

final counterProvider = NotifierProvider<CounterNotifier, int>(() {
  return CounterNotifier();
});

// 사용
ref.read(counterProvider.notifier).increment();

NotifierProvider는 StateNotifierProvider보다 몇 가지 장점이 있습니다:

  • ref 객체에 직접 접근 가능
  • 초기 상태를 동적으로 생성 가능
  • 코드가 더 명확하고 간결해짐


FutureProvider

비동기 데이터를 관리할 때 사용합니다.

final userProvider = FutureProvider<User>((ref) async {
  return await UserRepository().fetchUser();
});


StreamProvider

스트림 데이터를 관리할 때 사용합니다.

final userChangesProvider = StreamProvider<User>((ref) {
  return UserRepository().userChanges();
});


ref 객체 활용하기

Provider 내에서 ref 객체는 다른 Provider와 상호작용하는 핵심 도구입니다.

final combinedProvider = Provider<String>((ref) {
  final counter = ref.watch(counterProvider);
  final name = ref.watch(nameProvider);
  return '$name: $counter';
});

주요 메서드:

  • watch: Provider의 값을 구독하고 변경 시 재빌드
  • read: Provider의 값을 한 번만 읽음(이벤트 핸들러 내부 등에서 사용)
  • listen: Provider 값의 변화를 감지하여 콜백 함수 실행


3. MVVM 패턴과 Riverpod 통합

MVVM(Model-View-ViewModel) 패턴은 UI 로직과 비즈니스 로직을 분리하여 유지보수성과 테스트 용이성을 높입니다.
Riverpod와 MVVM을 통합하면 깔끔한 아키텍처를 구현할 수 있습니다.

MVVM 구성요소

Model: 데이터 구조와 비즈니스 로직을 담당
ViewModel: UI 상태를 관리하고 Model과 상호작용
View: UI 표시를 담당하며 ViewModel의 상태를 구독

Riverpod와 MVVM 연결 방식

  • Model 정의:
// user_model.dart
class User {
  final String id;
  final String name;
  final int age;
  
  User({required this.id, required this.name, required this.age});
  
  User copyWith({String? id, String? name, int? age}) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      age: age ?? this.age,
    );
  }
}
  • ViewModel 정의 (StateNotifier 활용):
// user_view_model.dart
class UserViewModel extends StateNotifier<User> {
  UserViewModel() : super(
    User(id: 'initial', name: 'Guest', age: 0)
  );
  
  void updateName(String name) {
    state = state.copyWith(name: name);
  }
  
  void incrementAge() {
    state = state.copyWith(age: state.age + 1);
  }
}
  • Provider 정의 및 등록:
// user_provider.dart
final userViewModelProvider = StateNotifierProvider<UserViewModel, User>((ref) {
  return UserViewModel();
});
  • View에서 Provider 사용:
// user_view.dart
class UserProfileScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ViewModel의 상태(User)를 감시
    final user = ref.watch(userViewModelProvider);
    // ViewModel의 메서드에 접근하기 위한 notifier
    final userVM = ref.read(userViewModelProvider.notifier);
    
    return Column(
      children: [
        Text('Name: ${user.name}'),
        Text('Age: ${user.age}'),
        ElevatedButton(
          onPressed: () => userVM.updateName('New Name'),
          child: Text('Update Name'),
        ),
        ElevatedButton(
          onPressed: () => userVM.incrementAge(),
          child: Text('Increment Age'),
        ),
      ],
    );
  }
}


4. 실제 예제 구현

간단한 투두 앱을 통해 MVVM과 Riverpod를 활용한 상태 관리를 구현해 보겠습니다.

1. Model 정의

// task_model.dart
class Task {
  final String id;
  final String title;
  final bool completed;
  
  Task({
    required this.id,
    required this.title,
    this.completed = false,
  });
  
  Task copyWith({String? id, String? title, bool? completed}) {
    return Task(
      id: id ?? this.id,
      title: title ?? this.title,
      completed: completed ?? this.completed,
    );
  }
}

2. ViewModel 정의

// task_view_model.dart
class TasksState {
  final List<Task> tasks;
  final bool isLoading;
  
  TasksState({required this.tasks, this.isLoading = false});
  
  TasksState copyWith({List<Task>? tasks, bool? isLoading}) {
    return TasksState(
      tasks: tasks ?? this.tasks,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

class TasksViewModel extends StateNotifier<TasksState> {
  TasksViewModel() : super(TasksState(tasks: []));
  
  void addTask(String title) {
    final task = Task(
      id: DateTime.now().toString(),
      title: title,
    );
    
    state = state.copyWith(
      tasks: [...state.tasks, task],
    );
  }
  
  void toggleTaskStatus(String id) {
    final updatedTasks = state.tasks.map((task) {
      if (task.id == id) {
        return task.copyWith(completed: !task.completed);
      }
      return task;
    }).toList();
    
    state = state.copyWith(tasks: updatedTasks);
  }
  
  void removeTask(String id) {
    final updatedTasks = state.tasks.where((task) => task.id != id).toList();
    state = state.copyWith(tasks: updatedTasks);
  }
}

3. Provider 등록

// task_provider.dart
final tasksProvider = StateNotifierProvider<TasksViewModel, TasksState>((ref) {
  return TasksViewModel();
});

4. View 구현

// task_screen.dart
class TasksScreen extends ConsumerWidget {
  final TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final tasksState = ref.watch(tasksProvider);
    final tasksVM = ref.read(tasksProvider.notifier);
    
    return Scaffold(
      appBar: AppBar(title: Text('Todo App with Riverpod MVVM')),
      body: Column(
        children: [
          // 새 태스크 추가 폼
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(
                      hintText: 'Add new task',
                    ),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {
                    if (_controller.text.isNotEmpty) {
                      tasksVM.addTask(_controller.text);
                      _controller.clear();
                    }
                  },
                ),
              ],
            ),
          ),
          
          // 태스크 리스트
          Expanded(
            child: tasksState.isLoading
                ? Center(child: CircularProgressIndicator())
                : ListView.builder(
                    itemCount: tasksState.tasks.length,
                    itemBuilder: (context, index) {
                      final task = tasksState.tasks[index];
                      return ListTile(
                        leading: Checkbox(
                          value: task.completed,
                          onChanged: (_) => tasksVM.toggleTaskStatus(task.id),
                        ),
                        title: Text(
                          task.title,
                          style: TextStyle(
                            decoration: task.completed
                                ? TextDecoration.lineThrough
                                : TextDecoration.none,
                          ),
                        ),
                        trailing: IconButton(
                          icon: Icon(Icons.delete),
                          onPressed: () => tasksVM.removeTask(task.id),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

이 예제에서 우리는:

  1. Task 모델로 데이터 구조 정의
  2. TasksViewModel에서 상태 관리 로직 구현
  3. tasksProvider로 ViewModel 노출
  4. ConsumerWidget을 통해 UI와 상태 연결

 

5. 성능 최적화 팁

Riverpod와 MVVM을 사용할 때 몇 가지 최적화 방법이 있습니다:


선택적 리빌딩

select 메서드를 사용하여 상태의 특정 부분만 감시할 수 있습니다:

// 전체 User 객체가 아닌 name 속성만 감시
final userName = ref.watch(userProvider.select((user) => user.name));


여러 Provider 분리

하나의 큰 Provider보다 여러 작은 Provider로 나누면 리빌딩 최적화에 도움이 됩니다:

// 하나의 큰 Provider 대신
final taskCompletedCountProvider = Provider<int>((ref) {
  final tasks = ref.watch(tasksProvider).tasks;
  return tasks.where((task) => task.completed).length;
});


비동기 상태 관리

FutureProvider를 사용한 비동기 데이터 로딩:

final tasksProvider = FutureProvider<List<Task>>((ref) async {
  final repository = ref.watch(tasksRepositoryProvider);
  return await repository.fetchTasks();
});


6. 결론

Riverpod는 Flutter에서 상태 관리의 새로운 표준을 제시합니다. MVVM 패턴과 결합하면 코드의 가독성, 유지보수성, 테스트 용이성이 크게 향상됩니다.

이 글에서 알아본 내용:

  • Riverpod의 다양한 Provider 유형과 각 사용 사례
  • MVVM 패턴을 Riverpod와 통합하는 방법
  • View, ViewModel, Provider 간의 상호작용
  • 예제를 통한 구현 방법
  • 성능 최적화 팁

다음 글에서는 Riverpod을 DI 용도로 사용하는 방법을 예제 프로젝트를 통해 정리해보려 합니다.
Dependency injection(DI)가 무엇인지에 대해서는 차차 알아가기로 하고 다음 포스팅은 CA(Clean Architecture) + MVVM 구조의 예제 애플리케이션의 구현 방식을 기반으로 포스팅할 예정이니, CA에 대해서는 제가 올린 포스팅 혹은 다른 글을 보고 공부하고 오시길 바랍니다.

참고 자료

  • Riverpod 공식 문서
  • Flutter 공식 문서: 상태 관리
  • MVVM 패턴 설명
  • Riverpod GitHub 레포지토리

'Flutter > Basic Knowledge' 카테고리의 다른 글

[Flutter] Fastlane 친해지기  (0) 2025.05.12
[Flutter] Riverpod - DI  (0) 2025.05.10
[Flutter] 앱 개발을 위한 클린 아키텍처 기본기 다지기 - 1단계  (0) 2025.05.02
[Dart] Stream 이란?  (2) 2024.01.22
[Dart] Asynchronous & Future  (1) 2024.01.12
'Flutter/Basic Knowledge' 카테고리의 다른 글
  • [Flutter] Fastlane 친해지기
  • [Flutter] Riverpod - DI
  • [Flutter] 앱 개발을 위한 클린 아키텍처 기본기 다지기 - 1단계
  • [Dart] Stream 이란?
halfcodx
halfcodx
iOS, Flutter Developer
  • halfcodx
    Mins_Programming
    halfcodx
  • 전체
    오늘
    어제
    • 분류 전체보기 (28)
      • My App (1)
        • NostelgiAlbum (0)
        • Ro_ad (1)
        • Growith (0)
      • Retrospect (2)
      • iOS (9)
        • iOS (6)
        • Swift (2)
      • Python (5)
        • Module (3)
        • Knowledge (2)
      • Flutter (10)
        • Basic Knowledge (10)
        • Implementation (0)
  • 블로그 메뉴

    • Home
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    파이썬
    Python
    combine
    DART
    combine 이란
    ios combine
    티스토리챌린지
    Flutter
    requests
    오블완
  • hELLO· Designed By정상우.v4.10.1
halfcodx
[Flutter] Riverpod - 상태 관리
상단으로

티스토리툴바