> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sajuapi.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 캐싱 전략

> 효과적인 캐싱으로 성능 최적화 및 비용 절감

## 캐싱이 필요한 이유

운세 데이터는 자연스러운 캐싱 기회를 제공합니다:

| 데이터 유형 | 캐시 기간 | 이유                   |
| ------ | ----- | -------------------- |
| 사주 계산  | 영구    | 결정론적 - 동일 입력 = 동일 출력 |
| 일일 운세  | 24시간  | 날짜에 따라 매일 변경         |
| 성격 프로필 | 영구    | 생년월일 기반, 절대 변경 안됨    |

## 캐싱 아키텍처

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   클라이언트 │────▶│   백엔드    │────▶│  API        │
│   캐시      │     │   캐시      │     │  (Redis)    │
│ (localStorage)│   │  (Redis)    │     │             │
└─────────────┘     └─────────────┘     └─────────────┘
      ↓                   ↓                   ↓
   1시간              24시간             내장
```

## 클라이언트 캐싱

### 브라우저 localStorage

```typescript theme={null}
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

```typescript theme={null}
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 구현

```typescript theme={null}
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);
}
```

### 캐시 키 전략

일관된 키 형식을 사용하세요:

```typescript theme={null}
// 좋음: 모든 관련 변수 포함
const key = `fortune:${dayMaster}:${date}:${model}`;

// 나쁨: 날짜 누락 - 오래된 데이터 반환
const key = `fortune:${dayMaster}`;
```

**키 구성요소:**

* `dayMaster` - 사용자의 일간(日干) (10가지 가능한 값)
* `date` - 오늘 날짜 (YYYY-MM-DD 형식)
* `model` - 사용된 AI 모델 (haiku, sonnet, gpt4o)

## 캐시 워밍

사용자 요청 전에 운세를 미리 생성하세요:

```typescript theme={null}
// 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}개 일간에 대한 캐시 워밍 완료`);
}
```

<Note>
  10개의 일간만 있으므로, 하루의 모든 가능한 운세를 단 10번의 API 호출로 미리 생성할 수 있습니다.
</Note>

## Upstash Redis (서버리스)

서버리스 배포에는 Upstash Redis를 사용하세요:

```typescript theme={null}
import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

// ioredis와 동일한 API
await redis.set('key', 'value', { ex: 86400 });
const value = await redis.get('key');
```

**환경 변수:**

```bash theme={null}
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx
```

## 캐시 무효화

### 시간 기반 (권장)

자정에 만료되도록 TTL 설정:

```typescript theme={null}
const ttl = getSecondsUntilMidnight();
await redis.setex(key, ttl, data);
```

### 수동 무효화

관리자 재설정 또는 콘텐츠 업데이트용:

```typescript theme={null}
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);
  }
}
```

## 버전 관리 캐싱

캐시 형식 변경을 우아하게 처리하세요:

```typescript theme={null}
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 |

<Tip>
  10개 일간으로 캐시 워밍 시 트래픽에 관계없이 **하루 약 \$0.01**만 소요됩니다.
</Tip>

## 캐시 디버깅

### 캐시 상태 확인

```typescript theme={null}
// API가 meta에 캐시 상태 반환
const response = await fetch('/api/daily-fortune', { ... });
const { data, meta } = await response.json();

console.log('캐시에서:', meta.cached);
console.log('캐시 키:', meta.cacheKey);
```

### 새 데이터 강제 조회

```typescript theme={null}
// 캐시 무효화 파라미터 추가
const response = await fetch('/api/daily-fortune?refresh=true', { ... });
```

## 모범 사례

<CardGroup cols={2}>
  <Card title="권장" icon="check">
    * 일간 + 날짜로 캐시
    * 데이터 신선도에 맞는 TTL 사용
    * 캐시 워밍 구현
    * 캐시 키 버전 관리
  </Card>

  <Card title="비권장" icon="xmark">
    * 공유 캐시에 사용자별 데이터 저장
    * 시간에 민감한 데이터에 무한 TTL 사용
    * 개발 환경에서 캐시 건너뛰기
    * 캐시 미스 처리 누락
  </Card>
</CardGroup>
