캐싱이 필요한 이유
운세 데이터는 자연스러운 캐싱 기회를 제공합니다:| 데이터 유형 | 캐시 기간 | 이유 |
|---|---|---|
| 사주 계산 | 영구 | 결정론적 - 동일 입력 = 동일 출력 |
| 일일 운세 | 24시간 | 날짜에 따라 매일 변경 |
| 성격 프로필 | 영구 | 생년월일 기반, 절대 변경 안됨 |
캐싱 아키텍처
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 클라이언트 │────▶│ 백엔드 │────▶│ API │
│ 캐시 │ │ 캐시 │ │ (Redis) │
│ (localStorage)│ │ (Redis) │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
↓ ↓ ↓
1시간 24시간 내장
클라이언트 캐싱
브라우저 localStorage
const CACHE_VERSION = 1;
const CACHE_TTL = 60 * 60 * 1000; // 1시간
interface CacheEntry<T> {
data: T;
expiry: number;
}
function getCached<T>(key: string): T | null {
const cacheKey = `fortune_v${CACHE_VERSION}_${key}`;
const cached = localStorage.getItem(cacheKey);
if (!cached) return null;
const entry: CacheEntry<T> = JSON.parse(cached);
if (Date.now() > entry.expiry) {
localStorage.removeItem(cacheKey);
return null;
}
return entry.data;
}
function setCache<T>(key: string, data: T, ttlMs = CACHE_TTL): void {
const cacheKey = `fortune_v${CACHE_VERSION}_${key}`;
const entry: CacheEntry<T> = {
data,
expiry: Date.now() + ttlMs,
};
localStorage.setItem(cacheKey, JSON.stringify(entry));
}
캐싱을 사용한 React Hook
function useCachedFortune(saju: Saju, userName: string) {
const [fortune, setFortune] = useState<FortuneData | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const today = new Date().toISOString().split('T')[0];
const cacheKey = `${saju.dayMaster.stem}_${today}`;
// 캐시 먼저 확인
const cached = getCached<FortuneData>(cacheKey);
if (cached) {
setFortune(cached);
setIsLoading(false);
return;
}
// API에서 조회
fetchFortune(saju, userName)
.then(data => {
setCache(cacheKey, data);
setFortune(data);
})
.finally(() => setIsLoading(false));
}, [saju, userName]);
return { fortune, isLoading };
}
서버 캐싱
Redis 구현
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function getCachedFortune(
dayMaster: string,
date: string
): Promise<FortuneData | null> {
const key = `fortune:${dayMaster}:${date}`;
const cached = await redis.get(key);
return cached ? JSON.parse(cached) : null;
}
async function cacheFortune(
dayMaster: string,
date: string,
data: FortuneData
): Promise<void> {
const key = `fortune:${dayMaster}:${date}`;
const ttl = getSecondsUntilMidnight();
await redis.setex(key, ttl, JSON.stringify(data));
}
function getSecondsUntilMidnight(): number {
const now = new Date();
const midnight = new Date(now);
midnight.setHours(24, 0, 0, 0);
return Math.floor((midnight.getTime() - now.getTime()) / 1000);
}
캐시 키 전략
일관된 키 형식을 사용하세요:// 좋음: 모든 관련 변수 포함
const key = `fortune:${dayMaster}:${date}:${model}`;
// 나쁨: 날짜 누락 - 오래된 데이터 반환
const key = `fortune:${dayMaster}`;
dayMaster- 사용자의 일간(日干) (10가지 가능한 값)date- 오늘 날짜 (YYYY-MM-DD 형식)model- 사용된 AI 모델 (haiku, sonnet, gpt4o)
캐시 워밍
사용자 요청 전에 운세를 미리 생성하세요:// cron으로 매일 자정에 실행
async function warmCache() {
const dayMasters = ['갑', '을', '병', '정', '무', '기', '경', '신', '임', '계'];
const today = new Date().toISOString().split('T')[0];
for (const dayMaster of dayMasters) {
const fortune = await generateFortune(dayMaster, today);
await cacheFortune(dayMaster, today, fortune);
// API 과부하 방지를 위한 요청 제한
await sleep(1000);
}
console.log(`${dayMasters.length}개 일간에 대한 캐시 워밍 완료`);
}
10개의 일간만 있으므로, 하루의 모든 가능한 운세를 단 10번의 API 호출로 미리 생성할 수 있습니다.
Upstash Redis (서버리스)
서버리스 배포에는 Upstash Redis를 사용하세요:import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
// ioredis와 동일한 API
await redis.set('key', 'value', { ex: 86400 });
const value = await redis.get('key');
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx
캐시 무효화
시간 기반 (권장)
자정에 만료되도록 TTL 설정:const ttl = getSecondsUntilMidnight();
await redis.setex(key, ttl, data);
수동 무효화
관리자 재설정 또는 콘텐츠 업데이트용:async function invalidateFortuneCache(dayMaster?: string) {
if (dayMaster) {
// 특정 일간 무효화
const keys = await redis.keys(`fortune:${dayMaster}:*`);
if (keys.length) await redis.del(...keys);
} else {
// 모든 운세 무효화
const keys = await redis.keys('fortune:*');
if (keys.length) await redis.del(...keys);
}
}
버전 관리 캐싱
캐시 형식 변경을 우아하게 처리하세요:const CACHE_VERSION = 2; // 형식 변경 시 증가
function getCacheKey(base: string): string {
return `v${CACHE_VERSION}:${base}`;
}
// 이전 키 (v1:fortune:...)는 자동으로 무시됨
// 새 키 (v2:fortune:...)가 사용됨
비용 절감 효과
적절한 캐싱 사용 시:| 시나리오 | 일일 API 호출 | 일일 비용 |
|---|---|---|
| 캐싱 없음 | 100,000 | ~$130 |
| 클라이언트 캐시만 | 50,000 | ~$65 |
| + 서버 캐시 | 10,000 | ~$13 |
| + 캐시 워밍 | 10 | ~$0.01 |
10개 일간으로 캐시 워밍 시 트래픽에 관계없이 하루 약 $0.01만 소요됩니다.
캐시 디버깅
캐시 상태 확인
// API가 meta에 캐시 상태 반환
const response = await fetch('/api/daily-fortune', { ... });
const { data, meta } = await response.json();
console.log('캐시에서:', meta.cached);
console.log('캐시 키:', meta.cacheKey);
새 데이터 강제 조회
// 캐시 무효화 파라미터 추가
const response = await fetch('/api/daily-fortune?refresh=true', { ... });
모범 사례
권장
- 일간 + 날짜로 캐시
- 데이터 신선도에 맞는 TTL 사용
- 캐시 워밍 구현
- 캐시 키 버전 관리
비권장
- 공유 캐시에 사용자별 데이터 저장
- 시간에 민감한 데이터에 무한 TTL 사용
- 개발 환경에서 캐시 건너뛰기
- 캐시 미스 처리 누락