Frontend

HTTP Cache 제대로 알기

mechaniccoder 2023. 11. 30. 22:57

네트워크로 데이터를 가져오는건 느립니다. 데이터가 지구 반대편에 위치한다면 더욱 느려지겠죠. 우리는 데이터를 더 빠르게 가져올 방법이 필요합니다. 그 중에 하나가 HTTP cache를 활용하는 것입니다. 프런트엔드 개발자로서 어떤 사이트든 개발자 도구로 분석해보면 보이지 않는 곳에서 HTTP cache가 유용하게 사용되고 있는 것을 확인할 수 있습니다. HTML을 가져올때, 이미지나 비디오를 가져올때, Javascript,CSS 등 critical resources에도 HTTP cache는 사용됩니다. 그럼 왜 HTTP cache가 필요한 걸까요?

HTTP cache가 필요한 이유

HTTP cache를 사용하면 느린 네트워크를 통해 데이터를 가져오는 것이 아니라 사용자와 가까운 브라우저, 웹 서버 등으로부터 데이터를 가져올 수 있기 때문에 더 빠르게 데이터를 가져옵니다. 느린 네트워크는 다음과 같이 좋지 않은 사용자 경험의 원인이 됩니다.

  • Rendering blocking 리소스인 CSS, JavaScript를 느리게 가져올수록 사용자는 더 느리게 화면을 보게됩니다.
  • 네트워크 리소스를 더 사용하기 때문에 모바일 데이터가 낭비됩니다.

Cache layers

사용자로부터 시작해 여러가지 cache layer에 대해 소개하겠습니다. 아래의 이미지는 사용자로부터 어떤 cache layer가 있는지 확인할 수 있습니다.

  • 사용자 - service worker - browser - cdn(proxy) - origin server
    이번 포스팅에선 HTTP cache를 다룹니다. Service worker에 대해서는 다음에 다뤄보도록 하겠습니다.

Fingerprint

Cache invalidation을 원활히 하기 위해서는 고유한 파일 이름을 사용하는 것이 좋은데요. 이를 fingerprint라고 합니다. 예를 들어보죠. 페이지가 로드되기 위해 script.js라는 파일이 있고 이를 1년 동안 캐싱하도록 설정했다고 하겠습니다. 그런데 일주일 후 요구사항이 변경돼서 script.js 파일이 변경됐습니다. 유저의 브라우저에는 이전에 캐싱한 파일이 남아있고 이를 1년동안 사용하도록 했기 때문에 변경된 코드를 새로 배포해도 변경사항이 적용되지 않을 것입니다.

이를 해결하기 위해 고유한 파일 이름을 사용합니다. 처음 배포한 파일의 이름이 script.102938.js이라고 해보죠. 파일의 contents로부터 고유한 값을 생성하기 때문에 만약 변경점이 있다고 하면 script.919283.js와 같이 파일의 이름이 바뀌게 됩니다. script.102938.js를 1년 동안 캐싱하더라도 새로 요청할 파일의 이름은 script.919283.js이기 때문에 변경된 파일을 가져올 수 있게 됩니다. 그 과정은 아래와 같아요.

  1. 웹 서버로부터 html(index.html)을 가져옵니다.
  2. 브라우저는 html로부터 script.102938.js를 가져옵니다. 캐시 설정이 1년이기 때문에 이를 브라우저에 캐싱합니다.
  3. 변경 사항이 적용된 후 script.919283.js를 바라보는 html 파일을 웹서버로 배포합니다.
  4. 사용자의 브라우저로부터 html 파일을 가져옵니다.
  5. script.919283.js를 요청합니다. 이 파일은 캐싱되지 않았기 때문에 변경점이 잘 반영됩니다.

script 파일을 예로 들었는데 image, video, css등 여러 파일에도 똑같이 fingerprint를 적용할 수 있습니다. 실제로 토스에서 fingerprint를 어떻게 활용하고 있는지 확인할 수 있습니다.

Cache-Control

기본적인 cache directive에 대해서는 여러 사이트에서 잘 설명하고 있습니다. 개인적으로 헷갈리는 것만 정리해보도록 하죠.

s-max-age

public에 대한 캐시 설정입니다.(CDN, proxy 등)

Cache-Control: s-max-age=3600

must-revalidate

cache를 사용하기 전에 서버에 검증을 먼저해야 합니다. 600초 안에 리소스를 요청하는 경우 캐시된 리소스를 사용하고 만료된 후에는 서버로 리소스가 변경됐는지 검증을 합니다.

Cache-Control: max-age=600, must-revalidate

stale-while-revalidate

지정된 시간내에 요청이 발생하는 경우 캐시된 리소스를 쓰는 동시에 비동기적으로 리소스에 대한 요청을 보냅니다. 리소스가 변경됐으면 캐시를 갈아끼우겠죠? 이 캐시 전략은 프런트엔드 개발자 분들이라면 친숙할겁니다. tanstack queryswr에서 사용되는 철학이기 때문이죠. stale-while-revalidate에 대한 더 자세한 내용은 web.dev 아티클에서 확인하세요.

실무에서 사용하기

그럼 지금까지 배운 캐시 전략을 실무에서는 어떻게 사용할 수 있을까요? 같이 한번 생각해보죠.

html

먼저 html입니다. html은 모든 필요한 리소스의 시작점이기 때문에 사용자 브라우저에 잘못 캐싱되면 무효화를 할 수가 없습니다. 따라서 캐싱은 하지만 서버에 검증 요청을 하도록 설정하는게 좋을 것 같네요.

아래와 같이 설정하면 매번 서버로 검증 요청을 보내지만 리소스가 변하지 않은 경우 304 Not Modified 응답을 받아서 캐싱된 리소스를 사용할 수 있습니다.

Cache-Control: max-age=0, must-revalidate

토스의 경우는 아래와 같이 public 캐시에 대한 설정을 추가했다고 합니다. 아래와 같이 브라우저에서는 항상 검증 요청을 보내고 CDN에서는 1년동안 캐시를 하도록 설정합니다. public 캐시를 1년으로 설정해도 문제가 안돼는 이유는 CDN은 manual하게 invalidation할 수가 있기 때문이죠. 배포를 하면 CDN invalidation 요청을 보내 캐싱된 리소스를 무효화하면 유저는 항상 최신 버전의 html파일을 바라볼 수가 있습니다.

Cache-Control: max-age=0, s-max-age=31536000

하지만 제가 여기서 궁금했던 점은 html 파일에 개인정보가 있는 경우입니다. CDN의 경우 public하게 열려있을 수 있어서 만약 개인정보가 들어있다면 이 설정은 하지 않는 것이 좋다고 생각합니다.(물론 CDN을 private하게 사용하는 경우도 있을 수 있겠네요.)

Media(image, video, audio...), javascript, css

이미지, 비디오와 같은 미디어 파일들이나 javascript, css의 경우 보통 번들러(webpack, vite)에서 fingerprint를 적용하게 됩니다. 따라서 최대 설정인 1년으로 캐시하면 좋겠네요.

Cache-Control: max-age=31536000

그럼 위의 고민들을 가지고 제 회사의 랜딩페이지를 분석해보겠습니다.

index.html

위에서 고민한대로 캐시 설정이 잘 되어있네요.

image

image의 경우 현재 fingerprint가 적용이 안되어 있어서 매번 검증 요청을 보내고 있네요. 이를 개선한다면 image에 fingerprint를 적용 후, 1년 캐시를 적용할 수 있을 것 같습니다.

js

js파일은 fingerprint가 적용되어 있어서 1년의 캐시기간을 설정했고 변하지 않을게 확실하기 때문에 immutable속성까지 준 것을 확인할 수 있습니다.

css

css도 마찬가지입니다.

마치며

HTTP 캐시를 잘 쓰면 트래픽에 따른 부하를 줄일 수 있고 더 빠른 리소스를 전달하기 때문에 사용자 경험을 향상시킬 수 있다고 생각합니다. 앞으로 HTTP cache뿐만 아니라 service worker를 통한 Cache API등 더 다양한 캐시에 대해 알아보도록 하겠습니다. 여기까지 읽어주셔서 감사합니다.

References