1편의 문제 4: 프론트엔드 배포 후 최대 15분간 일부 사용자는 새 번들을, 일부는 구 번들을 받는다. 마지막 남은 문제이자, 가장 자주 발생하는(매 FE 배포) 문제다.
무엇이 문제였나
기존 배포의 마지막 두 줄.
aws s3 sync ./dist s3://$BUCKET --deleteaws cloudfront create-invalidation --paths "/*""/*"는 캐싱된 모든 파일을 무효화하라는 뜻이고, CloudFront는 이 신호를 전 세계 엣지로 전파한다. 전파 완료까지 최대 15분. 그동안 서울 엣지는 새 파일, 도쿄 엣지는 구 파일을 서빙한다. 한국 사용자와 일본 사용자가 서로 다른 버전의 앱을 돌린다.
근본 원인은 Vite 빌드 결과물의 특성
dist/├── index.html└── assets/ ├── main-Bx3f9k2a.js ← 파일명에 해시 ├── vendor-Ck7m2pQr.js ← 파일명에 해시 └── ...JS·CSS는 내용이 바뀌면 파일명 해시도 바뀐다. 한 줄 고치면 main-Bx3f9k2a.js가 main-Zp4qR8nT.js가 된다. 반면 index.html은 이름이 고정이고 내용만 바뀐다. 그리고 그 안에 어떤 해시 파일을 로드할지가 적혀 있다.
<script type="module" src="/assets/main-Zp4qR8nT.js"></script>여기서 갈린다. 두 종류의 파일은 정반대의 캐시 전략을 원한다.
| 파일 | 파일명 변경 | 안전한 전략 |
|---|---|---|
assets/*-해시.js | 내용 바뀌면 이름도 바뀜 | 영구 캐시 (immutable) |
index.html | 이름 고정, 내용만 바뀜 | 캐시 금지 (no-cache) |
해시 파일은 캐시해도 안전하다. 내용이 바뀌면 이름이 달라져 브라우저가 알아서 새로 요청한다. index.html은 캐시하면 안 된다. 구 index.html을 받으면 그 안의 구 JS 파일명을 그대로 로드한다.
세 단계로 분리
# 1. 해시 파일 — 1년 영구 캐시aws s3 sync ./apps/frontend/dist s3://$BUCKET \ --delete --exclude "index.html" \ --cache-control "max-age=31536000,immutable"
# 2. index.html — 항상 새로aws s3 cp ./apps/frontend/dist/index.html s3://$BUCKET/index.html \ --cache-control "no-cache,no-store,must-revalidate"
# 3. 무효화는 index.html 하나만aws cloudfront create-invalidation \ --distribution-id $DIST_ID --paths "/index.html"1단계. --exclude "index.html"로 해시 파일만 올린다. immutable은 “이 파일은 절대 안 바뀐다”는 선언이라 브라우저가 만료 전 재검증조차 안 한다. --delete로 이전 빌드의 잔여 해시 파일을 정리한다.
2단계. sync 대신 cp를 쓴다. sync는 크기·수정시간이 같으면 건너뛸 수 있는데, index.html은 매 배포마다 새 Cache-Control 헤더와 함께 반드시 덮어써야 한다.
3단계. 무효화를 "/*"에서 "/index.html" 하나로 좁혔다. 해시 파일은 무효화가 필요 없다. 구 main-Bx3f9k2a.js는 여전히 유효한 파일이고 새 main-Zp4qR8nT.js는 완전히 새 이름이라, CloudFront가 처음 요청받으면 S3에서 가져와 캐시한다. 덤으로 무효화 대상이 줄어 전파가 빠르고 비용(월 1,000건 초과 시 과금)도 준다.
결과
변경 전: 사용자 A(서울)는 새 index.html, B(도쿄)는 구 index.html → 최대 15분 혼재
변경 후: index.html은 no-cache → 둘 다 항상 S3 원본에서 받음 → 배포와 동시에 전 세계가 새 진입점 (JS/CSS는 캐시돼 빠름)index.html을 no-cache로 두니 CloudFront 무효화 전파 여부와 무관하게 브라우저가 매번 S3 원본을 받는다. S3 응답은 즉시 반영되므로, 배포 완료 시점에 모두가 새 진입점을 받는다.
남는 한계
no-cache는 CloudFront 캐시를 없애는 것이지 브라우저 캐시를 없애는 게 아니다. 정확히는 “쓰기 전에 서버에 재검증하라”는 뜻이다. 브라우저는 If-None-Match(ETag)로 S3에 묻고, S3가 304 Not Modified면 로컬 캐시를 쓴다. 재검증 왕복은 있지만 실제 다운로드는 없어 빠르다. 매 로드마다 index.html 재검증이 S3까지 간다는 작은 오버헤드가 남지만, 서비스 정확성을 위해 감수할 만하다.
네 편에 걸쳐 배포 파이프라인의 위험을 위험도 순으로 잡았다: 워크플로우 통합으로 순서를 강제하고, 실패 시 자동 롤백을 넣고, 캐시를 파일 특성에 맞게 나눴다. 네 편 모두 한 가지를 향했다. “구버전과 신버전이 공존하는 창”을 없애는 것이다. 무중단 배포는 그 창을 0으로 좁히는 일이다.