HTTP 캐시는 이전에 가져온 리소스를 저장해두고 재사용하는 방식으로 같은 리소스를 매번 서버에서 다운로드하는 대신, 로컬에 저장된 복사본을 사용해 웹 성능을 크게 향상시킨다.
캐시가 없다면 웹 페이지를 새로고침할 때마다 모든 리소스를 다시 다운로드를 해야하기 때문에, 수백 개의 파일을 매번 받으면 로딩 시간이 길어지고, 서버 부하도 증가한다.
캐시의 필요성
웹 페이지는 HTML, CSS, JavaScript, 이미지 등 수백 개의 리소스로 구성되며, 이 중 많은 파일은 자주 변경되지 않는다.
로고 이미지, CSS 프레임워크, jQuery 같은 라이브러리는 몇 달 또는 몇 년 동안 같은 파일을 사용하는 경우도 있다.
블로그 페이지를 방문하면 처음에는 모든 데이터를 다운로드하지만, 캐시를 사용해 두 번째 방문에서는 필요한 HTML만 받으면 된다.
캐시의 종류
캐시는 위치에 따라 여러 종류로 나뉘며 브라우저 캐시, 프록시 캐시, 게이트웨이 캐시가 있다.
브라우저 캐시 (Private Cache)
사용자의 브라우저에 저장되는 캐시로 클라이언트 개인 전용 공간을 사용하며 다른 사용자와 공유되지 않는다. 방문한 웹사이트의 이미지, CSS, JavaScript가 저장되며 브라우저 설정에서 캐시를 지우지 않는 한 계속 유지된다.
프록시 캐시 (Shared Cache)
프록시 서버나 CDN에 저장되는 캐시다. 여러 사용자가 공유한다. 한 사용자가 다운로드한 파일을 다른 사용자도 사용할 수 있어 전체적인 효율이 높다.
게이트웨이 캐시
웹 서버 앞단에 위치한 리버스 프록시 캐시로 주로 서버의 부하를 줄이기 위해 사용한다. Nginx, Varnish가 게이트웨이 캐시로 사용된다.
캐시의 기본 동작
브라우저가 리소스를 요청할 때의 흐름을 살펴보자. 캐시가 히트(hit)하면 네트워크 요청 없이 즉시 로드되고, 캐시가 미스(miss)하면 서버에서 다운로드한다.
1. 브라우저가 /style.css를 요청
2. 캐시에 있는지 확인
3-a. 캐시에 있고 유효함 → 캐시에서 반환
3-b. 캐시에 없거나 만료됨 → 서버에 요청
4. 서버 응답을 캐시에 저장
5. 리소스 사용
캐시 제어 헤더
HTTP 헤더를 통해 캐시 동작을 다음과 같이 제어한다.
Cache-Control 헤더
Catche-Control 헤더는 가장 중요한 캐시 제어 헤더로, 응답이 어떻게 캐시되어야 하는지 지시한다.
-
Cache-Control: no-store: 캐시하지 않아야 한다는 명령으로 민감한 데이터에 사용한다. -
Cache-Control: no-cache: 캐시는 하되, 사용하기 전에 서버에 재검증하라는 의미로 캐시에 저장은 하지만 매번 서버에 해당 캐시의 유무를 확인한다. -
Cache-Control: max-age=3600: 캐시 유효 기간을 초 단위로 지정한다. 3600초(1시간) 동안은 서버에 묻지 않고 캐시를 사용한다. -
Cache-Control: public, max-age=86400: 프록시를 포함한 모든 캐시에 저장할 수 있으며, 정적 파일(이미지, CSS)에 적합하다. -
Cache-Control: private, max-age=3600: 브라우저에만 캐시하고, 프록시에는 캐시하지 않는다.
Expires 헤더
캐시 만료 시각을 절대 시간으로 지정한다. Cache-Control의 max-age가 있으면 Expires는 무시된다. 하위 호환성을 위해 함께 사용하는 경우가 많다.
Expires: Wed, 21 Oct 2025 07:28:00 GMT
Pragma 헤더
HTTP/1.0 호환성을 위한 헤더로, Pragma: no-cache는 Cache-Control: no-cache와 같은 의미다. 최신 브라우저는 Cache-Control을 사용하지만, 오래된 클라이언트를 위해 병행한다.
Pragma: no-cache
캐시 검증과 조건부 요청
캐시된 리소스가 만료되면 서버에 재검증을 요청한다. 파일이 변경되지 않았다면 다시 다운로드하지 않고 캐시를 계속 사용한다.
-
Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT: 서버는 리소스의 마지막 수정 시간을 보낸다. -
If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT: 해당 캐시가 만료되면 브라우저는 If-Modified-Since 헤더로 재검증한다. -
HTTP/1.1 304 Not Modified: 파일이 변경되지 않았다면 서버는304 Not Modified를 반환한다. -
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4": ETag는 리소스의 고유 식별자다. 파일 내용의 해시값이나 버전 번호를 사용한다. -
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4": 재검증 시If-None-Match헤더를 사용한다.
캐시 전략
리소스 종류와 변경 빈도에 따라 적절한 캐시 전략을 선택해야 한다.
정적 파일 (이미지, CSS, JavaScript)
변경이 거의 없는 파일은 긴 캐시 시간을 설정한다. 빌드 도구(Webpack, Vite)가 자동으로 파일명에 해시를 추가하므로, 내용이 변경되면 파일명도 바뀌어 자동으로 캐시가 갱신된다.
아래와 같이 헤더를 구성하면 1년 동안 캐시하며, 파일명에 해시가 포함되어 있으므로 변경 시 자동으로 새 파일로 인식된다.
Cache-Control: public, max-age=31536000, immutable
HTML 파일
HTML은 비교적 자주 변경되므로 짧은 캐시 시간이나 재검증을 사용한다.
// 5분 캐시 후 이후 재검증
Cache-Control: no-cache
또는
Cache-Control: max-age=300
API 응답
API는 각 데이터의 변경 빈도에 따라 다르게 설정한다.
# 실시간 데이터
Cache-Control: no-store
# 자주 변경되지 않는 데이터
Cache-Control: private, max-age=300
# 공개 데이터
Cache-Control: public, max-age=600
민감한 데이터
절대 캐시하지 않는다. 인증 정보, 결제 내역 등은 캐시되면 안 된다.
Cache-Control: no-store, private
캐시 무효화
캐시된 파일을 강제로 갱신해야 할 때 사용하는 방법들이다.
파일명에 해시 포함
파일명에 해시를 포함하는 방법으로 가장 권장되는 방식이다.
빌드 도구가 파일 내용의 해시를 파일명에 자동으로 추가한다. 내용이 변경되면 파일명도 바귀며 자동으로 캐시가 갱신된다.
style.abc123.css
app.def456.js
버전 쿼리 파라미터
URL에 버전 파라미터를 추가하는 방식으로 버전을 바꾸면 브라우저는 새 URL로 인식하여 다시 다운로드한다.
<link rel="stylesheet" href="/style.css?v=2" />
<script src="/app.js?v=1.2.3"></script>
서버 설정 변경
긴급한 버그 수정 시 서버 설정을 변경하여 캐시 시간을 줄인다.
# 기존
Cache-Control: max-age=86400
# 긴급 수정 후
Cache-Control: max-age=60
HTTP 캐시는 웹 성능의 핵심으로 적절한 캐시 전략과 검증 메커니즘을 사용하면 로딩 속도를 크게 개선하고, 서버 비용을 절감하며, 사용자 경험을 향상시킬 수 있다.