HTTP 캐싱 — Cache-Control, ETag, 조건부 요청
웹 성능 최적화의 기본은 캐싱입니다. Cache-Control 헤더 하나로 브라우저 캐싱을 제어할 수 있고, ETag로 변경 여부를 효율적으로 확인할 수 있습니다.
캐시 동작 흐름
1. 첫 번째 요청:
클라이언트 → GET /image.png → 서버
서버 → 200 OK + Cache-Control: max-age=3600 + ETag: "abc123"
→ 브라우저가 캐시에 저장

2. 1시간 이내 재요청 (캐시 유효):
브라우저 캐시에서 직접 반환 (서버 요청 없음!)
3. 1시간 이후 재요청 (캐시 만료):
클라이언트 → GET /image.png + If-None-Match: "abc123"
서버 → 304 Not Modified (변경 없음, 본문 없이 응답)
→ 캐시된 자원 재사용
Cache-Control 디렉티브
| 디렉티브 | 의미 |
|---|---|
| max-age=N | N초 동안 캐시 유효 |
| no-cache | 캐시 저장은 하되, 매번 서버에 검증 |
| no-store | 캐시에 저장하지 않음 (민감 데이터) |
| public | CDN 등 공유 캐시에 저장 가능 |
| private | 브라우저 캐시에만 저장 (개인 정보) |
| immutable | 만료 전에는 절대 재검증하지 않음 |
| stale-while-revalidate=N | 만료 후 N초간 캐시를 먼저 반환하고 백그라운드에서 갱신 |
# 정적 자원 (JS, CSS, 이미지) — 파일명에 해시 포함
Cache-Control: public, max-age=31536000, immutable
# API 응답 — 개인화된 데이터
Cache-Control: private, no-cache
# 로그인 페이지 — 캐시 금지
Cache-Control: no-store
ETag와 조건부 요청
ETag (Entity Tag)
자원의 버전 식별자입니다. 자원이 변경되면 ETag도 변경됩니다.
응답: ETag: "abc123"
재요청: If-None-Match: "abc123"
→ 변경 없으면: 304 Not Modified
→ 변경 있으면: 200 OK + 새 ETag + 새 본문
Last-Modified / If-Modified-Since
응답: Last-Modified: Wed, 15 Jan 2025 08:00:00 GMT
재요청: If-Modified-Since: Wed, 15 Jan 2025 08:00:00 GMT
→ 이후 변경 없으면: 304
→ 변경 있으면: 200
ETag가 Last-Modified보다 정확합니다 (1초 미만 변경, 내용 동일한데 날짜만 변경 등).
캐시 무효화 전략
파일명 해싱 (Cache Busting)
<!-- 파일 내용이 바뀌면 해시가 바뀜 → 새 URL → 새 캐시 -->
<script src="/app.abc123.js"></script>
<!-- 빌드 시 해시 변경 -->
<script src="/app.def456.js"></script>
CDN 캐시 퍼지
# Cloudflare 캐시 퍼지
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
-H "Authorization: Bearer $TOKEN" \
-d '{"purge_everything": true}'
핵심 포인트
- no-cache vs no-store: no-cache는 캐시하되 매번 검증, no-store는 아예 캐시 안 함
- 304 응답의 이점: 본문 없이 헤더만 전송 → 대역폭 절약
- CDN 캐시와 브라우저 캐시: public으로 CDN 허용, private로 브라우저만 허용
정리
HTTP 캐싱은 웹 성능의 기본입니다. max-age로 캐시 수명을 결정하고, ETag/Last-Modified로 조건부 요청을 처리합니다. 정적 자원에는 긴 max-age + 파일명 해시, API에는 no-cache 또는 짧은 max-age를 적용하는 것이 일반적입니다.