본문 바로가기
IT

HTTP 304 Not Modified — 캐시 검증·조건부 요청 동작과 디버깅

by 샤나엘 2026. 6. 28.
반응형

HTTP 304 Not Modified — 캐시 검증·조건부 요청 동작과 디버깅

개발자도구 네트워크 탭을 열면 304가 잔뜩 보일 때가 있다. 빨간 에러처럼 보이지만 사실 304는 좋은 신호다. "요청한 파일이 지난번과 똑같으니, 다시 안 보내고 너희가 가진 캐시를 그대로 쓰라"는 뜻이다. 덕분에 브라우저는 같은 이미지·CSS·JS를 매번 새로 내려받지 않고, 서버는 대역폭을 아낀다. 이 글은 304가 정확히 무슨 의미인지, ETag와 Last-Modified로 어떻게 "바뀐 거 없죠?"를 묻는지, 그리고 캐시가 안 깨질 때 어떻게 들여다보는지를 정리한다.

HTTP 304 Not Modified

이 글의 구성

 

🔎304 Not Modified의 의미
🔄조건부 요청 동작 원리
🏷ETag vs Last-Modified
⚖️304와 캐시 히트의 차이
💻curl·개발자도구로 확인
🛠캐시가 안 깨질 때 점검
💬자주 묻는 질문 5가지

🔎 304 Not Modified의 의미

304는 3xx 묶음에 속하지만, 다른 리다이렉트와 성격이 다르다. 주소를 옮기라는 게 아니라 "네가 캐시한 그 버전이 아직 최신이다"라고 알려주는 응답이다. 서버는 304를 보낼 때 본문(body)을 담지 않는다. 헤더만 짧게 돌려보내고, 브라우저는 자기 캐시에 있던 파일을 그대로 화면에 쓴다.

 

핵심은 "본문을 다시 안 보낸다"는 점이다. 같은 로고 이미지, 같은 스타일시트를 페이지마다 매번 수십 KB씩 내려받으면 느리고 낭비다. 그래서 브라우저는 한 번 받은 자원을 캐시에 두고, 다음에 필요할 때 서버에 "이거 바뀌었어?"만 물어본다. 안 바뀌었으면 서버는 304로 짧게 답하고, 실제 데이터 전송은 생략된다.

핵심 — 304는 에러가 아니다

네트워크 탭에 304가 보인다고 잘못된 게 아니다. 오히려 캐시가 잘 동작한다는 신호다. 서버는 본문 없이 헤더만 보내고, 브라우저는 로컬 캐시를 재사용한다. 전송량이 줄고 페이지가 빨라진다. 문제는 304가 떠야 할 자리에 매번 200(전체 재전송)이 뜨거나, 반대로 바꿨는데도 옛 버전이 계속 캐시될 때 생긴다.


🔄 조건부 요청 동작 원리

304는 혼자 나오지 않는다. 브라우저가 "조건부 요청(conditional request)"을 보냈을 때의 답이다. 흐름은 두 단계로 나뉜다.

200에서 304까지

 

01첫 요청 : 서버가 자원을 200으로 보내며 ETag·Last-Modified 같은 검증자(validator)를 헤더에 함께 담는다.
02브라우저는 자원과 검증자를 캐시에 저장한다. 캐시 신선도(Cache-Control max-age)가 끝나면 재검증이 필요해진다.
03재요청 : 브라우저가 저장한 검증자를 If-None-Match(ETag)·If-Modified-Since(날짜) 헤더에 실어 "이거 바뀌었어?"라고 묻는다.
04서버 판단 : 바뀌었으면 200으로 새 본문을, 안 바뀌었으면 304로 본문 없이 답한다.

여기서 신선도(freshness)와 재검증(revalidation)을 구분하면 그림이 또렷해진다. Cache-Control의 max-age가 아직 안 지났으면 브라우저는 서버에 묻지도 않고 캐시를 바로 쓴다(요청 자체가 안 나감). max-age가 지나 "신선하지 않은(stale)" 상태가 되면, 그제야 조건부 요청으로 재검증하고 그 답이 304 또는 200이다.


🏷 ETag vs Last-Modified

조건부 요청에 쓰는 검증자는 크게 두 가지다. 둘 다 "이 자원이 그때 그대로인가"를 판별하는 지문 역할을 한다.

검증자 응답 헤더 재요청 헤더
ETag ETag: "abc123" If-None-Match: "abc123"
Last-Modified Last-Modified: (날짜) If-Modified-Since: (날짜)

ETag는 자원 내용으로 만든 식별 토큰이다. 내용이 1바이트라도 바뀌면 ETag도 바뀌므로 더 정밀하다. Last-Modified는 마지막 수정 시각이라 초 단위까지만 구분하고, 내용은 그대로인데 파일을 다시 저장만 해도 시각이 바뀔 수 있다. 그래서 둘 다 있으면 서버는 보통 ETag(If-None-Match)를 우선 본다. 정밀함이 필요하면 ETag, 단순함이면 Last-Modified라고 기억하면 된다.


⚖️ 304와 캐시 히트의 차이

네트워크 탭을 보면 비슷해 보이지만 다른 세 가지 상태가 있다. 이 차이를 알면 "왜 어떤 건 요청이 나가고 어떤 건 안 나가나"가 이해된다.

상태 서버 요청 설명
200 (from cache) 안 나감 신선한 캐시 — 묻지 않고 디스크·메모리 캐시 바로 사용
304 Not Modified 나감(조건부) 재검증 결과 변경 없음 — 헤더만 오가고 본문은 캐시 재사용
200 (full) 나감 캐시 없음·변경됨 — 본문 전체를 새로 전송

요약하면 200(from cache)은 아예 안 묻는 것, 304는 묻고 "그대로네" 답을 받는 것, 200(full)은 묻고 새로 받는 것이다. 304도 왕복(round trip) 한 번은 발생하므로 완전 공짜는 아니다. 진짜 빠른 건 요청조차 안 나가는 신선한 캐시다. 그래서 자주 안 바뀌는 정적 파일은 max-age를 길게 줘서 304조차 줄이는 게 성능에 유리하다.


💻 curl·개발자도구로 확인

직접 조건부 요청을 보내 304를 재현해 보면 원리가 손에 잡힌다. 먼저 ETag를 받아 둔다.

# 1. 자원의 ETag·Last-Modified 확인 (헤더만)
curl -I https://example.com/style.css

# 응답 예시
HTTP/2 200
etag: "abc123"
last-modified: Wed, 18 Jun 2025 09:00:00 GMT
cache-control: max-age=3600

받은 ETag를 If-None-Match에 실어 다시 요청하면 304가 떨어진다.

# 2. 조건부 요청 — 바뀌지 않았으면 304
curl -I -H 'If-None-Match: "abc123"' https://example.com/style.css

# ✓ 변경 없음 → 본문 없이 304
HTTP/2 304

# 날짜 기반이면 If-Modified-Since 사용
curl -I -H 'If-Modified-Since: Wed, 18 Jun 2025 09:00:00 GMT' https://example.com/style.css

브라우저 개발자도구 네트워크 탭에서는 Status 열에 304가, Size 열에 본문 대신 헤더 크기만 표시된다. 강력 새로고침(Ctrl+Shift+R)을 하면 조건부 헤더를 빼고 보내 200(full)을 강제할 수 있어, 캐시 문제를 가를 때 유용하다.


🛠 캐시가 안 깨질 때 점검

304가 정상이지만, "분명히 파일을 바꿨는데 옛날 게 계속 보인다"는 반대 상황이 더 골치다. 이때 점검할 곳을 모았다.

증상 점검·해결
바꿨는데 옛 파일이 보임 max-age가 너무 김 → 강력 새로고침으로 확인, 캐시 정책 조정
정적 파일이 매번 200(full) ETag·Cache-Control 미설정 → 서버에서 캐시 헤더 추가
배포해도 사용자에게 반영 지연 파일명 해시(cache busting) → app.abc123.js 식으로 URL 변경
여러 서버의 ETag 불일치 서버마다 ETag 생성 방식 차이 → 일관된 ETag 또는 비활성화
중간 캐시(CDN)가 옛 버전 보관 CDN 캐시 무효화(purge)로 강제 갱신

가장 깔끔한 해법은 cache busting이다. 자주 바뀌는 정적 파일에 내용 해시를 파일명에 박으면(app.abc123.js), 내용이 바뀔 때 URL 자체가 달라져 브라우저가 무조건 새로 받는다. 이렇게 하면 정적 파일에 max-age를 아주 길게 줘도 안전하다. 새 배포는 새 파일명을 쓰니 캐시 충돌이 없다.

304는 고장이 아니라 캐시가 일하고 있다는 영수증이다. 진짜 빠른 건 304조차 안 나가는 신선한 캐시다.

 

— 묻지 않는 캐시가 제일 빠르다


💬 자주 묻는 질문 5가지

Q1304가 많이 보이는데 문제인가요?

아닙니다. 304는 캐시가 정상 작동한다는 신호입니다. 본문을 다시 안 받으니 전송량이 줄고 페이지가 빨라집니다. 다만 거의 안 바뀌는 정적 파일이라면 max-age를 길게 줘서 304 왕복조차 줄이면 더 좋습니다.

Q2304랑 디스크 캐시(from cache)는 뭐가 다른가요?

from cache는 서버에 아예 요청을 안 보내고 캐시를 바로 쓰는 것이고, 304는 요청을 보내 "바뀌었어?"를 묻고 "아니"라는 답을 받는 것입니다. 304도 네트워크 왕복이 한 번 있으므로, 요청조차 없는 신선한 캐시가 더 빠릅니다.

Q3ETag와 Last-Modified 중 뭘 쓰나요?

정밀함이 필요하면 ETag입니다. 내용이 1바이트만 바뀌어도 ETag가 달라져 정확합니다. Last-Modified는 초 단위 시각이라, 같은 초 안의 변경이나 내용은 그대로인데 재저장된 경우를 놓칠 수 있습니다. 둘 다 있으면 서버는 보통 ETag를 우선합니다.

Q4파일을 바꿨는데 옛날 게 계속 보여요.

신선도(max-age)가 안 지나 브라우저가 서버에 묻지 않고 옛 캐시를 쓰는 상황입니다. 임시로는 강력 새로고침(Ctrl+Shift+R)으로 확인하고, 근본적으로는 파일명에 내용 해시를 박는 cache busting으로 해결합니다. CDN을 쓰면 CDN 캐시도 무효화하세요.

Q5304를 직접 만들어 확인하려면요?

curl -I로 ETag를 받은 뒤, 그 값을 -H 'If-None-Match: "..."'에 실어 다시 요청하면 변경이 없을 때 304가 떨어집니다. 날짜 기반은 If-Modified-Since를 씁니다. 개발자도구 네트워크 탭에서도 Status 304와 작은 Size로 확인할 수 있습니다.


📌 결론

304 Not Modified는 에러가 아니라 캐시 검증이 성공했다는 응답이다. 브라우저가 ETag·Last-Modified로 "이거 바뀌었어?"라고 묻고, 서버가 본문 없이 "아니"라고 답하는 구조다. 덕분에 전송량이 줄고 페이지가 빨라진다.

실무 원칙은 다음과 같다.

 

🔄 조건부 요청을 이해한다. 신선하면 묻지 않고 캐시 사용, 신선하지 않으면 재검증해 304 또는 200을 받는다.

🏷 검증자를 알맞게 쓴다. 정밀함은 ETag(If-None-Match), 단순함은 Last-Modified(If-Modified-Since), 둘 다면 ETag 우선이다.

⚡ 캐시 전략을 설계한다. 정적 파일은 cache busting + 긴 max-age로 304 왕복조차 줄이고, 배포 반영이 안 되면 max-age·CDN purge를 점검한다.

304·캐시 점검 체크리스트

 

01304는 정상 — 캐시 재사용 신호로 이해.
02신선도(max-age) vs 재검증(304) 흐름 구분.
03ETag(정밀)·Last-Modified(단순) 검증자 선택.
04curl -I + If-None-Match로 304 직접 재현·확인.
05정적 파일은 cache busting + 긴 max-age로 304 왕복 절감.
06반영 지연 시 max-age·서버 ETag·CDN purge 점검.

본 글은 HTTP 304 응답과 캐시 검증 동작을 정리한 자료다. 캐시 정책 변경은 영향 범위를 확인한 뒤 신중히 적용한다.

 

#HTTP304 #NotModified #브라우저캐시 #조건부요청 #ETag #LastModified #IfNoneMatch #CacheControl #maxage #캐시버스팅 #웹성능 #CDN캐시 #개발자도구 #웹개발 #HTTP캐시

반응형

댓글