StateNotifierProvider란?
StateNotifierProvider는 복잡한 객체 상태를 불변성을 유지하면서 안전하게 관리할 수 있는 Riverpod의 핵심 Provider입니다. StateNotifier 클래스와 결합하여 여러 필드를 가진 복잡한 상태를 체계적으로 관리할 수 있습니다. 단순한 값 하나를 관리하는 StateProvider와 달리, 쇼핑 카트나 사용자 프로필 같은 복합적인 데이터 구조를 다룰 때 진가를 발휘합니다.
언제 사용하는가?
사용하기 좋은 경우:
• 복잡한 객체 상태: 여러 필드를 가진 클래스나 리스트 관리
• 상태 변경 로직이 복잡: 여러 조건을 확인하고 상태를 업데이트
• 불변성이 중요: 상태 변경 시 예측 가능한 동작이 필요
• 비즈니스 로직 분리: UI와 상태 관리 로직을 명확히 구분하고 싶을 때
사용하면 안 되는 경우:
• 단순한 원시 타입: 정수, 문자열, 불린 값 → StateProvider 사용
• 일회성 비동기 작업: API 호출 결과만 필요 → FutureProvider 사용
• 실시간 데이터: 지속적으로 변하는 스트림 → StreamProvider 사용
• 복잡한 객체 상태: 여러 필드를 가진 클래스나 리스트 관리
• 상태 변경 로직이 복잡: 여러 조건을 확인하고 상태를 업데이트
• 불변성이 중요: 상태 변경 시 예측 가능한 동작이 필요
• 비즈니스 로직 분리: 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. 상태를 직접 수정:
// ❌ 잘못된 방법
void badIncrement() {
state.value++; // 컴파일 에러!
}
// ✅ 올바른 방법
void goodIncrement() {
state = state.copyWith(value: state.value + 1);
}
2. copyWith 메소드 없이 상태 복사:
// ❌ 잘못된 방법 - 새 객체를 매번 완전히 생성
state = Counter(
value: state.value + 1,
status: state.status, // 변경되지 않는 값도 다시 설정
);
// ✅ 올바른 방법 - copyWith 사용
state = state.copyWith(value: state.value + 1);
성능 최적화 방법:
• 부분적 상태 감시:
• 불필요한 copyWith 호출 방지: 같은 값이면 업데이트 안 함
• freezed 패키지 활용: copyWith, ==, hashCode 자동 생성
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: 복잡한 객체, 클래스, 메소드를 통한 제어
• 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에 대해 알아보겠습니다.
핵심 포인트:
• 불변성을 유지하면서 안전한 상태 관리
• 비즈니스 로직을 캡슐화하여 코드 구조화
• copyWith 패턴으로 효율적인 상태 업데이트
• 복잡한 상태 변경 로직을 체계적으로 관리
다음 편 예고: 6편에서는 기존 Flutter ChangeNotifier 코드를 Riverpod과 연동하는 ChangeNotifierProvider에 대해 알아보겠습니다.