본문 바로가기

개발

캐시 관련 http 헤더 정리와 Next.js의 캐시 정책

Cache-Control, ETag, Last-Modified 등의 헤더들은 브라우저(혹은 중간 서버) 캐시가 어떻게 동작하는지 결정한다.

 

프론트엔드 개발자로서 리소스가 사용자에게 얼마나 빠르게 전달되는지는 중요하고, 캐시가 큰 역할을 하기 때문에 알아둘 필요가 있다.

 

모든 것을 다루기보다는 (주관적으로) 주요한 헤더와 디렉티브 위주로 다루려고 한다.

Expires

Response 헤더. http-date 형식으로 캐시가 만료되는 시간을 명시하고자 할 때 사용. 아래 설명할 max-age가 있다면 무시된다.

Cache-Control

Response, Request 헤더로 모두 쓰임. 디렉티브마다 사용 가능 여부가 다름.

 

  • max-age=N: N초 동안 응답이 fresh하다는 의미. fresh할 때는 네트워크 요청 없이 캐시된 값을 사용할 수 있다. 해당 시간이 지나면 stale하다고 표현함.
  • must-revalidate: 보통 max-age와 함께 사용됨. 응답이 stale하면 revalidate해야 함을 의미한다(아래에서 설명). 해당 디렉티브가 없고, 서버와의 연결이 끊겼다면 stale해도 사용될 수 있다고 함.
  • no-cache: 캐시로 저장할 수 있으나, 사용할 때 항상 revalidate해야 함을 의미한다. max-age=0, must-revalidate와 동일.
  • no-store: 캐시로 저장하지 말라는 의미.
  • stale-while-revalidate=N: fresh하지 않지만 N초보다는 전이라면 일단 사용하되 백그라운드에서 revalidate. N초도 더 지났다면 캐시 사용하지 않음.
  • public: 중간 서버도 캐시 저장 가능
  • private: 엔드 유저(주로 브라우저)만 저장 가능
  • immutable: 응답이 fresh한 동안에는 변경되지 않을 것임을 의미

revalidate?

revalidate한다는 것은 캐시를 사용해도 되는지 여부를 체크한다는 의미다.

 

캐시된 리소스가 최신이 맞다면, 서버는 304 (Not Modified)를 응답으로 내려주고 리소스를 주고받지 않아도 되기 때문에 효율적이다.

 

반대로 리소스가 변경되었다면, 실제 리소스를 응답한다. 이 때 추가적인 HTTP 요청이 발생하지 않는다.

 

위 과정은 조건부 요청을 통해 가능한데, 요청을 보낼 때 특정 헤더(아래 설명할 If- 등)를 포함하면 조건부 요청이 된다.

 

그리고 캐시를 사용해도 되는지 여부의 판단은 ETag 또는 Last-Modified 값을 활용한다.

ETag

Response 헤더.

  • Entity tag의 약어로, 리소스의 고유값을 의미
  • W/가 앞에 붙으면 weak validator가 사용되었음을 의미한다.
  • 바이트 단위로 일치하지는 않으나 'semantically equivalent'함을 나타내고 싶을 때 사용.
  • 반대로 바이트 단위로 완벽히 일치함을 나타내려면 strong validator를 사용.

Last-Modified

Response 헤더. 리소스가 마지막으로 변경된 일자.

If-None-Match

Request 헤더. 캐시된 리소스의 ETag 값을 넣음. 현재 서버 리소스의 ETag 값과 같은지 확인.

If-Modified-Since

Request 헤더. http-date 형식으로 해당 시점 이후에 리소스가 변경됐는지 확인.

 

조건부 요청을 발생시키는 위 두 헤더를 꼭 같이 쓸 필요는 없다.

 

같이 쓰면 If-None-Match가 우선권을 가진다고 하는데, ETag를 더 정확한 validation으로 취급하는 것으로 이해할 수 있다.

Next.js에서는?

next.js에서는 기본적으로 개별 페이지나 리소스에 대해 Cache-Control 헤더를 적용할 수 없다. (API Route 응답에는 가능함)

 

다만 production 빌드에서 각 리소스들의 응답 헤더를 보고, 정책을 이해해볼 수 있을 듯하다.

 

자바스크립트 파일

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

 

 

public이니 브라우저는 물론 중간 서버도 저장할 수 있고, 31536000은 1년으로 RFC에서 권장되는 캐시의 최대 기간이다.

 

이렇게 설정되는 이유를 immutable에서 유추해볼 수 있는데, Next.js에서 자바스크립트 파일의 내용이 변경되면 파일명에 다른 해시값이 포함되어 빌드되기 때문이다.

 

즉, 내용이 변경되면 어차피 경로(파일명)가 달라질 것이므로 최대한의 캐시를 적용한 것이다. 이런 패턴을 Cache Busting이라고도 한다.

 

public 폴더의 정적 리소스

Cache-Control: public, max-age=0

 

캐시하되, max-age=0으로 설정하여 매번 revalidate되도록 했다.

 

SSG 페이지

Cache-Control: s-maxage=31536000, stale-while-revalidate

 

중간 서버(CDN 등)에서는 최대 기간으로 캐시된다. s-maxage이므로 브라우저에는 적용되지 않는다.

 

max-age를 명시하지 않았는지는 잘 모르겠으나 브라우저에서 캐시 사용 전에 매번 revalidate되는 것을 확인할 수 있다. (아마도 항상 stale한 상태로 취급하고 revalidate하는 듯하다)

 

그리고 실제로 SSG으로 생성된 페이지를 Vercel이나 Netlify에 실제로 배포해보면 다음과 같은 값으로 응답이 된다.

 

Cache-Control: public, max-age=0, must-revalidate
Server: Vercel (또는 Netlify)

 

브라우저에서는 캐시하되 매번 revalidate 요청을 보내고, Vercel과 Netlify 등의 서버에서는 최대 기간 캐시를 저장해두고 백그라운드에서 revalidate을 하고 있을 것으로 생각된다.

 

SSR 페이지

Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate

 

SSR은 매 요청마다 응답을 새로 생성해야 하므로 캐시하지 않는 no-store가 가장 적절한 값일 듯하고, 실제 적용도 가장 엄격한 정책인 no-store로 될 것이다.

 

왜 위와 같이 상충하는 여러 디렉티브가 포함되는지는 잘 모르겠다.

 

아마도 브라우저나 중간 서버에 따라 특정 디렉티브에 대한 지원이 되지 않는 경우에 대비한 fallback으로 추측하지만 확실하지는 않다. (아시는 분 있다면 댓글 부탁 드립니다.)