AWS CloudFront 캐싱 문제 해결하기
개요
사내에서 퍼블리셔분이 작업해야 할 정적 사이트의 배포 환경을 구축하면서 발생한 캐싱 이슈를 해결한 경험을 공유합니다. 프로젝트는 Vite를 기반으로 구축했으며, 배포 환경은 AWS S3 + CloudFront를 사용했습니다.
정적 리소스(HTML, CSS, JavaScript)를 다루면서 CDN의 캐싱 문제가 발생할 것으로 예상했고, 이 문제를 방지하기 위해 어떤 작업을 선행했는지, 그리고 추가적으로 발생한 문제를 어떻게 해결했는지 그 과정을 남기려 합니다.
CDN이란?
CDN(Content Delivery Network) 은 전세계 각지에 설치된 캐시 서버를 사용하여, 사용자와 가까운 위치의 캐시 서버에서 컨텐츠를 제공하는 기술입니다. AWS CloudFront, Cloudflare 등이 대표적인 CDN 서비스입니다.
컨텐츠를 제공하는 서버와 사용자 간의 물리적인 거리를 줄임으로써 컨텐츠 전송 시간이 단축되며, 캐싱을 통해 원본 서버에 대한 부하를 줄여 트래픽을 분산하고 인프라 비용이 감소하는 등의 장점이 있습니다.
하지만 CDN의 장점을 극대화하기 위해서는 적절한 캐싱 정책이 필수입니다. 잘못된 캐싱 설정은 컨텐츠가 최신 상태로 반영되지 않거나, 불필요한 원본 요청을 유발할 수 있습니다.
캐싱 무효화
캐싱된 리소스가 오래된 경우, 이를 갱신하거나 무효화하는 과정을 캐싱 무효화라고 합니다. 정적 리소스의 최신 상태를 보장하기 위해 흔히 사용하는 방법은 다음과 같습니다:
- 파일 이름 변경: 변경된 파일마다 고유한 이름 사용. (
main.v1.js
,main.v2.js
등) - Cache-Control 헤더: 파일에 HTTP 캐싱 제어 헤더를 설정. (
no-cache
,max-age
등) - CDN 무효화 요청: 기존 캐싱된 리소스를 CDN에서 강제로 삭제.
- 쿼리 파라미터 사용: 요청 URL에 쿼리 파라미터를 추가하여 캐싱 구분. (
?v=123
)
제가 설정하고자 했던 이상적인 캐싱 동작은 다음과 같았습니다. 첫째로 파일 이름을 변경하지 않더라도 파일 내용이 변경되면 새로운 리소스로 간주해야 하고, 둘째로 파일이 변경되지 않았다면 장기간 캐싱을 유지해야 합니다.
그렇기에 Cache-Control 헤더나 파일명을 변경하는 방법은 적절하지 않았습니다. 또한 퍼블리셔분의 AWS 계정에는 CloudFront 접근 권한이 없었기에 무효화를 수행하기 힘들었고, 무효화도 일정 횟수를 초과하면 추가 비용이 발생하기에 이를 자주 수행하는 것은 좋은 선택이 아니었습니다.
남은 것은 쿼리 파라미터를 추가하는 방법이었지만, 이 역시 손으로 직접 작업한다면 휴먼 에러의 가능성이 있었습니다. 그런 가능성을 미연에 방지하기 위해, 빌드 시 자동으로 리소스 경로에 ?v=...
쿼리파라미터를 붙여주는 Vite 플러그인을 작성했습니다.
// vite.config.ts
export default defineConfig(() => {
return {
plugins: [
{
name: 'add-v-query-parameter',
transformIndexHtml(html) {
return html.replace(/(src="..."을 찾는 정규식)/g, (_, p1) => {
// 리소스 경로에 랜덤한 쿼리 파라미터를 붙여 return
});
},
transform(code, id) {
if (id.endsWith('.css')) {
return code.replace(/url(...)을 찾는 정규식/g, (match, p1) => {
// 리소스 경로에 랜덤한 쿼리 파라미터를 붙여 return
});
}
return code;
},
},
],
};
});
이를 통해 무효화와 캐싱이 잘 수행되어 매번 최신 컨텐츠를 사용할 수 있을 거라 생각했습니다.
클라우드프론트 캐싱 정책 수정
플러그인을 통해 쿼리 파라미터를 추가했지만, 실제 배포한 사이트에서는 쿼리 파라미터를 무시하고 캐싱하고 있었습니다. 이미지 파일을 변경해서 새로 빌드 후 배포해도, 여전히 예전 이미지를 캐싱해서 보여주는 문제가 있었습니다.
구글링을 통해 CloudFront의 기본 캐시 정책인 CachingOptimized는 쿼리 파라미터를 캐싱 키에 포함하지 않는다는 것을 알게 되었고, 새로운 캐시 정책을 추가해 적용함으로써 문제를 해결했습니다.
개선사항
현재 방식은 매 빌드마다 모든 정적 리소스에 대해 새로운 캐싱을 수행합니다. 이는 파일이 변경되지 않은 경우에도 쿼리 파라미터가 새로 생성되어 기존 캐싱이 무효화되기 때문에 다소 비효율적일 수 있습니다.
이상적으로는 각 정적 리소스를 개별적으로 체크하여 파일 컨텐츠를 기반으로 해시값을 생성하고, 이를 쿼리 파라미터로 사용하는 방법이 더 적합할 것입니다. 이렇게 하면 파일이 변경되지 않은 경우 동일한 해시값을 유지하여 기존 캐싱을 재활용할 수 있고, 파일이 변경된 경우에만 새로운 해시값이 생성되어 필요한 캐싱만 무효화됩니다. 이러한 접근은 불필요한 캐시 갱신을 방지하고 CDN 및 클라이언트 측 캐싱 효율을 극대화할 수 있다는 점에서 더 효율적입니다.
다만 현재 프로젝트 규모에서는 모든 정적 리소스를 새로 캐싱하는 방식도 충분히 문제없이 작동하고 있으며, 구현이 간단하다는 장점이 있습니다. 따라서 지금의 방법을 유지하되, 대규모 프로젝트나 자주 배포가 필요한 환경에서는 컨텐츠 기반 해싱을 도입하는 방식을 고려할 계획입니다.
결론
이번에는 AWS CloudFront와 S3를 활용한 정적 리소스 배포 과정에서 발생한 캐싱 문제를 해결하기 위해 랜덤 쿼리 파라미터와 CloudFront 캐싱 정책 수정을 적용한 사례를 다뤘습니다. 현재 방식은 간단하면서도 문제를 효과적으로 해결했으며, 프로젝트 규모와 배포 빈도를 고려했을 때 충분히 적합한 접근법이었습니다.
앞으로 더 큰 프로젝트나 복잡한 환경에서는 콘텐츠 기반 해싱 방식을 도입해 캐싱 효율을 극대화할 가능성을 열어두고 있습니다. CDN 캐싱 문제를 겪고 있는 분들께 이 글이 실질적인 도움이 되길 바랍니다.