1. 라이브러리
  2. HTTP와 웹
  3. HTTP 헤더

업데이트됨 1개월 전

모든 HTTP 요청은 표준 헤더가 제공하는 것 이상의 추가 정보를 담을 수 있습니다. 커스텀 헤더는 애플리케이션이 자기 자신과 소통하는 방식입니다—맥락을 전달하고, 요청을 추적하고, 기능을 제어하죠. HTTP의 여백에 남기는 메모 같은 존재입니다.

커스텀 헤더가 실제로 하는 일

표준 헤더는 캐싱, 콘텐츠 타입, 인증 같은 보편적인 문제를 처리합니다. 하지만 애플리케이션에는 고유한 관심사가 있습니다:

  • 이 요청을 어떤 버전의 API가 처리해야 하나요?
  • 수십 개의 마이크로서비스를 거치는 이 요청을 어떻게 추적할 수 있을까요?
  • 이 사용자에게 새로운 결제 화면을 보여줘야 할까요?

커스텀 헤더가 이 질문들에 답합니다:

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
X-API-Version: 2024-10-01
X-Feature-Flag: experimental-ui

이것들은 어떤 HTTP 명세의 일부도 아닙니다. 어떤 정보를 전달하고 어떻게 해석할지에 대한 시스템 간의 약속입니다.

X- 접두사: 좋은 의도로 만들어진 관례

커스텀 헤더는 전통적으로 확장임을 표시하기 위해 "X-"로 시작합니다:

X-Request-ID: 123
X-Powered-By: CustomServer/1.0

이 관례는 이메일 헤더(RFC 822)에서 유래했으며 합리적으로 보였습니다—커스텀과 표준을 명확히 구분하는 것이죠.

그런데 현실이 개입했습니다. 일부 "실험적" 헤더들이 너무 널리 사용되어 사실상 표준이 되었습니다. X-Forwarded-For는 어디에나 있습니다. 하지만 여전히 X- 접두사를 갖고 있는데, 제거하면 예전 이름에 의존하는 모든 것이 망가질 것이기 때문입니다.

RFC 6648(2012년)은 새로운 헤더에 대한 X- 접두사 사용을 공식적으로 폐기했습니다. 현재 권장 사항은 그냥 설명적인 이름을 사용하라는 것입니다.

Request-ID: 550e8400-e29b-41d4-a716-446655440000
API-Version: 2024-10-01

실제로는 두 관례가 공존합니다. 레거시 헤더는 X- 접두사를 유지합니다. 새로운 헤더는 사용할 수도, 사용하지 않을 수도 있습니다. 중요한 것은 자체 시스템 내에서의 일관성입니다.

요청 추적: 없어서는 안 될 기능

50개의 서비스로 구성된 시스템에서 새벽 3시에 요청이 실패했을 때, 요청 ID는 수백만 개의 로그 줄을 맹목적으로 뒤지는 것과 몇 분 만에 문제를 찾는 것의 차이를 만듭니다.

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

모든 서비스는 모든 동작마다 이 ID를 로그에 남깁니다:

[550e8400...] API Gateway: Received request
[550e8400...] Auth Service: Token validated
[550e8400...] User Service: Fetching profile
[550e8400...] Database: Query timeout after 30s
[550e8400...] User Service: Error - upstream timeout
[550e8400...] API Gateway: Returning 500

그 ID를 검색하면 전체 이야기가 보입니다. 데이터베이스가 타임아웃되었습니다. 사용자 서비스가 복구하지 못했습니다. 게이트웨이가 에러를 반환했습니다. ID가 없었다면 타임스탬프를 맞추며 추측해야 했을 것입니다.

패턴은 단순합니다: 엣지에서 ID를 생성하고, 모든 서비스로 전달하고, 어디서나 로그에 남깁니다.

function requestIdMiddleware(request, response, next) {
    const requestId = request.headers['x-request-id'] || generateUUID();
    request.requestId = requestId;
    response.set('X-Request-ID', requestId);
    next();
}

API 버전 관리

API는 발전합니다. 오래된 클라이언트는 망가집니다. 커스텀 헤더는 한 가지 해결책을 제공합니다:

GET /api/users
API-Version: 2024-10-01

동일한 엔드포인트가 버전 헤더에 따라 다른 응답을 제공합니다. URL 버전 관리와 비교해보세요:

GET /api/v2/users

둘 다 작동합니다. 헤더 버전 관리는 URL을 더 깔끔하게 유지합니다. URL 버전 관리는 더 눈에 잘 띄고 캐싱이 가능합니다. 필요에 따라 선택하되, 일관성을 유지하세요.

피처 플래그

헤더는 요청이 어떤 기능을 볼지 제어할 수 있습니다:

X-Feature-Flags: new-checkout,experimental-search

이를 통해 다음이 가능합니다:

  • A/B 테스트: 다른 사용자를 다른 경험으로 라우팅
  • 점진적 출시: 트래픽의 1%에 기능을 활성화한 다음 10%, 그 다음 전체에게
  • 개발: 외부에 노출하지 않고 프로덕션에서 새 기능 테스트

서버는 플래그를 확인하고 그에 따라 동작을 조정합니다.

요청 제한 정보

API는 종종 클라이언트에게 남은 용량을 알려줍니다:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1609459200

클라이언트는 한도에 도달해 오류가 발생하는 대신 스스로 속도를 조절할 수 있습니다. GitHub API는 이를 광범위하게 활용합니다—헤더가 남은 요청 수와 할당량이 재설정되는 시간을 정확히 알려줍니다.

프록시 헤더: 사실상의 표준

일부 커스텀 헤더는 너무 보편적이어서 사실상 표준이 되었습니다.

X-Forwarded-For는 프록시 체인을 통해 클라이언트 IP를 추적합니다:

X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178

각 프록시는 자신이 수신한 IP를 추가합니다. 가장 왼쪽이 원래 클라이언트입니다.

X-Forwarded-Proto는 원래 프로토콜을 기록합니다:

X-Forwarded-Proto: https

로드 밸런서가 TLS를 종료하고 백엔드에 일반 HTTP를 전달할 때 꼭 필요합니다. 이 헤더가 없으면 백엔드는 모든 요청이 안전하지 않다고 판단합니다.

X-Real-IP는 nginx의 더 간단한 대안으로, 원래 클라이언트 IP만 제공합니다:

X-Real-IP: 203.0.113.195

좋은 커스텀 헤더 설계하기

명확하고 설명적인 이름을 사용하세요:

// 명확한 예
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
X-API-Version: 2024-10-01

// 모호한 예
X-Req: 550e8400-e29b-41d4-a716-446655440000
X-V: 2024-10-01

헤더 하나에 목적 하나:

// 좋은 예
X-Request-ID: 550e8400
X-User-ID: user-123

// 문제 있는 예
X-Context: requestId=550e8400,userId=user-123

작게 유지하세요. 헤더에는 실질적인 크기 제한이 있습니다(일반적으로 전체 합산 8KB). 큰 데이터는 요청 본문에 담으세요.

누락된 헤더를 안전하게 처리하세요:

const apiVersion = request.headers['x-api-version'] || '2024-10-01';

CORS와 커스텀 헤더

브라우저는 교차 출처 요청에서 커스텀 헤더를 제한합니다. 커스텀 헤더가 제대로 동작하려면:

서버가 사전 요청(preflight) 응답에서 명시적으로 허용해야 합니다:

Access-Control-Allow-Headers: X-Request-ID, X-API-Version

클라이언트가 커스텀 응답 헤더를 읽으려면 노출 설정이 필요합니다:

Access-Control-Expose-Headers: X-Request-ID, X-RateLimit-Remaining

이러한 CORS 헤더가 없으면 브라우저는 조용히 커스텀 헤더 접근을 차단합니다.

보안

헤더는 노출됩니다. 프록시에 의해 로깅되고, CDN에 의해 캐싱되고, 브라우저 기록에 저장됩니다. 헤더에 절대 비밀 정보를 넣지 마세요:

// 위험한 예
X-Admin-Token: secret-admin-token
X-Database-Connection: production-master

// 안전한 예
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

모든 입력을 검증하세요. 커스텀 헤더는 사용자 입력입니다:

const apiVersion = request.headers['x-api-version'];

if (!/^\d{4}-\d{2}-\d{2}$/.test(apiVersion)) {
    return response.status(400).json({ error: 'Invalid API version format' });
}

구현

커스텀 헤더 전송 (클라이언트):

fetch('/api/data', {
    headers: {
        'X-Request-ID': crypto.randomUUID(),
        'X-API-Version': '2024-10-01'
    }
});

커스텀 헤더 읽기 (서버):

app.get('/api/data', function(request, response) {
    const requestId = request.headers['x-request-id'];
    const apiVersion = request.headers['x-api-version'];
    
    response.set('X-Request-ID', requestId);
    response.json({ data: 'value' });
});

핵심 정리

  • 커스텀 헤더는 표준 헤더가 다루지 않는 애플리케이션 특화 메타데이터를 전달합니다
  • X- 접두사는 새 헤더에 대해 폐기되었지만 기존 시스템에서는 여전히 광범위하게 쓰입니다
  • 요청 ID는 분산 시스템 디버깅에 필수입니다—엣지에서 생성하고, 모든 곳에 전파하세요
  • X-Forwarded-* 헤더는 프록시를 통해 클라이언트 정보를 보존하는 사실상의 표준입니다
  • CORS는 교차 출처 요청에서 커스텀 헤더 사용에 대한 명시적 허용이 필요합니다
  • 헤더에 민감한 데이터를 절대 넣지 마세요—어디서나 로깅됩니다
  • 다른 사용자 입력과 마찬가지로 커스텀 헤더 값을 검증하세요

커스텀 헤더에 대해 자주 묻는 질문

새로운 커스텀 헤더에 X- 접두사를 사용해야 하나요?

아니요. RFC 6648은 2012년에 X- 접두사를 폐기했습니다. 접두사 없이 설명적인 이름을 사용하세요: X-Request-ID 대신 Request-ID. X-를 사용하는 유일한 이유는 이미 X-를 사용하고 있는 기존 시스템의 헤더와 일관성을 맞추기 위해서입니다.

요청 ID가 올바르게 전달되지 않을 때 어떻게 디버깅하나요?

세 가지를 확인하세요: 헤더가 출발지에서 설정되고 있나요? 각 서비스가 이를 읽고 전파하고 있나요? 프록시나 로드 밸런서가 제거하고 있나요? 각 구간에 로깅을 추가해 ID가 사라지는 지점을 찾으세요. 대부분의 문제는 다운스트림 호출에 ID를 전달하는 것을 잊은 서비스에서 발생합니다.

커스텀 헤더의 최대 크기는 얼마인가요?

대부분의 서버는 모든 헤더 합산 8KB 한도를 적용합니다. 개별 헤더 값은 4KB 미만으로 유지해야 합니다. 더 많은 데이터를 전달해야 한다면 요청 본문을 사용하세요—헤더는 메타데이터를 위한 것이지 페이로드를 위한 것이 아닙니다.

왜 JavaScript로 커스텀 응답 헤더를 읽을 수 없나요?

CORS 때문입니다. 브라우저는 기본적으로 JavaScript에 "단순" 응답 헤더만 노출합니다. 교차 출처 요청에서 커스텀 헤더를 읽으려면 서버가 Access-Control-Expose-Headers에 해당 헤더를 포함해야 합니다. 동일 출처 요청에는 이 제한이 없습니다.

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

😔
🤨
😃