정적 사이트 배포 시 브라우저 캐시 무효화(Cache Invalidation) 전략
금요일 오후, 정적 사이트에 치명적인 결제를 고치는 핫픽스를 배포했습니다. Vercel 대시보드에는 초록색 체크마크가 떴습니다. 시크릿 모드를 열어 결제 버튼이 고쳐진 것을 확인하고 안도의 한숨을 내쉽니다. 하지만 10분 뒤, 고객센터 채널이 다시 불타오릅니다. 사용자들은 여전히 결제가 안 된다며 똑같이 깨진 예전 화면을 캡처해서 보내고 있습니다.
당신은 배포에 실패한 것이 아닙니다. '캐시 무효화(Cache Invalidation)'에 실패한 것입니다. 정적 사이트 운영에서 서버에 코드를 올리는 것은 절반의 성공에 불과합니다. 나머지 절반은 예전 파일을 끝까지 쥐고 놓지 않으려는 브라우저의 캐시 시스템과 싸워 이기는 것입니다. 만약 HTML의 캐시 전략과 CSS/JS 등 정적 자산(Asset)의 캐시 전략을 철저하게 분리하지 않았다면, 사용자들은 영원히 과거의 사이트에 갇혀 있게 됩니다.
1. "CDN 캐시 지우면 되는 거 아니야?"라는 치명적인 착각
배포 후 업데이트가 안 보일 때, 많은 운영자가 가장 먼저 하는 행동은 Cloudflare나 Netlify 대시보드로 달려가 "Purge Cache(캐시 삭제)" 버튼을 연타하는 것입니다. 하지만 이것은 엣지(Edge) 서버의 캐시를 지울 뿐, 이미 사용자의 브라우저 안에 깊숙이 저장된 로컬 캐시는 건드리지도 못합니다. 만약 사용자의 브라우저가 어제 `index.html`을 '24시간 유지'로 저장했다면, 당신이 CDN 캐시를 천 번 지워도 브라우저는 내일이 될 때까지 서버에 새 파일이 있는지 물어보지도 않습니다.
2. 무효화의 대원칙: 불변하는 자산(Immutable)과 변하는 문서(Mutable)
가장 확실하고 안전한 캐시 무효화 전략은 '파일명 해싱(File Hashing)'에 기반합니다. Astro, Vite, Webpack, Next.js 같은 현대적인 빌드 도구들은 코드가 한 글자라도 바뀔 때마다 `main-A4B7D9.css`, `app-88XF2.js`처럼 무작위 문자열을 파일명에 붙입니다.
파일 내용이 바뀌면 파일명도 통째로 바뀌기 때문에, 이 정적 자산들은 절대 변하지 않는 불변(Immutable)의 특성을 가집니다. 그래서 브라우저에게 "이 파일은 영원히 캐시해"라고 맘 편히 명령할 수 있습니다. 하지만 `index.html`은 다릅니다. HTML은 브라우저에게 '이번엔 어떤 해시 파일을 다운로드해야 하는지' 알려주는 이정표(Pointer) 역할을 합니다. 이정표가 과거를 가리키면 모든 것이 망가집니다. 따라서 HTML 파일은 절대 길게 캐시해서는 안 됩니다.
3. 배포 사고를 막는 완벽한 Cache-Control 헤더 설정
이 원칙을 지키기 위해, 호스팅 플랫폼(Netlify, Vercel, Cloudflare Pages, S3 등)의 설정 파일에서 두 가지 헤더를 정확히 분리해서 서빙해야 합니다.
- HTML 문서 (`*.html`): `Cache-Control: public, max-age=0, must-revalidate`
브라우저에게 이렇게 명령합니다. "이 파일을 로컬에 저장해도 좋아. 하지만 쓸 때마다 무조건 서버에 원본이 바뀌었는지 먼저 물어봐!" 만약 파일이 안 바뀌었다면 서버는 가벼운 `304 Not Modified`를 반환하므로 성능상 손해도 없습니다. - 해싱된 정적 자산 (`/assets/*` 등): `Cache-Control: public, max-age=31536000, immutable`
브라우저에게 이렇게 명령합니다. "이 파일은 1년 동안 절대 안 바뀌니까, 다음부터는 묻지도 따지지도 말고 네 하드디스크에 있는 걸 꺼내 써." 이 설정이 정적 사이트의 속도를 폭발적으로 높이는 핵심입니다.
4. 캐시 무효화의 숨겨진 최종 보스: 서비스 워커 (Service Worker)
헤더를 완벽하게 세팅했는데도 특정 사용자가 계속 구버전을 본다면, 높은 확률로 '서비스 워커'가 범인입니다. 과거에 PWA(Progressive Web App)를 시도하느라 서비스 워커를 설치했다면, 이 녀석이 브라우저의 HTTP 캐시를 무시하고 모든 네트워크 요청을 가로채서 구버전을 뱉어낼 수 있습니다. 이 상태에 빠지면 서버에서 할 수 있는 일은 없습니다. 사용자의 브라우저에 접속해 명시적으로 `navigator.serviceWorker.getRegistrations()`를 호출하고 기존 워커들을 강제로 삭제(unregister)하는 구명조끼 스크립트를 배포해야만 합니다.
5. 배포 후 화면이 깨질 때 확인하는 3단계 트러블슈팅
배포 후 화면이 바뀌지 않는다면 당황하지 말고 이 순서대로 원인을 좁히세요.
- 시크릿 모드 격리: 시크릿 모드(로컬 캐시를 완전히 무시함)에서 사이트를 엽니다. 여기서 업데이트가 잘 보이면 배포는 성공한 것이며, 순전히 로컬 브라우저의 캐시 정책 문제입니다.
- HTML 이정표 검증: 브라우저 개발자 도구의 네트워크(Network) 탭을 켜고 새로고침을 합니다. 가장 처음 로드되는 `index.html`의 응답 헤더를 확인하세요. `Cache-Control` 값이 `max-age=0`이 아니라면, 당신의 호스팅 플랫폼이 임의로 캐시를 먹이고 있는 것입니다. 당장 플랫폼 설정 파일(`netlify.toml`, `vercel.json` 등)을 고치세요.
- 해싱 누락 확인: 네트워크 탭에 로드된 CSS/JS 파일명에 무작위 문자열(Hash)이 붙어 있는지 확인하세요. 만약 그냥 `styles.css`나 `main.js`라면, 빌드 도구 설정에서 Hashing이 꺼져 있는 것입니다. 다음 배포 때 무조건 캐시가 꼬이게 됩니다.
무엇부터 시작할까
글을 다 읽었다면 지금 당장 여러분이 운영 중인 실서버 사이트에 접속하세요. 개발자 도구의 네트워크 탭을 열고, '캐시 사용 안 함(Disable cache)' 체크를 해제한 뒤 새로고침을 누릅니다. 맨 처음 로드되는 HTML 문서의 `Cache-Control` 헤더를 확인하세요. 만약 값이 `max-age=0`이 아니라면, 모든 작업을 멈추고 호스팅 설정부터 뜯어고쳐야 합니다.