본문 바로가기
C.W.K.
Stream
Lesson 01 of 05 · published

HTTP 캐시 헤더 — 저장과 재사용을 정하는 계약

~11 min · caching-perf, cache-control, etag, last-modified

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"캐시는 나중에 덧붙이는 장식이 아니야. 서버가 헤더로 정확히 설명하면 CDN, 브라우저, 역방향 프록시가 함께 움직이는 프로토콜이지. 규칙이 틀리면 오래된 데이터를 내보내거나, 한 사용자의 인증 응답을 다른 사용자에게 건넬 수도 있어."

캐시를 움직이는 헤더 다섯 가지

  • Cache-Control — 현대 HTTP 캐시 정책의 중심이야. 저장 허용 범위, 신선도, 재검증 규칙을 지시어로 표현해.
  • ETag — 내용을 직접 해석하지 않고도 버전을 비교할 수 있게 해 주는 검증자야. 조건부 재검증에 사용해.
  • Last-Modified — 수정 시각을 나타내는 검증자야. If-Modified-Since와 함께 사용해.
  • Expires — 절대 시각으로 만료 시점을 나타내는 오래된 방식이야. Cache-Control과 함께 있으면 Cache-Control의 신선도 규칙이 우선해.
  • Vary — 어떤 요청 헤더의 값에 따라 표현이 달라지는지 알려 줘. 캐시는 그 값을 캐시 키 선택에 반영해.

자주 쓰는 Cache-Control 지시어

Cache-Control은 여러 지시어를 쉼표로 조합해. 저장 가능 여부와 신선도는 서로 다른 축이므로 조합의 의미를 정확히 읽어야 해.

  • max-age=N — 응답이 생성된 뒤 N초 동안 신선하다는 뜻이야. 그동안 캐시는 재검증 없이 응답을 재사용할 수 있어.
  • s-maxage=N — CDN이나 프록시 같은 공유 캐시에만 적용되는 신선도 수명이야. 공유 캐시에서는 max-age를 덮어쓰므로, 브라우저 TTL은 짧게 두고 CDN TTL은 길게 둘 수 있어.
  • public — 공유 캐시를 포함한 캐시가 응답을 저장할 수 있음을 명시해. Authorization이 있는 요청의 응답을 공유 캐시에 저장하려면 특히 신중한 키 설계와 함께 명시적 허용이 필요해.
  • private — 응답을 개인 캐시에만 저장하도록 제한해. 브라우저 같은 개인 캐시는 저장할 수 있지만 CDN과 공유 프록시는 저장하면 안 돼.
  • no-cache — 저장은 허용하지만, 저장된 응답을 다시 사용하기 전에 원본 서버에 재검증하라고 요구해. 이름과 달리 “저장하지 마”라는 뜻은 아니야.
  • no-store — 어떤 캐시에도 응답을 저장하지 말라는 지시야. 민감한 데이터나 결제 정보처럼 남아서는 안 되는 응답에 써.
  • must-revalidate — 응답이 오래되면 원본 서버에 재검증해야 하며, 연결에 실패했다는 이유로 오래된 응답을 임의로 내주면 안 된다는 뜻이야.
  • stale-while-revalidate=N — 응답이 오래된 뒤에도 N초 동안은 저장된 응답을 내주면서 백그라운드에서 재검증할 수 있게 해. 지원하는 캐시에서는 대기 시간을 줄이는 데 효과적이야.
  • immutable — 응답이 신선한 동안 내용이 바뀌지 않는다고 알려 불필요한 재검증을 막아. 지문이 붙은 자산 URL(app.abc123.css)처럼 내용이 바뀌면 URL도 바뀌는 자원에 써.

가장 자주 쓰는 세 가지 조합

1. 버전이 붙은 정적 자산(파일 이름에 해시가 있는 CSS, JS, 이미지):

Cache-Control: public, max-age=31536000, immutable

1년 동안 신선하게 재사용해. 내용이 바뀌면 파일 이름과 URL도 바뀌므로, 오래 캐시해도 안전해.

2. 자주 변하는 API 응답(목록, 사용자 관리 화면):

Cache-Control: private, max-age=60, must-revalidate
ETag: "v17-abc"
Vary: Accept-Encoding, Authorization

브라우저의 개인 캐시에서 60초 동안 재사용한 뒤 조건부 요청으로 재검증해. private이 공유 캐시 저장을 막고, ETag는 내용이 그대로일 때 304 응답을 가능하게 해. Vary는 요청 헤더에 따라 표현을 올바르게 구분하도록 도와.

3. 개인 정보나 민감한 데이터(인증 응답, 결제 양식):

Cache-Control: no-store

브라우저와 공유 캐시 모두에 저장을 금지해. 토큰이나 세션 정보처럼 저장 흔적 자체를 피해야 하는 응답에 가장 분명한 선택이야.

명시하지 않은 기본값은 조용히 어긋날 수 있어. Cache-Control이 없는 응답은 상태 코드와 다른 헤더에 따라 캐시가 휴리스틱 신선도를 계산할 수도 있어. 엔드포인트마다 의도를 명시해. 저장 자체를 막으려면 no-store를 쓰고, 개인 캐시에 저장하되 즉시 오래된 것으로 취급하려면 private, max-age=0처럼 목적에 맞게 구분해야 해. 둘은 같은 정책이 아니야.

cwkPippa의 캐시 헤더

cwkPippa API 엔드포인트는 사용자별 데이터가 많아 기본적으로 Cache-Control: no-store를 사용해. 해시가 붙은 Vite 정적 자산에는 Cache-Control: public, max-age=31536000, immutable을 적용하고, 배포 진입점인 index.html에는 no-cache를 두어 재사용 전에 새 배포 여부를 확인하게 해. cwk-site 블로그 글의 public, s-maxage=300, stale-while-revalidate=86400은 CDN의 짧은 신선도와 오래된 응답의 제한적 재사용을 결합한 정책이야. 중요한 건 유행하는 조합을 베끼는 게 아니라 데이터의 민감도와 변경 주기에 맞추는 거야.

Code

FastAPI: 데이터의 신선도 모델에 맞춘 응답별 Cache-Control·python
# FastAPI — response 당 Cache-Control 설정, globally 아님
from fastapi import FastAPI, Response

app = FastAPI()

@app.get('/api/health')
async def health(response: Response):
    response.headers['Cache-Control'] = 'public, max-age=10'
    return {'ok': True}

@app.get('/api/me')
async def me(response: Response):
    # User 당 데이터 — share-cache 절대 안 함
    response.headers['Cache-Control'] = 'private, no-store'
    return {'user_id': 'u_42'}

@app.get('/api/posts')
async def list_posts(response: Response):
    # 자주 변하는 list — 짧은 캐시, revalidation 용 ETag, auth 용 Vary
    response.headers['Cache-Control'] = 'private, max-age=60, must-revalidate'
    response.headers['ETag']          = '"v17-abc"'
    response.headers['Vary']          = 'Accept-Encoding, Authorization'
    return {'items': [...]}

@app.get('/assets/app.abc123.css')
async def static_asset(response: Response):
    # Hashed filename — content 변할 수 없음; 영원 캐시
    response.headers['Cache-Control'] = 'public, max-age=31536000, immutable'
    return Response('body{...}', media_type='text/css')
HTTP 응답에 적용한 대표 Cache-Control 패턴·http
# Canonical Cache-Control 패턴 셋

# 1. Versioned static asset — 영원 캐시
GET /assets/app.abc123.css HTTP/1.1

HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: public, max-age=31536000, immutable

# 2. API list — 짧은 캐시 + conditional revalidation + Vary
GET /api/posts HTTP/1.1
Authorization: Bearer abc

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, max-age=60, must-revalidate
ETag: "v17-abc"
Vary: Accept-Encoding, Authorization

# 3. 민감 개인 데이터 — 절대 캐시 안 함
GET /api/me HTTP/1.1
Authorization: Bearer abc

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store
브라우저의 기본 캐시 동작과 요청별 fetch 정책·javascript
// Client 쪽 — Cache API + stale-while-revalidate 동작 가진 fetch
// (현대 브라우저가 server header 의 stale-while-revalidate 자동 처리)

// 그냥 정상 fetch; 브라우저가 Cache-Control 존중
const resp = await fetch('/api/posts');
const data = await resp.json();

// Client 에서 specific 캐시 정책 opt-in 도 가능
const freshOnly = await fetch('/api/posts', { cache: 'no-cache' });    // 항상 revalidate
const neverNetwork = await fetch('/api/posts', { cache: 'force-cache' }); // 캐시 있으면 서빙
const noCache = await fetch('/api/posts', { cache: 'no-store' });      // 캐시 완전 건너뜀

// 세밀 control 위한 Service Worker
// (여기 자세히 안 함 — fetch event + caches.match + cache.put 패턴)

External links

Exercise

FastAPI에 서로 다른 Cache-Control 정책을 쓰는 엔드포인트 세 개를 만들어. (1) /static/asset.css에는 public, max-age=31536000, immutable, (2) /api/posts에는 private, max-age=60, must-revalidate와 ETag 및 Vary, (3) /api/me에는 no-store를 적용해. 각각 curl -i로 호출해 응답 헤더를 읽고, 브라우저 DevTools의 네트워크 패널에서 연속 요청의 캐시 상태를 확인해. 두 번째 엔드포인트는 신선도가 끝난 뒤 304 재검증이 일어나는지 보고, 첫 번째 엔드포인트는 캐시에서 제공되는지 살펴봐. 보너스로 네 번째 엔드포인트에서는 Cache-Control을 생략하고 Chrome과 Firefox의 동작을 비교해.
Hint
FastAPI의 Response 객체에서 응답 헤더를 직접 설정할 수 있어. 데모에서는 ETag에 "v17-abc"처럼 따옴표를 포함한 고정 문자열을 써도 돼. DevTools의 네트워크 패널은 네트워크에서 받은 200, 304 Not Modified, 디스크 캐시, 메모리 캐시를 구분해 보여 줘. 단, DevTools의 캐시 비활성화 옵션이 켜져 있으면 결과가 달라지니 먼저 확인해. Cache-Control을 생략한 실험은 휴리스틱에 기대면 동작을 예측하기 어려운 이유를 보여 줄 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.