HTTP 캐시·브라우저·CDN·엣지: 무엇을 어디서 캐시할지 실무 결정표

성능

HTTP 캐시Cache-ControlCDN엣지웹 성능

이 글은 누구를 위한 것인가

  • Cache-Control을 붙였는데도 사용자마다 다른 화면이 나오는 프론트엔드·백엔드
  • nextjs-app-router-caching-revalidate-patterns와 함께 HTTP 레이어까지 맞추고 싶은 팀
  • API와 정적 자산의 캐시 전략을 표로 합의하려는 테크 리드

들어가며

웹 성능에서 캐시는 가장 싸고 강한 최적화다. 동시에 가장 잘못 이해되는 레이어다. 브라우저만 캐시하는 줄 알았는데 CDN이 끼었고, max-age=0인데도 304가 나오고, privates-maxage의 차이가 헷갈린다.

이 글은 RFC 전체가 아니라 역할 분리팀이 같은 그림을 그리기 위한 결정표다.


1. 네 종류의 "캐시 주체"

주체전형적 위치강점주의
브라우저(private)사용자 기기개인화·오프라인 근처탭·시크릿마다 다름
공유 캐시(CDN/proxy)POP, 리버스 프록시지연·원본 부하 감소개인 데이터 혼입 시 사고
원본 서버앱 서버권한·비즈니스 로직CPU·DB 부담
엣지 로직edge-functions-middleware-patterns 참고지역 라우팅·A/B캐시와 결합 시 무효화 복잡

한 응답이 이 네 겹을 순서대로 통과할 수 있다. 디버깅은 Age, CF-Cache-Status, x-cache응답 헤더로 어느 층에서 왔는지 추적하는 것이 빠르다.


2. Cache-Control 핵심만

  • public: 공유 캐시가 저장 가능(개인정보 응답에는 금지).
  • private: 브라우저만; 팀 대시보드·개인화 API에 적합.
  • max-age: 신선도 기간(초). 만료 후에는 재검증으로 이어짐.
  • s-maxage: 공유 캐시 한정 max-age. private과 조합 주의.
  • no-store: 아무 데도 저장하지 않음. 금융·건강 등.
  • must-revalidate: 신선도 만료 후 오래된 것 그대로 주면 안 됨(가능하면).
  • stale-while-revalidate: 만료 직후 짧은 기간 오래된 것 먼저 주고 백그라운드 갱신. UX에 유리.

**immutable**은 파일명에 해시가 붙은 버전드 정적 자산에 쓴다. 롱 max-age와 함께 쓰면 브라우저가 불필요한 재검증을 줄인다.


3. 무엇을 어디에 두는가 (결정 힌트)

리소스 유형브라우저CDN코멘트
번들 JS/CSS (해시 파일명)max-age + immutable동일배포마다 URL이 바뀌므로 안전
HTML 문서짧게 또는 no-cache상황별SPA 인덱스는 오래 캐시하면 안 됨
JSON API(공개)짧거나 privates-maxage개인화 없을 때만
JSON API(개인)private, no-store보통 비활성공유 캐시 금지
이미지·폰트길게길게CDN 친화

4. 재검증: ETag와 Last-Modified

max-age가 지나도 바로 네트워크로 새 파일을 받는 게 아니라, 조건부 요청(If-None-Match / If-Modified-Since)으로 304를 받을 수 있다. API는 ETag를 안정적으로 만들기 어려울 수 있어, 팀 내에서 해시 vs 버전 컬럼 정책을 정한다.


5. Next.js·풀스택과의 정렬

App Router의 revalidate, fetch 캐시 옵션은 프레임워크 레이어다. 그 위에 CDN이 HTTP 헤더를 어떻게 해석하는지가 얹힌다. 프로덕션에서는 원본이 보내는 Cache-Control엣지에서 덮어쓰기 규칙을 한 번에 점검한다.

core-web-vitals-seo-performance-guide에서 다룬 LCP 자원도 캐시 히트율이 곧 반복 방문 체감 속도다.


6. 무효화 전략

  • 버전 URL(권장): 에셋은 파일명 해시로 롤링.
  • 태그·퍼지: API 응답이 변할 때 CDN 퍼지 — 비용·전파 지연이 있다.
  • 짧은 TTL + SWR: 정적 퍼지 대신 짧은 신선도 + 백그라운드 갱신.

7. Vary·Cookie·개인화: 캐시를 망가뜨리는 조합

응답이 Set-Cookie로 세션을 심거나, Accept-Language에 따라 HTML이 달라지면, 공유 캐시는 같은 URL에 다른 바디를 저장해 다른 사용자에게 섞일 위험이 있다. 그래서 많은 사이트가 HTML에 private, no-store 또는 캐시 비활성을 건다.

Vary: Accept-Encoding은 일반적이고, 잘못된 Vary 조합은 캐시 적중률을 0에 가깝게 만든다. 디버깅할 때는 응답 헤더의 **Vary**와 요청 헤더를 나란히 본다. CDN 벤더 문서에 어떤 헤더를 캐시 키에 포함하는지(정규화 규칙 포함)가 정리되어 있는 경우가 많다.


8. Age 헤더: “얼마나 오래됐나”

Age프록시·CDN에 머문 시간(초)을 나타낸다. max-age와 함께 보면 남은 신선도를 추정할 수 있다. Agemax-age에 근접하면 곧 재검증이 일어날 타이밍이다.

원본이 Cache-Control을 잘못 보냈는데 CDN이 상한을 덮어쓰기하는 설정이 있으면, 개발자 도구에서 보이는 값과 실제 엣지 동작이 달라 혼란이 생긴다. “정책은 인프라 코드에, 문서에도” 남긴다.


9. GraphQL, GET, 그리고 HTTP 캐시

GraphQL을 GET 쿼리로 보내는 구성은 캐시 친화적일 수 있지만, 쿼리 문자열 폭발로 캐시 키가 비대해지고 URL 길이 제한에 걸릴 수 있다. POST 기반이면 기본 HTTP 캐시에서 이점이 적다는 전제가 있으므로, **APQ( persisted queries)**나 서버 측 캐시 레이어를 따로 논의한다.


10. Service Worker와 HTTP 캐시

브라우저의 HTTP 캐시Cache API(서비스 워커)는 별개다. PWA에서 오프라인 에셋을 다룰 때 두 층이 충돌하면 “왜 새 배포가 안 보이지?” 이슈가 난다. 배포 시에는 캐시 버스팅·스킵웨이팅 전략을 SW 코드에 명시한다(pwa-2026-practical-guide 맥락과 연결).


11. 디버깅 체크리스트 (현장용)

  1. 같은 URL, 시크릿 vs 일반 탭Set-Cookie 유무 차이
  2. curl -I와 브라우저 — 클라이언트가 보내는 Cache-Control 요청 지시자 차이
  3. CDN 대시보드 — Rule이 Cache-Control을 덮었는지
  4. 퍼지 직후에도 오래된 바디 — POP 전파 지연·로컬 SW
  5. API는 200인데 데이터만 옛것 — 앱 레이어 캐시(React Query 등)와 혼동하지 않기

12. 멀티 테넌시·B2B: 테넌트별 캐시 키

SaaS에서 경로만 같고 테넌트 헤더로 분기하면, 엣지에서 잘못된 캐시 키가 테넌트 간 누출을 일으킬 수 있다. 반드시:

  • 인증된 개인 데이터는 공유 캐시에 넣지 않거나
  • Edge에서 인증 검증 후 캐시 키에 테넌트 ID를 포함하는 명시적 규칙을 둔다

“공개 마케팅 페이지만 CDN”처럼 데이터 등급으로 레이어를 나누면 사고 표면이 줄어든다.


13. stale-while-revalidate 운영 시 주의점

SWR은 사용자에게 즉각적인 화면을 주지만, 최신이 아닌 데이터를 잠깐 보여 줄 수 있다. 주가·재고·자리 예약처럼 강한 신선도가 필요한 데이터에는 맞지 않을 수 있다. 반대로 뉴스·블로그 본문·카탈로그 정적 설명처럼 몇 초의 지연이 허용되는 리소스에는 UX 이점이 크다.

CDN마다 stale-while-revalidate 해석이 조금씩 다를 수 있어, 해당 벤더 문서를 확인하고 스테이징에서 헤더 전체를 캡처해 검증한다.


14. no-cacheno-store를 혼동하지 않기

많은 개발자가 **no-cache를 “캐시하면 안 된다”**로 오해한다. 일반적으로 no-cache저장은 가능하나 사용 전 재검증을 강하게 요구하는 쪽에 가깝다(브라우저·프록시 해석 차이는 존재). 저장 자체를 금지하는 데 가까운 의도라면 no-store를 검토한다.

팀 위키에 우리가 쓰는 값과 의도한 동작을 표로 적어 두면, 온콜 중 헤더 한 줄 수정으로 장애를 푸는 시간이 줄어든다.


15. 원본 보호: stale-if-error·헬스 체크

일부 인프라는 원본이 5xx일 때 오래된 캐시라도 제공하는 확장(Cache Control 확장 또는 CDN 기능)을 지원한다. 운영 편의는 크지만, 치명적으로 틀린 데이터(예: 잘못된 가격)가 캐시돼 있으면 복구가 어렵다. 반복 배포·데이터 정정 전략과 함께 캐시 TTL·퍼지 연동을 설계한다.


16. 로컬 개발과 프로덕션의 괴리

개발자 머신에서는 브라우저 확장, 기업 프록시, 로컬 HTTPS 인증서 때문에 Cache-Control이 프로덕션과 다르게 보일 수 있다. 문제 재현 시 시크릿 창, 확장 비활성화, **curl -v**로 분기한다. Next.js next dev는 프로덕션과 헤더·압축·스트리밍 동작이 다를 수 있으므로, 캐시 이슈는 가능하면 next build && next start 또는 스테이징에서 재확인한다.


맺음말

캐시는 "설정"이 아니라 제품 요구사항이다. 개인 데이터는 private·no-store로 막고, 공개 문서는 s-maxage로 싸고, 자산은 해시와 immutable로 끝낸다. 팀에 결정표 한 장만 있어도 인시던트가 줄어든다.