Flutter Dio 인터셉터를 이용한 API 응답 캐싱
Source: Dev.to
(위 링크에 있는 글의 전체 내용을 제공해 주시면 한국어로 번역해 드리겠습니다.)
How the Caching Interceptor Works
| Feature | Description |
|---|---|
| Persist GET responses | GET 메서드 응답만 캐시됩니다. 이는 일반적으로 데이터 조회 작업을 나타내며 캐시해도 안전합니다. |
| Network‑first strategy | 네트워크 연결이 가능한 경우, 인터셉터가 API에서 최신 데이터를 가져옵니다. |
| Fallback to cache | 네트워크 오류가 발생하면 인터셉터가 자동으로 이전에 캐시된 응답으로 폴백합니다. |
| Automatic cache updates | 성공적인 응답은 향후 오프라인 접근을 위해 자동으로 캐시를 업데이트합니다. |
1️⃣ Storage Abstraction Layer
스토리지 추상화를 만들면 인터셉터 로직을 건드리지 않고도 기본 스토리지 메커니즘(예: SharedPreferences, Hive, Secure Storage)을 교체할 수 있는 유연성을 얻을 수 있습니다.
/// Simple key‑value storage interface for caching
abstract class CacheStorage {
Future get(String key);
Future set(String key, String value);
Future remove(String key);
}
/// Implementation using SharedPreferences (via AppPreference)
class CacheStorageImpl implements CacheStorage {
CacheStorageImpl(this._pref);
final AppPreference _pref;
@override
Future get(String key) async => _pref.getString(key);
@override
Future set(String key, String value) async =>
await _pref.setString(key, value);
@override
Future remove(String key) async => await _pref.remove(key);
}
2️⃣ 캐싱 인터셉터 구현
import 'dart:convert';
import 'package:dio/dio.dart';
class CacheInterceptor implements Interceptor {
final CacheStorage storage;
CacheInterceptor(this.storage);
// -------------------------------------------------
// 1️⃣ onRequest – let the request pass through unchanged
// -------------------------------------------------
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) {
handler.next(options);
}
// -------------------------------------------------
// 2️⃣ onResponse – cache successful GET responses
// -------------------------------------------------
@override
void onResponse(
Response response, ResponseInterceptorHandler handler) {
if (response.requestOptions.method.toUpperCase() == 'GET') {
_saveResponseToCache(response);
}
handler.next(response);
}
// -------------------------------------------------
// 3️⃣ onError – try to serve cached data on network errors
// -------------------------------------------------
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Attempt to retrieve cached data for GET requests
if (err.requestOptions.method.toUpperCase() == 'GET') {
final cached = await _getCachedResponse(err.requestOptions);
if (cached != null) {
// Return cached response instead of the error
return handler.resolve(cached);
}
}
// Show a “no internet” modal for network‑related errors
if (_isNetworkError(err)) {
await NoInternetModalWidget.show();
}
// Propagate the original error if no cache is available
handler.next(err);
}
// -------------------------------------------------
// Helper methods
// -------------------------------------------------
bool _isNetworkError(DioException err) {
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.sendTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.connectionError;
}
Future _getCachedResponse(RequestOptions options) async {
try {
final cacheKey = '${options.uri}';
final cacheEntry = await storage.get(cacheKey);
if (cacheEntry == null) return null;
final Map decodedCache = jsonDecode(cacheEntry);
return Response(
requestOptions: options,
data: decodedCache,
statusCode: 200,
);
} catch (_) {
// Corrupted cache – ignore and let the error propagate
return null;
}
}
Future _saveResponseToCache(Response response) async {
final cacheKey = '${response.realUri}';
final cacheEntry = jsonEncode(response.data);
await storage.set(cacheKey, cacheEntry);
}
}
라이프사이클 메서드 요약
| Method | What It Does |
|---|---|
| onRequest | 요청을 변경 없이 그대로 통과시킵니다. |
| onResponse | 요청이 GET인 경우 전체 URI를 키로 사용해 응답을 캐시에 저장합니다. |
| onError | 실패한 GET 요청에 대해 캐시된 응답을 가져오려고 시도합니다. 찾으면 캐시 데이터를 사용해 오류를 해결하고, 그렇지 않으면 (네트워크 오류인 경우) “no internet” 모달을 표시하고 오류를 전파합니다. |
3️⃣ Dio와 인터셉터 사용하기
import 'package:dio/dio.dart';
class ApiClient {
late final Dio dio;
ApiClient(AppPreference appPreference) {
dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
),
);
// 1️⃣ Create the cache storage implementation
final cacheStorage = CacheStorageImpl(appPreference);
// 2️⃣ Add the cache interceptor
dio.interceptors.add(CacheInterceptor(cacheStorage));
// 3️⃣ (Optional) Add other interceptors such as logging, auth, etc.
dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
),
);
}
}
이제 ApiClient를 통해 수행되는 모든 GET 요청은 다음을 수행합니다:
- 먼저 네트워크 시도 – 가능한 경우 최신 데이터를 가져옵니다.
- 응답 캐시 – 성공적인 결과를 오프라인 사용을 위해 저장합니다.
- 캐시로 대체 – 네트워크가 실패할 경우 자동으로 캐시된 데이터를 제공합니다.
🎉 정리
- CacheInterceptor는 Dio 기반 Flutter 앱에 오프라인 지원을 손쉽게 추가할 수 있는 깔끔하고 재사용 가능한 방법을 제공합니다.
CacheStorage로 스토리지를 추상화함으로써,SharedPreferences에서 Hive, SQLite, 혹은 보안 스토리지로 단 한 줄의 코드만으로 전환할 수 있습니다.- 네트워크 우선 전략을 사용하면 사용자가 온라인일 때 항상 최신 정보를 확인할 수 있으며, 오프라인일 때도 기능적인 경험을 제공할 수 있습니다.
Dio 인터셉터를 이용한 투명 캐싱
class ProductRepository {
final ApiClient apiClient;
ProductRepository(this.apiClient);
Future> getProducts() async {
try {
final response = await apiClient.dio.get('/products');
return (response.data as List)
.map((json) => Product.fromJson(json))
.toList();
} catch (e) {
// Handle error or rethrow
rethrow;
}
}
}
네트워크가 사용 가능한 경우, 최신 데이터를 가져와 캐시합니다.
오프라인일 때는 오류를 발생시키지 않고 캐시된 응답을 반환합니다.
🔄 투명 캐싱
기존 API 호출을 변경할 필요 없이 캐싱이 자동으로 작동합니다.
📴 오프라인 복원력
네트워크 장애 시 자동으로 캐시된 데이터로 대체됩니다.
🎯 클린 아키텍처
스토리지 추상화를 통한 관심사의 분리.
🔌 쉬운 통합
의존성 주입 패턴과 간단히 통합할 수 있습니다.
가능한 확장
캐시 신선도 판단을 위한 타임스탬프 추가
Future _saveResponseToCache(Response response) async {
final cacheKey = '${response.realUri}';
final cacheData = {
'data': response.data,
'timestamp': DateTime.now().millisecondsSinceEpoch,
};
final cacheEntry = jsonEncode(cacheData);
await storage.set(cacheKey, cacheEntry);
}
LRU(Least Recently Used) 캐시 제거 구현
class CacheManager {
final int maxCacheSize;
final CacheStorage storage;
Future evictOldestCache() async {
// Implementation for removing oldest cached entries
}
}
엔드포인트별 다른 캐싱 동작 구성
class CacheConfig {
final Duration? ttl;
final bool enabled;
CacheConfig({this.ttl, this.enabled = true});
}
// Usage
final cacheConfig = {
'/products': CacheConfig(ttl: Duration(hours: 1)),
'/user/profile': CacheConfig(enabled: false),
};
Dio용 캐싱 인터셉터를 구현하면 Flutter 애플리케이션에 큰 이점을 제공합니다. 이 접근 방식은 오프라인‑우선 경험을 견고하게 제공하여 사용자가 인터넷 연결이 없어도 이전에 로드한 콘텐츠에 계속 접근할 수 있게 합니다.
스토리지‑추상화 패턴은 유연성과 유지보수성을 보장하고, 인터셉터는 비즈니스 로직을 어지럽히지 않으면서 캐시 관리의 복잡성을 매끄럽게 처리합니다. 네트워크‑우선 전략은 가능한 경우 최신 데이터를 보장하면서도 연결 문제 발생 시 기능을 유지합니다.
핵심 요점: 이 인터셉터는 네트워크 불안정성을 우아하게 처리하고 뛰어난 사용자 경험을 제공하는 탄탄한 Flutter 앱을 구축하기 위한 견고한 기반을 제공합니다. 위의 기본 구현을 시작점으로 삼고, 필요에 맞게 확장하세요.
이 글이 도움이 되었나요? Flutter 개발자 동료와 공유해 주세요!
행복한 코딩 되세요!