CDN의 캐시 적중률은 사이트 성능과 오리진 비용을 동시에 좌우합니다. 그런데 캐시는 단순히 max-age=3600 한 줄로 끝나지 않습니다. 진짜 효과를 보려면 변경 시 즉시 반영과 만료 직후 사용자 대기 시간을 동시에 해결해야 합니다.
캐시 헤더의 기본
가장 중요한 두 헤더는 Cache-Control과 ETag입니다.
Cache-Control: public, max-age=300, s-maxage=86400, stale-while-revalidate=3600
ETag: "abc123def"
public— 누구나 캐시 가능 (사용자별 데이터 없음)max-age=300— 브라우저 캐시 5분s-maxage=86400— CDN(공유 캐시) 1일stale-while-revalidate=3600— 만료 후 1시간 stale 허용ETag— 콘텐츠 식별자, 변경 시 다른 값
Stale-While-Revalidate (SWR)
전통적 캐시 동작:
요청 → Fresh? → 캐시 응답
↓ no
오리진 호출 → 사용자 대기 (느림)
SWR 적용:
요청 → Fresh? → 캐시 응답
↓ no (stale 윈도우 내)
Stale 응답 즉시 (사용자 빠름)
+ 백그라운드로 재검증 → 다음 요청부터 Fresh
사용자는 만료 직후에도 즉시 응답을 받고, 캐시는 백그라운드에서 갱신됩니다. 평균 응답 시간이 극적으로 개선됩니다.
예시 설정
# 오리진(Nginx) 응답 헤더
add_header Cache-Control "public, max-age=60, stale-while-revalidate=86400" always;
브라우저 입장:
- 60초간 캐시 사용 (즉시 응답)
- 60~86400초 사이면 stale 응답 + 백그라운드 갱신
- 86400초 이후엔 오리진 호출 강제
CDN 측 동작은 Cloudflare·Fastly·Akamai 모두 SWR을 지원합니다(설정 명칭은 약간 다름).
ETag와 Last-Modified — 조건부 요청
캐시가 만료됐을 때 항상 전체 콘텐츠를 다시 받을 필요는 없습니다. 변경되지 않았으면 헤더만 받고 끝낼 수 있습니다.
요청:
GET /style.css HTTP/2
If-None-Match: "abc123def"
응답 (변경 없음):
HTTP/2 304 Not Modified
ETag: "abc123def"
(본문 없음)
ETag는 콘텐츠 해시(파일이 같으면 같은 값)가 일반적입니다. Last-Modified는 파일 변경 시간 기준이며 정확도가 떨어집니다.
Nginx는 정적 파일에 ETag를 자동 생성합니다. 동적 페이지라면 애플리케이션에서 직접 계산해 헤더로 내야 합니다.
Vary 헤더 — 캐시 분기
같은 URL이라도 사용자에 따라 다른 콘텐츠가 나가야 할 수 있습니다.
Vary: Accept-Encoding
Vary: User-Agent
Vary: Accept-Language
Vary로 명시한 헤더가 다르면 CDN은 별도로 캐시합니다.
Accept-Encoding— gzip·brotli 별 분리 (필수)Accept-Language— 다국어 페이지 분리User-Agent— 모바일·데스크톱 분리 (캐시 적중률 떨어뜨림, 신중)
너무 많은 Vary는 캐시 적중률을 망칩니다. 꼭 필요한 것만 추가하세요.
Cache-Key 커스터마이징
기본 Cache-Key는 URL이지만, CDN별로 키를 조정할 수 있습니다.
Cloudflare 예시:
Cache Key:
- Hostname: include
- Path + Query string: include
- Cookies: exclude (또는 화이트리스트만)
- User-Agent: exclude
쿠키를 무시하면 로그인 사용자도 동일 캐시 적중. 반대로 일부 쿠키(예: 다크모드)는 키에 포함하고 싶을 수도 있습니다.
Surrogate-Key / Cache-Tag — 빠른 무효화
수많은 URL을 한 번에 무효화하려면 URL 패턴 매칭이 비효율적입니다. 태그 기반 무효화가 답입니다.
응답 헤더:
Surrogate-Key: blog post-123 author-jane category-tech
또는 (Cloudflare/Fastly):
Cache-Tag: blog post-123 author-jane category-tech
CMS에서 글을 수정하면 post-123 태그가 붙은 모든 캐시(목록 페이지·상세 페이지·작가 페이지)를 한 번에 무효화:
# Fastly API 예시
curl -X POST "https://api.fastly.com/service/SERVICE_ID/purge/post-123" \
-H "Fastly-Key: $TOKEN"
URL 일일이 명시하지 않아도 됩니다. 대규모 사이트에서 핵심 기능.
캐시 가능성 분석
페이지를 캐시 가능한 것과 불가능한 것으로 분류해야 정책이 깔끔해집니다.
캐시 가능 (긴 TTL)
- 정적 자원: CSS·JS·이미지·폰트 (파일명에 해시 → 영구 캐시)
- 공개 페이지: 블로그 글, 상품 페이지
- 자주 안 바뀌는 API 응답
캐시 가능 (짧은 TTL + SWR)
- 홈페이지, 카테고리 페이지
- 뉴스·실시간성 콘텐츠
- 검색 결과 (인기 키워드)
캐시 불가
- 로그인 사용자 페이지
- 결제·주문 페이지
- 사용자별 맞춤 콘텐츠
- 실시간 데이터 (재고·가격)
명시적으로 캐시 안 하려면:
Cache-Control: no-store
또는
Cache-Control: private, no-cache
파일명 해시 — 영구 캐시의 핵심
정적 자원에 콘텐츠 해시를 파일명에 포함하면 영구 캐시 가능합니다.
<!-- 옛 방식 (캐시 문제) -->
<link rel="stylesheet" href="/style.css">
<!-- 해시 포함 (영구 캐시 가능) -->
<link rel="stylesheet" href="/style.4f8e1b.css">
Cache-Control: public, max-age=31536000, immutable
immutable 키워드는 “이 자원은 절대 안 바뀐다” — 브라우저가 새로고침 시에도 재검증 안 함. webpack·vite·rollup 같은 빌드 도구가 자동으로 해시를 생성합니다.
동적 페이지의 캐싱 — Edge Includes
페이지 대부분은 정적이지만 일부 영역(예: 로그인 메뉴)만 사용자별이라면, ESI(Edge Side Includes)를 활용합니다.
<!-- 캐시 가능한 본문 -->
<header>
<esi:include src="/api/user-menu" />
</header>
<article>...본문...</article>
CDN이 본문은 캐시하고, <esi:include> 부분만 매번 가져옵니다. Varnish·Fastly가 강력하게 지원합니다.
비슷한 패턴으로 클라이언트 측 페치(JS로 로그인 메뉴만 fetch)도 자주 쓰입니다.
캐시 적중률 측정
캐시 정책의 효과는 적중률(Hit Ratio) 로 측정합니다.
적중률 = (Edge에서 응답한 요청) / (전체 요청)
- 정적 사이트 + 좋은 정책: 95%+
- 일반 동적 사이트: 60~80%
- 사용자별 페이지 위주: 20~40%
CDN 대시보드에서 확인 가능합니다. 적중률이 낮으면 다음을 점검:
- Cache-Control 헤더 누락
- 잘못된 Vary
- 쿠키가 Cache-Key에 포함됨
- TTL이 너무 짧음
점진적 도입 전략
처음부터 완벽한 정책을 짤 필요는 없습니다.
1단계 — 정적 자원만
이미지·CSS·JS에만 긴 TTL.
Cache-Control: public, max-age=31536000, immutable
2단계 — 공개 HTML에 짧은 TTL + SWR
Cache-Control: public, s-maxage=60, stale-while-revalidate=86400
3단계 — Cache-Tag 도입
CMS 변경 시 자동 무효화. 적중률 90%+ 목표.
4단계 — ESI 또는 Client Fetch
동적 영역만 분리해 더 많은 페이지를 캐시 가능하게.
마무리
좋은 캐시 정책은 한 줄로 끝나지 않지만, 효과는 즉각적입니다. SWR + Cache-Tag + 파일명 해시 세 가지만 잘 잡으면 오리진 부하의 90%를 줄이고, 사용자 응답 시간을 절반 이하로 만들 수 있습니다.
TCP-80.NET의 전용서버 + Cloudflare·Fastly 조합으로 글로벌 사용자에게 빠른 응답을 제공하실 수 있으며, 캐시 헤더 설계 가이드는 @tcp80net으로 문의해 주세요.