Provider란?
Provider는 Riverpod에서 한 번 생성되면 절대 변하지 않는 값을 관리하는 도구입니다. StateProvider와 달리 Provider는 앱이 시작될 때 값이 결정되면 그 이후로는 바뀌지 않아요.
쉽게 말해서 "상수의 고급 버전"이라고 생각하면 됩니다. 일반 상수와 달리 Provider는 다른 Provider의 값을 참조할 수 있고, 필요할 때만 생성되는 지연 초기화(lazy initialization) 기능도 있어요.
Provider는 주로 앱의 설정값, 서비스 객체, 또는 다른 Provider들을 조합해서 만든 계산된 값을 저장할 때 사용합니다.
쉽게 말해서 "상수의 고급 버전"이라고 생각하면 됩니다. 일반 상수와 달리 Provider는 다른 Provider의 값을 참조할 수 있고, 필요할 때만 생성되는 지연 초기화(lazy initialization) 기능도 있어요.
Provider는 주로 앱의 설정값, 서비스 객체, 또는 다른 Provider들을 조합해서 만든 계산된 값을 저장할 때 사용합니다.
언제 Provider를 사용하는가?
Provider는 변하지 않는 값을 관리할 때 사용해야 합니다.
사용하기 좋은 경우:
• 앱 설정값 (API URL, 앱 버전, 테마 색상)
• 서비스 객체 (HTTP 클라이언트, 데이터베이스, 저장소)
• 계산된 값 (다른 Provider들을 조합한 결과)
• 환경별 설정 (개발/운영 환경 구분)
사용하면 안 되는 경우:
• 사용자 입력으로 바뀌는 값 (카운터, 토글, 폼 입력)
• 시간에 따라 변하는 값 (현재 시간, 실시간 데이터)
• 사용자 상호작용으로 변경되는 상태
만약 값이 앱 실행 중에 바뀔 수 있다면 Provider 대신 StateProvider나 다른 Provider를 사용해야 합니다.
사용하기 좋은 경우:
• 앱 설정값 (API URL, 앱 버전, 테마 색상)
• 서비스 객체 (HTTP 클라이언트, 데이터베이스, 저장소)
• 계산된 값 (다른 Provider들을 조합한 결과)
• 환경별 설정 (개발/운영 환경 구분)
사용하면 안 되는 경우:
• 사용자 입력으로 바뀌는 값 (카운터, 토글, 폼 입력)
• 시간에 따라 변하는 값 (현재 시간, 실시간 데이터)
• 사용자 상호작용으로 변경되는 상태
만약 값이 앱 실행 중에 바뀔 수 있다면 Provider 대신 StateProvider나 다른 Provider를 사용해야 합니다.
기본 사용법
1. Provider 만들기
Dart
// 앱 설정
final appConfigProvider = Provider<AppConfig>((ref) {
return AppConfig(
apiUrl: 'https://api.example.com',
appVersion: '1.0.0',
timeout: Duration(seconds: 30),
);
});
// HTTP 클라이언트
final httpClientProvider = Provider<HttpClient>((ref) {
return HttpClient()..timeout = Duration(seconds: 30);
});
// 간단한 문자열 값
final appNameProvider = Provider<String>((ref) => 'My Flutter App');
Provider는 함수를 받아서 그 함수가 반환하는 값을 저장합니다. 이 함수는 앱에서 해당 Provider를 처음 사용할 때 딱 한 번만 실행되고, 그 이후로는 같은 값을 계속 반환해요.
2. Provider 읽기
Dart
class ApiService extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final config = ref.watch(appConfigProvider);
final httpClient = ref.read(httpClientProvider);
return Text('API URL: ${config.apiUrl}');
}
}
Provider의 값을 읽을 때는
ref.watch()
또는 ref.read()
를 사용합니다. Provider는 값이 변하지 않으므로 둘 중 어느 것을 써도 결과는 같아요.3. 다른 Provider 참조하기
Dart
// 기본 설정
final apiUrlProvider = Provider<String>((ref) => 'https://api.example.com');
final timeoutProvider = Provider<Duration>((ref) => Duration(seconds: 30));
// 다른 Provider들을 조합한 HTTP 클라이언트
final httpClientProvider = Provider<HttpClient>((ref) {
final url = ref.watch(apiUrlProvider);
final timeout = ref.watch(timeoutProvider);
return HttpClient()
..baseUrl = url
..timeout = timeout;
});
Provider 안에서
ref.watch()
를 사용해서 다른 Provider의 값을 참조할 수 있어요. 이렇게 하면 Provider들을 조합해서 더 복잡한 객체를 만들 수 있습니다.실무 예제
API 클라이언트 설정
Dart
// 환경별 API URL
final apiUrlProvider = Provider<String>((ref) {
return kDebugMode
? 'https://dev-api.example.com' // 개발 환경
: 'https://api.example.com'; // 운영 환경
});
// HTTP 클라이언트
final httpClientProvider = Provider<Dio>((ref) {
final apiUrl = ref.watch(apiUrlProvider);
final dio = Dio(BaseOptions(
baseUrl: apiUrl,
connectTimeout: Duration(seconds: 30),
receiveTimeout: Duration(seconds: 30),
));
// 로깅 인터셉터 추가 (개발 환경에서만)
if (kDebugMode) {
dio.interceptors.add(LogInterceptor());
}
return dio;
});
// API 서비스
final userApiProvider = Provider<UserApi>((ref) {
final httpClient = ref.watch(httpClientProvider);
return UserApi(httpClient);
});
이렇게 하면 환경에 따라 다른 API URL을 사용하면서도, HTTP 클라이언트 설정을 중앙에서 관리할 수 있어요.
테마 설정
Dart
// 기본 색상 팔레트
final colorPaletteProvider = Provider<ColorPalette>((ref) {
return ColorPalette(
primary: Colors.blue,
secondary: Colors.green,
background: Colors.white,
surface: Colors.grey[100]!,
);
});
// 라이트 테마
final lightThemeProvider = Provider<ThemeData>((ref) {
final colors = ref.watch(colorPaletteProvider);
return ThemeData(
brightness: Brightness.light,
primarySwatch: colors.primary,
backgroundColor: colors.background,
cardColor: colors.surface,
);
});
// 다크 테마
final darkThemeProvider = Provider<ThemeData>((ref) {
final colors = ref.watch(colorPaletteProvider);
return ThemeData(
brightness: Brightness.dark,
primarySwatch: colors.primary,
backgroundColor: Colors.black,
cardColor: Colors.grey[800]!,
);
});
// 앱에서 사용
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final lightTheme = ref.watch(lightThemeProvider);
final darkTheme = ref.watch(darkThemeProvider);
return MaterialApp(
theme: lightTheme,
darkTheme: darkTheme,
home: HomeScreen(),
);
}
}
색상 팔레트를 한 곳에서 정의하고, 그것을 기반으로 라이트/다크 테마를 만드는 방식이에요. 색상을 바꾸고 싶으면 colorPaletteProvider만 수정하면 됩니다.
계산된 값 만들기
Dart
// 사용자 권한
final userRoleProvider = StateProvider<UserRole>((ref) => UserRole.guest);
// 사용자가 관리자인지 확인 (계산된 값)
final isAdminProvider = Provider<bool>((ref) {
final role = ref.watch(userRoleProvider);
return role == UserRole.admin;
});
// 사용자가 접근할 수 있는 메뉴 목록 (계산된 값)
final availableMenusProvider = Provider<List<MenuItem>>((ref) {
final isAdmin = ref.watch(isAdminProvider);
final role = ref.watch(userRoleProvider);
final menus = <MenuItem>[
MenuItem('홈', Icons.home),
MenuItem('프로필', Icons.person),
];
if (role != UserRole.guest) {
menus.add(MenuItem('대시보드', Icons.dashboard));
}
if (isAdmin) {
menus.add(MenuItem('관리자 설정', Icons.settings));
menus.add(MenuItem('사용자 관리', Icons.people));
}
return menus;
});
사용자 권한에 따라 보여줄 메뉴가 자동으로 계산되는 방식이에요. userRoleProvider가 바뀌면 관련된 모든 계산된 값들이 자동으로 업데이트됩니다.
Provider의 특별한 기능들
지연 초기화 (Lazy Initialization)
Dart
final expensiveServiceProvider = Provider<ExpensiveService>((ref) {
print('ExpensiveService 생성됨!'); // 실제 사용될 때만 출력됨
return ExpensiveService();
});
Provider는 실제로 사용될 때까지 생성되지 않아요. 위 예제에서 expensiveServiceProvider를 선언만 하고 아무도 사용하지 않으면 ExpensiveService는 절대 생성되지 않습니다.
자동 의존성 관리
Dart
final databaseProvider = Provider<Database>((ref) => Database());
final userRepositoryProvider = Provider<UserRepository>((ref) {
final database = ref.watch(databaseProvider);
return UserRepository(database);
});
final userServiceProvider = Provider<UserService>((ref) {
final repository = ref.watch(userRepositoryProvider);
return UserService(repository);
});
Provider들 사이의 의존성이 자동으로 관리돼요. userServiceProvider를 사용하면 Riverpod가 알아서 databaseProvider와 userRepositoryProvider를 먼저 생성해줍니다.
주의사항
상태가 바뀌는 값은 사용하지 마세요
Dart
// ❌ 이렇게 하지 마세요
final currentTimeProvider = Provider<DateTime>((ref) => DateTime.now());
// 위젯에서 사용하면 시간이 업데이트되지 않음
class TimeDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final time = ref.watch(currentTimeProvider);
return Text('${time.hour}:${time.minute}'); // 계속 같은 시간만 표시됨
}
}
Provider는 한 번 생성되면 값이 바뀌지 않기 때문에, 위 예제에서 시간은 앱이 시작될 때의 시간에서 멈춰있을 거예요. 시간처럼 계속 바뀌는 값은 StreamProvider를 사용해야 합니다.
무거운 작업은 Provider에서 하지 마세요
Dart
// ❌ 이렇게 하지 마세요
final heavyComputationProvider = Provider<List<int>>((ref) {
// 무거운 계산 작업
final result = <int>[];
for (int i = 0; i < 1000000; i++) {
result.add(i * i);
}
return result;
});
// ✅ 이렇게 하세요
final heavyComputationProvider = FutureProvider<List<int>>((ref) async {
// 비동기로 무거운 작업 처리
return await compute(heavyComputation, null);
});
Provider는 동기적으로 실행되기 때문에 무거운 작업을 하면 앱이 멈출 수 있어요. 시간이 오래 걸리는 작업은 FutureProvider를 사용하세요.
다른 Provider와 비교
vs StateProvider
• Provider: 절대 안 바뀌는 값 (설정, 서비스 객체)
• StateProvider: 앱 실행 중에 바뀔 수 있는 값 (카운터, 토글)
vs FutureProvider
• Provider: 즉시 사용할 수 있는 값 (동기적)
• FutureProvider: 시간이 걸리는 비동기 작업의 결과
vs StreamProvider
• Provider: 한 번 정해지면 안 바뀌는 값
• StreamProvider: 시간에 따라 계속 바뀌는 값들의 흐름
• Provider: 절대 안 바뀌는 값 (설정, 서비스 객체)
• StateProvider: 앱 실행 중에 바뀔 수 있는 값 (카운터, 토글)
vs FutureProvider
• Provider: 즉시 사용할 수 있는 값 (동기적)
• FutureProvider: 시간이 걸리는 비동기 작업의 결과
vs StreamProvider
• Provider: 한 번 정해지면 안 바뀌는 값
• StreamProvider: 시간에 따라 계속 바뀌는 값들의 흐름
정리
Provider는 Riverpod에서 불변 값을 관리하는 기본 도구입니다. 앱의 설정이나 서비스 객체처럼 한 번 정해지면 바뀌지 않는 값들을 관리할 때 사용하세요.
핵심 포인트:
• 한 번 생성되면 절대 안 바뀌는 값만 사용
• 다른 Provider들을 조합해서 복잡한 객체 생성 가능
• 지연 초기화로 필요할 때만 생성됨
• 의존성이 자동으로 관리됨
실무에서 자주 사용하는 패턴:
• API 클라이언트 설정
• 테마나 스타일 정의
• 환경별 설정 관리
• 계산된 값 캐싱
Provider만 잘 활용해도 앱의 설정과 서비스 객체들을 깔끔하게 관리할 수 있고, 다른 Provider들과 조합해서 복잡한 의존성 구조도 쉽게 만들 수 있습니다.
다음 편 예고: 다음 글에서는 비동기 작업의 결과를 관리하는 FutureProvider에 대해 알아보겠습니다.
핵심 포인트:
• 한 번 생성되면 절대 안 바뀌는 값만 사용
• 다른 Provider들을 조합해서 복잡한 객체 생성 가능
• 지연 초기화로 필요할 때만 생성됨
• 의존성이 자동으로 관리됨
실무에서 자주 사용하는 패턴:
• API 클라이언트 설정
• 테마나 스타일 정의
• 환경별 설정 관리
• 계산된 값 캐싱
Provider만 잘 활용해도 앱의 설정과 서비스 객체들을 깔끔하게 관리할 수 있고, 다른 Provider들과 조합해서 복잡한 의존성 구조도 쉽게 만들 수 있습니다.
다음 편 예고: 다음 글에서는 비동기 작업의 결과를 관리하는 FutureProvider에 대해 알아보겠습니다.