1. 라이브러리
  2. HTTP와 웹
  3. HTTP 메서드

업데이트됨 1개월 전

GET은 "이거 보내줘", HEAD는 "이거 어떻게 생겼어?"입니다.

차이는 그게 전부입니다. HEAD 요청은 GET과 동일한 응답을 요청합니다—동일한 상태 코드, 동일한 헤더—하지만 서버는 본문을 보내지 않습니다. 리소스를 실제로 받지 않고도 그것에 대한 모든 것을 알 수 있습니다. 받기 전에 먼저 살펴보는 정찰입니다.

HEAD가 하는 일

GET 요청:

GET /large-video.mp4 HTTP/1.1
Host: example.com
→
HTTP/1.1 200 OK
Content-Type: video/mp4
Content-Length: 524288000
Last-Modified: Wed, 15 May 2024 10:00:00 GMT

[524 MB의 비디오 데이터]

HEAD 요청:

HEAD /large-video.mp4 HTTP/1.1
Host: example.com
→
HTTP/1.1 200 OK
Content-Type: video/mp4
Content-Length: 524288000
Last-Modified: Wed, 15 May 2024 10:00:00 GMT

[본문 없음]

비디오를 단 한 바이트도 다운로드하지 않고, 파일이 524 MB짜리 MP4이며 마지막으로 수정된 시간까지 알 수 있습니다.

HEAD를 언제 사용하나요

HEAD가 특히 유용한 경우는 리소스를 실제로 가져오지 않고 정보만 필요할 때입니다:

리소스 존재 여부 확인:

HEAD /api/users/johndoe HTTP/1.1
→ 200 OK (사용자 존재)
→ 404 Not Found (사용자 없음)

링크 확인: 웹 크롤러와 링크 체커는 페이지를 다운로드하지 않고 링크를 검증합니다:

async function checkLink(url) {
  const response = await fetch(url, { method: 'HEAD' });
  return response.ok;
}

다운로드 전 파일 크기 확인:

HEAD /downloads/large-file.zip
→ Content-Length: 1073741824

// 1 GB—다운로드 전에 사용자에게 먼저 물어보기

리소스 최신 여부 확인:

HEAD /data/report.pdf
→ Last-Modified: Wed, 15 May 2024 10:00:00 GMT
→ ETag: "abc123"

// 캐시된 버전과 비교

콘텐츠 유형 파악:

HEAD /file-without-extension
→ Content-Type: application/pdf

// PDF 파일임을 확인

HEAD와 캐싱

HEAD 응답은 GET과 동일한 캐싱 규칙을 따릅니다. 브라우저와 프록시는 HEAD 응답을 캐시하고, 원본 서버에 다시 접속하지 않고도 반환할 수 있습니다.

덕분에 HEAD는 캐시 유효성 검사에 유용합니다:

HEAD /api/data HTTP/1.1
If-Modified-Since: Wed, 15 May 2024 10:00:00 GMT
→
HTTP/1.1 304 Not Modified

// 캐시가 여전히 유효함, 다운로드 불필요

구현 요구 사항

RFC 91101은 서버가 HEAD 요청에 대해 GET과 동일한 헤더를 응답으로 보내야 한다고 명시합니다. "must"가 아닌 "should"를 사용하는 이유는, 일부 헤더가 콘텐츠를 실제로 생성하는 과정에서만 결정될 수 있기 때문입니다—어차피 버릴 콘텐츠를 생성하는 것은 HEAD의 목적에 어긋납니다.

올바르게 구현된 HEAD 핸들러가 해야 할 일:

  • 권한 확인
  • 콘텐츠 유형 결정
  • 콘텐츠를 생성하지 않고도 알 수 있다면 Content-Length 계산
  • 캐시 헤더와 ETag 설정
  • 본문은 보내지 않음

효율적인 구현 예시:

app.head('/articles/:id', (req, res) => {
  const metadata = getArticleMetadata(req.params.id);

  if (!metadata) {
    return res.status(404).end();
  }

  res.set('Content-Length', metadata.size);
  res.set('Content-Type', 'text/html');
  res.set('Last-Modified', metadata.modified);
  res.set('ETag', metadata.etag);
  res.status(200).end();
});

메타데이터만 조회하세요—버릴 목적으로 전체 콘텐츠를 생성하지 마세요.

존재 여부 확인: HEAD vs. GET

HEAD의 장점:

  • 대역폭 절약 (본문 전송 없음)
  • 대용량 리소스에서 더 빠름
  • 의도를 명확하게 표현 ("그냥 확인 중")

GET의 장점:

  • 어차피 콘텐츠가 필요한 경우
  • HEAD 후 GET 대신 요청 한 번으로 처리
  • 일부 서버는 HEAD를 제대로 지원하지 않음

일반적인 기준: 존재 여부나 메타데이터만 확인할 때는 HEAD를 사용하세요. 리소스가 존재한다면 콘텐츠도 필요할 때는 GET을 사용하세요.

// 다운로드 전에 대용량 파일 존재 여부 확인
const headResponse = await fetch(url, { method: 'HEAD' });

if (headResponse.ok) {
  const size = headResponse.headers.get('Content-Length');

  if (confirm(`${formatBytes(size)} 다운로드하시겠습니까?`)) {
    const getResponse = await fetch(url);
    // 파일 다운로드
  }
}

보안 고려 사항

HEAD 요청으로도 정보가 노출될 수 있습니다. GET과 동일한 보호 조치를 적용하세요:

인증 필요: HEAD에도 GET과 동일한 인증 및 권한 검사를 적용해야 합니다:

HEAD /users/123/private-data
→ 401 Unauthorized (인증되지 않은 경우)
→ 403 Forbidden (인증은 됐지만 권한이 없는 경우)

정보 노출 위험: 헤더가 민감한 정보를 드러낼 수 있습니다:

HEAD /internal-document.pdf
→ Last-Modified: 문서 수정 시각이 노출됨
→ Content-Length: 문서 크기가 노출됨
→ X-Author: 작성자 이름이 노출될 수 있음

GET에서도 노출되지 않을 정보가 HEAD 응답에 포함되지 않도록 하세요.

서버 지원의 불일치

서버는 GET 엔드포인트에 대해 HEAD를 지원해야 하지만, 현실은 다소 제각각입니다:

  • 일부 서버는 405 Method Not Allowed를 반환합니다
  • 일부는 GET과 다른 헤더를 반환합니다
  • 일부 프레임워크는 GET 핸들러를 HEAD로 자동 변환하지 않습니다

클라이언트 측 대처 방법:

async function headRequest(url) {
  try {
    return await fetch(url, { method: 'HEAD' });
  } catch (error) {
    // HEAD가 실패하면 GET으로 대체
    return await fetch(url);
  }
}

HEAD와 Range 요청

HEAD를 Range 헤더와 함께 사용하면 서버가 부분 콘텐츠를 지원하는지 확인할 수 있습니다:

HEAD /large-file.zip HTTP/1.1
Range: bytes=0-0
→
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 1
Content-Range: bytes 0-0/1073741824

// 서버가 Range 요청을 지원하며, 전체 파일 크기는 1 GB

다운로드 관리자는 이를 통해 병렬 청크 다운로드가 가능한지 판단합니다.

API에서의 HEAD

RESTful API는 리소스 엔드포인트에 대해 HEAD를 지원해야 합니다:

HEAD /api/v1/users/12345
→ 200 OK (사용자 존재)
→ 404 Not Found (사용자 없음)

HEAD /api/v1/articles/latest
→ Last-Modified: Wed, 15 May 2024 10:00:00 GMT
→ ETag: "abc123"

일부 API는 상태 확인(헬스 체크)에 HEAD를 활용합니다:

HEAD /health
→ 200 OK (서비스 정상)
→ 503 Service Unavailable (서비스 다운)

성능 패턴

조건부 GET: 다운로드 전에 최신 여부 먼저 확인:

const headResponse = await fetch(url, { method: 'HEAD' });
const etag = headResponse.headers.get('ETag');

if (etag === cachedETag) {
  return cachedContent;
}

const getResponse = await fetch(url);

다운로드 전 크기 사전 확인:

const head = await fetch(downloadUrl, { method: 'HEAD' });
const size = parseInt(head.headers.get('Content-Length'));

if (size > MAX_SIZE) {
  throw new Error('파일이 너무 큽니다');
}

리소스 가용성 폴링:

async function waitForResource(url) {
  while (true) {
    const response = await fetch(url, { method: 'HEAD' });

    if (response.ok) {
      return true;
    }

    if (response.status === 404) {
      await sleep(1000);
      continue;
    }

    throw new Error(`예상치 못한 상태 코드: ${response.status}`);
  }
}

HTTP HEAD 메서드 자주 묻는 질문

GET을 지원하면서 HEAD를 지원하지 않는 서버가 있는 이유는 무엇인가요?

일부 웹 프레임워크는 GET 핸들러에서 HEAD 핸들러를 자동으로 만들어주지 않아 개발자가 별도로 구현해야 합니다. 대충 만들어진 구현은 HEAD 지원을 그냥 빠뜨립니다. 또한 HEAD를 고려하지 않고 허용 메서드를 명시적으로 제한해서 405 Method Not Allowed를 반환하는 경우도 있습니다.

HEAD 요청을 캐시할 수 있나요?

네. HEAD 응답은 GET과 동일한 캐싱 규칙을 따릅니다. 브라우저와 프록시는 HEAD 응답을 캐시하고, 이후 HEAD 요청에 대해 원본 서버에 접속하지 않고도 반환할 수 있습니다. Cache-Control, ETag, Last-Modified 같은 캐시 헤더는 GET과 동일하게 작동합니다.

HEAD가 GET보다 빠른가요?

네트워크 전송 측면에서는 그렇습니다—본문이 없으니 데이터가 적습니다. 하지만 서버 측에서는 구현 방식에 따라 다릅니다. 잘 구현된 HEAD 핸들러는 메타데이터만 조회합니다. 제대로 구현되지 않은 경우 전체 응답을 생성한 뒤 본문을 버리므로, 전송 시간만 절약되는 셈입니다.

API 엔드포인트가 존재하는지 확인하기 위해 호출 전에 HEAD를 사용해야 하나요?

일반적으로는 그럴 필요가 없습니다. 데이터가 필요하다면 그냥 GET을 호출하세요—요청 두 번보다 한 번이 낫습니다. HEAD가 의미 있는 경우는 메타데이터만 진짜로 필요할 때입니다: 다운로드 버튼을 표시하기 전에 파일이 있는지 확인하거나, 캐시를 갱신하기 전에 콘텐츠가 바뀌었는지 확인하거나, 페이지를 다운로드하지 않고 링크를 검증할 때가 그런 경우입니다.

출처

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

😔
🤨
😃