에듀테크 프로젝트 대시보드
HTTP 캐싱 (Cache-Control·ETag)
Cache-Control · ETag · 304 Not Modified · 강제 캐시(fresh)와 조건부 재검증(revalidation)
Cache-Control, ETag,
Last-Modified 헤더다.
HTTP 캐시는 이 둘을 단계적으로 줄인다. 가장 센 방법은 “묻지도 말고 그냥 로컬 것 써”, 그다음은 “바뀌었는지만 물어보고, 안 바뀌었으면 본문은 받지 마”.
서버가 Cache-Control: max-age=60 을 주면, 브라우저는 그 응답을 받은 뒤
60초 동안은 “신선하다(fresh)” 고 보고 네트워크를 아예 건드리지 않는다.
이때 개발자 도구 Network 탭에 (disk cache), (memory cache) 라고 뜨고 시간은 0 ms에 가깝다.
60초가 지나면 캐시는 “상했다(stale)” 상태가 된다. 버리는 게 아니라, 다음 요청 때 서버에 “아직 유효해?” 하고 재검증한다(3절).
| 지시어 | 뜻 |
|---|---|
max-age=N | N초 동안 fresh. 그동안 요청 안 감 |
no-cache | 저장은 하되 쓰기 전에 매번 재검증 (이름과 달리 "캐시 안 함"이 아님) |
no-store | 아예 저장 금지 (은행 거래내역 등 민감 정보) |
private / public | 브라우저만 저장 / 중간 CDN·프록시도 저장 가능 |
immutable | fresh 동안엔 새로고침해도 재검증조차 하지 마 (해시 파일명 정적 자원) |
아래는 브라우저가 logo.png 를 반복 요청하는 상황이다.
[요청 보내기] 를 눌러 보라. 캐시가 fresh면 요청이 서버까지 가지 않고(HIT),
stale이거나 비어 있으면 서버까지 간다(MISS / 재검증).
해 보기: max-age=10 으로 두고 빠르게 두 번 → 두 번째는 HIT(0 ms). 11초 기다렸다 다시 →
stale이라 서버로 가지만 파일이 그대로면 304(본문 0 KB). 중간에 서버 파일 수정 후 요청하면 200(전체 전송).
캐시가 stale이 되면 브라우저는 저장해 둔 검증자(validator) 를 요청 헤더에 실어 보낸다.
ETag: "a1b2c3"
→ 요청에
If-None-Match: "a1b2c3"
→ 서버 비교
같음 → 304 (본문 없음)
/
다름 → 200 + 새 본문·새 ETag
| 검증자 | 응답 헤더 | 요청 헤더 | 특징 |
|---|---|---|---|
| ETag (콘텐츠 지문) | ETag: "a1b2c3" | If-None-Match | 내용이 1바이트만 달라도 감지. 1초 안에 두 번 바뀌어도 정확 |
| 수정 시각 | Last-Modified | If-Modified-Since | 초 단위 해상도. 내용 같아도 touch하면 바뀜. ETag 없을 때의 대안 |
304 Not Modified 응답은 상태줄과 헤더뿐, 본문이 없다. 그래서 왕복 1번은 들지만 2MB 대신 몇백 바이트만 오간다. 그리고 브라우저는 기존 캐시의 신선도 타이머를 리셋한다.
왼쪽에서 요청을 만들고 [전송] 하면 서버가 어떻게 판단하는지 실제 헤더로 보여준다.
# 여기에 실제 요청/응답 헤더가 표시됩니다
def fetch(url): entry = cache.get(url) # ① 저장된 게 있고 아직 신선하면 → 네트워크 0회 if entry and entry.age < entry.max_age and not entry.no_cache: return entry.body # HIT (disk/memory cache) # ② 없거나 stale → 조건부 요청으로 재검증 req = Request(url) if entry: req.headers["If-None-Match"] = entry.etag req.headers["If-Modified-Since"] = entry.last_modified resp = network.send(req) if resp.status == 304: # 안 바뀜: 본문 없음 entry.refresh(resp.headers) # 신선도 타이머 리셋 return entry.body cache.store(url, resp) # 200: 새 본문 + 새 헤더 저장 return resp.body
| 자원 | 헤더 | 이유 |
|---|---|---|
app.4f9a2c.js (해시 파일명) | Cache-Control: public, max-age=31536000, immutable | 내용 바뀌면 파일명이 바뀌니 1년 캐시해도 안전 |
index.html | Cache-Control: no-cache | 항상 재검증 — 새 배포를 즉시 반영해야 함 |
| 사용자 프로필 API | Cache-Control: private, max-age=0, must-revalidate + ETag | 남과 공유 금지, 매번 확인하되 안 바뀌면 304 |
| 로그인·결제 응답 | Cache-Control: no-store | 어디에도 남기면 안 됨 |
no-cache, 정적 자원은 해시 파일명 + 1년.
그리고 no-cache ≠ no-store 를 꼭 구분할 것.