Intro

새로운 버전을 배포했는데 사용자가 이전 버전을 계속 사용해서 문제가 생기는 상황을 경험해 보셨나요?
이 문제는 HTTP 캐시 문제일 확률이 높습니다.
이 글에서는 HTTP 캐시의 원리를 차근차근 살펴보고, 브라우저와 CDN이 어떻게 캐시를 판단하고 동작하는지,
그리고 실무에서 안전하게 캐시를 설계하는 방법까지 정리해보겠습니다.
1. 캐시란 ?
캐시는 아주 단순한 개념입니다. 실제 데이터 저장소에서 직접 가져오는 대신에 이전에 가져온 데이터를 사용하는 것이 캐시의 개념입니다.
“한 번 가져온 것을 원래 위치보다 가까운 곳에 저장해두고 빠르게 다시 사용하는 것”
1-1. 캐시 대상
HTTP에서 말하는 리소스(Resource) 란, 브라우저가 요청할 수 있는 모든 파일을 의미합니다.
| 종류 | 예시 | 캐시 중요도 |
| HTML | index.html, about.html | 높음 |
| CSS | style.css, main.css | 높음 |
| JavaScript | app.js, bundle.js | 높음 |
| 이미지 | logo.png, hero.jpg | 높음 |
| 폰트 | Pretendard.woff2 | 중간 |
| API 응답 | /api/users | 중간 |
1-2. 캐시의 역할
캐시의 유무는 성능과 비용, 사용자 경험 전반에 영향을 미칩니다.
| 캐시없는 상황 | 캐시 상황 | |
| 속도 | 매번 네트워크 왕복 | 즉시 로드 |
| 서버 부하 | 모든 요청 처리 | 대부분 요청 없음 |
| 트래픽 비용 | 매번 데이터 전송 | 거의 전송 없음 |
| 사용자 경험 | 느림 | 빠르고 쾌적 |
2. 브라우저의 서버에 요청
2-1. 웹 페이지의 리소스 요청
www.naver.com에 접속하면 브라우저는 HTML 하나만 요청하지 않습니다.
- 사용자가 URL 입력
- GET /index.html
- HTML 파싱 중 발견된 리소스 자동 요청
- <link href="style.css"> → GET /style.css
- <script src="app.js"> → GET /app.js
- <img src="logo.png"> → GET /logo.png
- JavaScript 실행 중 동적 요청
- fetch('/api/users')
- 브라우저 자동 요청
- favicon.ico
Chrome DevTools Network 탭에서는 다음과 같이 보입니다.

중요한 점은 각 요청은 서로 독립적으로 캐시된다는 것입니다.
같은 페이지의 리소스라도 캐시 정책은 모두 다를 수 있습니다.
3. 캐시의 종류
3-1. 브라우저 캐시: Memory vs Disk
| Memory Cache | Disk Cache | |
| 저장 위치 | RAM | SSD/HDD |
| 속도 | 매우 빠름 (~0ms) | 빠름 (~5ms) |
| 수명 | 탭 종료 시 삭제 | 수 주~수 개월 |
| 대상 | 현재 페이지 리소스 | 큰 파일, 재방문 리소스 |
| DevTools | (memory cache) | (disk cache) |
3-2. CDN 캐시
CDN(Content Delivery Network)은 전 세계에 분산된 캐시 서버입니다.
- 여러 사용자가 공유
- 원본 서버 부하 감소
- 사용자와 가까운 위치에서 응답
- 개인정보 저장 금지 (보안 문제)
3-3. 캐시 무효화( invalidation)의 행동 주체
버그 수정 후 배포했다고 가정해보겠습니다.
캐시 주체가 CDN이라면 cache invalidation 요청으로 캐시를 무효화할 수 있습니다. 행동 주체가 개발자입니다.
하지만 브라우저 캐시의 경우 캐시 사용의 판단 주체가 client이고 max-age가 만료되기 전까지는 이전 캐시를 사용하게 됩니다. 캐시 무효화의 행동주체가 client이고 개발자가 제어할 수 없습니다.
결론
브라우저 캐시는 서버에서 강제로 제어할 수 없습니다.
처음부터 올바르게 설계해야 합니다.
4. 브라우저 캐시 제어
브라우저는 HTTP Response Header를 보고 캐시를 판단합니다.
응답 헤더의 Cache-Control이 `no-store`인 경우 리소스를 저장하지 않습니다.
아래는 nextjs 애플리케이션의 index.html 요청입니다. 항상 서버로 부터 새로운 요청을 하기 위해 캐시가 되지 않습니다.

캐시 컨트롤이 있는 경우입니다.
아래의 경우 각 항목은 다음을 의미합니다.
public 캐시
어떤 캐시든 이 리소스를 저장할 수 있고 브라우저, CDN, 프록시 서버 등 모든 레이어에서 캐시 저장 가능하다는 의미입니다.
만약 private라고 되어 있다면 개인화된 캐시임으로 브라우저에서만 저장 가능함을 의미합니다.
max-age=31536000
이 캐시는 1년 동안 유효합니다
immutable
절대 변경되지 않으니 재검증을 할 필요가 없습니다.

5. Cache-Control 헤더 정리
5-1. max-age: 유효 기간
| 값 | 의미 |
| max-age=0 | 매번 재검증 |
| max-age=60 | 1분 |
| max-age=3600 | 1시간 |
| max-age=86400 | 1일 |
| max-age=31536000 | 1년 |
5-2. no-cache vs no-store
no-cache
Cache-Control: no-cache
- 저장함
- 사용 전 반드시 서버에 재검증이 필요함
- 재검증 결과에 따라 캐시 사용여부 결정 (304 응답)
no-store
Cache-Control: no-store
- 브라우저, CDN, 어디도 저장하지 않음
- 매번 origin 서버로 부터응답
- 가장 안전하지만 가장 느림
| no-chace | no-store | |
| 캐시 저장 | O | X |
| 재검증 | O | X |
| 304 | O | X |
| 보안 | 중간 | 최고 |
5-3. public vs private
Cache-Control: public, max-age=3600
Cache-Control: private, max-age=3600
- public: 브라우저 + CDN 저장 가능
- private: 브라우저만 저장
5-4. s-maxage: CDN 전용 설정
Cache-Control: s-maxage=31536000, max-age=0
이 설정은 CDN 전용설정이라 할 수 있습니다. 위 예시를 설명하면 다음과 같습니다.
max-age가 0이기 떄문에 브라우저는 매번 재요청을 합니다.
s-maxage=31536000이기 때문에 1년간 데이터를 저장합니다.
따라서 캐시의 저장 위치는 CDN이 되며 재배포시 개발자에 의해 캐시를 무효화할 수 있습니다.
핵심은 개발자에 의한 캐시 데이터 제어 가능 여부입니다.
5-5. stale-while-revalidate
Cache-Control: max-age=60, stale-while-revalidate=300
- max-age=60: 60초까지는 데이터의 신선도를 보장하며 즉시 캐시 데이터를 사용합니다.
- stale-while-revalidate=300. 캐시 신서도 보장시간(max-age)이후에는 300초 동안은 캐시를 응답하되 백그라운드에서 데이터를 갱신합니다.
6. private과 Authorization 헤더
Authorization 헤더가 있으면 기본적으로 CDN은 캐시하지 않습니다.
하지만 서버가 public이나 s-maxage를 명시하면 캐시됩니다.
결론적으로 개인 데이터에는 반드시 private을 명시해야 합니다.
7. 캐시 재검증 (Revalidation)
서버가 데이터가 변경됐는지 판단하는 두 가지 항목이 있습니다.
| Etag | Last-Modified | |
| 정확도 | 높음 | 낮음 |
| 권장 | 우선 | 보조 |
7-1. ETag Header
파일 내용의 해시값, 파일 내용이 변경되면 해시값이 변경되어 새로운 요청이 발생합니다.
http 헤더의 Etag는 특정 버전의 리소스를 실벽합니다. 웹 서버가 내용을 확인하고 변하지 않았으면, 이 ETag가 유지되어 서버에 요청을 보내지 않고 캐시 hit처리를 할 수 있습니다.
이후 If-None-Match 헤더와 If-Match 헤더를 통해 ETag의 값이 활용도힙
구체적인 시나리오를 통해 설명해보겠습니다.
최초 요처청에서 사용자 정보를 조회했습니다.
요청
GET /api/users/42 HTTP/1.1
응답
HTTP/1.1 200 OK
ETag: "x234dff"
Cache-Control: private, max-age=0
이후 다시 재조회를 할 때 If-None-Match를 통해 변경 여부를 갱신 여부를 판단합니다.
요청
GET /api/users/42
HTTP/1.1
If-None-Match: "x234dff"
응답
HTTP/1.1 304 Not Modified
ETag: "x234dff"
이 경우 처음 요청 했을 때 ETag값이없던 x234dff가 동일하여 캐시를 사용하고 304응답( Not Modified )을 했습니다.
다음은 수정 시나리오에서 If-Match 항목 활용 시나리오입니다.
관리자가 처음 사용자 프로필을 조회한 이후에 해당 프로필을 수정하려고 합니다. 그런데 다른 관리자가 이 사용자를 수정한 경우, 수정 요청이 발생하면 안된다고 가정해보겠습니다.
첫 조회시 응답에서 ETag가 "x234dff"을 확인했습니다.
이 경우 클라이언트는 다음과 같이 서버에 요청보낼 수 있습니다.
PATCH /api/users/42 HTTP/1.1
If-Match: "x234dff"
Content-Type: application/json
{
"title": "Senior Frontend Developer"
}
위 내용을 해석해보면 "내가 조회한 사용자 프로필의 버전은 x234dff였는데 현재 최신 버전과 동일하다면 수정해줘" 라고 할 수 있습니다.
즉 내가 보고있던 버전이 최신 버전인 경우 일치(match)함으로 요청이 수락되고 다를 경우 요청이 412 코드와 함께 요청이 실패합니다."니가 보고 있는건 최신 데이터가 아니니까 최신 데이터를 조회한 이후에 다시 수정 요청을 해"라는 의미로 해석 할 수 있습니다.
HTTP/1.1 412 Precondition Failed
7-2 Last-Modified Header
서버가 판단하는 리소스의 마지막 변경된 시점을 나타내는 응답 해더입니다.
Last-Modified: Wed, 04 Dec 2024 08:15:30 GMT
ETag와 마찬가지로 아래처럼 조건부 요청이 가능합니다. 단지 변경 확인의 기준이 날짜라는 차이점이 있습니다.
아래 요청은 "변경이 됐으면 새로 데이터를 조회해줘"라는 의미입니다.
GET /articles/10
If-Modified-Since: Wed, 04 Dec 2024 08:15:30 GMT
아래 요청의 의미는 "내가 조회한 시점 이후에 누군가 수정했음면, 내 요청은 거부해라"라는 의미입니다.
PATCH /articles/10
If-Unmodified-Since: Wed, 04 Dec 2024 08:15:30 GMT
8. 파일명 해시 전략 (Cache Busting)
- 파일 내용이 바뀌면 파일명도 바뀐다
- 파일명이 바뀌면 캐시는 무효화된다
- HTML은 해시를 붙이면 안 됩니다.
| 리소스 | 전략 |
| HTML | 고정 URL + no-cache |
| JS/CSS | 해시 포함 + 1년 캐시 |
9. 캐시와 URL의 관계
캐시는 URL을 키로 저장됩니다
같은 파일이라도 URL이 다르면 완전히 다른 캐시입니다.
10. 실전 Cache-Control 전략
| 리소스 | 권장 설정 |
| HTML | no-cache |
| JS/CSS | public, max-age=31536000, immutable |
| 개인 API | private, no-cache |
| 민감 정보 | no-store |
참고
https://yozm.wishket.com/magazine/detail/2341/
프론트엔드 개발자가 알아야 할 ‘캐싱’ 개념 정리 | 요즘IT
프론트엔드 개발자라면 아마 웹사이트 성능을 최적화하는 것에 관심이 있을 것이다. 웹사이트 로딩 시간을 줄이고, 물 흐르듯 자연스러운 사용자 경험을 만드는 것은 비즈니스에도 큰 영향을
yozm.wishket.com
https://toss.tech/article/smart-web-service-cache
웹 서비스 캐시 똑똑하게 다루기
웹 성능을 위해 꼭 필요한 캐시, 제대로 설정하기 쉽지 않습니다. 토스 프론트엔드 챕터에서 올바르게 캐시를 설정하기 위한 노하우를 공유합니다.
toss.tech
'programming' 카테고리의 다른 글
| 객체지향 클래스 설계의 세 가지 관점: 개념, 명세, 구현 (0) | 2025.09.06 |
|---|---|
| 견고한 소프트웨어를 위한 오류 핸들링 (0) | 2024.07.01 |
| [설계] 오용하기 어려운 코드 설계하기 (0) | 2024.06.30 |
| [설계] 단일 책임 원칙에 근거한 모듈 설계 (0) | 2024.06.28 |