🎯 면접에서 받은 깨달음
어느 날 면접을 보러 갔는데 감사하게도 제 프로젝트, 포트폴리오, 블로그 글을 모두 살펴봐주신 면접관님을 만나게 되었습니다.
그때 받은 질문 하나가 아래와 같습니다.
🧑🏻💻면접관: 제가 프로젝트를 다 살펴보았는데요. 날씨 앱에 아주 치명적인 문제가 있습니다. API는 많이 호출하면 호출할수록 서버 부하가 올 수 있기 때문에 정해진 쿼타가 있습니다. 그 쿼타의 존재를 알고 있었나요?
👩🏻 : ...몰랐습니다.
🧑🏻💻면접관: 쿼타가 있기 때문에 많은 트래픽이 발생한다면 서비스가 멈출 수 있는 아주 위험한 상황이 올 수 있어요. 그 문제점을 이제라도 알게 되었으니 어떻게 수정할 생각인가요? 이미 답을 알고 있을 테니 한 번 생각해 보고 말해주세요.
👩🏻 : ...로컬스토리지나 세션스토리지 등을 이용하면 될 것 같습니다.
🧑🏻💻면접관: 맞습니다.
📱 기존 날씨 앱의 문제점
제가 만든 날씨 앱은 간단한 앱이었지만, 총 6개의 API를 사용하고 있었습니다.
사용 중인 API들
- HTML5 Geolocation: 사용자의 현재 위치 좌표 획득을 위해 사용
- 카카오 맵 API: 위도/경도를 주소로 변환을 위해 사용
- 기상청 종관예보 API: 어제의 온도를 가져오기 위해 사용 (11시 갱신)
- 기상청 단기예보 API: 오늘, 내일, 글피, 그글피의 데이터(온도, 날씨)를 가져오기 위해 사용
- 기상청 중기육상예보 API: 주간 날씨 데이터를 가져오기 위해 사용 (6시/18시 갱신)
- 기상청 중기기온조회 API: 주간 온도 데이터를 가져오기 위해 사용 (6시/18시 갱신)
주가 되는 기상청 API 4개는 모두 데이터 갱신 주기가 달랐습니다. 그렇기 때문에 실시간으로 데이터를 받아와야 한다고 생각해 캐싱 기능을 고려하지 않았습니다. 그 잘못된 생각으로 사용자가 페이지를 새로고침할 때마다 API를 매번 새롭게 호출되고 있었습니다..
😱 쿼타의 존재를 알게 된 후
면접 중이라 크게 당황했으나 그 와중에서 굉장히 좋은 경험을 하고 있다는 것을 깨달았습니다. 이렇게까지 친절하게 알려주신다는 것에 감동받았습니다. 그때의 일을 계기로 이 날씨 앱을 수정해야겠다는 마음을 먹게 되었고, 그리 어렵지 않게 코드를 수정하게 되었습니다.
API 쿼타의 존재를 알게 된 후, 제가 사용하고 있는 기상청 API는 모두 아래와 같은 안내를 하고 있었습니다.
왜 그때는 보이지 않았는지🤦🏻♀️...
🤔 어떤 캐싱 방법이 최적일까?
이제는 로컬스토리지, 세션스토리지 등 여러 방법 중 '어떤 방법이 나의 프로젝트에 가장 적합할까?'를 고민하게 되었습니다.
1. LocalStorage
- 장점: 영구 저장 (직접 삭제하기 전까지), 5-10MB 용량, 구현 간단
- 단점: 동기적 처리 (대용량 데이터시 느림)
- 날씨 앱에 적합한 이유: 날씨 데이터는 작고, 일정 시간 캐싱이 필요
2. SessionStorage
- 장점: 탭/브라우저 닫으면 자동 삭제, 임시 데이터에 적합
- 단점: 새 탭에서는 데이터 없음
- 사용 시나리오: 임시 검색 결과, 폼 데이터 등
- 결론: 브라우저 탭을 닫으면 데이터가 삭제되므로 날씨 앱에는 적합하지 않음
3. Cookie
- 장점: 서버로 자동 전송, 만료 시간 설정 가능
- 단점: 4KB 용량 제한, 매 요청마다 전송되어 트래픽 증가
- 사용 시나리오: 인증 토큰, 작은 설정값
4. IndexedDB
- 장점: 더 큰 데이터 저장 가능
- 단점: 구현이 복잡함
따라서 간단한 날씨 앱에는 로컬 스토리지가 적절한 선택이었습니다.
🔧 캐시 구현하기
그리하여 캐시에 관련된 cache.ts를 만들어 캐시를 가져오는 함수와 없다면 세팅하는 함수를 만들었습니다.
첫 번째 구현
import { userLocation } from "../types/type";
const CACHE_DURATION = 1000 * 60 * 60 * 24;
export const getCachedWeather = (location: userLocation) => {
const key = `weather_${location.x}_${location.y}`;
const cached = localStorage.getItem(key);
if (!cached) return null;
const { data, time } = JSON.parse(cached);
if (Date.now() - time > CACHE_DURATION) {
localStorage.removeItem(key);
return null;
}
return data;
};
export const setCachedWeather = (location: userLocation, data: any) => {
const key = `weather_${location.x}_${location.y}`;
const cacheData = {
data,
time: Date.now(),
};
localStorage.setItem(key, JSON.stringify(cacheData));
};
🚨 문제 발견!
하지만 한 가지 더 문제점이 발견되었습니다.
00:00시, 다시 말해 자정이 지나면 날짜가 바뀌는데 캐시는 여전히 "어제에 해당하는 날짜에 오늘"을 보여줄 수 있는 문제가 발생했습니다.
6월 18일 23:30에 캐시 저장
어제: 6/17, 오늘: 6/18, 내일: 6/19
6월 19일 00:10에 접속 (날짜 바뀜!)
실제 - 어제: 6/18, 오늘: 6/19, 내일: 6/20
하지만 캐시는 - 어제: 6/17, 오늘: 6/18, 내일: 6/20 ❌
캐시 key에 날짜 정보가 없어서, 6월 18일에 저장된 캐시가 6월 19일에도 그대로 사용되는 문제인 것입니다.
key에 오늘 날짜를 주어 해결하여 자정이 되면 캐시를 다시 저장하게 수정하였습니다.
최종 구현
import { DATES } from "../constants/date";
import { userLocation } from "../types/type";
import { getFormattedDate } from "./date";
type CacheType = "shortTerm" | "weekly";
// 캐시 지속 시간 30분
const CACHE_DURATION = 30 * 60 * 1000;
export const getCachedWeather = (location: userLocation, type: CacheType) => {
const today = getFormattedDate(0); // 오늘 날짜 가져오기
const key = `weather_${type}_${today}_${location.x}_${location.y}`; // 날짜와 타입 포함
const cached = localStorage.getItem(key);
if (!cached) return null;
const { data, time } = JSON.parse(cached);
// 30분 지났으면 캐시 삭제
if (Date.now() - time > CACHE_DURATION) {
localStorage.removeItem(key);
return null;
}
return data;
};
export const setCachedWeather = (
location: userLocation,
data: any,
type: CacheType
) => {
const today = getFormattedDate(0); // 오늘 날짜 가져오기
const key = `weather_${type}_${today}_${location.x}_${location.y}`; // 날짜와 타입 포함
const cacheData = {
data,
time: Date.now(),
};
localStorage.setItem(key, JSON.stringify(cacheData));
};
🎯 타입 안전성 개선
그리고 `setCachedWeather`의 매개변수 `data`가 `any`로 되어 있는 점이 굉장히 찜찜했습니다.
여기에서 `data`는 `CacheType`에 따라 완전히 다른 데이터 구조를 가지게 됩니다. 단기예보 데이터일 때는 배열 형태의 날씨 데이터, 주간예보 데이터일 때는 주간 온도와 주간 날씨 아이콘을 가진 튜플 형태인데, 이런 복잡한 타입을 정의하기 어려워 급하게 `any` 타입을 쓰게 되었습니다.
하지만 `any`를 쓰는 것은 TypeScript를 쓰는 이유가 없어지는 것으로 지양해야 하므로 `unknown` 타입으로 수정하게 되었습니다.
`any`와 `unknown` 차이
any | unknown |
모든 타입 할당 가능 | 모든 타입 할당 가능 |
타입 체크 없이 모든 연산 허용 | 타입 가드 후에만 연산 허용 |
타입 안전성 ❌ | 타입 안전성 ✅ |
📈 최종 결과
Before (캐싱 전)
- 사용자가 새로고침할 때마다 4개 API 모두 호출
- API 쿼타 초과 위험성 높음
After (캐싱 후)
- 같은 위치, 같은 날짜는 캐시 활용
- API 호출량 대폭 감소
- 사용자 경험 개선 (빠른 로딩)
🎓 배운 점
- API 쿼타의 중요성: 모든 외부 API에는 제한이 있다는 것을 항상 고려하기
- 캐싱 전략: 데이터의 특성을 고려한 설계의 중요성
- 타입 안전성: `any` 대신 `unknown`으로 더 안전한 코드 작성
- 사용자 관점: 개발자 편의가 아닌 실제 서비스 운영 관점에서 생각하기
면접에서 받은 피드백 덕분에 더 나은 개발자가 될 수 있었습니다.
이제는 모든 외부 API 사용 시 쿼타 및 그에 따른 해결책을 고려하게 되었고, 사용자 관점에서 서비스를 바라보는 시각을 기를 수 있었습니다. 또한 좋은 면접관을 만나는 것도 정말 큰 행운이라는 걸 깨달았습니다. 면접 때도 감사하다고 말씀드렸지만, 이 자리를 빌려 다시 한번 감사합니다🙇🏻♀️
'코딩 > 트러블슈팅' 카테고리의 다른 글
Notice와 WOD 메뉴의 공통 컴포넌트화 과정에서의 TanStack Query (1) | 2025.01.27 |
---|---|
"어제보다 더 춥다고?": 6개의 API를 사용한 날씨 비교 서비스 개발기 (1) | 2025.01.19 |
크로스핏의 다양한 기록 정렬: 17:17과 3R+12, 누가 더 잘한 걸까? (0) | 2025.01.03 |
Tanstack Query - prefetchQuery로 공휴일 데이터 패칭 최적화하기 (0) | 2024.11.19 |
React-Quill 게시물 저장 방식 개선하기: HTML에서 Delta로 (0) | 2024.11.10 |