OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 95 / 120
stack-analysis
CHAPTER 95 / 120
읽기 약 2
FUNCTION

캐싱 전략: HTTP Cache + CDN + Redis + SWR


핵심 개념

5계층 캐시·Cache-Control·SWR·revalidate — 응답 시간 100배 감소.

본문

5계층 캐시 (Layered Cache)

📋 코드 (18줄)
[L1] 브라우저 메모리 (in-memory)
- TanStack Query / SWR
- 페이지 이동 시 즉시 응답

[L2] 브라우저 디스크
- HTTP Cache-Control
- 새로고침에도 유지

[L3] CDN (Edge)
- Cloudflare / CloudFront
- 글로벌 분산

[L4] 서버 메모리 (Redis)
- DB 쿼리 결과 캐시
- 분산 환경 공유

[L5] 데이터베이스
- 실제 데이터

Cache-Control 헤더

TYPESCRIPT📋 코드 (22줄)
// 정적 자산 — 1년 + immutable
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');


// API 응답 — 60초
res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300');
// max-age: 브라우저 60초
// s-maxage: CDN 300초


// SWR (Stale-While-Revalidate)
res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=600');
// 60초 fresh
// 그 후 600초간 stale 응답 + 백그라운드 갱신


// 사용자별 — 캐시 X
res.setHeader('Cache-Control', 'private, no-store');


// 인증 필요 — private
res.setHeader('Cache-Control', 'private, max-age=300');

ETag (조건부 요청)

TYPESCRIPT📋 코드 (19줄)
import crypto from 'crypto';

app.get('/api/posts', async (req, res) => {
  const posts = await db.post.findMany();
  const etag = `"${crypto.createHash('md5').update(JSON.stringify(posts)).digest('hex')}"`;

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();  // 변경 없음
  }

  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'private, must-revalidate');
  res.json(posts);
});


// → 클라이언트가 같은 데이터 재요청 시
// → 서버: 304 (8 bytes 응답) vs 200 (전체 응답)
// → 트래픽 95% 감소

CDN — Cloudflare

📋 코드 (11줄)
[Cache Rules]
URL: /_next/static/* → Cache Everything, TTL 1 year
URL: /api/* → Bypass Cache (or 60s)
URL: / → Standard cache (Cache-Control 따름)
URL: /images/* → 1 year, immutable


[Page Rules — Pro]
- 캐시 키 커스텀 (헤더·쿠키 기반)
- A/B 테스트
- 장치별 캐시

Redis 캐시 패턴

TYPESCRIPT📋 코드 (33줄)
// Cache Aside (Lazy Loading)
async function getProduct(id: string) {
  const cacheKey = `product:${id}`;

  // 1. 캐시 확인
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // 2. DB 조회
  const product = await db.product.findUnique({ where: { id } });
  if (!product) return null;

  // 3. 캐시 저장 (TTL + jitter)
  const ttl = 300 + Math.floor(Math.random() * 30);  // 5분 ± 10%
  await redis.setex(cacheKey, ttl, JSON.stringify(product));

  return product;
}


// Write Through
async function updateProduct(id: string, data: any) {
  const product = await db.product.update({ where: { id }, data });
  await redis.setex(`product:${id}`, 300, JSON.stringify(product));
  return product;
}


// Write Behind (대규모)
async function updateView(productId: string) {
  await redis.incr(`product:${productId}:views`);
  // 별도 worker가 주기적으로 DB로 flush
}

SWR 패턴 (클라이언트)

TSX📋 코드 (18줄)
// TanStack Query
const { data, isLoading, isFetching } = useQuery({
  queryKey: ['posts', page],
  queryFn: () => fetchPosts(page),
  staleTime: 1000 * 60,         // 1분간 fresh
  gcTime: 1000 * 60 * 5,        // 5분 후 GC
  placeholderData: keepPreviousData,
});


// SWR
import useSWR from 'swr';

const { data, isLoading } = useSWR(`/api/posts?page=${page}`, fetcher, {
  revalidateOnFocus: false,
  refreshInterval: 60000,
  keepPreviousData: true,
});

캐시 무효화 전략

TYPESCRIPT📋 코드 (29줄)
// 1. TTL 기반 (자동 만료)
await redis.setex(key, 300, value);


// 2. 명시적 무효화
async function updatePost(id: string, data: any) {
  const post = await db.post.update({ where: { id }, data });
  await redis.del(`post:${id}`);
  await redis.del('posts:list');  // 목록도
}


// 3. 태그 기반 (Next.js)
await fetch('/api/posts/123', {
  next: { tags: ['post:123', 'posts'] },
});

// 무효화
import { revalidateTag } from 'next/cache';
revalidateTag('post:123');  // 한 글
revalidateTag('posts');      // 모든 목록


// 4. 버전 기반 (Cache Busting)
const version = await redis.get('posts:version');
const cacheKey = `posts:v${version}:${query}`;

// 무효화
await redis.incr('posts:version');  // 모든 기존 캐시 무효

Cache Stampede 방지

TYPESCRIPT📋 코드 (30줄)
// 인기 키가 동시 만료 → DB 동시 폭주
async function getCachedWithLock(key: string, fetcher: () => Promise<any>, ttl: number) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  // 락 획득 시도
  const lockKey = `lock:${key}`;
  const lockId = crypto.randomUUID();
  const acquired = await redis.set(lockKey, lockId, 'PX', 5000, 'NX');

  if (!acquired) {
    // 다른 요청이 갱신 중 → 잠시 대기
    await new Promise(r => setTimeout(r, 100));
    return getCachedWithLock(key, fetcher, ttl);
  }

  try {
    const fresh = await fetcher();
    await redis.setex(key, ttl, JSON.stringify(fresh));
    return fresh;
  } finally {
    // 락 해제 (Lua로 원자적)
    await redis.eval(
      `if redis.call('GET', KEYS[1]) == ARGV[1] then
         return redis.call('DEL', KEYS[1])
       else return 0 end`,
      1, lockKey, lockId
    );
  }
}

다음 챕터

CH.96 "데이터베이스 쿼리 최적화: N+1 + 인덱스".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 캐싱 전략 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

캐싱 전략 관련 베스트 프랙티스 5가지를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 캐싱 전략
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 시장의 캐싱 전략
트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
캐싱 전략: HTTP Cache + CDN + Redis + SWR 이 3가지만 확실히 잡으세요
1.5계층 캐시 (Browser → CDN → Redis → DB) = 응답 100배 차이
2.SWR (Stale-While-Revalidate) = 항상 빠름 + 백그라운드 갱신
3.캐시 무효화는 4가지 — TTL·명시·태그·버전 적절 조합


공유하기
진행도 95 / 120