1. 라이브러리
  2. HTTP와 웹
  3. HTTP 상태 코드

업데이트됨 1개월 전

브라우저가 서버에 리소스를 요청할 때, 이미 그 리소스를 캐시에 가지고 있는 경우가 많습니다. 이때 핵심 질문은 "이 파일을 주세요"가 아니라 "마지막으로 받은 이후로 이 파일이 변경되었나요?"입니다.

304 Not Modified 상태 코드는 서버의 답변입니다: 변경된 것 없습니다. 갖고 있던 것 그대로 쓰세요.

이 단 하나의 응답은 — 본문 없이 헤더만 포함된 — 전체 다운로드를 절약합니다. 2MB짜리 이미지가 200바이트짜리 확인 응답으로 바뀌는 것입니다. 이것이 모든 페이지 로드의 모든 리소스에 걸쳐 누적되면, 304가 HTTP에서 가장 중요한 성능 최적화 중 하나인 이유를 알 수 있습니다.

조건부 요청 패턴

304는 대화의 일부로만 의미가 있습니다. 작동 방식은 다음과 같습니다.

첫 번째 방문: 리소스 가져오기

GET /style.css HTTP/1.1
Host: www.example.com

HTTP/1.1 200 OK
Content-Type: text/css
Last-Modified: Wed, 21 Oct 2024 07:00:00 GMT
ETag: "686897696a7c876b7e"
Cache-Control: max-age=0, must-revalidate

body { color: blue; }

브라우저는 CSS를 캐시에 저장하면서 두 가지 정보를 함께 보관합니다: 마지막으로 수정된 시각과 ETag(콘텐츠의 지문).

재방문: 변경 여부 확인

캐시 유효성 검사가 필요할 때, 브라우저는 조건부 요청을 보냅니다:

GET /style.css HTTP/1.1
Host: www.example.com
If-Modified-Since: Wed, 21 Oct 2024 07:00:00 GMT
If-None-Match: "686897696a7c876b7e"

브라우저가 이렇게 묻는 것입니다: "이 날짜에 이 지문을 가진 버전이 있습니다. 아직 유효한가요?"

서버의 결정

리소스가 변경되지 않은 경우:

HTTP/1.1 304 Not Modified
ETag: "686897696a7c876b7e"
Cache-Control: max-age=3600

본문 없음. 내용 없음. 확인만. 브라우저는 캐시된 복사본을 사용합니다.

리소스가 변경된 경우:

HTTP/1.1 200 OK
Content-Type: text/css
Last-Modified: Wed, 21 Oct 2024 08:30:00 GMT
ETag: "7789a8b7c93d987e8"
Cache-Control: max-age=3600

body { color: red; }

새 콘텐츠, 새 지문, 캐시 업데이트.

두 가지 유효성 검사 방법: 시간 vs. 지문

HTTP는 두 가지 유효성 검사 메커니즘을 제공합니다. 각각 장단점이 있습니다.

Last-Modified (타임스탬프)

클라이언트가 보내는 것:

If-Modified-Since: Wed, 21 Oct 2024 07:00:00 GMT

서버가 확인하는 것: 이 날짜 이후에 파일이 수정되었는가?

장점: 단순합니다. 파일 시스템 타임스탬프를 사용합니다. 사람이 읽기 쉽습니다.

한계: 1초 단위 정밀도. 같은 초 안에 파일이 두 번 변경되거나, 콘텐츠 변경 없이 타임스탬프만 업데이트되면 유효성 검사가 올바르게 작동하지 않습니다.

ETag (지문)

클라이언트가 보내는 것:

If-None-Match: "686897696a7c876b7e"

서버가 확인하는 것: 이 지문이 현재 콘텐츠와 일치하는가?

장점: 정밀합니다. 콘텐츠 변경을 즉시 감지합니다. 시계에 의존하지 않습니다.

필요 사항: 지문을 계산하거나 별도로 관리해야 합니다.

둘 다 사용하기

클라이언트는 두 헤더를 모두 보낼 수 있습니다. 둘 중 하나라도 리소스가 변경되지 않았음을 나타내면 서버는 304를 반환할 수 있습니다. 둘 다 있을 때는 ETag가 우선합니다. 더 정밀하기 때문입니다.

권장 사항: 둘 다 구현하세요. ETag는 정밀도를 위해, Last-Modified는 구형 클라이언트와의 호환성을 위해.

ETag 생성

ETag는 여러 방법으로 계산할 수 있습니다:

콘텐츠 해시 (가장 정밀):

const crypto = require('crypto');
const hash = crypto.createHash('md5').update(content).digest('hex');
const etag = `"${hash}"`;

파일 메타데이터 (더 빠르지만 덜 정밀):

const stats = fs.statSync(filePath);
const etag = `"${stats.size}-${stats.mtime.getTime()}"`;

버전 번호 (버전이 있는 리소스의 경우):

const etag = `"${resourceId}-${version}"`;

강한 ETag vs. 약한 ETag

강한 ETag는 바이트 단위로 동일함을 의미합니다:

ETag: "686897696a7c876b7e"

약한 ETag는 의미상 동등함을 나타냅니다(W/ 접두사):

ETag: W/"686897696a7c876b7e"

약한 ETag는 압축 방식이나 공백 차이 같은 사소한 변경을 허용하면서도 콘텐츠가 기능적으로 동일하다는 것을 나타냅니다.

304 응답: 헤더만

304 응답에는 본문이 포함되어서는 안 됩니다. 헤더만 있습니다:

HTTP/1.1 304 Not Modified
Date: Wed, 21 Oct 2024 07:28:00 GMT
ETag: "686897696a7c876b7e"
Cache-Control: max-age=3600

포함해야 할 것: Date, ETag, Cache-Control, Expires(사용하는 경우)

포함하지 말아야 할 것: Content-Type, Content-Length(또는 0으로 설정), 모든 본문 내용

304 응답에 ETag를 포함하는 것이 중요합니다 — 클라이언트가 이후 요청에서 If-None-Match 헤더에 무엇을 보내야 할지 알 수 있기 때문입니다.

서버 구현

app.get('/style.css', function(request, response) {
    const resource = loadResource('style.css');
    const currentETag = calculateETag(resource);
    const currentModified = resource.lastModified;

    const clientETag = request.headers['if-none-match'];
    const clientModifiedSince = request.headers['if-modified-since'];

    // 리소스가 변경되지 않았는지 확인
    if (clientETag === currentETag ||
        (clientModifiedSince && new Date(clientModifiedSince) >= currentModified)) {
        response.status(304);
        response.setHeader('ETag', currentETag);
        response.setHeader('Cache-Control', 'max-age=3600');
        response.end(); // 본문 없음
    } else {
        response.status(200);
        response.setHeader('Content-Type', 'text/css');
        response.setHeader('ETag', currentETag);
        response.setHeader('Last-Modified', currentModified.toUTCString());
        response.setHeader('Cache-Control', 'max-age=3600');
        response.send(resource.content);
    }
});

로드 밸런싱: ETag 일관성 문제

로드 밸런싱 환경에서는 모든 서버가 동일한 콘텐츠에 대해 동일한 ETag를 생성해야 합니다. 서버 A가 같은 파일에 대해 "abc123"을 계산하고 서버 B가 "xyz789"를 계산하면, 유효성 검사가 무작위로 실패합니다 — 클라이언트가 보낸 ETag가 일치하지 않아 304 대신 전체 다운로드가 발생합니다.

해결 방법:

  • 콘텐츠 기반 해시 사용 (같은 콘텐츠 = 서버와 무관하게 동일한 해시)
  • 서버 간 ETag 계산 공유
  • ETag를 일관되게 처리하는 CDN 사용

304 직접 확인해보기

  1. DevTools 열기 (F12)
  2. Network 탭으로 이동
  3. 페이지 로드
  4. 새로 고침
  5. 304 상태 코드 찾기
  6. Size 열에서 아주 작은 전송량 또는 "from cache" 확인

브라우저가 이 모든 것을 자동으로 처리합니다. 304를 발견했다면, HTTP가 제 역할을 하는 것을 보고 있는 겁니다 — 변경 없음을 확인하고, 다운로드를 절약하고, 웹을 더 빠르게 만드는 것을.

304 Not Modified 자주 묻는 질문

304 Not Modified와 캐시에서 리소스를 가져오는 것의 차이점은 무엇인가요?

캐시에서 리소스를 가져온다는 것은 브라우저가 서버에 아무 요청도 보내지 않았다는 뜻입니다 — Cache-Control에 따라 캐시된 복사본이 아직 만료되지 않은 상태였던 것입니다. 304는 브라우저가 캐시된 복사본을 검증하기 위해 서버에 요청을 보냈고, 서버가 여전히 유효하다고 확인한 것입니다. 304는 서버와 한 번 통신해야 하지만, 캐시 히트는 그렇지 않습니다.

304 응답에 본문이 포함될 수 있나요?

아니요. HTTP 명세가 이를 명시적으로 금지합니다. 304 응답에는 헤더만 포함되어야 합니다. 본문을 포함하면 대역폭을 낭비하고 프로토콜을 위반합니다 — 304의 핵심 목적은 리소스 전송 자체를 피하는 것입니다.

캐시가 유효한 것 같은데 서버가 304 대신 200을 반환하는 이유는 무엇인가요?

여러 가능성이 있습니다: 로드 밸런싱 환경에서 ETag 생성이 서버마다 일관되지 않을 수 있고, 서버가 조건부 요청을 구현하지 않았을 수 있으며, If-Modified-Since 타임스탬프에 1초 정밀도 문제가 있을 수 있고, 서버 시계가 동기화되지 않았을 수도 있습니다. 조건부 요청 헤더가 올바르게 전송되는지, 서버가 ETag/Last-Modified 유효성 검사를 지원하는지 확인하세요.

ETag와 Last-Modified 중 무엇을 사용해야 하나요?

둘 다 사용하세요. ETag는 더 정밀하며 모든 콘텐츠 변경을 감지할 수 있습니다. Last-Modified는 더 단순하고 구형 시스템과의 호환성을 제공합니다. 둘 다 있을 때는 ETag가 우선합니다. 둘 다 구현하는 데 단점이 없습니다.

이 페이지가 도움이 되었나요?

😔
🤨
😃