웹 페이지 로딩 속도 개선의 핵심! HTTP 캐싱: 강력한 캐시와 협상 캐시 완전 정복

웹 페이지 로딩 속도 개선의 핵심! HTTP 캐싱: 강력한 캐시와 협상 캐시 완전 정복

브라우저 캐싱 메커니즘 알아볼까요?

우리 모두 브라우저로 웹페이지를 열 때, 입력한 URL (유알엘) 주소를 바탕으로 해당 서버에 필요한 데이터 자원을 요청해서 받아온다는 사실은 잘 알고 있을 겁니다.

그런데 이 과정에서 화면이 하얗게 보이는 로딩 시간(일명 '흰 화면 현상')이 발생하기도 하고, 페이지가 화면에 그려지기까지 시간이 좀 걸릴 수 있는데요.

사용자 경험을 향상시키려고 할 때, DNS (디엔에스) 캐싱, CDN (씨디엔) 캐싱, 브라우저 캐싱, 로컬 페이지 캐싱 등 다양한 캐싱 기술들이 정말 중요해집니다.

좋은 캐싱 전략은 불필요한 자원 요청을 줄이고, 서버의 부담을 낮추며, 페이지 로딩 속도를 개선할 수 있습니다.

이 글에서는 HTTP (에이치티티피)의 강력한 캐싱(strong caching)과 협상 캐싱(negotiated caching)에 대해 집중적으로 살펴보겠습니다.

기본 원리

브라우저가 어떤 자원을 불러올 때, 가장 먼저 Expires (익스파이어스)와 Cache-Control (캐시 컨트롤) 요청 헤더를 살펴보고 강력한 캐싱 전략을 적용할지 말지를 결정합니다.

이를 통해 브라우저가 원격 서버에 자원을 요청해야 할지, 아니면 로컬 캐시에서 가져와야 할지를 판단하게 된답니다.

강력한 캐싱 (Strong Caching)

브라우저에서 강력한 캐싱은 두 가지로 나뉘는데요, 바로 Expires (익스파이어스) (HTTP/1.0 명세에 정의됨)와 Cache-Control (캐시 컨트롤) (HTTP/1.1에 정의됨)입니다.

Expires (익스파이어스)

Expires (익스파이어스)는 HTTP/1.0 (에이치티티피 일점영) 표준으로, 리소스의 만료 시간을 나타내는 응답 헤더 필드입니다.

이 값은 서버가 반환하는 절대적인 타임스탬프 값입니다.

브라우저가 처음 리소스를 요청하면, 서버 응답 헤더에 Expires (익스파이어스) 필드가 포함되어 옵니다.


다음에 브라우저가 동일한 리소스를 요청할 때는 이전 응답에서 받았던 Expires (익스파이어스) 값을 확인합니다.


만약 현재 요청 시간이 Expires (익스파이어스)에 명시된 만료 시간보다 이전이라면, 브라우저는 캐시된 리소스를 바로 사용합니다.


하지만 Expires (익스파이어스)는 로컬 시스템 시간에 의존하기 때문에, 클라이언트와 서버 시계가 맞지 않으면 캐시가 일관되지 않는 문제가 생길 수 있습니다.

Cache-Control (캐시 컨트롤)

앞서 말했듯이, Expires (익스파이어스)에는 단점이 있습니다.

클라이언트의 로컬 시간과 서버 시간이 다르면 캐시 정확도에 영향을 줄 수 있다는 점이죠.

이 문제를 해결하기 위해 HTTP/1.1 (에이치티티피 일점일)에서는 Cache-Control (캐시 컨트롤) 필드를 도입했고, 이 필드는 Expires (익스파이어스)보다 우선합니다.

Expires (익스파이어스)와 달리, Cache-Control (캐시 컨트롤)은 절대적인 타임스탬프 대신 상대적인 시간을 사용해 캐시 만료를 정의합니다.

가장 흔히 사용되는 Cache-Control (캐시 컨트롤) 지시어는 다음과 같습니다.

max-age: 리소스가 신선한 상태로 유지되는 시간(초)을 지정합니다 (예: 3600). 즉, 현재 시간으로부터 3600초 동안 유효하다는 뜻입니다.


s-maxage: max-age와 유사하지만, 프록시 서버에서의 캐싱에만 적용됩니다.


private: 리소스는 개인 캐시(예: 클라이언트 브라우저)에 의해서만 캐시될 수 있고, 프록시 서버 같은 공유 캐시에는 저장될 수 없습니다.


public: 리소스는 클라이언트와 프록시 서버 모두에 캐시될 수 있습니다.


no-store: 어떤 종류의 캐싱도 허용하지 않습니다.


no-cache: 리소스는 로컬 캐시에 저장되지만, 클라이언트에게 제공되기 전에 반드시 원서버(origin server)와 재검증 과정을 거쳐야 합니다.

협상 캐싱 (Negotiated Caching)

강력한 캐싱은 브라우저 단독으로 결정됩니다.

만약 강력한 캐싱 조건에 맞지 않으면(캐시가 만료되었거나 없는 경우), 브라우저는 서버에 요청을 보내 협상 캐싱이 적용되는지 확인합니다.

만약 캐시가 여전히 유효하다면, 서버는 새로운 리소스 데이터를 보내는 대신 304 Not Modified (304 낫 모디파이드) 상태 코드를 반환합니다.

협상 캐싱(조건부 캐싱이라고도 불림)은 서버에 의해 결정되며, 두 쌍의 헤더를 사용합니다.

브라우저가 첫 요청을 보내면, 서버는 응답에 Last-Modified (라스트 모디파이드) 또는 ETag (이태그) 헤더를 포함시킵니다.

이후 요청에는 해당 요청 헤더(If-Modified-Since (이프 모디파이드 신스) 또는 If-None-Match (이프 넌 매치))를 포함시켜 리소스가 변경되었는지 여부를 판단합니다.

Last-Modified (라스트 모디파이드): 리소스의 최종 수정 시간을 나타내며, 서버가 반환합니다.


If-Modified-Since (이프 모디파이드 신스): 브라우저가 요청 시 보내며, 이전에 받았던 Last-Modified (라스트 모디파이드) 값을 담고 있습니다.


ETag (이태그): 리소스의 고유한 식별자입니다.

리소스가 변경되면 ETag (이태그) 값도 따라서 변경됩니다.

파일 내용이 그대로여도 수정 시간이 바뀔 수 있는 Last-Modified (라스트 모디파이드)와 달리, ETag (이태그)는 더 정확한 캐시 유효성 검사를 보장합니다.


If-None-Match (이프 넌 매치): 브라우저가 요청 시 보내며, 이전에 받았던 ETag (이태그) 값을 담고 있습니다.

강력한 캐싱과 요청 흐름

브라우저가 리소스를 요청할 때의 흐름을 정리해보겠습니다.

먼저, 브라우저는 로컬 캐시 기록이 있는지 확인합니다.

만약 기록이 없다면, 서버에 요청을 보내고 반환된 Last-Modified (라스트 모디파이드) 같은 값을 저장합니다.


캐시 기록이 있다면, 브라우저는 먼저 강력한 캐싱이 여전히 유효한지 확인합니다 (Cache-Control (캐시 컨트롤)이 Expires (익스파이어스)보다 우선합니다).

캐시가 여전히 유효하다면, 브라우저는 캐시된 리소스를 제공합니다 (HTTP 상태 코드 200).


강력한 캐싱이 만료되었다면, 브라우저는 협상 캐싱 전략을 사용하여 요청을 시작합니다.

서버는 먼저 ETag (이태그) 값을 확인합니다.


클라이언트의 ETag (이태그)가 서버의 것과 일치하면, 서버는 304 Not Modified (304 낫 모디파이드)를 반환합니다 (리소스 데이터는 보내지 않습니다).


만약 서버가 ETag (이태그)를 사용하지 않는다면, If-Modified-Since (이프 모디파이드 신스)를 확인합니다.

제공된 값이 서버의 마지막 수정 시간과 일치하면, 서버는 304 Not Modified (304 낫 모디파이드)를 반환합니다.


ETag (이태그)도, If-Modified-Since (이프 모디파이드 신스)도 일치하지 않는다면, 서버는 새로운 리소스를 보내고 캐시를 업데이트합니다.

ETag (이태그)는 왜 필요할까요?

ETag (이태그)는 Last-Modified (라스트 모디파이드)의 몇 가지 문제를 해결하기 위해 도입되었습니다.

타임스탬프 부정확성: 파일 내용이 그대로인데도 마지막 수정 타임스탬프가 변경될 수 있어, 불필요한 캐시 무효화를 초래할 수 있습니다.


잦은 수정: Last-Modified (라스트 모디파이드)는 초 단위까지만 지원하므로, 자주 업데이트되는 파일에는 충분하지 않을 수 있습니다.

ETag (이태그)는 더 나은 정밀도를 보장합니다.


일부 서버는 파일의 마지막 수정 시간을 정확하게 파악할 수 없어서, ETag (이태그)가 더 나은 대안이 됩니다.

HTTP 상태 코드의 차이점

200: 요청 성공, 서버가 리소스의 새로운 복사본을 반환합니다.


200 (from memory cache / from disk cache): 강력한 캐싱이 여전히 유효하여 브라우저가 로컬 캐시에서 리소스를 로드한 경우입니다.


304 Not Modified: 요청에 협상 캐싱이 사용되었고, 서버가 캐시된 리소스가 여전히 유효하다고 판단한 경우입니다.

참고:


from memory cache (프롬 메모리 캐시): 리소스가 메모리에서 검색되었습니다 (예: 페이지 내에서 새로고침 - F5키가 아닌, 링크 클릭이나 자바스크립트를 통한 이동 등).


from disk cache (프롬 디스크 캐시): 리소스가 디스크 저장소에서 검색되었습니다 (예: 브라우저 탭을 닫았다가 다시 열거나, 강력 새로고침(Ctrl+F5)이 아닌 일반 새로고침(F5)).

캐시 우선순위 규칙

Expires (익스파이어스)와 Cache-Control (캐시 컨트롤)이 모두 존재하면, Cache-Control (캐시 컨트롤)이 우선합니다.


Cache-Control (캐시 컨트롤) > Expires (익스파이어스)

강력한 캐싱과 협상 캐싱이 모두 존재하면, 브라우저는 먼저 강력한 캐싱을 확인합니다.

캐시가 여전히 유효하면 사용하고, 그렇지 않으면 협상 캐싱을 적용합니다.


강력한 캐싱 > 협상 캐싱

ETag (이태그)와 Last-Modified (라스트 모디파이드)가 모두 존재하면, ETag (이태그)가 우선합니다.


ETag (이태그) > Last-Modified (라스트 모디파이드)

추가 참고 사항

HTTP/1.0 (에이치티티피 일점영) 명세에는 Pragma (프라그마)라는 오래된 캐싱 지시어가 있었는데요, 이는 Cache-Control: no-cache (캐시 컨트롤: 노 캐시)와 동일한 효과를 가져, 브라우저가 원서버와 리소스를 재검증하도록 강제했습니다.

캐싱 우선순위 순서(일반적으로 고려되는 순서)는 다음과 같습니다.


Pragma (프라그마) -> Cache-Control (캐시 컨트롤) -> Expires (익스파이어스) -> ETag (이태그) -> Last-Modified (라스트 모디파이드)

휴리스틱 캐싱 (Heuristic Caching)

응답에 Expires (익스파이어스)나 Cache-Control (캐시 컨트롤) 헤더는 없지만 Last-Modified (라스트 모디파이드) 헤더가 포함된 경우, 브라우저는 휴리스틱 캐싱 전략을 적용합니다.

브라우저가 사용하는 공식은 다음과 같습니다.


(현재 시간 - Last-Modified 시간) * 0.1


이 캐싱 전략은 서버가 명시적으로 캐시 정책을 정의하지 않은 경우에만 적용됩니다.

더 자세한 내용은 'HTTP 휴리스틱 캐싱 (Cache-Control 및 Expires 헤더 누락 시) 설명' 문서를 참고하면 좋습니다.

기타 고려 사항

협상 캐싱은 강력한 캐싱과 함께 사용될 때 효과적입니다.

강력한 캐싱이 적용되지 않으면(예: no-cache 또는 만료된 경우), 협상 캐싱을 통해 서버와 통신하여 리소스 변경 여부를 확인합니다.


대부분의 웹 서버는 기본적으로 협상 캐싱을 활성화하며, 일반적으로 Last-Modified (라스트 모디파이드)와 ETag (이태그)를 동시에 사용합니다.

중요한 시나리오

분산 시스템에서는 여러 서버에 걸쳐 Last-Modified (라스트 모디파이드) 타임스탬프가 일관되도록 보장해야 합니다.

그렇지 않으면 로드 밸런싱으로 인해 캐시 유효성 검사가 일관되지 않아 불필요한 리소스 재요청이 발생할 수 있습니다.


분산 시스템에서는 ETag (이태그)를 비활성화하는 것이 권장되기도 합니다.

서로 다른 서버가 서로 다른 ETag (이태그) 값을 생성하여 불필요한 캐시 미스(cache miss)를 유발할 수 있기 때문입니다.