FEInterview Prep

DevOwen

HTTP 캐싱 완벽 가이드

Cache-Control 의 디렉티브 이름은 *직관과 어긋나는* 함정이 많다. no-cache 는 캐시 안 하는 게 아니고, no-store 가 진짜 핵 옵션이다. 신선도 vs 검증의 두 축으로 설계해야 길을 잃지 않는다.

2025-11-24·12분 읽기
브라우저성능
원문 보기 ↗

핵심 요약

(1) 신선도: Cache-Control: max-age (브라우저+공유), s-maxage (공유 전용 — max-age 를 덮어씀), Expires (절대 시각, max-age 있으면 무시), immutable (재검증 생략 — 핑거프린트된 자산만!), stale-while-revalidate/stale-if-error (오래됐어도 일단 제공). (2) 검증: ETag (강/약), Last-ModifiedIf-None-Match/If-Modified-Since304 Not Modified 응답 — 본문 미전송. (3) : 기본은 (스킴, 호스트, 경로, 쿼리). VaryAccept-Encoding 같은 헤더를 키에 추가 — 남용하면 파편화. (4) 함정: no-cache ≠ no store, no-store 는 핵 옵션 + BFCache 차단, Vary: * = 사실상 캐시 불가, Vary: Cookie = 모든 사용자가 별 항목. (5) 레시피: 핑거프린트 자산 → public, max-age=31536000, immutable. 자주 변하는 HTML → public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=600 + ETag. 인증 페이지 → private, no-cache + ETag. API → 짧은 s-maxage + stale-while-revalidate.

HTTP 캐싱을 '리소스마다 max-age 를 적당히 박는 일' 로 보지 말고 '(1) 신선도(freshness): 재검증 없이 제공할 수 있는 기간, (2) 검증(validation): 만료 후 변경 여부를 저렴하게 확인하는 메커니즘, (3) 키(key)와 변형(Vary): 같은 URL 의 다른 버전 분리, 이 세 축의 설계 문제' 로 본다. 브라우저/CDN/리버스 프록시/Redis 라는 계층 스택 위에서 각 축이 어떻게 적용되는지를 그릴 수 있어야 진짜로 다룰 수 있다.

캐싱 헤더 한 줄이 비용·LCP·복원력·보안 을 동시에 흔든다.

  • Cache-Control: no-cache'캐시 끄기' 로 오해해 매 요청 origin 까지 보내는 사이트가 흔하다 — 사실 저장하되 매번 재검증 의미
  • no-store'안전한 기본값' 으로 박으면 BFCache 자격 까지 잃어 뒤로가기 UX 가 망가진다
  • Vary: User-Agent 한 줄이 캐시를 수만 개 변형 으로 폭발시켜 hit 율을 0 에 가깝게 만든다
  • s-maxage 를 모르면 공유 캐시(CDN) 와 브라우저 에 다른 TTL 을 줄 수 없다

프런트엔드 면접에서 '정적 자산/HTML/API 의 캐시 헤더를 어떻게 다르게 두나' 는 단골 질문이고, 제대로 답하려면 디렉티브 이름의 함정 을 정확히 짚어야 한다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

`no-cache` 는 *저장하되 매번 검증* 이다

디렉티브 이름이 직관과 정반대다. 정리하면:

디렉티브의미저장재사용 전
no-cache저장은 한다, 매번 origin 과 재검증검증 강제
no-store어디에도 저장 금지 (브라우저/프록시/CDN)매번 새로
max-age=0, must-revalidate즉시 stale + 검증 강제검증 강제
private, max-age=0브라우저만 보관, 즉시 stale브라우저만검증

'캐시하지 마세요' 가 진짜 의도면 no-store. 단, 이는 BFCache 자격까지 막아 뒤로가기 UX 가 깨질 수 있다.

no-cacheno-storemust-revalidateBFCache
자주 하는 오해

민감 데이터에 no-cache 를 박고 저장이 막혔다 고 안심하는 것. 디스크 캐시에 그대로 남는다.

`s-maxage` 와 `stale-while-revalidate` 가 운영의 핵심

공유 캐시(CDN) 와 브라우저에 다른 TTL 을 주는 게 부하·신선도·UX 를 동시에 잡는 핵심.

Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=600
ETag: "abc123"
  • max-age=60 → 브라우저는 1분 동안 자기 캐시 사용
  • s-maxage=300 → CDN 은 5분 동안 origin 부하 흡수 (max-age 덮어씀)
  • stale-while-revalidate=60 → 5분 지난 직후 60초 동안은 오래된 사본을 즉시 제공 + 백그라운드 재검증 → 사용자는 즉시 응답
  • stale-if-error=600 → origin 5xx 라도 10분간 오래된 사본으로 버팀

사용자에게는 항상 빠른 응답, origin 에는 예측 가능한 부하 를 동시에 준다.

s-maxagestale-while-revalidatestale-if-error공유 캐시
자주 하는 오해

max-age 만 길게 박는 것. 브라우저 캐시까지 길어져 배포해도 사용자는 옛날 버전 을 본다.

`Vary` 는 *정말 응답이 변하는 헤더만*

Vary 는 캐시 키에 헤더를 추가한다. 잘못 쓰면 hit 율이 0 에 수렴한다.

Vary 값효과주의
Accept-Encodinggzip/brotli 변형 분리✅ 거의 항상 안전
Accept-Language언어별 분리정규화 안 하면 en-US/en-GB 다 따로
User-Agent브라우저별 분리❌ 수만 개 변형 폭발
Cookie쿠키 값별 분리❌ 사용자마다 별 항목
*어떤 캐시 키도 안 만듦사실상 캐시 불가

원칙: 응답을 진짜로 바꾸는 헤더만, 가능하면 CDN 에서 정규화 후 키에 사용.

Vary캐시 키정규화Cookie
자주 하는 오해

'안전하게 다 분리하자'Vary: User-Agent, Cookie 를 박는 것. 캐시가 죽고 origin 부하만 폭발한다.

읽는 순서

  1. 1이론

    MDN 의 Cache-Control 페이지를 보면서 본 글의 5가지 함정 (no-cache ≠ no store 등) 을 직접 표로 정리한다.

  2. 2구현

    Express/Next 라우트 한 곳에 s-maxage+stale-while-revalidate 를 적용하고, cf-cache-statusHIT 로 바뀌는지 DevTools Network 로 확인한다.

  3. 3실무

    현재 사이트의 정적/HTML/API 응답 헤더를 캡처해 어디서 캐시 누수가 일어나는지 찾아 PR 로 묶는다.

  4. 4설명

    팀에 '캐시 hit 율을 10% 올리는 5가지 헤더 변경' 5분 발표를 준비. 축: s-maxage, stale-while-revalidate, immutable, Vary 정규화, ETag.

면접 연결 질문

hard정적 JS 번들, HTML 문서, 인증된 API 응답 — 각각에 어떤 `Cache-Control` 을 두고, *왜* 그렇게 하는지 설명해보세요.
힌트

[감점 답변] '다 max-age 1년'. [좋은 답변] (1) 핑거프린트된 JS → public, max-age=31536000, immutable (URL 자체가 변하므로 영구). (2) HTML → public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=600 + ETag (사용자에겐 빠르게, CDN 으로 origin 보호, 장애 내성). (3) 인증 API → private, no-cache + ETag (공유 금지, 매번 검증 + 304 로 비용 절감).

medium`ETag` 와 `Last-Modified` 가 동시에 있으면 어떻게 동작하나요? 둘 중 하나만 두고 싶다면 무엇을 권하나요?
힌트

[감점 답변] '무조건 둘 다'. [좋은 답변] 클라이언트가 두 헤더 모두 조건부 요청에 보내고, 서버가 어느 것이든 일치하면 304 를 응답. ETag 가 더 정밀(바이트 단위) 하지만 서버 클러스터 에서 일관된 ETag 생성 비용이 있다. 콘텐츠 해시 가 가능하면 ETag 만, 수정 시각만 가능하면 Last-Modified 로 충분.

hard사용자가 *뒤로가기* 로 페이지를 빠르게 복원하는 BFCache 가 동작하지 않는 사이트가 있다. `Cache-Control` 관점에서 가능한 원인은?
힌트

[감점 답변] 'JS 가 무거워서'. [좋은 답변] 문서 응답에 Cache-Control: no-store 가 가장 흔한 차단 요인 — 브라우저가 어떤 사본도 보관하지 못해 BFCache 자격 자체가 사라진다. 보안상 필요한 페이지가 아니라면 no-store 대신 private, no-cache 또는 must-revalidate 로 대체.

자기 점검

본인 사이트에서 `cf-cache-status` (또는 X-Cache) 를 확인해 HTML 의 hit 율을 측정해보세요.
MISSHITCache-Statuscf-cache-status
자주 하는 오해

'CDN 만 붙이면 다 캐시된다'. 기본적으로 Cloudflare 같은 CDN 은 HTML 을 캐시하지 않는다 — 명시적으로 켜야 한다.

`Vary: Cookie` 를 박은 페이지의 캐시 hit 율을 추정해보고 그 이유를 설명해보세요.
키 파편화사용자별정규화hit 율
자주 하는 오해

'쿠키마다 응답이 다를 수 있으니 안전하다'. 실제로는 세션 쿠키 한 글자 차이 도 별 항목이 되어 hit 율이 0 에 수렴한다.