서비스를 구현할 때, 성능을 최대한으로 끌어올리기 위해 캐시를 흔히 사용한다.
여러 종류의 캐시가 있지만, 여기서는 HTTP Cache에 대해 알아보려고 한다.
Cache-Control 헤더
브라우저가 HTML, CSS, JS, 이미지, 비디오 파일 등 서버에 처음 요청할 때, 브라우저와 서버는 완전한 HTTP 요청/응답을 주고받는다.
이 때, Cache-Control 헤더 응답 값에 따라서 처음 요청한 리소스의 캐싱 방식이 결정되게 된다.
max-age
예를 들어 Cache-Control 헤더 값으로 max-age=10 을 설정하면, 리소스에 대한 캐시의 유효한 시간은 10초가 된다.
예를 들어 위와 같은 요청에서 완전한 HTTP 요청/응답을 주고받고 status 200이 반환된 것을 확인할 수 있다.
이 때 Cache-Control의 max-age가 최대치인 31536000 으로 설정되어 있다.
그럼 위에서 언급했듯이 해당 리소스의 캐시는 31536000초가 지나는 동안 유효하다.
한번 더 요청을 보내보자.
해당 리소스는 캐시되어있는 상태이기 때문에 서버에 요청을 보내지 않는다. 메모리에 유효한 캐시가 남아있기 때문에 상태 코드에 (메모리 캐시에서) 라는 텍스트를 확인할 수 있다.
한번 브라우저에 캐시가 저장되면 만료될 때까지 캐시는 계속 브라우저에 남아 있게 되므로, 서버의 어떤 작업으로도 유효한 캐시를 지우기는 어렵다고 한다.
캐시의 유효시간이 지난 이후 재검증
max-age에서 설정한 캐시의 유효시간이 다 지나더라도 캐시가 완전히 사라지지는 않는다.
대신 브라우저는 서버에 조건부 요청을 통해 캐시가 유효한지 재검증(Revalidation)을 수행한다.
서버에 재검증을 해서 비록 리소스의 유효한 캐시 시간은 지났지만, 서버가 가지고 있는 리소스와 비교하여 변함이 없다면 304 Not Modified 응답을 내려주게 된다.
위의 리소스는 max-age=0 으로 설정되어 있으므로, 매 요청마다 서버에 조건부 요청을 보내게 된다.
이 때 캐시된 리소스가 서버의 리소스와 차이가 없다면 위처럼 304 Not Modified 응답을 내려주게 되는 것이다. (max-age가 0이라고 해서 캐시가 안되는 것이 아니다!!)
304 Not Modified 응답은 HTTP 본문을 포함하지 않기 때문에 매우 빠르게 내려받을 수 있다고 한다.
실제로 위를 보면 18.7kB 의 리소스를 불러오기 위해 고작 135B 크기의 네트워크 송수신만 주고받은 것을 확인할 수 있었다.
If-None-Match / If-Modified-Since
그럼 서버가 304 Not Modified 응답을 내려주기 위해 캐시된 리소스와 현재 서버 리소스 간 차이가 없음을 어떻게 알아낼까??
If-None-Match 재검증 요청 헤더는 캐시된 리소스의 ETag 값과 현재 서버 리소스의 ETag 값이 같은지 확인한다.
위와 같이 If-None-Match에 있는 Etag 값이 현재 서버 리소스의 ETag값과 같다면 리소스 간 변화가 없었다는 뜻이므로 그냥 캐시되어있는 리소스를 가져다 쓰라고 요청하는 것이다.
If-Modified-Since 재검증 요청 헤더는 캐시된 리소스의 Last-Modified 값 이후에 서버 리소스가 수정되었는지 확인한다고 한다.
Etag 값과 Last-Modified 값은 기존에 받았던 리소스의 응답 헤더에 있는 값을 사용하며, 위와 같은 재검증 로직을 통해 유효시간이 지난 캐시된 리소스의 유효함을 다시 한번 검증한다.
no-cache / no-store
no-cache는 대부분의 브라우저에서 max-age=0과 같은 의미를 가진다.
위에서 봤듯이 max-age=0으로 설정하더라도 재검증 로직을 거쳐 304 Not Modified 응답을 받았었다.
이는 결국 리소스가 브라우저에 캐시되어 있다는 것이고, 결국 캐시는 저장되지만 매 요청마다 서버에 재검증을 거쳐야 한다는 것을 알 수 있다.
반면 no-store는 리소스를 캐시 조차 안해야 할 때 사용한다.
Cache-Control 헤더에 no-store를 명시해주면 어떤 경우에서도 브라우저는 해당 리소스를 캐시하지 않게된다.
CDN(Content Delivery Network)
CDN은 물리적으로 떨어져 있는 사용자에게 컨텐츠를 더 빠르게 제공하기 위해 고안된 기술이다.
서버를 분산시켜 캐싱해두고 사용자의 컨텐츠 요청이 들어오면 사용자와 가장 가까운 위치에 존재하는 서버로 매핑시켜 요청된 콘텐츠의 캐싱된 내용을 내어주는 방식으로 빠르게 데이터를 전송할 수 있게 된다.
위와 같이 서버의 응답을 CDN이 캐시할 수 있고, 브라우저는 CDN의 응답을 또 캐시할 수 있게 되므로 리소스는 여러 레이어에서 캐싱될 수 있다. 이렇게 다소 복잡한 로직속에서 캐시는 여러 곳에서 생길 수 있으므로, HTTP 캐시는 세심하게 다루어야 한다고 주의하고 있다.
public / private
public은 모든 사람과 중간 CDN이 해당 리소스를 캐시할 수 있음을 나타내고, private은 끝에 사용자 브라우저만 해당 리소스를 캐시할 수 있음을 나타낸다.
Cache-Control: public, max-age=86400 과 같이 ,로 구분하여 max-age 값과 같이 설정할 수 있다.
s-max-age
CDN 에서만 적용되는 max-age를 설정하려면 s-max-age를 사용하면 된다.
만약 s-max-age=31536000, max-age=0 으로 Cache-Control헤더를 설정하면, 해당 리소스는 CDN 에서는 1년동안 캐시되지만 브라우저에서는 매번 서버에 재검증 요청을 보내게 된다.
이처럼 HTTP 캐시와 관련된 기본적인 내용은 알아보았다.
해당 내용을 응용하여 나의 Vercel로 배포한 Next 프로젝트에 적용해보기로 했다.
Vercel's Edge Network
나는 Next 프로젝트를 Vercel로 배포하고 있다.
Vercel 에서는 Edge Network라는 CDN을 제공해주고 있다.
Static File Caching
Vercel에서 정적 파일들은 자동적으로 Edge Network에 캐싱된다고 한다(31일간).
Cache-Control 헤더는 "public, max-age=0, must-revalidate" 로 자동 설정 되어있다고 공식문서에 언급되어 있고, 이는 사용자별 브라우저에 파일이 캐싱되는 것을 방지하기 위함이라고 한다.(캐싱이 아예 안되는 것은 아니지만 max-age=0 이므로 브라우저 캐싱은 진행하되, 매번 서버에 재검증 요청을 보냄)
만약 재배포를 해서 파일 내용이 바뀌었는데, 사용자별 브라우저에 캐시되어있는 파일을 그대로 띄워주면 안되기 때문에 위처럼 설정해 놓은 것 같다.
x-vercel-cache
그러면 우리는 Static File이 Vercel's Edge Network 라는 CDN에서 잘 가져오는지 확인해봐야 한다.
해당 동작이 잘 이루어지는지는 x-vercel-cache 라는 헤더 value를 보면 알 수 있다.
- MISS
말 그대로 CDN에서 해당 내용을 찾지 못했으므로 origin server에서 내용을 그냥 가져왔다는 의미이다.
- HIT
CDN에서 해당 내용을 찾아서 응답을 CDN에서 내려받았다는 의미이다.
- STALE
CDN에서 내용을 찾아 CDN에서 응답을 내려줬고, 내용을 업데이트 하기 위해 origin server로 background 요청이 이루어졌다는 의미이다.
- REVALIDATED
CDN이 refresh 되어서 응답이 origin server에서 제공되었음을 나타낸다.
예를 들어 On-demand revalidation을 통해 CDN이 refresh 되면 다시 origin server에서 응답을 받아와야 하고, 이 때 x-vercel-cache 값이 REVALIDATED로 된 응답이 오게 된다.
나의 경우 /place/[id] 경로의 장소 상세 페이지를 ISR를 수행하는 Static File로 동작하게끔 설정해두었다.(ISR로 동작하는 방법은 다음 블로그에 정리할 예정)
장소 상세 페이지로의 이동을 위해 해당 장소 상세 페이지에 이동하는 카드를 클릭하기 전, Next/Link를 통해 prefetch 할 수 있도록 구현하였다.
prefetch한 네트워크를 보면, 해당 파일을 CDN에서 찾을 수 있었으므로 x-vercel-cache가 HIT인 응답을 받았음을 확인할 수 있다.
CDN에 내용이 아직 없다면, 첫 요청에는 MISS가 뜨고 다음 요청부터는 HIT으로 응답되는 것을 볼 수 있었다.(서버의 Full Route Cache에 캐싱된 내용을 첫 요청 때 CDN에 저장하는 듯 하다)
Cache-Control 헤더도 Static File에 대한 디폴트 값으로 응답을 내려받는 것을 확인할 수 있다!!
dynamic-content
dynamic-content도 캐싱이 가능하다.
다만 Static file과 달리 직접 헤더에 접근하여 세팅해주어야 한다.
export async function GET() {
return new Response('Cache Control example', {
status: 200,
headers: {
'Cache-Control': 'max-age=10',
'CDN-Cache-Control': 'max-age=60',
'Vercel-CDN-Cache-Control': 'max-age=3600',
},
});
}
위는 Dynamic Content의 캐시를 위해 Vercel 공식문서에서 제공하는 예시이다.
CDN-Cache-Control로 브라우저 캐시와 별개로 Vercel Edge Cache와 다른 CDN 캐시를 설정할 수 있고, Vercel-CDN-Cache-Control로 Vercel-Edge-Cache에 대해서만 설정할 수 있다.
내 프로젝트에서는 아직 Dynamic Content에 대한 캐싱에 대해 큰 필요성을 느끼지 못했고, 무수히 많은 Dynamic Content를 무작정 CDN에 캐싱해도 될까에 대한 의문이 들어 아직 설정하지는 않았다.
Cache Invalidation
Vercel 에서는 매 배포마다 캐싱 로직에 필요한 유니크한 key가 제공된다.
따라서 매 배포마다 이전 캐시는 자동적으로 없어지게 된다.
만약 Vercel's Edge Network에 있는 캐시 내용을 전부 무효화 해야 한다면 재배포를 통해 원하는 결과를 얻을 수 있다.
또한 revalidation을 통해서도 Cache Invalidation을 수행할 수 있다.
내 프로젝트에서 Static File로 동작하는 공간 상세 페이지에서는 스크랩 기능이 있다.
해당 파일에서 revalidation이 동작하였고, 그럼 Vercel에서는 해당 파일을 CDN에서 찾아 Invalidation을 진행하는 듯 하다.
실제로 해당 스크랩 기능을 수행 후
해당 장소 페이지 파일에 대해 X-Vercel-Cache 값이 MISS로 뜨는 것을 확인했고, 이를 통해 CDN Invalidation이 수행되었다고 판단할 수 있었다.
또, 그 이후에 해당 페이지에 대한 네트워크 요청을 확인해본 결과
X-Vercel-Cache 값이 REVALIDATED로 뜨는 것을 확인할 수 있었고, 이를 통해 origin server에서 응답을 다시 불러왔음을 알 수 있었다!
훌륭한 여러 블로그들 덕분에 HTTP 캐시에 대해 알아보고, 내 프로젝트에서도 적용할 수 있었다.
이러한 것 하나하나가 서비스의 성능을 좌우하기 때문에 더욱 꼼꼼한 공부가 필요하다고 느낀 계기가 되었다.
혹시 더 보완할 점이 생긴다면 바로 보완해야겠다~!!
참고
https://toss.tech/article/smart-web-service-cache
웹 서비스 캐시 똑똑하게 다루기
웹 성능을 위해 꼭 필요한 캐시, 제대로 설정하기 쉽지 않습니다. 토스 프론트엔드 챕터에서 올바르게 캐시를 설정하기 위한 노하우를 공유합니다.
toss.tech
https://vercel.com/docs/edge-network/caching#static-files-caching
Caching on Vercel's Edge Network
Vercel's Edge Network caches your content at the edge in order to serve data to your users as fast as possible. Learn how Vercel caches works in this guide.
vercel.com
'FrontEnd > React' 카테고리의 다른 글
효율적인 협업을 위하여 - 2 (Feat: 컨벤션) (0) | 2024.07.04 |
---|---|
효율적인 협업을 위하여 - 1 (Feat: Git Merge) (2) | 2024.07.03 |
아토믹 디자인(Atomic Design) 도입하기 (0) | 2024.05.08 |
React 렌더링 최적화에 대한 고찰 (2) | 2024.04.10 |
Context API 에서 Recoil로 마이그레이션 일지 (3) | 2024.04.06 |