[HTTP] 브라우저와 캐시

브라우저에서 데이터를 캐싱하는 매커니즘에 대해서 간략히 정리해보겠다.
캐시는 생각보다 성능과 비용 전반에 큰 영향을 미치는 요인 중 하나다. 특히 브라우저가 캐시를 들고서 재사용을 해야 우리 서버에 쌓이는 부담을 최소화할 수 있다.




Cache Control 헤더

cache-control은 캐싱에 있어서 가장 중요한 특수 응답 헤더다.

값은 이런 식으로 들어갈 수 있다.
보통 max-age에 초 단위 값을 넣어서 만료시간을 직접 지정하는 형태로 자주 쓴다.

그럼 브라우저는 딸려온 만료 정보를 토대로 캐시 전략을 세운다.

Cloudflare의 R2 조회를 예시로 들어보겠다.
이런 식으로 cache-control이 날라오면,

14400초=4시간동안은 변하지 않을테니 그대로 캐시해놓고 써도 된다는 의미다.
브라우저는 저 만료시간이 지나면 캐시를 버리고 다시 서버에 API를 날려서 값을 가져온다.

AWS-Cloudfront 옵션도 비슷하다.
아래의 경우에는 1년짜리 만료가 붙어있는 캐시 전략이다.

public과 immutable이란 키워드도 더 붙어있는데, 전자는 누구나 캐시해도 상관없는 데이터임을 알려주는 것이다. 브라우저든, 중간 CDN이든 들고있어도 문제없다는 말이다.
immutable은 비표준 확장 옵션이다. 사실 크롬을 포함한 상당수의 메이저 브라우저가 미지원하는 거라서 큰 의미는 없다.




간단한 실습

이미지 파일 하나 읽어서 서빙하는 간단한 서버 올려보겠다.

package main

import (
	"io/ioutil"
	"net/http"
)

func main() {
	http.HandleFunc("/image", func(w http.ResponseWriter, r *http.Request) {
		// 이미지 파일 읽기
		img, err := ioutil.ReadFile("bonobono.jpg")
		if err != nil {
			http.Error(w, "Image not found", http.StatusNotFound)
			return
		}

		// Content-Type 설정
		w.Header().Set("Content-Type", "image/jpeg")

		// 이미지 파일 반환
		w.Write(img)
	})

	// 서버 시작
	http.ListenAndServe(":8080", nil)
}

그러면 조회 자체는 잘 될 것이다.

이런 형태의 미디어 데이터가 웹페이지의 html img 태그 등으로 간접참조되어있다면, 그때부터 캐시 재사용을 적극적으로 시도한다.
그냥 이미지를 브라우저 뷰어로 직접 조회한다면 캐시가 적용되지 않을 수 있다. 일단 크롬은 그랬다.

그래서 테스트용 웹페이지를 또 파서 방금 서빙한 이미지를 참조하도록 했다.

<!DOCTYPE html>
<html>
<head>
    <title>Local</title>
</head>
<body>
    <h1>Local</h1>
    <p>Local page</p>

    <img src="http://localhost:8080/image" alt="Local">
</body>
</html>

이렇게 뜨면 되긴 된 것인데,

당연히 아무것도 한게 없으니 캐시가 되지는 않는다.

이번에는 Cache Control 옵션을 줘서 명시적으로 max-age를 줘서 캐싱을 유도해보겠다.

이러면 1시간 동안은 캐시를 들고있으라는 것이다.


다시 실행하고, 재시도해보면, 그때부터는 "memory cache"를 타는 것을 확인할 수 있다.
저게 API를 쏘지 않고 그냥 캐시를 사용했다는 의미다.




Cache-Control 만료와 304 응답

Status Code 304 (Not Modified)는 Cache Control과 더불어서 캐싱에 주요한 요소 중 하나다.

캐시 데이터가 만료되었을 경우, 브라우저는 무작정 데이터를 갱신받지는 않는다.
우선 If-Modified-Since, If-None-Match 헤더를 보내서 캐시를 갱신할 필요가 있는지 서버에게 물어본다.

그리고 만약 304 상태코드가 응답으로 떨어진다면, 브라우저는 기존 캐시를 그냥 쓴다. (강제는 아님)

아니라면 새 데이터를 받아와서 쓸 것이다.





레거시 헤더: Pragma

HTTP 1.1에서 Cache Control이 나오기 전, HTTP 1.0까지만 쓰였던 어정쩡한 헤더다.
하위호환을 위해서 일부 시스템에서 기본으로 내려줄 뿐, 실질적으로 사용되는 일은 거의 없다.




잘 안씀: Expires

Cache Control은 현재로부터 "N 시간 동안" 캐시가 유지되어야 함을 표현한다.
그래서 그때그때 상대 기준으로 캐시 만료일을 적용하기 간편한데, 이건 특정 날짜를 지정해서 만료일을 적용한다.

쓰기 불편해서 사장된 옵션이다.




참조
https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Cache-Control
https://toss.tech/article/smart-web-service-cache
https://www.cloudflare.com/ko-kr/learning/cdn/glossary/what-is-cache-control/