캐싱은 서버가 하지 않는다. 브라우저가 한다.
서버는 오직 캐싱 정책만 알려준다. 이를 위해 HTTP의 헤더를 이용한다.
HTTP의 헤더의 내용은 널리 알려준 필드들과, 커스텀 필드를 넣을 수 있다.
브라우저 - 서버가 서로 통신하는 상황을 상상해보자.
브라우저는 이미 구현된 소프트웨어고, 널리 알려진 헤더에 대한 동작들도 구현되어있다.
이로 인해 아래의 행동에 대한 응답이 달라진다.
1 ) http://localhost:3000/version.json 를 주소창에 입력하고 엔터하는 경우 : 200(from disk)
2 ) http://localhost:3000/version.json 최초 요청 이후 새로고침 하는 경우 304
3) http://localhost:3000/version.json 에서 강제 새로 고침을 하는 경우(cmd + shift + R) 200
캐싱의 위치
여기서 다루는 캐싱은 오직 브라우저의 캐싱을 다룬다. CDN 캐싱이 아니다.
애초에 대부분의 브라우저 - 서버간의 통신에서 성능을 최적화하기 위해 캐싱을 한다는 전략은 브라우저에 저장해둔다는 의미이다.
캐싱은 2군데에다가 할 수 있다.
1. 메모리
브라우저를 종료하면 캐싱된 데이터가 날라간다. 속도는 빠르다
2. 디스크
브라우저를 종료해도 캐싱된 데이터가 유지된다. 속도는 느리다
메모리 혹은 디스크에 캐싱을 해두는 전략은 브라우저의 내부 정책을 따른다.
참고로 디스크에 저장해두는 것은 MAC의 경우 ~/Library/Caches/Google/Chrome/Default/Cache 를 접속해보라.
브라우저의 요청 방식
1) 주소창 입력
일반적인 접속으로 취급 (탭 최초 방문), 따라서 우선 캐싱되어있는지 확인하는 과정을 거친다.
캐싱되어있다면 서버에 요청을 보내지 않고, 200(from disk) 혹은 200(from memory)로 응답을 사용한다. (브라우저의 캐시 사용)
2) 새로고침
현재 페이지 재검사, 즉 캐시 무효화를 시도하는 행위로 취급한다. 따라서 서버에 요청을 보내는 과정을 거친다.
서버로부터 요청을 특정 조건이 만족되었을 때 특정 헤더를 실어 보내고, 데이터가 바뀌지 않았다면 서버는 304를 반환해준다.
304 status를 받은 브라우저는 캐싱되어있는 데이터를 사용한다.
3) 강제 새로고침
캐싱을 무시하고 서버로부터 데이터를 새롭게 받아온다는 의미이다. 성공만 한다면 항상 200이 들어온다.
캐싱과 관련된 헤더 필드
1. Cache-Control
가장 많이 알려진, 캐싱 정책을 결정하기 위한 필드다. 필드의 대표적 directive(지시어)들은 다음과 같다.
(참고로 보낼 때는 ,(콤마)로 구분하여 작성하여 보낸다.)
참고로 사용하는 디렉티브는 요청/응답에 따라 다르다.
즉, 클라이언트에서 보낼 때 사용하는 디렉티브와 응답으로 오는 디렉티브가 달라질 수 있다.
| 디렉티브 | 요청(req) | 응답(res) | 설명 |
| no-store | ✅ | ✅ | 캐시 저장 금지. 민감정보에 사용 (ex. 로그인 응답) |
| no-cache | ✅ | ✅ | 캐시해도 되지만, 재사용 전 서버에 재검사 필요 |
| max-age=<초> | ✅ | ✅ | 응답이 유효한 시간 (초). 그 시간 동안은 캐시 재검사 안 함 |
| public | ❌ | ✅ | 모든 캐시(공유 캐시 포함)가 사용 가능 |
| private | ❌ | ✅ | 오직 브라우저 같은 개인 캐시만 사용 가능 (프록시는 사용 금지) |
| must-revalidate | ❌ | ✅ | 캐시가 만료되면 반드시 서버에 재검사 (오프라인 사용 불가) |
| immutable | ❌ | ✅ | 절대 변하지 않는 리소스라고 간주 (버전 붙은 파일에 사용) |
| only-if-cached | ✅ | ❌ | 캐시가 있을 때만 응답 사용, 없으면 실패 (오프라인 캐시 강제 사용용) |
| no-transform | ✅ | ✅ | 중간 캐시나 CDN이 압축, 리사이징 등 변형하지 말라는 지시 |
예를 들어, 클라이언트가 서버로부터 no-cache 디렉티브를 받았다면, 브라우저에 저장해두지 않을 것이다.
만약 서버가 클라이언트로부터 no-cache 디렉티브를 받았다면 '항상 검사하려고 하는구나' 하고 304 응답을 내려줄 수도 있을 것이다.
2. ETag
Entity tag로, 버전정보를 의미하며 서버가 응답 헤더에 보내주는 필드다. (물론 클라이언트에서 보내줄 수도 있긴 하다)
해당 헤더가 포함된 res를 받은 브라우저는 다음 요청부터 자동으로 If-None-Match 필드를 포함시켜 req를 보낸다.
3. If-None-Match
버전 정보를 담아 보내는 필드이며, 클라이언트가 서버로 보내는 필드다.
서버는 해당 필드를 보고, 버전 정보가 달라졌는지를 판단 후 304 혹은 200 응답 여부를 결정한다.
4. Last-Modified
마지막으로 수정된 시간을 알려주는 필드로, 서버가 보내주는 필드다.
해당 헤더가 포함된 res를 받은 브라우저는 다음 요청부터 자동으로 If-Modified-Since 필드를 포함시켜 req를 보낸다.
5. If-Modified-Since
이전 요청으로 서버로부터 받았던 Last-Modfied 값이며, 그 이후 변경되었는지 확인하기 위해 클라이언트가 보내는 필드다.
해당 시간 이후로 응답이 달라졌다면 서버는 새로운 요청을 보내줄 것이고, 아니라면 304(not modfied)를 보내준다.
그렇다면 ETag(If-None-Match) , Last-Modified(If-Modified-Since) 2가지 필드 중 보통 우선순위가 높은 것은?
ETag가 우선 순위가 더 높다.
다만 둘 다 사용하는 이유는 정밀한 ETag와 보편적인 Last-Modified를 사용함으로써 확실하게 판단할 수 있기 때문이다.
304는 보통 서버가 설정해서 보내줘야하는데, ETag || Last-Modified 값을 기반으로 비교하고 304를 세팅하는 편이다.
따라서 단순하게 헤더에 Cache-Control: no-cache를 한다고 해서 304가 반드시 오지는 않는다.
서버에서 no-cache를 받고 처리를 해주는 로직이 추가되어있어야만 304가 반환된다.
Etc.
const response = await fetch('/version.json', {
headers: {
// 'Cache-Control': 'max-age=0',
'Cache-Control': 'no-cache',
// 'Cache-Control': 'no-cache, no-store, must-revalidate',
},
cache: 'no-cache',
})
fetch 함수에서의 cache 옵션은 브라우저에게 알리기 위한 편의성 옵션이라고 생각하면 된다.
cache: 'no-cache' 로 달아두면 실제 req의 헤더에는 Cache-Control: max-age=0 으로 세팅되어 보내진다.
단, 헤더의 Cache-Control 필드를 직접 설정한 경우(위의 경우)에는 수동 설정이 우선 적용된다.
'Develop > Frontend' 카테고리의 다른 글
| 구글 태그 매니저, GTM의 원리 (0) | 2025.09.26 |
|---|---|
| history stack (2) | 2025.06.26 |
| Tanstack Query (React Query) 필수 지식 (0) | 2025.05.11 |
| 하이드레이션 에러(Hydration Error) (0) | 2025.05.07 |
| css는 모듈로 뽑을 수 있을까? (1) | 2025.04.29 |