분명 테스트 때는 정상 동작했는데 막상 배포하면
갑자기 기능이 안되거나, js 오류가 나거나
배포한 내용을 반영하지 않거나 하는 이상 현상을 겪게 될 때가 있다.
(나도 알고 싶지 않았다.)
그 원인을 파악하다 보면 브라우저 캐시라는 걸 알게되고,
CDN이라는 개념도 인지하게 된다.
오늘도 회사에서 html 배포 후에 퍼지를 돌렸는데도
최신 소스가 반영이 안됐는데 js는 또 최신 소스가 반영돼서
오류가 났던 경험을 가지며 결국 총망라 정리를 하게 됐다.
HTML은 구버전(구버전 JS 파일명 참조) + JS는 신버전(새 파일명으로 배포됨)
→ 브라우저가 존재하지 않는 JS 파일을 요청해서 오류 발생
이 글에서는 브라우저 캐시의 기본 개념부터 시작해,
실무에서 CDN과 함께 어떻게 캐시 전략을 설계하는지
실제 사례와 함께 정리해본다.
브라우저 캐시란?
브라우저 캐시는 한 번 받아온 정적 리소스를
사용자의 로컬(본인들 컴퓨터 메모리 또는 디스크)에 사본으로 저장해두고,
다음 요청 시 서버로 가지 않고 로컬 사본을 재사용하는 방식이다.

다음과 같이, 첫 번째 요청 때는
브라우저에서 웹서버로 직접 리소스 요청을 하고
리소스 서빙을 할 때 웹서버에서는 응답값과 함께 캐시 헤더를 보내주게 된다.
그리고 그 속성에 따라 리소스를 캐시 하도록 지정이 되어있다면
브라우저 로컬에 저장하게 된다.

이후 재요청 시에, 캐시가 만료되기 전이라면 캐싱된 리소스를 사용하고
캐시가 만료되었다면 다시 웹서버로 재검증 요청을 하게 된다.
브라우저 캐시의 핵심 트레이드오프는
성능(로컬 캐시 재사용)과 최신성(서버 최신 리소스 보장) 사이의 균형이라고 할 수 있다.
이 균형을 잘 맞추지 못하면
새로운 리소스가 배포됐는데 캐싱된 구버전의 리소스가 보여져서
대참사가 날 수 있다. (알고 싶지 않았다)
캐시 동작 메커니즘
브라우저 캐시는 두 가지 설정으로 제어된다.
유효기간(얼마나 캐시할지)과 재검증 기준(언제 서버에 확인할지)이다.
유효기간 설정
| 헤더 | HTTP 버전 | 방식 | 예시 | 권장 |
|---|---|---|---|---|
| Cache-Control | HTTP 1.1+ | 상대시간 (초 단위) | max-age=3600 (받은 시간으로부터 3600초까지 캐시) | 권장 |
| Expires | HTTP 1.0+ | 절대시간 (특정 날짜) | Wed, 22 Apr 2027 (까지 캐시) | 레거시 |
Cache-Control이 더 현대적인 방법이기 때문에 권장한다. 그리고

다음과 같이 두 헤더가 모두 있으면 Cache-Control이 우선 적용된다.
그리고 네이버는 해당 파일을 1년간 캐시 하기로 한 것이다.
캐시하기로 한 파일은
https://s.pstatic.net/shopping.phinf/20260407_7/54cf5d03-5006-47ae-a530-03abce88ca9b.jpg?type=f294_378
=> 이런 url을 가지는데 이렇게 URL에 파일 내용 기반의 해시값을 넣은 기법을 핑거프린팅이라고 한다.
UUID는 랜덤값이라 내용이 바뀌지 않아도 매번 달라지지만, 해시는 내용이 바뀔 때만 달라진다는 차이가 있다.
그리고 이런 파일을 왜 1년간 캐시하는지는 아래에서 논하겠다.
재검증 기준
| 헤더 | 기준 | 재검증 요청 헤더 | 우선순위 |
|---|---|---|---|
| ETag | 파일 내용 해시값 | If-None-Match | 높음 |
| Last-Modified | 최종 수정 시간 | If-Modified-Since | 낮음 |
재검증 기준은 다음의 두 값을 통해 진행한다.
서버 리소스가 바뀌지 않았으면 304 Not Modified(바디 없음, 빠름),
바뀌었으면 200 OK와 함께 새 리소스를 전송한다.

위 이미지 기준으로 다시 봐보자.
1. Cache 시간은 60초이다.
2. 60초가 지나면 Etag값과 Last-modified 값 기준으로 웹서버에 요청을 보낸다
3. 근데 Etag값과 Last-modefied 값에 변화가 없으면 304 not Modified 값을 전달하고, 캐시된 파일을 그대로 사용하는 거다.
4. 만약 변경이 있으면 200OK와 함께 새 리소스를 전송하는 것.
Cache-Control 주요 옵션
Cache-Control의 주요 옵션들도 한 번 알아보면 좋다. 사실 이런거까지 모두 제어하는 실무를 본 적이 아직까지는 없다.
| 옵션 | 설명 |
|---|---|
| public | 브라우저 + CDN 모두 캐시 가능 |
| private | 브라우저만 캐시 (개인화 데이터용) |
| no-store | 저장 자체 금지 (민감 데이터용) |
| no-cache | 저장은 하되 사용 전 항상 재검증 (캐시를 하지 않겠다는 뜻이 아니다.) |
| must-revalidate | 만료 후 반드시 재검증 |
| immutable | 수명 동안 절대 변하지 않음, 강력 새로고침도 재검증 안 함 |
no-cache는 "저장은 하되, 사용 전에 항상 서버에 재검증하라"는 뜻이다.
저장 자체를 금지하는 건 no-store이다.
이름이 헷갈리지만 실무에서 매우 중요한 차이이다.

뉴스도있고, 개인 페이지도 있고 등등
개인화 + 보안 + 실시간성 때문에 절대적으로 최신성이 중요한 페이지라서인가 (내 추측)
저장도 하지말고, 하물며 저장하게 되더라도 다 검증을 무조건 하라는 것으로 속성을 저장했다.
결론 : 이 페이지는 절대 캐시하지 마라

이렇게 되면 CDN, 브라우저 모두 캐시 하고 유효기간도 가져가라는 뜻.
핑거프린팅(Fingerprinting) 전략 (중요하다)
속성들 많이 봤지만, 가장 강력한 캐시 전략은 파일 내용의 해시를 파일명에 포함시키는 것이다.
내용이 바뀌면 파일명 자체가 달라지기 때문에, 브라우저와 CDN이 완전히 다른 리소스로 인식해서 퍼지도 필요 없어진다.

=> 참고로 유명한 퍼지 사이트에서도 최고의 전략은 퍼지 자체를 최소화 하는 것이라고 한다.

쿼리스트링 방식의 문제
쿼리스트링 방식(main.js?timestamp=202604221800)은 브라우저에서는 잘 동작하지만 CDN에서 문제가 생길 수 있다.
즉
main.js?v=111과 main.js?v=222를 동일한 리소스로 보고 구버전을 계속 서빙할 수 있다.이 부분에 대해서는 CDN 담당자에게 반드시 확인이 필요함.
=> 글쎄 나도 이런거까지는 알고 싶지 않았다니까 (어떻게 알게 됐냐면. . 눈물)
HTML은 왜 핑거프린팅을 안 할까 ?
아니 공부하다 보니 js,css는 핑거프린팅 하는데
왜 html은 no-cache 전략으로 가라는 건지 이해가 안 가기 시작했다.
"index.a1s2d3.html 이렇게 하면 HTML도 핑거프린팅되는 거 아닌가?!"
그래 아니다.
왜냐하면 js 같은 경우에는 html에서 직접 서빙을 한다.
그렇기 때문에, html이 js의 최신 파일명을 명시를 해주게 된다.
하지만 html 같은 경우에는 상위 주체가 없다.
누가 html의 핑커프린팅 파일명을 알려줘서 이게 최신 파일이라고 알려줄 것인가 ?
브라우저는 새 html 파일명을 알 방법이 없다.
그렇기 때문에 캐시된 구버전 HTML만을 계속 사용할 뿐.
따라서, html은 Cache-Control 속성으로 no-cache를 사용해야 한다.
반면 HTML의 새 파일명을 알려줄 상위 주체는 존재하지 않음.
HTML은 모든 요청의 시작점(진입점)이기 때문임.
HTML의 최신성은 핑거프린팅이 아닌 no-cache로 해결한다.
CDN 캐시와 퍼지 전략
브라우저 캐시 전략만 알면 안된다.
왜냐면 실제 프로덕션 환경에서는 브라우저와 원본 서버 사이에 CDN(Content Delivery Network)이 존재하기 때문이다.

브라우저에서 로컬 캐시를 사용하다가 로컬 캐시가 만료되면 바로 웹서버로 가는 것이 아니라
CDN에 요청을 한다. 근데 또 CDN에 캐싱된 구버전의 파일이 있으면 웹서버까지 가지 않고 바뀐거 없네 !
라고 판단해서 다시 구버전 파일을 서빙한다.
그렇기 때문에, 실무에서는 이 CDN 캐싱 시간까지 지정을 해주어야 한다.
이 경우에는 max-age뿐 아니라 s-maxage까지 알아야 한다.
s-maxage: CDN 전용 캐시 설정
max-age는 브라우저 캐시 유효기간이고,
s-maxage는 CDN 같은 공유 캐시의 유효기간이다.
이 두 값을 분리해서 브라우저와 CDN을 독립적으로 제어할 수 있다.
Cache-Control: max-age=31536000, s-maxage=86400
=> 이렇게 되면 브라우저 캐시는 1년이지만, CDN에서 하루 동안 캐시한다.
하루가 지나면 CDN이 원본 서버에 재검증 요청을 한다.
단, 배포가 발생하면 하루를 기다리지 않고 퍼지로 즉시 캐시를 삭제할 수 있다.
각 전략마다 사용한 페이지를 찾고싶었으나, 찾지 못했다. 하지만 전략에 대해 잘 설명이 되어있는 글 발견

대충 리소스 별로 전략을 다르게 가져가야 한다는 이야기다.
| 모든 사용자 공통 HTML | max-age=0, s-maxage=86400 | 매 요청마다 최신 여부 확인 | 1일 캐시 | 최신성은 브라우저, 성능은 CDN이 담당 |
| 준정적 페이지 (상품, 블로그) | max-age=120, s-maxage=86400 | 짧게 캐시 (2분) | 1일 캐시 | 약간의 지연 허용, 성능과 최신성 균형 |
| 개인화 데이터 (로그인, 사용자 정보) | private, max-age=0 | 매 요청마다 최신 확인 | 캐시 안 함 | 사용자별 데이터 보호 (CDN 차단) |
| 정적 리소스 (JS, CSS, 폰트) | max-age=31536000, immutable | 1년 캐시 | 1년 캐시 | 해시 기반 파일, 재검증 없이 성능 극대화 |
CDN 퍼지(Purge)란
CDN하면 퍼지 개념도 꼭 알아야 한다.
배포 직후 CDN에 저장된 구버전 캐시를 강제로 삭제하는 기능을 말 한다.
파일 하나씩 퍼지하는 것보다 와일드카드 패턴으로 한 번에 처리하는 것이 효율적이다.
purge("/index.html")
purge("/main.js")
purge("/m/*") ← /m/ 하위 전체 한 번에
실무 캐시 전략 최종 정리
1. JS/Css는 핑거프린팅 적용해서 퍼지 없이 최신성이 반영 되도록 하자.
2. HTML은 트래픽 상황에 따라 전략을 다르게 가져가자.
- 트래픽 적음 → Cache-Control: no-cache, s-maxage=0 CDN 캐시 안 함, 퍼지 불필요
- 트래픽 많음 → Cache-Control: max-age=0, s-maxage=86400 CDN 하루 캐시, 배포 시 퍼지 자동화 필수
3. JS/CSS가 핑거프린팅이 되어있어도 HTML이 구버전이면 구버전 JS 파일명을 참조하게 되어서 오류가 난다. HTML 최신성 관리가 핵심이다.
4. 원본 서버 배포 후에는 CDN 퍼지 API를 자동 호출하게 해서 CDN의 구버전 캐시를 삭제하도록 해주자. 와일드카드 퍼지를 활용하면 파일 하나씩 퍼지하는 것보다 훨씬 효율적이다.
5. 상황별로 Cache-Control 속성을 잘 사용해서 퍼지 횟수를 최대한 줄이고 최신성과 성능의 트레이드오프를 잘 지켜나가자.