개발

Flutter Riverpod Provider 완벽 가이드 5편 - StateNotifierProvider

복잡한 상태를 안전하고 효율적으로 관리하는 방법

2025년 9월 20일
18분 읽기
Flutter Riverpod Provider 완벽 가이드 5편 - StateNotifierProvider

StateNotifierProvider란?

StateNotifierProvider는 복잡한 객체 상태를 불변성을 유지하면서 안전하게 관리할 수 있는 Riverpod의 핵심 Provider입니다. StateNotifier 클래스와 결합하여 여러 필드를 가진 복잡한 상태를 체계적으로 관리할 수 있습니다. 단순한 값 하나를 관리하는 StateProvider와 달리, 쇼핑 카트나 사용자 프로필 같은 복합적인 데이터 구조를 다룰 때 진가를 발휘합니다.

언제 사용하는가?

사용하기 좋은 경우:
• 복잡한 객체 상태: 여러 필드를 가진 클래스나 리스트 관리
• 상태 변경 로직이 복잡: 여러 조건을 확인하고 상태를 업데이트
• 불변성이 중요: 상태 변경 시 예측 가능한 동작이 필요
• 비즈니스 로직 분리: UI와 상태 관리 로직을 명확히 구분하고 싶을 때

사용하면 안 되는 경우:
• 단순한 원시 타입: 정수, 문자열, 불린 값 → StateProvider 사용
• 일회성 비동기 작업: API 호출 결과만 필요 → FutureProvider 사용
• 실시간 데이터: 지속적으로 변하는 스트림 → StreamProvider 사용

기본 사용법

Dart
// 1. 상태 클래스 정의
class Counter {
  final int value;
  final String status;
  
  const Counter({
    required this.value,
    required this.status,
  });
  
  // copyWith 메소드로 불변성 유지
  Counter copyWith({
    int? value,
    String? status,
  }) {
    return Counter(
      value: value ?? this.value,
      status: status ?? this.status,
    );
  }
}

// 2. StateNotifier 클래스 정의
class CounterNotifier extends StateNotifier<Counter> {
  CounterNotifier() : super(const Counter(value: 0, status: 'idle'));
  
  void increment() {
    state = state.copyWith(
      value: state.value + 1,
      status: 'incremented',
    );
  }
  
  void reset() {
    state = const Counter(value: 0, status: 'reset');
  }
}

// 3. Provider 정의
final counterProvider = StateNotifierProvider<CounterNotifier, Counter>((ref) {
  return CounterNotifier();
});

실무 예제: 쇼핑 카트 관리

Dart
class CartItem {
  final String id;
  final String name;
  final int price;
  final int quantity;
  
  const CartItem({
    required this.id,
    required this.name,
    required this.price,
    required this.quantity,
  });
  
  CartItem copyWith({int? quantity}) {
    return CartItem(
      id: id,
      name: name,
      price: price,
      quantity: quantity ?? this.quantity,
    );
  }
}

class Cart {
  final List<CartItem> items;
  final int totalPrice;
  
  const Cart({
    required this.items,
    required this.totalPrice,
  });
  
  Cart copyWith({List<CartItem>? items}) {
    final newItems = items ?? this.items;
    final newTotal = newItems.fold(0, (sum, item) => sum + (item.price * item.quantity));
    
    return Cart(
      items: newItems,
      totalPrice: newTotal,
    );
  }
}

class CartNotifier extends StateNotifier<Cart> {
  CartNotifier() : super(const Cart(items: [], totalPrice: 0));
  
  void addItem(CartItem item) {
    final existingIndex = state.items.indexWhere((i) => i.id == item.id);
    
    if (existingIndex >= 0) {
      // 기존 아이템 수량 증가
      final updatedItems = [...state.items];
      updatedItems[existingIndex] = updatedItems[existingIndex].copyWith(
        quantity: updatedItems[existingIndex].quantity + item.quantity,
      );
      state = state.copyWith(items: updatedItems);
    } else {
      // 새 아이템 추가
      state = state.copyWith(items: [...state.items, item]);
    }
  }
  
  void removeItem(String itemId) {
    final updatedItems = state.items.where((item) => item.id != itemId).toList();
    state = state.copyWith(items: updatedItems);
  }
  
  void clearCart() {
    state = const Cart(items: [], totalPrice: 0);
  }
}

final cartProvider = StateNotifierProvider<CartNotifier, Cart>((ref) {
  return CartNotifier();
});

주의사항과 팁

자주 하는 실수들:

1. 상태를 직접 수정:
`dart
// ❌ 잘못된 방법
void badIncrement() {
state.value++; // 컴파일 에러!
}

// ✅ 올바른 방법
void goodIncrement() {
state = state.copyWith(value: state.value + 1);
}
`

2. copyWith 메소드 없이 상태 복사:
`dart
// ❌ 잘못된 방법 - 새 객체를 매번 완전히 생성
state = Counter(
value: state.value + 1,
status: state.status, // 변경되지 않는 값도 다시 설정
);

// ✅ 올바른 방법 - copyWith 사용
state = state.copyWith(value: state.value + 1);
`

성능 최적화 방법:
• 부분적 상태 감시: ref.watch(cartProvider.select((cart) => cart.totalPrice))
• 불필요한 copyWith 호출 방지: 같은 값이면 업데이트 안 함
• freezed 패키지 활용: copyWith, ==, hashCode 자동 생성

다른 Provider와 비교

StateNotifierProvider vs StateProvider:
• StateNotifierProvider: 복잡한 객체, 클래스, 메소드를 통한 제어
• StateProvider: 원시 타입 (int, String, bool), 직접 할당

StateNotifierProvider vs NotifierProvider:
• StateNotifierProvider: Riverpod 1.0+, StateNotifier 상속
• NotifierProvider: Riverpod 2.0+, Notifier 상속, 향상된 성능

선택 기준:
• 새 프로젝트: NotifierProvider 사용 (Riverpod 2.0+)
• 기존 프로젝트: StateNotifierProvider 유지 또는 점진적 마이그레이션
• 복잡한 상태: 둘 다 적합
• 단순한 상태: StateProvider 고려

정리

StateNotifierProvider는 복잡한 상태를 체계적으로 관리하는 강력한 도구입니다. 쇼핑 카트, 사용자 프로필, 폼 데이터 등 여러 필드를 가진 상태를 다룰 때 특히 유용합니다.

핵심 포인트:
• 불변성을 유지하면서 안전한 상태 관리
• 비즈니스 로직을 캡슐화하여 코드 구조화
• copyWith 패턴으로 효율적인 상태 업데이트
• 복잡한 상태 변경 로직을 체계적으로 관리

다음 편 예고: 6편에서는 기존 Flutter ChangeNotifier 코드를 Riverpod과 연동하는 ChangeNotifierProvider에 대해 알아보겠습니다.
#Flutter
#Riverpod
#StateNotifierProvider
#StateNotifier
#State Management