캐시는 어디에 몇 겹으로 있나: 브라우저부터 CDN까지
고쳤는데 옛 화면이 보일 때 어느 캐시를 봐야 하는지 브라우저·CDN·프레임워크·데이터 계층 순으로 나누고, 원인을 좁히는 순서를 정리했습니다.
고쳤는데 왜 그대로 보일까
코드를 고치고 배포했는데 브라우저에는 옛날 화면이 그대로 나옵니다. 새로고침해도 같습니다. 시크릿 창으로 열면 정상입니다. 이럴 때 "캐시 때문"이라는 건 알겠는데, 어느 캐시인지를 모르면 고칠 수가 없습니다.
요청 하나가 응답을 받기까지 캐시는 여러 겹입니다. 겹마다 지우는 방법과 통제하는 방법이 다릅니다. 순서대로 짚습니다.
1겹: 브라우저 메모리·디스크 캐시
브라우저는 받은 응답을 저장해두고 다음 요청에 재사용합니다. 규칙을 정하는 것은 응답 헤더입니다.
Cache-Control: max-age=3600
이 응답을 받은 브라우저는 1시간 동안 서버에 묻지도 않고 저장본을 씁니다. 서버를 고쳐도 그 1시간 동안은 반영되지 않습니다. 개발자 도구를 열고 새로고침해도 마찬가지입니다. 이때 필요한 것이 강제 새로고침이거나, 개발자 도구의 캐시 사용 안 함 옵션입니다.
max-age가 지나면 브라우저는 바로 버리지 않고 서버에 물어봅니다. 이때 쓰이는 것이 검증 헤더입니다.
ETag: "a1b2c3"
Last-Modified: Wed, 10 Sep 2026 12:00:00 GMT
브라우저가 If-None-Match: "a1b2c3"으로 물으면 서버는 내용이 그대로일 때 304 Not Modified만 보냅니다. 본문을 다시 보내지 않으니 전송량이 줄어듭니다.
2겹: CDN과 리버스 프록시
사용자와 서버 사이에 있는 중간 캐시입니다. 여기 저장된 응답은 여러 사용자에게 공유됩니다. 이 점이 브라우저 캐시와 결정적으로 다릅니다.
그래서 사용자마다 달라야 하는 응답을 실수로 중간 캐시에 올리면, 다른 사람의 페이지가 보이는 사고가 납니다. 로그인 상태에 따라 내용이 달라지는 페이지가 대표적인 위험 지점입니다.
이걸 구분하는 것이 private와 public입니다.
Cache-Control: private, no-store # 개인화된 응답 — 중간 캐시에 저장 금지
Cache-Control: public, max-age=31536000, immutable # 누구에게나 같은 정적 파일
private는 "브라우저는 저장해도 되지만 공유 캐시는 안 된다"는 뜻입니다. no-store는 아예 저장하지 말라는 뜻이고요. 개인정보가 담긴 응답에는 no-store가 맞습니다.
응답이 요청 헤더에 따라 달라진다면 Vary로 알려야 합니다. 예를 들어 언어별로 다른 내용을 준다면 Vary: Accept-Language가 없을 때 한국어 사용자가 영어 페이지를 받게 됩니다.
3겹: 프레임워크와 애플리케이션 캐시
최근 프레임워크들은 자체 캐시 계층을 갖고 있습니다. 렌더링 결과를 저장해두거나, 데이터 조회 결과를 재사용하거나, 빌드 시점에 페이지를 미리 만들어두는 식입니다.
여기서 흔히 겪는 일이 "DB는 바뀌었는데 페이지가 안 바뀌는" 상황입니다. 브라우저 캐시를 아무리 지워도 소용없습니다. 서버가 저장해둔 렌더링 결과를 그대로 주고 있기 때문입니다.
해결은 무효화입니다. 데이터가 바뀌는 시점에 해당 경로의 캐시를 명시적으로 버려야 합니다. 글을 발행했다면 목록 페이지, 상세 페이지, 사이트맵을 함께 무효화해야 하는 식입니다. 하나라도 빠뜨리면 그 페이지만 옛 내용으로 남습니다.
4겹: 데이터 계층 캐시
Redis 같은 저장소에 조회 결과를 담아두는 경우입니다. 직접 만든 것이니 무효화도 직접 해야 합니다.
여기서 자주 나오는 실수는 쓰기 경로에서 캐시를 지우지 않는 것입니다. 읽을 때만 캐시를 보고, 업데이트할 때는 DB만 고치면 캐시는 옛 값을 계속 들고 있습니다. 만료 시간이 지나야 겨우 맞아집니다.
정적 파일은 반대로 간다
지금까지는 "캐시 때문에 안 바뀐다"였는데, 정적 파일은 오히려 오래 캐시되는 것이 정답입니다.
빌드 도구가 파일 이름에 해시를 붙이는 이유가 이것입니다. app.a1b2c3.js처럼요. 내용이 바뀌면 이름이 바뀌므로, 파일 자체는 1년 동안 캐시해도 안전합니다. 새 배포에서는 HTML이 새 파일 이름을 가리키게 되니까요.
그래서 HTML은 짧게, 해시가 붙은 정적 파일은 길게가 기본 전략입니다. HTML까지 길게 캐시하면 새 파일 이름을 아무도 못 받아갑니다.
순서대로 좁히는 법
옛 내용이 보일 때는 바깥쪽부터 안쪽으로 확인합니다.
- 시크릿 창에서 열어본다. 정상이면 브라우저 캐시입니다.
- 시크릿에서도 옛날이면 브라우저 문제가 아닙니다. 다음으로 갑니다.
curl -sI <url>로 헤더를 본다. CDN이 준 응답이면 대개 캐시 적중 여부를 알려주는 헤더가 붙어 있습니다.- 서버에 직접 요청해본다. CDN을 우회해 원본 서버 응답이 최신이면 CDN 캐시를 비우면 됩니다.
- 원본 서버도 옛날이면 프레임워크나 애플리케이션 캐시입니다. 무효화 코드가 도는지 확인합니다.
이 순서를 지키면 엉뚱한 곳을 고치는 일이 줄어듭니다.
no-cache는 캐시하지 말라는 뜻인가요?
아닙니다. 헷갈리기 쉬운 이름인데, no-cache는 **"저장은 하되 쓰기 전에 서버에 확인하라"**는 뜻입니다. 저장 자체를 막는 것은 no-store입니다. 민감한 정보를 담은 응답에 no-cache만 걸어두면 디스크에는 남습니다.
배포할 때마다 CDN 캐시를 통째로 비우면 안 되나요?
가능하지만 좋은 습관은 아닙니다. 캐시를 전부 버리면 그 직후 모든 요청이 원본 서버로 몰려 부하가 튑니다. 바뀐 경로만 지정해 비우거나, 파일 이름에 해시를 붙여 애초에 비울 필요가 없게 만드는 편이 낫습니다.
사용자에게 강제로 새로고침을 요청해도 되나요?
임시방편은 되지만 해결은 아닙니다. 대부분의 사용자는 강제 새로고침 방법을 모르고, 모바일에서는 더 어렵습니다. HTML을 짧게 캐시하고 정적 파일에 해시를 붙이는 구성으로 바꾸면 이 요청 자체가 필요 없어집니다.
헤더를 한 번 찍어보는 것부터
자기 사이트에 curl -sI를 한 번 날려보면 지금 어떤 캐시 정책이 걸려 있는지 바로 보입니다. 의도한 적 없는 헤더가 붙어 있는 경우가 흔합니다.
만든 프로젝트가 배포된 상태라면 Vibeollio에 프로젝트 등록해서 공개해보세요. 캐시 문제는 여러 사람이 각자 다른 시점에 접속할 때 드러나는데, 혼자 새로고침하는 것으로는 재현되지 않는 경우가 많습니다.
