서버 및 호스팅 성능

브라우저 캐시 설정: Cache-Control, 검증자와 안전한 파일 버전 관리

버전 파일은 긴 유효 기간을 쓸 수 있지만 HTML과 개인 응답에는 별도 규칙이 필요합니다.

브라우저가 먼 서버 대신 쌓여 있는 캐시 리소스를 재사용하는 모습
일러스트: 재사용 가능한 로컬 응답은 추가 네트워크 전송을 피합니다.

브라우저 캐시는 같은 리소스를 다시 다운로드하지 않고 저장된 HTTP 응답을 재사용합니다. 콘텐츠가 바뀔 때 URL도 바뀌는 공개 파일에는 긴 신선도를, 항상 최신이어야 하는 HTML에는 검증을 적용하고 개인화 응답은 명시적으로 보호합니다. 효율적인 정책은 모든 URL에 긴 max-age를 붙이는 것이 아니라 재사용과 업데이트 조건을 정하는 일입니다.

신선한 응답과 오래된 응답, 검증을 구분합니다

캐시는 본문과 관련 헤더를 저장합니다. 응답이 신선하면 서버에 묻지 않고 사용할 수 있습니다. 오래된 상태가 되면 허용된 오래된 응답 정책이 없는 한 보통 네트워크 통신이 필요합니다. 만료는 파일을 즉시 지우는 것이 아니라 재사용 조건을 바꿉니다.

검증은 저장된 표현이 현재 버전과 같은지 확인합니다. 같으면 서버는 본문 없이 304 Not Modified를 반환할 수 있습니다. 바이트는 절약하지만 요청과 응답은 필요합니다. 신선한 로컬 적중은 왕복 자체를 피하므로 둘의 성능 효과는 다릅니다.

브라우저의 개인 캐시는 프록시나 CDN의 공유 캐시와 다릅니다. 한 사용자가 재사용해도 되는 응답을 다른 사용자에게 제공하면 위험할 수 있습니다. 애플리케이션 설계에서 이를 구분합니다. 서버 응답 시간 가이드는 원본 작업의 위치를 설명합니다. 캐시는 그 빈도를 줄일 수 있지만 캐시 없는 연산 자체가 빨라지는 것은 아닙니다.

응답에 맞게 Cache-Control을 선택합니다

각 지시문이 허용하는 것은 다릅니다. 특히 no-cache는 이름과 달리 저장 금지가 아닙니다.

지시문 응답에서의 의미 판단 기준
max-age=N 초 단위 신선도 검증 없이 재사용할 기간
s-maxage=N 공유 캐시의 신선도 CDN과 브라우저 분리
no-cache 재사용 전 검증 재사용 HTML을 최신으로 유지
no-store 저장 금지 민감한 콘텐츠 보호
private 공유 캐시 저장 금지 개인 응답을 개인 캐시에 한정
public 다른 조건 아래 공유 저장 허용 공개 콘텐츠의 의도적 공유
immutable 신선한 동안 표현 불변 콘텐츠 해시 파일
must-revalidate 성공한 검증 없이 오래된 응답 사용 금지 검증 없는 오래된 제공 방지

쿠키가 있다는 이유만으로 응답이 안전하게 비공개 처리되지는 않습니다. 정책을 명시하고 CDN이 덮어쓰지 않도록 합니다. private는 암호화가 아니며 브라우저 저장은 허용합니다. 저장하면 안 되는 민감한 계정 응답에는 Cache-Control: no-store와 공유 캐시 우회를 함께 적용합니다.

모순되는 값을 기계적으로 쌓기보다 명확한 규칙을 사용합니다. 지금 no-store를 붙여도 이전의 느슨한 정책으로 저장된 복사본은 회수되지 않습니다. 이전 응답의 무효화와 수정은 별도로 계획합니다. MDN HTTP 캐시 가이드에서 기본 모델을 확인할 수 있습니다.

파일이 바뀌면 URL도 바꿉니다

파일명의 콘텐츠 해시는 URL을 특정 빌드 결과에 연결합니다. app.8f3a1c2d.js가 바뀌면 새 해시로 배포하고 HTML 참조를 바꿉니다. 이전 URL은 이전 바이트를 계속 반환해야 합니다. 불변 URL의 내용을 덮어쓰면 긴 수명을 안전하게 만든 약속이 깨집니다.

공개 해시 파일에는 다음과 같은 예시 정책을 사용할 수 있습니다.

http
Cache-Control: public, max-age=31536000, immutable

값은 1년을 초로 표현한 정책 선택이며 필수 성능 기준은 아닙니다. 저장 공간 때문에 브라우저가 더 일찍 제거할 수 있습니다. 긴 기간은 재사용을 허용할 뿐 오프라인 사용이나 메모리 상주를 보장하지 않습니다.

파일을 먼저 배포하고 이를 참조하는 HTML을 나중에 게시합니다. 이전 HTML과 열린 탭을 위해 이전 파일도 보관합니다. 늦은 동적 import는 새 배포 뒤에도 옛 청크를 요청할 수 있습니다. 즉시 삭제하면 일반 배포가 기능 오류로 이어집니다. 빌드 매니페스트와 적절한 보관 정책을 사용합니다.

쿼리 매개변수 버전도 모든 관련 캐시 키가 이를 포함하면 작동합니다. 파일명 해시가 확인하기는 더 쉽습니다. 요청마다 새 타임스탬프를 넣으면 같은 내용도 재사용할 수 없습니다.

ETag와 Last-Modified로 HTML을 검증합니다

안정적인 URL의 공개 HTML에는 Cache-Control: no-cache와 서버 검증자가 유용한 경우가 많습니다. ETag는 표현 버전 식별자이며 반드시 해시일 필요는 없습니다. Last-Modified는 수정 시각입니다. 내용과 관련 변형이 바뀌면 검증자도 실제로 달라져야 합니다.

브라우저는 저장된 ETag를 If-None-Match로 보낼 수 있습니다. 일치하면 304, 아니면 최신 표현을 받습니다. If-Modified-Since는 Last-Modified를 이용한 시간 기반 검증입니다. 둘 다 적용되면 ETag 조건이 우선합니다. 모든 페이지와 배포에 같은 ETag를 고정하지 않습니다.

브라우저에는 저장해도 CDN에는 저장하면 안 되는 개인 HTML은 private, no-cache를 출발점으로 삼을 수 있습니다. 더 민감하면 no-store를 선택합니다. 인증, 권한, 로그아웃을 실제로 시험해야 하며 헤더가 접근 제어를 대신하지 않습니다. 정적 파일 규칙이 인증된 다운로드를 공개 캐시로 바꾸면 안 됩니다.

검증은 전송량을 줄여도 원본 계산 전체를 없애지는 못할 수 있습니다. ETag를 계산하기 위해 전체 응답을 생성한다면 많은 작업이 남습니다. 캐시로 서버 부하가 줄었다고 판단하기 전에 전송과 백엔드 처리를 따로 측정합니다.

공개 파일 전용 디렉터리에서 Apache를 설정합니다

아래 .htaccess는 공개 콘텐츠 해시 파일만 있는 디렉터리용입니다. Apache HTTP Server 2.4, mod_headers, 관련 override 권한이 필요합니다. 업로드, 계정 내보내기, 단지 같은 확장자를 가진 모든 파일에 적용하는 전역 정책이 아닙니다.

허용 확장자 앞에 최소 8자리 소문자 16진수 해시가 있는 파일을 찾습니다.

apache
# public/assets/.htaccess
<FilesMatch "\.[a-f0-9]{8,}\.(css|js|mjs|png|jpg|jpeg|webp|avif|svg|woff2)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

HTML 정책은 실제 HTML 생성 위치나 애플리케이션에 둡니다. 파일명 규칙이 재작성 경로도 처리한다고 가정하면 안 됩니다. 다른 모듈, 프레임워크, 프록시가 이미 Cache-Control을 설정한다면 중복 원인을 제거하고 최종 헤더를 확인합니다. Apache 헤더 테이블과 내부 리디렉션이 합치는 방식에 영향을 줄 수 있습니다.

스테이징에서 정상 파일뿐 아니라 없는 파일과 인증 엔드포인트도 요청합니다. 오류나 개인 응답이 1년짜리 공개 객체가 되면 안 됩니다. 관리형 호스팅에서는 설정이 무시되거나 덮어써지는지, 어느 계층이 소유하는지 먼저 확인합니다.

캐시 대상을 넓히지 않고 Nginx를 설정합니다

전용 경로를 사용하고 빌드 해시가 있는 공개 파일에만 긴 정책을 적용합니다. 아래 조각은 문서 루트에 /assets/가 있는 기존 server 블록 안에 둡니다. 현재 라우팅과 location 우선순위를 먼저 검토합니다.

실제 파일만 제공하며 없는 파일을 애플리케이션으로 전달하지 않습니다.

nginx
location ~ ^/assets/.+\.[a-f0-9]{8,}\.(css|js|mjs|png|jpg|jpeg|webp|avif|svg|woff2)$ {
    try_files $uri =404;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

always를 넣지 않은 것은 오류 상태에 무조건 적용하지 않기 위해서입니다. 설치 버전의 add_header 상속을 확인합니다. 하위 location의 헤더 추가가 보안 헤더 상속을 바꿀 수 있으며 새 버전에는 추가 상속 제어가 있습니다. 필요한 보안 헤더를 보존하고 최종 응답을 검사합니다.

필요 권한으로 nginx -t를 실행한 뒤 다시 로드합니다. 이는 문법 검사이지 캐시 설계 안전성 검사는 아닙니다. 원본과 CDN 경유 응답을 모두 확인합니다. 중간 규칙이 덮어쓰면 올바른 원본 헤더도 충분하지 않습니다.

응답 변형과 다른 캐시도 확인합니다

Vary는 표현을 구분하는 요청 헤더를 알려 줍니다. 인코딩, 언어 등으로 실제 응답이 달라지면 키에 반영해야 합니다. Vary: Cookie는 개인화의 일반 해결책이 아닙니다. 변형 수를 크게 늘릴 수 있으며 명시적인 비공개 정책이나 공유 캐시 우회를 대신하지 않습니다.

CDN에는 별도 키, 수명, 무효화 방식이 있습니다. CDN을 지워도 브라우저 복사본은 보통 남습니다. 공개 파일을 즉시 바꾸려면 엣지만 지우는 것보다 새 URL이 안정적입니다. CDN과 엣지 캐시 가이드에서 관계를 설명합니다.

서비스 워커는 요청을 가로채고 HTTP 캐시와 별개의 Cache Storage를 관리할 수 있습니다. 이전 워커나 cache-first 전략 때문에 헤더가 맞아도 오래된 내용이 나올 수 있습니다. 워커와 저장 상태를 검사하고 해당 계층을 우회해 재시험합니다. 일반 배포마다 사용자의 오프라인 데이터를 무차별 삭제하지 않습니다.

정상 방문과 업데이트, 비공개 상태를 검증합니다

DevTools Network의 Disable cache를 해제한 상태에서 정상 방문과 재방문을 수행합니다. 응답 헤더와 전송 정보를 확인합니다. 메모리 또는 디스크 캐시와 네트워크 304는 다릅니다. 강력 새로고침은 캐시 동작을 바꾸므로 유일한 시험으로 삼지 않습니다.

공개 HTML, 해시 파일, 민감한 페이지, 없는 URL을 포함한 작은 검증표를 유지합니다. 각 항목의 저장 허용 범위와 업데이트 방식을 기록하면 유지보수자가 규칙의 목적을 이해하고 단일 페이지 시험이 놓치는 광범위한 변경을 찾을 수 있습니다.

일부 서버는 HEAD를 다르게 처리하므로 헤더 확인에도 실제 GET을 사용합니다.

bash
curl -sS -D - -o /dev/null https://example.com/assets/app.8f3a1c2d.js

파일을 변경 배포하고 HTML이 새 이름을 참조하는지 확인합니다. 이전 탭을 열어 둔 채 늦게 로드되는 기능도 실행합니다. 익명, 로그인, 로그아웃 상태를 각각 시험합니다. 개인 페이지가 공유 적중이 되거나 없는 파일이 캐시된 HTML을 반환하면 안 됩니다.

성능 체크리스트에 정책과 검증을 적고 서버 성능 모음에서 후속 조사를 선택합니다. 대표적인 재방문 흐름을 측정합니다. 목표는 경고만 없애는 것이 아니라 안전한 재사용과 신뢰할 수 있는 업데이트입니다.

관련 가이드