
백엔드 담당자가 Open Weather API를 활용하여 구현한 API가 상당히 특이했다. 시간대별로 응답 결과가 달랐는데, 0~3시에 호출할 때 아래와 같이 가장 많은 시간대별 날씨 정보를 얻을 수 있었고, 21~23시에 조회 시 빈 배열을 반환했다. 메인페이지가 외부 API에 강하게 의존하고 있다는 문제도 있었기 때문에 캐싱이 필수적이었다.
// 0~3시에 호출
{
"today_weather": [
{
"time": "2025-05-20 03:00",
"score": 75.5,
"체감온도": "17.5℃ (보통)",
"풍속": "4.0m/s (보통)",
"습도": "89% (나쁨)",
"구름": "100% (많음)",
"미세먼지": "25.88μg/m³ (좋음)",
"초미세먼지": "21.19μg/m³ (보통)"
},
{
"time": "2025-05-20 06:00",
"score": 76.0,
"체감온도": "17.4℃ (보통)",
"풍속": "3.3m/s (보통)",
"습도": "93% (나쁨)",
"구름": "100% (많음)",
"미세먼지": "25.88μg/m³ (좋음)",
"초미세먼지": "21.19μg/m³ (보통)"
},
{
"time": "2025-05-20 09:00",
"score": 76.6,
"체감온도": "19.3℃ (보통)",
"풍속": "2.0m/s (좋음)",
"습도": "93% (나쁨)",
"구름": "100% (많음)",
"미세먼지": "25.88μg/m³ (좋음)",
"초미세먼지": "21.19μg/m³ (보통)"
},
{
"time": "2025-05-20 12:00",
"score": 77.6,
"체감온도": "21.2℃ (보통)",
"풍속": "3.5m/s (보통)",
"습도": "86% (나쁨)",
"구름": "100% (많음)",
"미세먼지": "25.88μg/m³ (좋음)",
"초미세먼지": "21.19μg/m³ (보통)"
},
{
"time": "2025-05-20 15:00",
"score": 73.0,
"체감온도": "24.9℃ (높음)",
"풍속": "4.0m/s (보통)",
"습도": "64% (나쁨)",
"구름": "79% (많음)",
"미세먼지": "25.88μg/m³ (좋음)",
"초미세먼지": "21.19μg/m³ (보통)"
},
{
"time": "2025-05-20 18:00",
"score": 75.2,
"체감온도": "22.8℃ (높음)",
"풍속": "3.3m/s (보통)",
"습도": "71% (나쁨)",
"구름": "3% (적음)",
"미세먼지": "25.88μg/m³ (좋음)",
"초미세먼지": "21.19μg/m³ (보통)"
},
{
"time": "2025-05-20 21:00",
"score": 79.3,
"체감온도": "20.0℃ (보통)",
"풍속": "1.6m/s (좋음)",
"습도": "84% (나쁨)",
"구름": "5% (적음)",
"미세먼지": "25.88μg/m³ (좋음)",
"초미세먼지": "21.19μg/m³ (보통)"
}
]
}
// 21~23시에 호출
{
"today_weather": [ ]
}
static const String CACHE_KEY_DATA = 'weather_data'; // 날씨 데이터
static const String CACHE_KEY_DATE = 'weather_date'; // 캐시 날짜
static Future<List<WeatherData>> fetchWeatherData(String? token, {bool forceRefresh = false}) async {
if (token == null) return [];
final now = DateTime.now();
final today = DateFormat('yyyy-MM-dd').format(now);
final currentHour = now.hour;
final prefs = await SharedPreferences.getInstance();
// API 비가용 시간대 판단 (21-23시)
final isApiUnavailableTime = currentHour >= 21 && currentHour <= 23;
// 캐시 우선 전략
if (!forceRefresh) {
final cachedDataStr = prefs.getString(CACHE_KEY_DATA);
final cachedDateStr = prefs.getString(CACHE_KEY_DATE);
if (cachedDataStr != null && cachedDateStr == today) {
debugPrint('오늘 날짜의 캐시된 데이터 사용');
try {
return _parseCachedData(cachedDataStr);
} catch (e) {
debugPrint('캐시된 데이터 파싱 오류: $e');
}
}
}
// API 비가용 시간대 특별 처리
if (isApiUnavailableTime) {
debugPrint('API 비가용 시간대 (21시~23시): 캐시된 데이터 사용');
return _getCachedDataOrEmpty(prefs, today);
}
// 실제 API 호출
return await _callWeatherAPI(token, prefs, today);
}
// 날짜 변경 감지 및 자동 갱신
static Future<bool> checkAndUpdateIfNeeded(String? token) async {
if (token == null) return false;
final prefs = await SharedPreferences.getInstance();
final cachedDateStr = prefs.getString(CACHE_KEY_DATE);
final now = DateTime.now();
final today = DateFormat('yyyy-MM-dd').format(now);
// 날짜 변경 감지
if (cachedDateStr == null || cachedDateStr != today) {
debugPrint('날짜가 변경되어 데이터 업데이트: $cachedDateStr -> $today');
final currentHour = now.hour;
if (currentHour >= 21) {
// 21시 이후에는 날짜만 업데이트
await prefs.setString(CACHE_KEY_DATE, today);
return false;
}
// API 호출 및 캐싱
await fetchWeatherData(token, forceRefresh: true);
return true;
}
return false;
}
// 자정 직후 최적 데이터 수집
static Future<void> fetchMidnightData(String? token) async {
if (token == null) return;
final now = DateTime.now();
final hour = now.hour;
// 자정 직후 시간인지 확인 (0시~2시)
if (hour >= 0 && hour < 3) {
debugPrint('자정 직후 시간에 데이터 가져오기 시도');
await fetchWeatherData(token, forceRefresh: true);
}
}
0-3시에 응답 데이터가 가장 많기 때문에 해당 시간대에 데이터를 선제적으로 확보해야 한다.
class _FirstStatusInfoState extends State<FirstStatusInfo> with WidgetsBindingObserver {
Timer? _midnightTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initializeData();
_setupMidnightTimer(); // 자정 타이머 설정
}
// 앱이 포그라운드로 복귀 시 날짜 확인
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_checkDateAndRefresh();
}
}
// 자정 타이머 설정
void _setupMidnightTimer() {
final now = DateTime.now();
final tomorrow = DateTime(now.year, now.month, now.day + 1);
final duration = tomorrow.difference(now);
_midnightTimer = Timer(duration, () {
debugPrint('자정이 되어 데이터 새로고침');
_refreshData(forceRefresh: true);
_setupMidnightTimer(); // 다음 자정 타이머 설정
});
}
}
WidgetsBindingObserver를 통해 앱의 포그라운드/백그라운드 전환 감지Future<void> _initializeData() async {
final token = Provider.of<AppState>(context, listen: false).accessToken;
final now = DateTime.now();
final hour = now.hour;
final isPostMidnight = hour >= 0 && hour < 3;
setState(() {
_weatherDataFuture = Future(() async {
try {
if (isPostMidnight) {
// 자정 직후
final prefs = await SharedPreferences.getInstance();
final lastMidnightRefresh = prefs.getString('last_midnight_refresh');
final today = DateFormat('yyyy-MM-dd').format(now);
if (lastMidnightRefresh != today) {
debugPrint('자정 직후 첫 방문: 강제 새로고침');
await prefs.setString('last_midnight_refresh', today);
return await WeatherService.fetchWeatherData(token, forceRefresh: true);
} else {
debugPrint('자정 직후 재방문: 이미 갱신했으므로 캐시 사용');
return await WeatherService.fetchWeatherData(token, forceRefresh: false);
}
} else {
// 일반 시간대 처리
final dateChanged = await WeatherService.checkAndUpdateIfNeeded(token);
if (dateChanged) {
return await WeatherService.fetchWeatherData(token, forceRefresh: true);
} else {
return await WeatherService.fetchWeatherData(token, forceRefresh: false);
}
}
} catch (e) {
return <WeatherData>[];
}
});
});
}
Flutter는 처음이기도 하고 API 응답 방식이 특이해서 어떻게 해야 할지 고민되었는데 다행히 완성은 했다...시간에 쫓겨서 정신 없이 개발했지만...