S3를 오리진으로 두고 CloudFront로 SPA를 배포하면, 파일을 새로 올렸는데도 일부 사용자가 이전 화면을 보거나 앱이 시작하지 않는 일이 생길 수 있다. 이전 index.html은 예전 해시의 JavaScript 파일을 가리키는데 새 오리진에는 그 파일이 없으면, 앱은 시작 단계에서 404를 받고 멈춘다. 웹뷰처럼 앱 데이터를 오래 보존하는 환경에서는 재설치나 데이터 삭제 후에만 정상으로 보일 수도 있다.
- 사용자의 WebView가 이전
index.html을 로컬 캐시에서 읽는다. - HTML은 이미 삭제됐거나 다른 경로로 이동한 이전 해시 에셋을 요청한다.
- 에셋 요청이 404가 되면 SPA가 초기화되지 못한다.
- 네이티브 앱이 웹 페이지의 초기화 신호를 기다리는 구조라면 로딩 화면이 끝나지 않을 수 있다.
S3 + CloudFront 에서 캐시를 사용하는 곳:
- 브라우저(WebView)
- CloudFront 캐시
- S3 오리진 객체와 메타데이터
S3 객체의 Cache-Control 메타데이터가 응답 헤더에 담긴다.
CloudFront는 캐시 정책의 TTL과 오리진 응답 헤더를 함께 사용해 엣지 캐시의 유효 기간을 결정하고, 브라우저도 같은 응답 헤더를 보고 로컬 캐시를 판단한다.
docs.aws.amazon.com
S3 객체 메타데이터 문서
Find and change select Amazon S3 object metadata.
docs.aws.amazon.com
CloudFront 만료 제어
Control how long files stay in the CloudFront cache and in browser caches.
따라서 CloudFront 무효화를 했으니 최신 파일을 받는다고 생각하면 안된다. 브라우저가 아직 유효하다고 판단한 index.html을 그대로 쓸 수 있다.
SPA에서 캐시 규칙
Vite 같은 번들러가 만드는 SPA 결과물은 크게 세 종류로 나눌 수 있다.
| 파일 | 역할 | 권장 캐시 |
|---|---|---|
assets/app-<hash>.js, assets/app-<hash>.css | 내용이 바뀌면 파일명도 바뀌는 해시 에셋 | public, max-age=31536000, immutable |
index.html | 현재 배포의 해시 에셋 목록을 가리키는 진입점 | no-cache |
version.json 같은 버전 확인 파일 | 현재 버전을 판단하는 작은 메타 파일 | no-cache |
해시가 붙은 에셋은 동일한 URL에서 내용이 바뀌지 않는다. 캐시를 길게 적용해도 새로 배포하면 바뀐 URL을 참조하므로 안전하다. 반대로 index.html은 URL이 고정되어 있고 매 배포마다 내용이 달라진다. 캐싱을 해버리면 이전 번들을 가리키는 HTML이 남는다.
no-cache는 파일을 저장하지 말라는 뜻은 아니다. 캐시된 응답을 사용하기 전에 서버에 재검증하라는 뜻이다. 변경되지 않았다면 304 응답으로 본문 전송을 줄이고, 변경됐다면 새 파일을 받아온다. 다만 CloudFront의 캐시 정책에서 Minimum TTL이 0보다 크면 오리진의 no-cache나 no-store보다 최소 TTL이 우선한다. 진입점 파일을 항상 재검증하려면 해당 캐시 동작의 Minimum TTL도 함께 확인해야 한다.
docs.aws.amazon.com
Cache behavior settings - Amazon CloudFront
Configure cache behavior settings for your CloudFront distribution to control how CloudFront handles requests for different URL path patterns, including origin selection, protocol policies, and caching options.
위 링크에서 우선순위를 명시하고 있음.
구버전 HTML과 새 버전 정보가 충돌
버전 확인 파일을 별도로 두고 있는데, 캐시된 index.html은 이전 버전 코드를 실행하지만, version.json은 새 버전을 응답하면 앱은 계속 업데이트가 필요하다고 판단한다.
새로고침 URL에 타임스탬프 쿼리를 붙이면 일시적으로 해결된 것처럼 보일 수 있다. 하지만 /와 /?t=...는 캐시 키가 다를 수 있으므로, 다음에 다시 /로 접근하면 기존 HTML 캐시를 또 사용할 수 있다. 임시 쿼리 문자열보다 진입점 자체의 캐시 규칙을 바로잡는 편이 낫다.
S3 업로드 시 Cache-Control 분리
CI/CD 구성 시 해시 에셋과 고정 이름 파일을 한 번에 동기화하지 않고, 서로 다른 명령으로 올렸다.
# 해시 에셋: index.html과 버전 파일은 제외한다.
aws s3 sync ./dist "s3://$BUCKET" \
--exclude "*.map" \
--exclude "index.html" \
--exclude "version.json" \
--cache-control "public,max-age=31536000,immutable"
# 진입점과 버전 파일: 사용할 때마다 재검증하게 한다.
aws s3 cp ./dist/index.html "s3://$BUCKET/index.html" \
--cache-control "no-cache"
aws s3 cp ./dist/version.json "s3://$BUCKET/version.json" \
--cache-control "no-cache"
첫 번째 명령은 파일명 자체가 배포 버전인 에셋에 장기 캐시를 설정한다. 나머지 두 명령은 고정 URL 파일을 별도로 업로드해 S3 객체 메타데이터에 다른 Cache-Control 값을 지정한다.
이미 배포된 객체의 캐시 메타데이터를 바꿔야 한다면 실제로 메타데이터가 교체되는지 확인해야 한다. 같은 파일을 다시 동기화했을 때 전송이 생략될 수 있으므로, 배포 후 S3 객체의 메타데이터와 CloudFront를 거친 최종 응답 헤더를 함께 확인하는 것이 안전하다.
CloudFront에서는 캐시 정책도 같은 방향이어야 한다
S3에서 no-cache 여도 CloudFront 캐시 정책에 따라서 기대한 대로 동작하지 않을 수 있다.
- 캐시를 최대한 사용하지 않으려면 behavior의 Minimum TTL을 0 으로 하기
- 그 외 일반적으로 캐시를 사용할 경우 behavior는 TTL 적절한 범위인지 확인
- query string, 쿠키, 헤더를 캐시 키에 포함할 필요가 있는지 별도로 판단한다. 불필요한 값을 넣으면 같은 파일의 캐시가 지나치게 쪼개진다. 캐시 키 정책 문서를 확인하고 진행.
- Response headers policy로
Cache-Control을 추가했다면, 이것은 브라우저에 보내는 응답 헤더를 바꾸는 설정이며 CloudFront 엣지 캐시 TTL 자체를 바꾸는 것은 아니다. 응답 헤더 추가 삭제 정책 확인.
docs.aws.amazon.com
캐시 키 정책 문서
Use cache policies to control the cache key for cache behaviors in your Amazon CloudFront distributions.
docs.aws.amazon.com
CloudFront 응답에 헤더 추가 삭제 정책
Use a response headers policy to specify the HTTP headers that Amazon CloudFront adds or removes in HTTP responses.