개발

Flutter Riverpod 이해하기 1편 - StateProvider

간단한 상태 관리의 모든 것

2025년 9월 20일
10분 읽기
Flutter Riverpod 이해하기 1편 - StateProvider

StateProvider란?

StateProvider는 Riverpod에서 가장 기본적인 상태 관리 도구입니다. 간단한 값 하나를 저장하고, 그 값을 읽거나 변경할 수 있게 해주는 Provider예요.

쉽게 말해서 "전역 변수의 안전한 버전"이라고 생각하면 됩니다. 일반 변수와 달리 StateProvider는 값이 변경될 때마다 그 값을 사용하는 모든 위젯을 자동으로 다시 그려줍니다.

언제 StateProvider를 사용하는가?

StateProvider는 간단한 상태에만 사용해야 합니다.

사용하기 좋은 경우:
• 숫자 카운터 (좋아요 수, 장바구니 개수)
• true/false 토글 (다크모드, 알림 설정)
• 문자열 입력값 (검색어, 사용자명)
• 선택된 인덱스 (현재 탭, 선택된 아이템)

사용하면 안 되는 경우:
• 여러 필드가 있는 복잡한 객체
• 리스트나 맵 같은 복잡한 데이터 구조
• 비즈니스 로직이 들어가는 복잡한 상태

만약 사용자 정보처럼 이름, 이메일, 나이가 모두 들어있는 객체를 관리하려면 StateProvider 대신 StateNotifierProvider를 사용해야 합니다.

기본 사용법

1. StateProvider 만들기

Dart
// 카운터 (숫자)
final counterProvider = StateProvider<int>((ref) => 0);

// 다크모드 (true/false)
final darkModeProvider = StateProvider<bool>((ref) => false);

// 검색어 (문자열)
final searchProvider = StateProvider<String>((ref) => '');
StateProvider를 만들 때는 초기값을 지정해야 합니다. 위 예제에서는 카운터는 0, 다크모드는 false, 검색어는 빈 문자열로 시작합니다.

2. 상태 읽기

Dart
class CounterDisplay extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Text('현재 카운트: $count');
  }
}
ref.watch()를 사용해서 상태를 읽습니다. 이렇게 하면 카운터 값이 바뀔 때마다 이 위젯이 자동으로 다시 그려집니다.

3. 상태 변경하기

Dart
class CounterButtons extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Row(
      children: [
        ElevatedButton(
          onPressed: () {
            // 직접 값 변경
            ref.read(counterProvider.notifier).state++;
          },
          child: Text('+1'),
        ),
        ElevatedButton(
          onPressed: () {
            // 함수로 값 변경
            ref.read(counterProvider.notifier).update((state) => state - 1);
          },
          child: Text('-1'),
        ),
      ],
    );
  }
}
상태를 변경할 때는 ref.read().notifier.state를 사용합니다. 직접 값을 대입하거나 update() 함수를 사용할 수 있어요.

실무 예제

다크모드 토글

Dart
final darkModeProvider = StateProvider<bool>((ref) => false);

class DarkModeSwitch extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isDark = ref.watch(darkModeProvider);
    
    return Switch(
      value: isDark,
      onChanged: (value) {
        ref.read(darkModeProvider.notifier).state = value;
      },
    );
  }
}
스위치를 켜고 끄면 다크모드 상태가 바뀝니다. 이 상태를 앱 전체에서 사용해서 테마를 바꿀 수 있어요.

현재 탭 관리

Dart
final selectedTabProvider = StateProvider<int>((ref) => 0);

class TabView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final selectedIndex = ref.watch(selectedTabProvider);
    
    return Scaffold(
      body: [HomeTab(), SearchTab(), ProfileTab()][selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: selectedIndex,
        onTap: (index) {
          ref.read(selectedTabProvider.notifier).state = index;
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '홈'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: '검색'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '프로필'),
        ],
      ),
    );
  }
}
탭을 터치하면 선택된 탭 인덱스가 바뀌고, 그에 따라 보여지는 화면도 바뀝니다.

주의사항

복잡한 객체는 사용하지 마세요

Dart
// ❌ 이렇게 하지 마세요
class User {
  final String name;
  final String email;
  final int age;
  
  User({required this.name, required this.email, required this.age});
}

final userProvider = StateProvider<User>((ref) => User(name: '', email: '', age: 0));

// 이름만 바꾸려면?
ref.read(userProvider.notifier).state = User(
  name: '새이름',
  email: ref.read(userProvider).email,  // 기존값 그대로 복사해야 함
  age: ref.read(userProvider).age,      // 기존값 그대로 복사해야 함
);
이렇게 하면 코드가 복잡해지고 실수하기 쉽습니다. 복잡한 객체는 StateNotifierProvider를 사용하세요.

상태 초기화

Dart
class ResetButton extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () {
        // 초기값으로 돌아가기
        ref.read(counterProvider.notifier).state = 0;
        
        // 또는 Provider 자체를 리셋
        ref.invalidate(counterProvider);
      },
      child: Text('초기화'),
    );
  }
}

다른 Provider와 비교

vs Provider
• Provider: 한 번 만들어지면 절대 안 바뀌는 값 (설정, 서비스 객체)
• StateProvider: 앱 실행 중에 바뀔 수 있는 값 (카운터, 토글)

vs StateNotifierProvider
• StateProvider: 간단한 값 (int, bool, String)
• StateNotifierProvider: 복잡한 객체나 여러 필드가 있는 상태

vs FutureProvider
• StateProvider: 즉시 사용할 수 있는 값
• FutureProvider: 비동기 작업(API 호출 등)의 결과

정리

StateProvider는 Riverpod의 가장 기본적인 상태 관리 도구입니다. 간단한 값 하나를 전역적으로 관리하고 싶을 때 사용하세요.

핵심 포인트:
• 간단한 타입(int, bool, String)만 사용
ref.watch()로 읽기, ref.read().notifier.state로 쓰기
• 복잡한 객체는 StateNotifierProvider 사용
• 상태가 바뀌면 자동으로 UI 업데이트

StateProvider만 잘 활용해도 카운터, 토글, 탭 선택 등 앱에서 자주 사용하는 간단한 상태들을 효과적으로 관리할 수 있습니다.

다음 편 예고: 다음 글에서는 불변 값을 관리하는 Provider에 대해 알아보겠습니다.
#Flutter
#Riverpod
#StateProvider
#State Management