캐싱이 필요한 이유
운세 데이터는 자연스러운 캐싱 기회를 제공합니다:
데이터 유형 캐시 기간 이유 사주 계산 영구 결정론적 - 동일 입력 = 동일 출력 일일 운세 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 사용
개발 환경에서 캐시 건너뛰기
캐시 미스 처리 누락