회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
프론트엔드 개발자라면 아마 웹사이트 성능을 최적화하는 것에 관심이 있을 것이다. 웹사이트 로딩 시간을 줄이고, 물 흐르듯 자연스러운 사용자 경험을 만드는 것은 비즈니스에도 큰 영향을 끼친다. 웹을 최적화할 수 있는 방법에는 여러 가지가 있는데, 그중에서 빠질 수 없는 개념이 바로 캐싱이다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
프론트엔드 개발자라면 아마 웹사이트 성능을 최적화하는 것에 관심이 있을 것이다. 웹사이트 로딩 시간을 줄이고, 물 흐르듯 자연스러운 사용자 경험을 만드는 것은 비즈니스에도 큰 영향을 끼친다. 웹을 최적화할 수 있는 방법에는 여러 가지가 있는데, 그중에서 빠질 수 없는 개념이 바로 캐싱이다.
캐싱은 어떤 데이터를 한 번 받아온 후에 그 데이터를 불러온 저장소보다 가까운 곳에 임시로 저장하여, 필요시 더 빠르게 불러와서 사용하는 프로세스를 의미한다. 메모리 계층 구조에서 캐시는 디스크나 메인 메모리보다 더 빠르게 데이터를 불러와서 사용해야 할 때 쓰인다. 이러한 장점이 있는 만큼 단위 메모리당 비용이 비싼 편이다. 그래서 엔지니어 입장에서는 재사용을 충분히 많이 할 수 있는 데이터만 선별적으로 잘 캐싱해서, 성능과 비용을 모두 아끼는 것이 중요하다고 볼 수 있다.
이번 글에서는 프론트엔드 개발자가 알아야 할 캐싱의 종류와 개념을 살펴보려고 한다.
클라이언트는 서버로부터 HTTP 요청을 통해 필요한 데이터(HTML, CSS, JS, 이미지 등)를 불러온다. 기본적으로는 웹사이트가 실행될 때마다, 클라이언트는 해당 웹사이트를 그리기 위해 필요한 데이터를 전부 다 불러와야 한다.
예를 들어, 어떤 블로그를 들어가 본다고 가정해 보자. 블로그에는 포스팅이 가장 메인이지만 그 외에도 섬네일, 사이드바, 내비게이션바 같은 요소도 고정적으로 들어가 있다.
우리가 블로그 홈 화면에서 어떤 포스팅 상세로 들어갔다가 다시 뒤로 가기로 돌아오면, 이전에 본 동일한 홈 화면으로 돌아오게 된다. 그렇다면 원칙적으로는 모든 HTML, CSS, JS, 이미지를 서버에서 다시 모두 불러와야 하는 것이 맞다. 그렇다면 이 부분의 부담을 줄여볼 수 없을까?
이전에 한 번 불러왔던 데이터고, 거의 바뀔 일이 없는 데이터라면 캐싱을 적용해 볼 수 있다. 예를 들면, 블로그의 로고, 이미지, 제목, 사이드바 목차 같은 정보가 해당될 수가 있다. 여기에 적용하는 캐시를 브라우저 캐시(Browser Cache)라고 한다. 브라우저 캐시는 브라우저나 HTTP 요청을 하는 클라이언트 애플리케이션에 의해 내부 디스크에 이루어지는 캐시이며, HTTP 캐시라고 불리기도 한다. 이러한 캐시는 단일 사용자를 대상으로 하는 사설 캐시(Private Cache)이며, 해당 사용자의 정보만을 저장한다.
브라우저에서 캐싱을 처리하는 속성은 여러 가지가 있는데, 대표적으로 ETag, Cache-Control이 있다.
ETag
HTTP 헤더의 ETag는 특정 버전의 리소스를 식별한다. 웹 서버가 내용을 확인하고 변하지 않았으면, 이 ETag가 동일한 값을 그대로 가지고 있게 되어 웹 서버로 전체 요청을 보내지 않기 때문에 효율적으로 캐시를 관리할 수 있다. 만약 해당 요청의 리소스가 변경되었다면 새로운 ETag가 생성된다. 요구사항에 따라 서버에서 ETag가 무기한 지속되도록 설정하는 것도 가능하다.
캐싱된 리소스가 불러온 리소스와 일치하는지를 확인할 수도 있다. HTTP 헤더의 If-Match라는 프로퍼티를 사용하면 된다. 현재 캐싱이 이루어진 요청의 ETag가 ‘x234dff’라고 했을 때, 해당 페이지의 변경을 가하는 경우 이를 판별하고 싶다면 If-Match에 기존 ETag 값인 ‘x234dff’ 값을 넣어서 요청을 할 수 있다. 만약 두 해시 ETag, If-Match가 일치하지 않는다면 문서가 변경되었다는 의미다. 이때는 412(Precondition Failed, 전제조건 실패) 상태코드를 내려주게 된다. 이를 충돌 피하기(mid-air collisions)라고 한다.
이와 반대로 변경되지 않은 리소스의 캐시를 해야 할 수도 있다. 만약 ETag가 너무 오래되어 사용할 수 없는 경우라면, 이번에는 HTTP 헤더에 If-None-Match라는 프로퍼티에 ETag 값을 넣어줄 수 있다. 서버에서는 클라이언트의 ETag를 현재 버전 리소스의 ETag와 비교하고, 일치한다면 현재 ETag가 유효하며 리소스의 변경이 없다는 의미이다. 이때 304(Not Modified) 상태코드를 내려준다.
Cache-Control
프로그램 성능과 관련해 이런 말이 있다. “가장 빠른 코드는 실행하지 않는 코드다.” HTTP 요청과 관련해 비슷하게 생각해 볼 수가 있다. 서버와 통신하지 않을 수 있다면, 그 방법이 성능 관점에서 가장 좋은 선택이다. 이러한 요청을 보낼지 말지 여부를 판단할 때, HTTP 헤더의 Cache-Control이라는 요소를 통해 캐싱을 제어할 수 있다.
Cache-Control에 ‘no-cache’라는 속성이 있는데, 많은 사람들이 이 속성을 캐시를 쓰지 않는 것으로 오해하곤 한다. 정확하게는 캐시를 쓰지 않는 것이 아니라, 캐시를 먼저 사용하기 이전에 서버에 해당 캐시를 사용해도 되는지에 관해 검증 요청을 보내는 속성이다. no-cache 속성이 없는 경우 캐시가 있다면 바로 캐시를 쓰지만, ‘no-cache’ 속성이 있다면 캐시를 바로 쓰지 않고, 서버에 이 캐시를 써도 되는지 허락을 맡고 쓰기 때문에 요청에 대한 시간이 소요될 수 있다.
또한 Cache-Control에는 ‘no-store’라는 속성이 있다. 이 속성이 캐시를 쓰지 않는다는 말에 더 가까운 속성이다. no-store 속성을 사용하면 반환된 응답에 대해 브라우저가 캐싱을 하지 않는다. 매번 요청을 보낼 때마다 전체 데이터를 받아오는 식으로 처리하게 된다. 개인정보 등 private한 데이터가 있는 경우 이 속성을 사용할 수 있다.
덧붙여서 private, public 키워드도 사용할 수 있다. 쉽게 말하면 ‘public’은 캐싱이 가능하다는 의미고, ‘private’은 응답을 캐싱하는 과정에서 엔드 유저만 캐싱이 가능하고, 그 가운데 거치는 매개체에서는 캐싱할 수 없는 속성이다. 사설 캐시(Private Cache, Browser Cache)라는 이름으로 불리는 이 로컬 캐시는 단일 사용자의 개인화된 콘텐츠에만 재사용되는 캐시다. 이러한 사설 캐시에 반대되는 개념이 공유 캐시(Shared Cache)다.
웹 캐시에는 브라우저 캐시뿐만 아니라 다른 캐시도 존재한다. 대표적으로 프록시 캐시(Proxy Cache)가 있다. 프록시 캐시는 공유 캐시(Shared Cache)로 한 명 이상의 사용자에 의해 재사용되는 응답을 저장한다. 대표적으로 ISP(Internet Service Provider)에서 많이 조회되는 리소스를 재사용하기 위해 웹 프록시를 설치했을 수 있다.
예를 들어, 우리가 구글에 들어간다고 가정해 보자. 매번 구글에 접속할 때마다 미국에 있는 구글 서버에서 웹사이트 데이터를 불러온다면 상당한 시간이 걸릴 것이다. 이 데이터를 우리에게 더 가까이 둘 수 있다면 여러 이점이 있지 않을까? 이때 프록시 캐시를 사용하면 구글 웹사이트에 관한 리소스를 한국 서버에 둘 수 있을 것이다.
프록시 캐시에서도 위에 브라우저 캐시처럼 Cache-Control 속성이 있으며, 여기에서 캐시에 관한 설정을 지시한다. (public 또는 private으로 할지, 시간은 얼마나 할지, max-age 등) 그런데 프록시 캐시의 경우 하나 더 신경써야 할 부분이 있다.
바로 캐시 무효화에 관한 내용이다. 만약 클라이언트에서 캐시를 적용하지 않았는데, 웹 브라우저가 임의로 캐싱한다면 업데이트가 되지 않아 문제가 발생할 수 있다. 이럴 경우 캐시를 무효화해야 한다.
이때 사용하는 옵션으로 앞서 살펴본 no-cache, no-store가 있고, 또 하나 ‘must-revalidate’가 있다. 이 속성은 캐시가 만료된 후 최초 조회할 때 원 서버에 검증을 반드시 거쳐야 한다. 만약 원 서버에 접근이 안 된다면 504 Gateway Timeout 에러가 발생한다. 여기에 max-age로 시간을 부여할 수도 있다. 캐시가 저장한 데이터를 사용할 때 stale(최신화가 필요한)이라는 표현을 쓰는데, 이러한 stale한 데이터를 쓰고 싶지 않을 때 must-revalidate 속성을 사용하면 된다.
no-cache와의 차이점은 프록시 캐시 서버와 원 서버 사이의 네트워크 단절이 일어난 경우에 발생한다는 것이다. no-cache는 위에서 언급한 stale한 데이터라도 보여주어야 할 때 사용하고, 이러한 데이터를 사용해서 문제가 발생한다면 504 에러를 반환하는 must-revalidate를 사용하면 된다.
프록시 서버에서 캐시를 관리할 수 있으니 충분하다고 생각하는 사람들도 있을 것이다. 하지만 현실은 그렇지 않다. 앞서 본 방문자(클라이언트) <-> 프록시 서버(캐시) <-> 오리진 서버의 단일 구조로 모든 세상이 이루어져 있지 않기 때문이다. 하나의 서버에 여러 개의 프록시 서버가 붙어 있고, 또 서비스 대상에 따라 지역이 여러 군데일 수도 있다.
물론 구글 같은 회사라면 서비스를 제공하는 모든 지역에 프록시 서버를 둘 수 있겠지만, 모든 회사가 구글같을 순 없다. 이렇게 전 세계의 수많은 클라이언트에 빠르게 콘텐츠를 제공하기 위해 나온 도구가 바로 CDN이다.
CDN은 Content Delivery Network의 약자로 분산 노드로 구성된 네트워크다. CDN은 성능 향상을 위해 클라이언트의 요청이 같은 서버로 가는 것을 막는다. CDN은 각 지역의 엔드 유저에 따라 요청을 오리진 서버가 아닌 다른 서버로 가도록 분산시키는 역할을 한다. 이 과정에서 캐싱이 사용된다.
만약 엔드 유저의 요청이 CDN 노드에서 가져올 수 있다면 그 콘텐츠를 전달하지만, 그렇지 못한 경우라면 오리진 서버로 요청을 전송한다. 최초 한 번은 오리진 서버에 반드시 갔다 와야 하므로 느릴 수 있지만, 그다음부터는 CDN을 통해 빠르게 해당 콘텐츠를 내려받을 수 있다는 장점이 있다.
이러한 CDN을 서비스하는 대표적인 회사로 Cloudflare가 있다. Cloudflare는 추가적인 캐싱 레이어를 설정할 수 있게 해서, 사용자가 이전에 사이트를 방문했다면 그 정보를 브라우저에서 캐싱할 수도 있다. 그래서 Cloudflare를 ‘CDN 캐시’ 서비스라고 부르기도 한다.
CDN과 캐시를 분리하여 생각하기는 어렵다. 실제로 Cloudflare는 엣지 캐시를 사용하고 있다. 엣지 캐시란 네트워크의 엣지에서 정적 에셋 파일(이미지, CSS, JS 등)을 캐싱해서 엔드 유저에게 빠르게 도달하고, 콘텐츠 전송 시 서버 로드를 줄여주는 역할을 한다.
지금까지 프론트엔드 개발자가 알아야 하는 캐싱 개념에 관해 알아보았다. 물론 이외에도 더 다양한 캐싱이 있지만, 특히 웹 프론트엔드 개발자라면 실무에서 많이 접할 수 있는 캐싱을 중심으로 살펴보았다. 또한 글에서 다루진 않았지만 자주 쓰는 데이터 패칭 라이브러리(swr, react-query, apollo-client 등)에서 캐싱을 어떻게 처리하는지도 추가로 학습하면 도움이 될 것이다.
캐싱은 웹사이트 성능을 높이기 위해 필수적이다. 그러나 마구잡이로 썼을 때 stale한 데이터를 엔드 유저에게 보여줄 수도 있고, 비싼 저장공간이라 비용적으로 부담이 될 수도 있다. 그러니 각 비즈니스에 맞게 잘 활용할 수 있는 방안을 마련하면 좋을 것이다.
<참고>
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.