stack-analysis
CHAPTER 95 / 120
읽기 약 2분
FUNCTION
캐싱 전략: HTTP Cache + CDN + Redis + SWR
핵심 개념
5계층 캐시·Cache-Control·SWR·revalidate — 응답 시간 100배 감소.
본문
5계층 캐시 (Layered Cache)
[L1] 브라우저 메모리 (in-memory)
- TanStack Query / SWR
- 페이지 이동 시 즉시 응답
[L2] 브라우저 디스크
- HTTP Cache-Control
- 새로고침에도 유지
[L3] CDN (Edge)
- Cloudflare / CloudFront
- 글로벌 분산
[L4] 서버 메모리 (Redis)
- DB 쿼리 결과 캐시
- 분산 환경 공유
[L5] 데이터베이스
- 실제 데이터Cache-Control 헤더
// 정적 자산 — 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 (조건부 요청)
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
[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 캐시 패턴
// 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 패턴 (클라이언트)
// 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,
});캐시 무효화 전략
// 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 방지
// 인기 키가 동시 만료 → 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