개발

Flutter Riverpod Provider 완벽 가이드 7편 - NotifierProvider

Riverpod 2.0의 차세대 상태 관리 솔루션

2025년 9월 20일
20분 읽기
Flutter Riverpod Provider 완벽 가이드 7편 - NotifierProvider

NotifierProvider란?

NotifierProvider는 Riverpod 2.0에서 도입된 최신 상태 관리 방식으로, StateNotifierProvider의 개선된 버전입니다. 더 간단한 API, 향상된 성능, 그리고 더 나은 개발 경험을 제공합니다. 기존의 StateNotifier보다 적은 보일러플레이트 코드로 동일한 기능을 구현할 수 있으며, 자동 dispose 기능과 향상된 타입 안전성을 제공합니다.

언제 사용하는가?

사용하기 좋은 경우:
• 새로운 프로젝트: Riverpod 2.0+ 환경에서 상태 관리
• 복잡한 상태: 여러 필드를 가진 객체나 비즈니스 로직이 포함된 상태
• 성능이 중요: 최적화된 리빌드와 메모리 관리가 필요한 앱
• 현대적 개발 경험: 최신 Dart 기능과 도구 활용

사용하면 안 되는 경우:
• Riverpod 1.x 환경: 버전 제약으로 사용 불가
• 단순한 상태: 원시 타입 하나만 관리 → StateProvider 사용
• 일회성 비동기: 단순 API 호출 → FutureProvider 사용
• 실시간 스트림: 지속적 데이터 변화 → StreamProvider 사용

기본 사용법

Dart
// 1. 상태 클래스 정의
class Counter {
  final int value;
  final String status;
  
  const Counter({
    required this.value,
    required this.status,
  });
  
  Counter copyWith({
    int? value,
    String? status,
  }) {
    return Counter(
      value: value ?? this.value,
      status: status ?? this.status,
    );
  }
}

// 2. Notifier 클래스 정의
class CounterNotifier extends Notifier<Counter> {
  @override
  Counter build() {
    // 초기 상태 반환
    return const Counter(value: 0, status: 'idle');
  }
  
  void increment() {
    state = state.copyWith(
      value: state.value + 1,
      status: 'incremented',
    );
  }
  
  void decrement() {
    state = state.copyWith(
      value: state.value - 1,
      status: 'decremented',
    );
  }
  
  void reset() {
    state = const Counter(value: 0, status: 'reset');
  }
}

// 3. Provider 정의 - 타입 추론으로 더 간단함
final counterProvider = NotifierProvider<CounterNotifier, Counter>(CounterNotifier.new);

실무 예제: 할 일 목록 관리

Dart
class Todo {
  final String id;
  final String title;
  final bool isCompleted;
  final DateTime createdAt;
  
  const Todo({
    required this.id,
    required this.title,
    required this.isCompleted,
    required this.createdAt,
  });
  
  Todo copyWith({
    String? id,
    String? title,
    bool? isCompleted,
    DateTime? createdAt,
  }) {
    return Todo(
      id: id ?? this.id,
      title: title ?? this.title,
      isCompleted: isCompleted ?? this.isCompleted,
      createdAt: createdAt ?? this.createdAt,
    );
  }
}

class TodoList {
  final List<Todo> todos;
  final TodoFilter filter;
  
  const TodoList({
    required this.todos,
    required this.filter,
  });
  
  TodoList copyWith({
    List<Todo>? todos,
    TodoFilter? filter,
  }) {
    return TodoList(
      todos: todos ?? this.todos,
      filter: filter ?? this.filter,
    );
  }
  
  List<Todo> get filteredTodos {
    switch (filter) {
      case TodoFilter.all:
        return todos;
      case TodoFilter.active:
        return todos.where((todo) => !todo.isCompleted).toList();
      case TodoFilter.completed:
        return todos.where((todo) => todo.isCompleted).toList();
    }
  }
  
  int get activeCount => todos.where((todo) => !todo.isCompleted).length;
  int get completedCount => todos.where((todo) => todo.isCompleted).length;
}

enum TodoFilter { all, active, completed }

class TodoListNotifier extends Notifier<TodoList> {
  @override
  TodoList build() {
    return const TodoList(
      todos: [],
      filter: TodoFilter.all,
    );
  }
  
  void addTodo(String title) {
    if (title.trim().isEmpty) return;
    
    final newTodo = Todo(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: title.trim(),
      isCompleted: false,
      createdAt: DateTime.now(),
    );
    
    state = state.copyWith(
      todos: [...state.todos, newTodo],
    );
  }
  
  void toggleTodo(String id) {
    final updatedTodos = state.todos.map((todo) {
      if (todo.id == id) {
        return todo.copyWith(isCompleted: !todo.isCompleted);
      }
      return todo;
    }).toList();
    
    state = state.copyWith(todos: updatedTodos);
  }
  
  void deleteTodo(String id) {
    final updatedTodos = state.todos.where((todo) => todo.id != id).toList();
    state = state.copyWith(todos: updatedTodos);
  }
  
  void updateFilter(TodoFilter filter) {
    state = state.copyWith(filter: filter);
  }
  
  void clearCompleted() {
    final activeTodos = state.todos.where((todo) => !todo.isCompleted).toList();
    state = state.copyWith(todos: activeTodos);
  }
}

final todoListProvider = NotifierProvider<TodoListNotifier, TodoList>(TodoListNotifier.new);

주의사항과 팁

자주 하는 실수들:

1. build() 메소드에서 비동기 작업:
`dart
// ❌ 잘못된 방법
class BadNotifier extends Notifier {
@override
String build() {
fetchData(); // 비동기 작업은 build()에서 하면 안 됨
return 'initial';
}
}

// ✅ 올바른 방법
class GoodNotifier extends Notifier {
@override
String build() {
// build()는 동기적이어야 함
Future.microtask(() => fetchData()); // 다음 틱에서 실행
return 'initial';
}

Future fetchData() async {
// 비동기 작업은 별도 메소드에서
}
}
`

2. ref 사용 시점 혼동:
`dart
// ❌ 잘못된 방법
class BadNotifier extends Notifier {
@override
int build() {
final otherValue = ref.watch(otherProvider); // build()에서는 OK
return 0;
}

void someMethod() {
final value = ref.watch(otherProvider); // 메소드에서는 read() 사용
}
}

// ✅ 올바른 방법
class GoodNotifier extends Notifier {
@override
int build() {
final otherValue = ref.watch(otherProvider); // build()에서는 watch
return 0;
}

void someMethod() {
final value = ref.read(otherProvider); // 메소드에서는 read
}
}
`

베스트 프랙티스:
• build() 메소드는 순수하게 유지
• ref.watch는 build()에서만, ref.read는 메소드에서
• 복잡한 로직은 private 메소드로 분리
• 에러 상태도 상태 클래스에 포함
• 테스트하기 쉬운 구조로 설계

다른 Provider와 비교

NotifierProvider vs StateNotifierProvider:
• NotifierProvider: Riverpod 2.0+, Notifier 상속, build() 메소드, 자동 dispose, 향상된 성능
• StateNotifierProvider: Riverpod 1.0+, StateNotifier 상속, 생성자, 수동 dispose, 기본 성능

NotifierProvider vs ChangeNotifierProvider:
• NotifierProvider: 불변(immutable) 상태, 우수한 성능, 높은 예측 가능성, Riverpod 최적화
• ChangeNotifierProvider: 가변(mutable) 상태, 보통 성능, 낮은 예측 가능성, Flutter 기본

선택 기준:
• 새 프로젝트 + Riverpod 2.0+: NotifierProvider 최우선
• 복잡한 상태 관리: NotifierProvider
• 최고 성능 필요: NotifierProvider
• Riverpod 1.x: StateNotifierProvider
• 기존 코드 유지: ChangeNotifierProvider

정리

NotifierProvider는 Riverpod 2.0의 플래그십 상태 관리 솔루션입니다. 기존 StateNotifierProvider의 모든 장점을 유지하면서 더 간단하고 성능이 좋은 API를 제공합니다.

핵심 포인트:
• 최신 Riverpod: 2.0+에서만 사용 가능한 차세대 솔루션
• 향상된 성능: 자동 최적화와 효율적인 리빌드
• 간단한 API: 적은 보일러플레이트와 직관적인 구조
• 완전한 타입 안전성: 컴파일 타임 에러 방지

다음 편 예고: 8편에서는 비동기 작업과 상태 관리를 완벽하게 결합하는 AsyncNotifierProvider에 대해 알아보겠습니다.
#Flutter
#Riverpod
#NotifierProvider
#Notifier
#Riverpod 2.0