FutureProvider란?
FutureProvider는 Riverpod에서 비동기 작업의 결과를 관리하는 도구입니다. API 호출, 파일 읽기, 데이터베이스 조회처럼 시간이 걸리는 작업의 결과를 쉽게 관리할 수 있게 해줘요.
쉽게 말해서 "Future를 위젯에서 편리하게 사용할 수 있게 해주는 도구"입니다. 일반 Future와 달리 FutureProvider는 로딩 상태, 에러 상태, 완료 상태를 자동으로 관리해주고, 결과가 나오면 관련된 위젯들을 자동으로 다시 그려줍니다.
FutureProvider는 한 번 실행되면 결과를 캐시해서 같은 데이터를 여러 번 요청하지 않아요. 하지만 필요하다면 강제로 새로고침할 수도 있습니다.
쉽게 말해서 "Future를 위젯에서 편리하게 사용할 수 있게 해주는 도구"입니다. 일반 Future와 달리 FutureProvider는 로딩 상태, 에러 상태, 완료 상태를 자동으로 관리해주고, 결과가 나오면 관련된 위젯들을 자동으로 다시 그려줍니다.
FutureProvider는 한 번 실행되면 결과를 캐시해서 같은 데이터를 여러 번 요청하지 않아요. 하지만 필요하다면 강제로 새로고침할 수도 있습니다.
언제 FutureProvider를 사용하는가?
FutureProvider는 일회성 비동기 작업에 사용해야 합니다.
사용하기 좋은 경우:
• API에서 데이터 가져오기 (사용자 정보, 상품 목록)
• 파일에서 설정 읽어오기 (JSON 설정 파일, 이미지 파일)
• 데이터베이스 조회 (사용자 목록, 주문 내역)
• 시간이 걸리는 계산 작업 (복잡한 수학 계산, 이미지 처리)
사용하면 안 되는 경우:
• 실시간으로 계속 바뀌는 데이터 (채팅 메시지, 주식 가격)
• 사용자 입력에 따라 즉시 바뀌는 상태 (토글, 카운터)
• 이미 동기적으로 사용할 수 있는 값 (설정값, 상수)
만약 데이터가 실시간으로 계속 업데이트된다면 FutureProvider 대신 StreamProvider를 사용해야 합니다.
사용하기 좋은 경우:
• API에서 데이터 가져오기 (사용자 정보, 상품 목록)
• 파일에서 설정 읽어오기 (JSON 설정 파일, 이미지 파일)
• 데이터베이스 조회 (사용자 목록, 주문 내역)
• 시간이 걸리는 계산 작업 (복잡한 수학 계산, 이미지 처리)
사용하면 안 되는 경우:
• 실시간으로 계속 바뀌는 데이터 (채팅 메시지, 주식 가격)
• 사용자 입력에 따라 즉시 바뀌는 상태 (토글, 카운터)
• 이미 동기적으로 사용할 수 있는 값 (설정값, 상수)
만약 데이터가 실시간으로 계속 업데이트된다면 FutureProvider 대신 StreamProvider를 사용해야 합니다.
기본 사용법
1. FutureProvider 만들기
Dart
// 사용자 정보 가져오기
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get('/api/user');
return User.fromJson(response.data);
});
// 설정 파일 읽기
final configProvider = FutureProvider<AppConfig>((ref) async {
final configFile = await rootBundle.loadString('assets/config.json');
return AppConfig.fromJson(jsonDecode(configFile));
});
// 복잡한 계산
final heavyComputationProvider = FutureProvider<List<int>>((ref) async {
return await compute(heavyCalculation, 1000000);
});
FutureProvider는 async 함수를 받아서 그 함수가 반환하는 Future의 결과를 관리해요. 함수가 실행되는 동안은 로딩 상태가 되고, 완료되거나 에러가 발생하면 그에 맞는 상태로 바뀝니다.
2. FutureProvider 사용하기
Dart
class UserProfile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('에러: $error'),
data: (user) => Column(
children: [
Text('이름: ${user.name}'),
Text('이메일: ${user.email}'),
],
),
);
}
}
FutureProvider의 결과는
AsyncValue
라는 타입으로 반환돼요. when()
메소드를 사용하면 로딩, 에러, 데이터 상태를 각각 다르게 처리할 수 있습니다.3. 매개변수가 있는 FutureProvider
Dart
// 특정 사용자 정보 가져오기
final userByIdProvider = FutureProvider.family<User, String>((ref, userId) async {
final response = await http.get('/api/user/$userId');
return User.fromJson(response.data);
});
// 사용할 때
class UserDetail extends ConsumerWidget {
final String userId;
UserDetail({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userByIdProvider(userId));
return userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('사용자를 찾을 수 없습니다'),
data: (user) => Text('사용자: ${user.name}'),
);
}
}
family
키워드를 사용하면 매개변수를 받는 FutureProvider를 만들 수 있어요. 같은 매개변수로 여러 번 호출해도 결과가 캐시되므로 네트워크 요청이 중복되지 않습니다.실무 예제
API 데이터 가져오기
Dart
// 상품 목록 API
final productsProvider = FutureProvider<List<Product>>((ref) async {
try {
final response = await http.get('/api/products');
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => Product.fromJson(json)).toList();
} else {
throw Exception('상품 목록을 가져올 수 없습니다');
}
} catch (e) {
throw Exception('네트워크 에러: $e');
}
});
// 상품 목록 화면
class ProductList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: Text('상품 목록'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
// 새로고침
ref.refresh(productsProvider);
},
),
],
),
body: productsAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$error'),
ElevatedButton(
onPressed: () => ref.refresh(productsProvider),
child: Text('다시 시도'),
),
],
),
),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('₩${product.price}'),
leading: Image.network(product.imageUrl),
);
},
),
),
);
}
}
API 호출이 실패했을 때 에러 메시지를 보여주고, 새로고침 버튼으로 다시 시도할 수 있게 만든 예제예요.
다른 Provider에 의존하는 FutureProvider
Dart
// 현재 사용자 ID
final currentUserIdProvider = StateProvider<String?>((ref) => null);
// 현재 사용자의 프로필 (사용자 ID가 있을 때만 로드)
final currentUserProfileProvider = FutureProvider<User?>((ref) async {
final userId = ref.watch(currentUserIdProvider);
if (userId == null) {
return null; // 로그인하지 않은 상태
}
final response = await http.get('/api/user/$userId');
return User.fromJson(response.data);
});
// 현재 사용자의 주문 내역 (프로필이 로드된 후에 실행)
final userOrdersProvider = FutureProvider<List<Order>>((ref) async {
final user = await ref.watch(currentUserProfileProvider.future);
if (user == null) {
return []; // 로그인하지 않은 상태
}
final response = await http.get('/api/orders?userId=${user.id}');
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => Order.fromJson(json)).toList();
});
하나의 FutureProvider가 다른 FutureProvider의 결과에 의존하는 방식이에요.
ref.watch().future
를 사용하면 다른 FutureProvider의 결과를 기다릴 수 있습니다.로컬 파일에서 데이터 읽기
Dart
// 앱 설정 파일 읽기
final appSettingsProvider = FutureProvider<AppSettings>((ref) async {
try {
final settingsFile = await rootBundle.loadString('assets/settings.json');
final Map<String, dynamic> data = jsonDecode(settingsFile);
return AppSettings.fromJson(data);
} catch (e) {
// 파일이 없으면 기본 설정 반환
return AppSettings.defaultSettings();
}
});
// 사용자 즐겨찾기 목록 (로컬 저장소에서 읽기)
final favoritesProvider = FutureProvider<List<String>>((ref) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getStringList('favorites') ?? [];
});
// 큰 이미지 파일 로드
final largeImageProvider = FutureProvider.family<Image, String>((ref, imagePath) async {
final bytes = await rootBundle.load(imagePath);
return Image.memory(bytes.buffer.asUint8List());
});
로컬 파일이나 SharedPreferences에서 데이터를 읽어오는 작업도 시간이 걸리므로 FutureProvider를 사용하는 것이 좋아요.
FutureProvider의 특별한 기능들
캐싱과 새로고침
Dart
class RefreshButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: [
ElevatedButton(
onPressed: () {
// 강제 새로고침 (캐시 무시하고 다시 실행)
ref.refresh(productsProvider);
},
child: Text('새로고침'),
),
ElevatedButton(
onPressed: () {
// 캐시 무효화 (다음에 사용할 때 다시 실행됨)
ref.invalidate(productsProvider);
},
child: Text('캐시 무효화'),
),
],
);
}
}
FutureProvider는 결과를 자동으로 캐시해서 같은 요청을 여러 번 하지 않아요. 하지만 데이터가 바뀌었을 수도 있으니 강제로 새로고침할 수 있는 기능도 제공합니다.
에러 처리
Dart
final userDataProvider = FutureProvider<UserData>((ref) async {
try {
final response = await http.get('/api/userdata');
if (response.statusCode == 401) {
throw AuthException('로그인이 필요합니다');
} else if (response.statusCode == 404) {
throw NotFoundException('사용자를 찾을 수 없습니다');
} else if (response.statusCode != 200) {
throw ServerException('서버 에러: ${response.statusCode}');
}
return UserData.fromJson(response.data);
} on SocketException {
throw NetworkException('네트워크 연결을 확인해주세요');
} catch (e) {
throw UnknownException('알 수 없는 에러가 발생했습니다: $e');
}
});
다양한 종류의 에러를 구분해서 처리하면 사용자에게 더 명확한 메시지를 보여줄 수 있어요.
주의사항
무한 루프 주의
Dart
// ❌ 이렇게 하지 마세요
final badProvider = FutureProvider<String>((ref) async {
final value = ref.watch(anotherFutureProvider.future);
// anotherFutureProvider가 badProvider를 참조하면 무한 루프!
return 'result: $value';
});
// ✅ 이렇게 하세요
final goodProvider = FutureProvider<String>((ref) async {
// 다른 Provider를 참조할 때는 순환 참조가 없는지 확인
final config = ref.watch(configProvider.future);
return await processWithConfig(config);
});
FutureProvider들이 서로를 참조할 때는 순환 참조가 발생하지 않도록 주의해야 해요.
너무 자주 새로고침하지 마세요
Dart
// ❌ 이렇게 하지 마세요
class BadWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// 버튼을 누를 때마다 새로고침 (과도함)
ref.refresh(heavyComputationProvider);
},
child: Text('계산하기'),
);
}
}
// ✅ 이렇게 하세요
class GoodWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final computationAsync = ref.watch(heavyComputationProvider);
return Column(
children: [
computationAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('에러: $error'),
data: (result) => Text('결과: ${result.length}'),
),
ElevatedButton(
onPressed: computationAsync.isLoading ? null : () {
// 로딩 중이 아닐 때만 새로고침
ref.refresh(heavyComputationProvider);
},
child: Text('다시 계산'),
),
],
);
}
}
무거운 작업을 너무 자주 새로고침하면 성능 문제가 생길 수 있어요.
다른 Provider와 비교
vs Provider
• Provider: 즉시 사용할 수 있는 값 (동기적)
• FutureProvider: 시간이 걸리는 비동기 작업의 결과
vs StateProvider
• StateProvider: 사용자가 직접 변경할 수 있는 상태
• FutureProvider: 비동기 작업의 결과 (한 번 완료되면 직접 변경 불가)
vs StreamProvider
• FutureProvider: 한 번 실행되고 끝나는 작업 (API 호출, 파일 읽기)
• StreamProvider: 계속해서 값이 들어오는 작업 (실시간 데이터, 타이머)
• Provider: 즉시 사용할 수 있는 값 (동기적)
• FutureProvider: 시간이 걸리는 비동기 작업의 결과
vs StateProvider
• StateProvider: 사용자가 직접 변경할 수 있는 상태
• FutureProvider: 비동기 작업의 결과 (한 번 완료되면 직접 변경 불가)
vs StreamProvider
• FutureProvider: 한 번 실행되고 끝나는 작업 (API 호출, 파일 읽기)
• StreamProvider: 계속해서 값이 들어오는 작업 (실시간 데이터, 타이머)
정리
FutureProvider는 Riverpod에서 비동기 작업의 결과를 관리하는 핵심 도구입니다. API 호출이나 파일 읽기처럼 시간이 걸리는 작업을 할 때 사용하세요.
핵심 포인트:
• 비동기 작업의 로딩/에러/완료 상태를 자동 관리
• 결과를 캐시해서 중복 요청 방지
•
•
실무에서 자주 사용하는 패턴:
• API에서 데이터 가져오기
• 로컬 파일이나 설정 읽기
• 다른 FutureProvider 결과에 의존하는 체인
• 에러 상황에 맞는 적절한 처리
주의할 점:
• 실시간 데이터는 StreamProvider 사용
• 순환 참조 방지
• 적절한 에러 처리와 사용자 경험 고려
FutureProvider만 잘 활용해도 대부분의 비동기 데이터 로딩 작업을 깔끔하게 처리할 수 있습니다. 로딩 스피너, 에러 메시지, 새로고침 기능까지 모두 간단하게 구현할 수 있어요.
다음 편 예고: 다음 글에서는 실시간으로 계속 변하는 데이터를 관리하는 StreamProvider에 대해 알아보겠습니다.
핵심 포인트:
• 비동기 작업의 로딩/에러/완료 상태를 자동 관리
• 결과를 캐시해서 중복 요청 방지
•
when()
메소드로 각 상태에 맞는 UI 표시•
refresh()
로 강제 새로고침 가능실무에서 자주 사용하는 패턴:
• API에서 데이터 가져오기
• 로컬 파일이나 설정 읽기
• 다른 FutureProvider 결과에 의존하는 체인
• 에러 상황에 맞는 적절한 처리
주의할 점:
• 실시간 데이터는 StreamProvider 사용
• 순환 참조 방지
• 적절한 에러 처리와 사용자 경험 고려
FutureProvider만 잘 활용해도 대부분의 비동기 데이터 로딩 작업을 깔끔하게 처리할 수 있습니다. 로딩 스피너, 에러 메시지, 새로고침 기능까지 모두 간단하게 구현할 수 있어요.
다음 편 예고: 다음 글에서는 실시간으로 계속 변하는 데이터를 관리하는 StreamProvider에 대해 알아보겠습니다.