본문으로 건너뛰기

📋 HTTP 헤더

📖 정의

HTTP 헤더는 클라이언트와 서버가 추가 정보를 주고받기 위한 메타데이터입니다. 요청과 응답에 대한 정보, 인증, 캐싱, 콘텐츠 타입 등을 정의합니다.

🎯 비유로 이해하기

우편 봉투

HTTP 메시지 = 편지
├─ 봉투 (헤더)
│ ├─ 보내는 사람 주소 (User-Agent)
│ ├─ 받는 사람 주소 (Host)
│ ├─ 우편 종류 (Content-Type)
│ ├─ 중요도 (Priority)
│ └─ 반송 주소 (Referer)
└─ 편지 내용 (바디)

봉투(헤더)를 먼저 보고:
- 누가 보냈는지 확인
- 어떤 종류의 편지인지 파악
- 어떻게 처리할지 결정

💡 헤더의 구조

헤더 형식:
Header-Name: value

예시:
Content-Type: application/json
Authorization: Bearer abc123
User-Agent: Mozilla/5.0

📬 요청 헤더 (Request Headers)

Host

요청하는 서버의 호스트명과 포트

GET /api/users HTTP/1.1
Host: example.com
// fetch는 자동으로 Host 헤더 설정
fetch('https://example.com/api/users');

특징:

  • HTTP/1.1부터 필수
  • 하나의 IP에 여러 도메인 호스팅 가능 (가상 호스트)
  • 브라우저가 자동으로 설정

User-Agent

클라이언트 애플리케이션 정보

GET /api/users HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
// Node.js에서 커스텀 User-Agent 설정
fetch('https://api.example.com/users', {
headers: {
'User-Agent': 'MyApp/1.0'
}
});

용도:

  • 브라우저/OS 식별
  • 통계 수집
  • 호환성 처리
  • 봇 탐지

Accept

클라이언트가 수용 가능한 미디어 타입

GET /api/users HTTP/1.1
Host: example.com
Accept: application/json, text/html
Accept-Language: ko-KR, en-US
Accept-Encoding: gzip, deflate, br
fetch('https://api.example.com/users', {
headers: {
'Accept': 'application/json',
'Accept-Language': 'ko-KR,ko;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br'
}
});

Accept 헤더 종류:

Accept
├─ Accept: application/json - JSON 선호
├─ Accept: text/html - HTML 선호
└─ Accept: */* - 모든 타입 허용

Accept-Language
├─ Accept-Language: ko-KR - 한국어 선호
└─ Accept-Language: en-US,en;q=0.9 - 영어도 가능 (우선순위 낮음)

Accept-Encoding
├─ Accept-Encoding: gzip - gzip 압축 지원
└─ Accept-Encoding: br, gzip - Brotli 선호, gzip도 가능

Accept-Charset (거의 사용 안 함)
└─ Accept-Charset: utf-8 - UTF-8 인코딩

Authorization

인증 정보

GET /api/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// JWT 토큰 인증
fetch('https://api.example.com/profile', {
headers: {
'Authorization': 'Bearer ' + token
}
});

// Basic 인증
const credentials = btoa('username:password');
fetch('https://api.example.com/profile', {
headers: {
'Authorization': 'Basic ' + credentials
}
});

인증 방식:

Bearer Token (가장 일반적)
Authorization: Bearer <token>
├─ JWT, OAuth 2.0에서 사용
└─ 예: Bearer eyJhbGci...

Basic Authentication
Authorization: Basic <base64-credentials>
├─ username:password를 Base64 인코딩
├─ 안전하지 않음 (HTTPS 필수)
└─ 예: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

API Key
Authorization: ApiKey <api-key>
또는
X-API-Key: <api-key>

Content-Type

요청 바디의 미디어 타입

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json

{
"name": "홍길동",
"email": "hong@example.com"
}
// JSON 전송
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: '홍길동',
email: 'hong@example.com'
})
});

// 폼 데이터 전송
const formData = new FormData();
formData.append('name', '홍길동');
formData.append('file', fileInput.files[0]);

fetch('https://api.example.com/upload', {
method: 'POST',
body: formData
// Content-Type은 자동 설정 (multipart/form-data)
});

주요 Content-Type:

application/json
├─ JSON 데이터
└─ 가장 일반적인 API 형식

application/x-www-form-urlencoded
├─ 폼 데이터 (기본값)
└─ key1=value1&key2=value2

multipart/form-data
├─ 파일 업로드
└─ 여러 파트로 구성

text/plain
├─ 일반 텍스트
└─ 간단한 데이터

application/xml
├─ XML 데이터
└─ 레거시 API

클라이언트가 저장한 쿠키 전송

GET /api/profile HTTP/1.1
Host: example.com
Cookie: sessionId=abc123; userId=456
// 브라우저가 자동으로 쿠키 전송
fetch('https://example.com/api/profile', {
credentials: 'include' // 쿠키 포함
});

Referer

이전 페이지 URL

GET /api/products/123 HTTP/1.1
Host: example.com
Referer: https://example.com/products

용도:

  • 유입 경로 분석
  • 보안 검증
  • 로깅

기타 요청 헤더

Origin
├─ 요청이 시작된 오리진
└─ CORS 검증에 사용

If-None-Match
├─ ETag 값과 비교
└─ 캐시 검증

If-Modified-Since
├─ 날짜 이후 수정되었는지 확인
└─ 캐시 검증

Range
├─ 일부 데이터만 요청
└─ 예: bytes=0-1023

📤 응답 헤더 (Response Headers)

Content-Type

응답 바디의 미디어 타입

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"id": 123,
"name": "홍길동"
}
fetch('https://api.example.com/users/123')
.then(response => {
console.log(response.headers.get('Content-Type'));
// "application/json; charset=utf-8"
return response.json();
});

Content-Length

응답 바디의 크기 (바이트)

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 256

{"id":123,"name":"홍길동"}

클라이언트에 쿠키 저장

HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure; SameSite=Strict
Set-Cookie: userId=456; Max-Age=3600
// 서버에서 쿠키 설정 (Node.js/Express)
res.cookie('sessionId', 'abc123', {
httpOnly: true, // JavaScript 접근 차단
secure: true, // HTTPS만
sameSite: 'strict',
maxAge: 3600000 // 1시간
});

쿠키 속성:

HttpOnly
├─ JavaScript로 접근 불가
└─ XSS 공격 방어

Secure
├─ HTTPS에서만 전송
└─ 중간자 공격 방어

SameSite
├─ Strict: 같은 사이트에서만
├─ Lax: 일부 크로스 사이트 허용
└─ None: 모든 크로스 사이트 허용 (Secure 필수)

Max-Age / Expires
├─ Max-Age: 3600 (초 단위)
└─ Expires: Wed, 21 Oct 2025 07:28:00 GMT

Domain / Path
├─ Domain: example.com (하위 도메인 포함)
└─ Path: / (전체 경로)

Cache-Control

캐싱 정책

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Wed, 26 Jan 2025 10:00:00 GMT
fetch('https://api.example.com/users')
.then(response => {
const cacheControl = response.headers.get('Cache-Control');
console.log(cacheControl); // "public, max-age=3600"
});

캐싱 지시자:

Cache-Control: no-cache
├─ 캐시 사용 전 서버에 검증
└─ 항상 최신 데이터 보장

Cache-Control: no-store
├─ 캐시하지 않음
└─ 민감한 데이터 (비밀번호 등)

Cache-Control: public, max-age=3600
├─ 공개 캐시 허용
├─ 1시간 동안 캐시
└─ 정적 리소스 (이미지, CSS 등)

Cache-Control: private, max-age=3600
├─ 브라우저만 캐시
└─ 사용자별 데이터

Cache-Control: must-revalidate
├─ 캐시 만료 시 반드시 검증
└─ 오래된 데이터 방지

Location

리다이렉트 또는 생성된 리소스 위치

HTTP/1.1 201 Created
Location: /api/users/123

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: '홍길동' })
})
.then(response => {
if (response.status === 201) {
const location = response.headers.get('Location');
console.log('새 리소스:', location); // "/api/users/123"
}
});

Access-Control-* (CORS)

교차 출처 리소스 공유

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
// 서버에서 CORS 설정 (Node.js/Express)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});

CORS 헤더:

Access-Control-Allow-Origin
├─ 허용할 오리진 지정
├─ *: 모든 오리진 허용 (보안 주의)
└─ https://example.com: 특정 오리진만

Access-Control-Allow-Methods
├─ 허용할 HTTP 메서드
└─ GET, POST, PUT, DELETE, OPTIONS

Access-Control-Allow-Headers
├─ 허용할 요청 헤더
└─ Content-Type, Authorization

Access-Control-Allow-Credentials
├─ 쿠키 포함 허용 여부
└─ true (Origin이 *이면 안 됨)

Access-Control-Max-Age
├─ Preflight 결과 캐시 시간 (초)
└─ 86400 (24시간)

🔐 보안 헤더

Strict-Transport-Security (HSTS)

HTTPS 강제

HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains
// 서버 설정 (Node.js/Express)
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});

Content-Security-Policy (CSP)

XSS 공격 방어

HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com

CSP 지시자:

default-src 'self'
├─ 기본 정책: 같은 오리진만
└─ 모든 리소스 타입의 기본값

script-src 'self' https://trusted.com
├─ 스크립트 출처 제한
└─ XSS 공격 방어

style-src 'self' 'unsafe-inline'
├─ 스타일시트 출처 제한
└─ 인라인 스타일 허용

img-src * data:
├─ 이미지 출처 제한
└─ 모든 출처, data URI 허용

frame-ancestors 'none'
├─ iframe 삽입 차단
└─ 클릭재킹 방어

X-Content-Type-Options

MIME 스니핑 방지

HTTP/1.1 200 OK
X-Content-Type-Options: nosniff

X-Frame-Options

클릭재킹 방어

HTTP/1.1 200 OK
X-Frame-Options: DENY

옵션:

DENY
├─ 모든 iframe 삽입 차단
└─ 가장 안전

SAMEORIGIN
├─ 같은 오리진에서만 iframe 허용
└─ 일반적으로 사용

ALLOW-FROM https://example.com
├─ 특정 오리진에서만 허용
└─ 구형 브라우저 지원

📊 커스텀 헤더

X- 접두사 (비표준, 레거시)

GET /api/users HTTP/1.1
Host: example.com
X-API-Key: abc123xyz
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
fetch('https://api.example.com/users', {
headers: {
'X-API-Key': 'abc123xyz',
'X-Request-ID': crypto.randomUUID()
}
});

일반적인 커스텀 헤더:

X-API-Key
├─ API 키 인증
└─ 대안: Authorization 헤더

X-Request-ID
├─ 요청 추적용 고유 ID
└─ 로깅, 디버깅

X-Forwarded-For
├─ 프록시를 거친 원본 IP
└─ 예: X-Forwarded-For: 203.0.113.1, 198.51.100.178

X-Real-IP
├─ 실제 클라이언트 IP
└─ Nginx 등에서 사용

X-Correlation-ID
├─ 마이크로서비스 간 추적
└─ 분산 시스템

💡 실전 예시

완전한 요청/응답 예시

요청:
POST /api/users HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: application/json
Accept-Language: ko-KR,ko;q=0.9
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Length: 58
Origin: https://example.com
Referer: https://example.com/register

{
"name": "홍길동",
"email": "hong@example.com"
}

응답:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 156
Location: /api/users/123
Cache-Control: no-cache, no-store, must-revalidate
Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure; SameSite=Strict
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"id": 123,
"name": "홍길동",
"email": "hong@example.com",
"createdAt": "2025-01-26T10:00:00Z"
}

헤더 읽기/쓰기 (JavaScript)

// 요청 헤더 설정
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
'Accept': 'application/json',
'X-Request-ID': crypto.randomUUID()
},
body: JSON.stringify({
name: '홍길동',
email: 'hong@example.com'
})
});

// 응답 헤더 읽기
const contentType = response.headers.get('Content-Type');
const location = response.headers.get('Location');
const requestId = response.headers.get('X-Request-ID');

console.log('Content-Type:', contentType);
console.log('Location:', location);
console.log('Request ID:', requestId);

// 모든 헤더 출력
response.headers.forEach((value, key) => {
console.log(`${key}: ${value}`);
});

서버에서 헤더 설정 (Node.js/Express)

app.post('/api/users', (req, res) => {
// 새 사용자 생성
const newUser = {
id: 123,
name: req.body.name,
email: req.body.email
};

// 응답 헤더 설정
res.status(201);
res.set({
'Content-Type': 'application/json',
'Location': `/api/users/${newUser.id}`,
'Cache-Control': 'no-cache, no-store, must-revalidate',
'X-Request-ID': req.headers['x-request-id'] || generateId(),
'Strict-Transport-Security': 'max-age=31536000',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY'
});

// 쿠키 설정
res.cookie('sessionId', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});

res.json(newUser);
});

🤔 자주 묻는 질문

Q1. 커스텀 헤더 이름 규칙은?

A:

과거 (X- 접두사):
X-API-Key: abc123
X-Custom-Header: value

현재 (권장):
API-Key: abc123
Custom-Header: value

이유:
- X- 접두사는 RFC 6648에서 비권장
- 표준화될 때 혼란 방지
- 단, 이미 널리 사용되는 X- 헤더는 계속 사용

예외:
X-Forwarded-For - 너무 널리 사용됨
X-Real-IP - Nginx 표준
X-Request-ID - 로깅/추적 표준

Q2. 헤더 크기 제한은?

A:

일반적인 제한:
├─ Apache: 8KB (기본값)
├─ Nginx: 4KB-8KB
├─ Node.js: 80KB (http.maxHeaderSize)
└─ Cloudflare: 32KB

초과 시:
- 431 Request Header Fields Too Large
- 413 Request Entity Too Large

해결 방법:
1. 헤더 대신 바디 사용
2. 토큰 길이 줄이기
3. 서버 설정 변경 (주의 필요)

❌ 나쁜 예:
Authorization: Bearer <5KB-JWT-token>

✅ 좋은 예:
- 짧은 토큰 사용
- 세션 ID + 서버 사이드 저장

Q3. 대소문자 구분되나?

A:

헤더 이름: 대소문자 구분 안 함
├─ Content-Type = content-type = CONTENT-TYPE
└─ HTTP/2에서는 모두 소문자로 변환

헤더 값: 대소문자 구분됨
├─ Content-Type: application/json (O)
├─ Content-Type: Application/JSON (X)
└─ 값은 대소문자 구분

권장 사항:
- 헤더 이름: 각 단어 첫 글자 대문자 (Content-Type)
- 헤더 값: 스펙에 따름

Q4. 민감한 정보를 헤더에 넣어도 되나?

A:

✅ 안전한 헤더 사용:
Authorization: Bearer <token>
├─ HTTPS 사용 필수
├─ 로깅 시 마스킹
└─ 짧은 만료 시간

❌ 위험한 사용:
Authorization: Bearer <token> (HTTP)
├─ 평문 전송 위험
└─ 중간자 공격 가능

X-API-Key: <secret>
├─ URL에 포함 시 로그에 노출
└─ 브라우저 히스토리에 남음

주의사항:
1. HTTPS 필수
2. 서버 로그에서 민감한 헤더 제외
3. 브라우저 개발자 도구에 노출됨
4. 프록시, CDN에 전달됨

Q5. Preflight 요청이란?

A:

Preflight = CORS 사전 요청

언제 발생?
├─ GET, POST, HEAD 외 메서드 (PUT, DELETE 등)
├─ 커스텀 헤더 사용
└─ Content-Type이 특정 값 외 (application/json 등)

과정:
1. OPTIONS 요청 (Preflight)
OPTIONS /api/users HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

2. 서버 응답
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

3. 실제 요청
POST /api/users HTTP/1.1
Content-Type: application/json
Authorization: Bearer token
...

최적화:
- Access-Control-Max-Age로 캐싱 (24시간)
- Simple Request 조건 만족 (Preflight 회피)

🎓 실습하기

1. 헤더 디버깅

// 모든 요청/응답 헤더 로깅
async function debugFetch(url, options = {}) {
console.log('=== 요청 ===');
console.log('URL:', url);
console.log('Method:', options.method || 'GET');
console.log('Headers:', options.headers || {});

const response = await fetch(url, options);

console.log('=== 응답 ===');
console.log('Status:', response.status, response.statusText);
console.log('Headers:');
response.headers.forEach((value, key) => {
console.log(` ${key}: ${value}`);
});

return response;
}

// 사용
await debugFetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ name: '홍길동' })
});

2. 헤더 미들웨어 (Express)

// 보안 헤더 자동 설정
function securityHeaders(req, res, next) {
res.set({
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Content-Security-Policy': "default-src 'self'"
});
next();
}

// CORS 설정
function corsHeaders(req, res, next) {
res.set({
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
});

if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}

next();
}

app.use(securityHeaders);
app.use(corsHeaders);

🔗 관련 문서

🎬 마치며

HTTP 헤더는 클라이언트와 서버 간의 메타데이터를 주고받는 중요한 수단입니다. 적절한 헤더 사용은 보안, 성능, 호환성을 향상시킵니다!

다음 단계: 쿠키와 세션을 읽어보며 HTTP의 무상태성을 극복하는 방법을 알아보세요.