들어가며 — HTTP는 "단순한 텍스트 프로토콜"이 아니다
GET /api/users/42 HTTP/1.1
Host: api.example.com
이 네 줄이 HTTP의 모든 것인 것처럼 보이지만, 현대 웹은 수십 개의 헤더와 세맨틱이 얽힌 복잡한 협약 시스템이다. POST와 PUT의 차이, 200과 204의 차이, 302와 307의 차이, Cache-Control의 10가지 지시어, Content-Type과 Accept의 협상 규칙, Cookie의 6가지 속성, CORS의 preflight/credentials 규칙, CSP/HSTS/Referrer-Policy/Permissions-Policy 같은 보안 헤더들...
하나라도 잘못 이해하면 버그 또는 보안 사고로 이어진다. 2020년 이후 SameSite=Lax 기본값 전환으로 전 세계 광고/분석 도구가 대거 부서졌고, CORS preflight 한 번에 10,000ms 왕복이 생기면 모바일 로딩이 치명적으로 느려진다. CSP unsafe-inline 하나를 방심하면 XSS가 들어오고, HSTS preload 목록에 올려놓고 도메인을 잃어버리면 복구가 수년 걸린다.
이 글은 HTTP 프로토콜의 세맨틱 레이어를 처음부터 해부한다. 전송(TLS, HTTP/3)은 다른 글에서 다뤘으므로 여기서는 "RFC 9110 HTTP Semantics"와 관련 보안 표준을 중심으로 본다. 매일 쓰는 API, 매일 보는 브라우저 에러, 매일 읽는 로그의 "왜"를 명쾌하게 설명하는 게 목표다.
1. HTTP 버전 — 세맨틱은 공통, 전송만 달라졌다
2022년 RFC 9110 "HTTP Semantics"가 HTTP/1.1, HTTP/2, HTTP/3의 공통 세맨틱을 분리해 정의했다.
- 전송(framing): HTTP/1.1 (텍스트, RFC 9112), HTTP/2 (바이너리 + 멀티플렉싱, RFC 9113), HTTP/3 (QUIC 기반, RFC 9114)
- 세맨틱: 메서드, 상태, 헤더, 콘텐츠 협상, 인증 등 (RFC 9110, 9111 cache)
즉 GET /foo HTTP/1.1과 GET /foo (HTTP/2 HEADERS 프레임) + GET /foo (HTTP/3 QPACK)은 전송 방식만 다를 뿐 의미는 같다. 이 글은 세맨틱에 집중.
2. 메서드 — 9개지만 실전은 6개
2.1 RFC 9110 표준 메서드
| 메서드 | Safe | Idempotent | Cacheable | 대표 용도 |
|---|---|---|---|---|
GET | ✅ | ✅ | ✅ | 리소스 조회 |
HEAD | ✅ | ✅ | ✅ | GET의 헤더만 |
POST | ❌ | ❌ | 조건부 | 생성/임의 액션 |
PUT | ❌ | ✅ | ❌ | 전체 교체 |
DELETE | ❌ | ✅ | ❌ | 삭제 |
PATCH | ❌ | ❌ | ❌ | 부분 수정 (RFC 5789) |
OPTIONS | ✅ | ✅ | ❌ | 지원 메서드 조회 / CORS preflight |
CONNECT | ❌ | ❌ | ❌ | 터널 (HTTPS proxy) |
TRACE | ✅ | ✅ | ❌ | 디버그 에코 (보안상 대부분 비활성화) |
2.2 Safe의 의미
Safe = 서버의 상태를 바꾸지 않는다. GET으로 "좋아요"를 올리게 만들면 prefetch, link preview, crawler가 의도치 않게 호출해 상태가 바뀐다. 2005~2006년 Google Web Accelerator가 GET 링크를 prefetch해서 "Delete"가 불릴 수 있다는 게 큰 이슈였다.
규칙: 상태 변경은 반드시 POST/PUT/DELETE/PATCH.
2.3 Idempotent의 의미
같은 요청을 여러 번 보내도 서버 상태가 한 번 보낸 것과 같음.
- PUT
/users/42(body:{name: "Alice"}) 여러 번 → 최종 상태 동일 - POST
/users(body:{name: "Alice"}) 여러 번 → 사용자 여러 명 생성! - DELETE
/users/42여러 번 → 첫 번째는 200, 이후는 404 (상태는 "존재하지 않음"으로 동일)
네트워크 재시도와의 관계: Idempotent 메서드는 클라이언트/프록시가 안전하게 재시도 가능. POST는 안 된다. 실무에서 POST 재시도를 원하면 idempotency key 헤더로 서버가 중복 제거하는 패턴을 쓴다(Stripe, AWS 등).
2.4 PUT vs PATCH — 혼동 1위
- PUT: "리소스 전체 교체". body가 완전한 표현
- PATCH: "부분 수정". body가 변경사항(diff, JSON Patch, JSON Merge Patch)
PUT 시 빠진 필드는 null 또는 default가 된다(또는 서버가 거절). PATCH는 포함된 필드만 변경. 실무에서 대부분은 PATCH가 맞다.
2.5 PATCH의 body 포맷
- JSON Merge Patch (RFC 7396): 단순.
{name: "Bob"} - JSON Patch (RFC 6902): 배열 조작, 이동, 조건부 — 복잡하지만 강력
[{ "op": "replace", "path": "/name", "value": "Bob" }]
대부분의 API가 Merge Patch를 쓰지만 배열 중 일부만 바꾸는 경우 Patch가 낫다.
3. 상태 코드 — 10개만 알면 80% 커버
3.1 1xx — Informational (거의 안 쓰임)
100 Continue: 큰 body 전송 전 "보내도 돼?" 확인101 Switching Protocols: WebSocket 업그레이드103 Early Hints(RFC 8297): 최종 응답 전 preload 힌트 — HTTP/2+에서 성능 개선용
3.2 2xx — Success
200 OK: 기본201 Created: POST로 새 리소스 생성 →Location헤더에 URL202 Accepted: 비동기 작업 수락 (아직 미완료)204 No Content: 성공, 응답 body 없음 (DELETE, 빈 PUT 결과)206 Partial Content: Range 요청 응답
3.3 3xx — Redirection 4형제 (가장 혼동)
| 코드 | 메서드 유지 | 영구성 | 의미 |
|---|---|---|---|
301 | 아니요 (대부분 GET으로) | 영구 | Moved Permanently. 검색엔진에 "도메인 옮김" |
302 | 아니요 (대부분 GET으로) | 일시 | Found. 과거 "Found"의 모호한 이름 |
303 | 아니요 (무조건 GET) | 일시 | See Other. POST 후 GET 패턴(PRG) |
307 | 예 | 일시 | Temporary Redirect. 메서드 그대로 |
308 | 예 | 영구 | Permanent Redirect. 메서드 그대로 |
실전 선택:
- POST form → 성공 →
303+Location: /success: 새로고침해도 POST 재전송 안 됨 - HTTPS 강제 리디렉션:
301또는308(HSTS로 대체 권장) - 로드 밸런서의 일시적 다른 호스트 안내:
307
3.4 4xx — Client Error
400 Bad Request: 요청 자체가 malformed401 Unauthorized: 인증 필요 / 실패 (이름이 틀림 — 사실 "Unauthenticated"가 맞음)403 Forbidden: 인증됐으나 권한 없음404 Not Found: 리소스 없음 또는 존재를 감추고 싶을 때405 Method Not Allowed: GET만 지원하는 리소스에 POST 등 →Allow헤더 필수406 Not Acceptable:Accept헤더와 맞는 표현 없음409 Conflict: 낙관적 잠금 실패, 중복 등410 Gone: 의도적으로 영구 삭제.404보다 명확412 Precondition Failed:If-Match등 조건부 요청 실패413 Payload Too Large415 Unsupported Media Type:Content-Type서버가 모름418 I'm a teapot: 농담. RFC 2324 만우절 → 실제로 쓰는 서비스도 있음 (Google)422 Unprocessable Entity: 문법 OK, 의미론적 오류 (validation 실패)429 Too Many Requests: Rate limit →Retry-After헤더
3.5 5xx — Server Error
500 Internal Server Error: 일반 서버 에러502 Bad Gateway: upstream(백엔드) 응답 이상 → 대부분 "백엔드 죽음"503 Service Unavailable: 과부하/점검.Retry-After권장504 Gateway Timeout: upstream 응답 없음
3.6 401 vs 403 — 결정 트리
로그인 안 됨 → 401 Unauthorized (이름만 Unauthorized, 의미는 Unauthenticated)
로그인 됐는데 권한 없음 → 403 Forbidden
존재를 숨기고 싶음 → 404 Not Found
"비밀 리소스"는 존재 여부 자체가 정보가 된다(예: 사설 지라 티켓). 이 경우 404가 보안상 더 안전.
4. 헤더 — 주요 카테고리
4.1 Content Negotiation
클라이언트가 선호를 표현 → 서버가 선택.
Accept: application/json, text/html;q=0.9
Accept-Language: ko-KR, ko;q=0.9, en;q=0.5
Accept-Encoding: br, gzip
q= (quality, 0.0~1.0)로 우선순위. 기본은 1.0. 서버는 Content-Type, Content-Language, Content-Encoding으로 응답 + Vary 헤더로 "이 응답은 이 헤더에 의존함" 캐시 힌트.
4.2 Range Requests
GET /video.mp4 HTTP/1.1
Range: bytes=0-999
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-999/1048576
Content-Length: 1000
비디오 스트리밍, 다운로드 재개에 필수. Accept-Ranges: bytes 헤더로 서버가 지원 여부 표시.
4.3 Conditional Requests
GET /article/42 HTTP/1.1
If-None-Match: "abc123"
If-Modified-Since: Mon, 15 Apr 2026 10:00:00 GMT
HTTP/1.1 304 Not Modified
ETag 또는 Last-Modified 기반. 응답 body 전송 절약.
- Strong ETag:
"abc123"— byte-exact 매칭 - Weak ETag:
W/"abc123"— 의미론적 매칭 (압축 방식 달라도 OK)
4.4 Compression
gzip: 20년 표준br(Brotli): Google 2015, 15~25% 더 작음. 현재 거의 모든 브라우저 지원zstd: 2024년 RFC 8478 → 일부 브라우저가 지원 시작
원문 대비 40~80% 압축. CDN 대부분 자동. 동적 vs 사전 압축 구분.
4.5 보안 관련은 별도 섹션에서 집중 다룸 (6~10절)
5. 캐시 — Cache-Control 완벽 해부
5.1 응답 헤더
no-store: 절대 저장 금지 (개인정보)no-cache: 저장은 OK, 사용 전 반드시 revalidateprivate: 공용 캐시(프록시/CDN) 금지, 브라우저만public: 모든 캐시 가능max-age=<초>: 신선함 기간s-maxage=<초>: 공용 캐시용 (max-age 오버라이드)must-revalidate: stale이면 반드시 origin 확인stale-while-revalidate=<초>: stale 응답 즉시 반환 + 백그라운드 갱신stale-if-error=<초>: origin 장애 시 stale 허용immutable: URL이 바뀌면 콘텐츠도 바뀜(hash URL) — 절대 revalidate 안 함
5.2 요청 헤더 (드물게)
Cache-Control: no-cache: "fresh 응답 받고 싶어" (Ctrl+F5)Cache-Control: max-age=0: 만료 강제
5.3 실전 템플릿
정적 자산 (hash URL):
Cache-Control: public, max-age=31536000, immutable
HTML (frequently changing):
Cache-Control: no-cache
API 응답 (개인 데이터):
Cache-Control: private, max-age=60
CDN 공유 + 브라우저 짧게:
Cache-Control: public, max-age=60, s-maxage=3600
5.4 Vary
Vary: Accept-Encoding, Accept-Language → 캐시가 이 헤더들 조합별로 다른 엔트리를 유지. 잘못 쓰면 캐시 히트율 폭락.
6. 쿠키 — 복잡한 상태의 왕
6.1 기본 구조
Set-Cookie: session=abc123; Path=/; Max-Age=3600; Secure; HttpOnly; SameSite=Lax
6.2 속성
Domain=: 공유할 도메인..example.com= 서브도메인 포함. 명시 안 하면 발급한 도메인만.Path=: 특정 경로 하위만Max-Age=<초>: 만료 (절대값은Expires=)Secure: HTTPS에서만 전송HttpOnly: JS에서document.cookie접근 불가 (XSS 방어)SameSite=Lax|Strict|None: CSRF/트래킹 방어 핵심Partitioned: CHIPS — 제3자 쿠키의 per-top-site 저장
6.3 SameSite — 2020년 이후 최대 변화
Strict: 외부 사이트에서 온 어떤 요청에도 쿠키 안 보냄. 외부 링크로 로그인 상태 사라짐.Lax(현재 기본값): Top-level GET 탐색은 허용, 그 외 크로스사이트 요청에는 쿠키 차단None: 모든 크로스사이트 요청에 쿠키 보냄. 반드시Secure함께 (브라우저가 강제)
중요한 예시:
사용자가 evil.com에서 <form action="bank.com/transfer">를 submit → POST
- SameSite=Strict: 쿠키 안 보냄 → 로그인 상태 없음 → 요청 거부
- SameSite=Lax: POST는 cross-site → 쿠키 안 보냄 → 거부 ✅
- SameSite=None: 쿠키 보냄 → CSRF 성공 ❌
2020년 Chrome 80부터 명시 안 한 쿠키는 Lax 기본. iframe, 광고, 분석 스크립트가 대거 깨지면서 혼란.
6.4 Third-party cookie의 종말
2024년 Chrome은 3rd-party cookie 단계적 폐지 시작. 2025년 전면 제한 예정.
- 광고/분석이 의존해온 모델 붕괴
- 대안: Google의 Privacy Sandbox (Topics API, FLEDGE, CHIPS)
- CHIPS(
Partitioned): 3rd-party가 top-level site별로 격리된 저장소 사용
6.5 쿠키의 크기 제한
- RFC 6265 권장: 쿠키 하나 4096 byte, 도메인당 50개, 총 3000개
- 현실: 브라우저마다 조금 다름 → 토큰을 쿠키에 저장할 땐 JWT 길이 주의. HTTP/2 헤더 압축(HPACK)이 있어도 초기 요청에서 쿠키가 TCP segment 여러 개 차지 가능
6.6 Prefix
__Secure-: Secure 속성 필수__Host-: Secure + Path=/ + Domain 없음 필수. 서브도메인 takeover 방어
Set-Cookie: __Host-session=abc; Secure; Path=/; SameSite=Lax
7. CORS — 크로스오리진의 미스터리
7.1 Same-Origin Policy
브라우저 보안의 근간: https://a.com의 JS는 https://b.com의 응답을 읽을 수 없다. "origin" = scheme + host + port.
7.2 Simple Request — preflight 없음
- 메서드: GET, HEAD, POST
- Content-Type:
application/x-www-form-urlencoded,multipart/form-data,text/plain만 - 커스텀 헤더 없음
서버가 Access-Control-Allow-Origin: * 또는 Access-Control-Allow-Origin: https://a.com 반환 시 브라우저가 JS에 응답 공개.
7.3 Preflight — OPTIONS 왕복
비-simple 요청은 본 요청 전에 OPTIONS로 preflight:
OPTIONS /api/users HTTP/1.1
Origin: https://a.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Max-Age로 preflight 결과를 캐시. 0~24시간이 일반적(Chromium은 2시간 상한).
7.4 Credentials (쿠키, Auth 헤더)
브라우저의 fetch(url, { credentials: 'include' }) 시:
- 서버:
Access-Control-Allow-Credentials: true - 서버:
Access-Control-Allow-Origin에*불가, 구체 origin 지정 필수
7.5 흔한 삽질
*은 credentials와 양립 불가: 서버에서ACAO: *+ACAC: true하면 브라우저 거부- Preflight 매번 날아감:
Max-Age설정하자 - Authorization 헤더:
Access-Control-Allow-Headers에 명시 필요 - Exposed headers: JS에서 읽으려면
Access-Control-Expose-Headers에 나열
7.6 CORS ≠ 보안
CORS는 브라우저 내에서만 강제. 서버 간 요청, curl, Postman은 CORS 무시. 실제 인증/권한은 서버가 직접 체크해야.
8. CSP — XSS를 근본적으로 막는 헤더
8.1 기본 구조
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'
브라우저가 허용된 소스 외의 스크립트/리소스 로드/실행 차단.
8.2 주요 디렉티브
default-src: 기본script-src,style-src,img-src,font-src,connect-src,frame-src,media-srcobject-src 'none': Flash/plugin 차단 (권장 기본)base-uri 'self':<base>태그 남용 방어frame-ancestors:X-Frame-Options의 후계 (clickjacking)form-action: form submit 대상 제한upgrade-insecure-requests: HTTP를 자동 HTTPS로
8.3 unsafe-inline — 위험한 익숙함
대부분의 legacy 사이트가 <script> 인라인 코드를 쓴다. CSP 적용 시 막혀서 'unsafe-inline' 추가하게 됨. 하지만 이러면 XSS 보호가 사실상 사라진다.
8.4 nonce vs hash vs strict-dynamic
Nonce (난수):
<script nonce="rAnDom123">...</script>
Content-Security-Policy: script-src 'nonce-rAnDom123'
요청마다 랜덤 생성, 매번 HTML에 삽입. 서버 렌더링 앱에 적합.
Hash:
Content-Security-Policy: script-src 'sha256-BASE64...'
정적 스크립트의 SHA-256으로 허용. 내용 바뀌면 재계산.
strict-dynamic:
Content-Security-Policy: script-src 'nonce-X' 'strict-dynamic'
nonce 있는 스크립트가 document.createElement('script')로 더 많은 스크립트를 로드하는 것도 신뢰. allow-list 없이 운영.
8.5 Reporting
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":31536000,"endpoints":[{"url":"/csp-report"}]}
위반 시 실제 차단 없이 JSON 리포트만. 프로덕션 도입 전 dry-run 필수.
8.6 Google의 strict CSP
Google은 모든 서비스에 다음과 유사한 strict CSP 적용:
Content-Security-Policy: script-src 'nonce-X' 'strict-dynamic'; object-src 'none'; base-uri 'self'; require-trusted-types-for 'script'
9. HSTS — HTTP 강제 차단
9.1 기본
Strict-Transport-Security: max-age=31536000; includeSubDomains
브라우저가 이 헤더를 본 후, 지정 기간 동안 HTTP 요청을 자동으로 HTTPS로 변환. MITM으로 HTTP 다운그레이드 공격 방지.
9.2 includeSubDomains
example.com에 설정 → 모든 서브도메인(api.example.com, www.example.com)에 적용. 하나의 서브도메인이라도 HTTPS 없으면 접근 불가.
9.3 Preload — 브라우저 내장 목록
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
hstspreload.org에 제출 후 심사 통과 → Chrome/Firefox/Safari에 내장. 첫 방문 전부터 HTTPS 강제.
위험: 한번 preload에 올라가면 내리기 매우 어렵다(브라우저 업데이트 주기). 서브도메인 하나가 HTTPS 인증서 갱신 실패로 만료되면 전체 서비스 접근 불가. 테스트/스테이징 도메인에 preload 쓰지 말 것.
10. Referrer-Policy — 개인정보 누출 방지
10.1 왜 위험한가
기본적으로 브라우저가 Referer: https://bank.com/account/123 같은 URL을 외부 사이트에 보낸다. 사용자 경로, 검색어, 내부 ID가 유출.
10.2 정책 값
no-referrer: 절대 안 보냄no-referrer-when-downgrade(과거 기본값): HTTPS→HTTP일 때만 안 보냄origin: origin만 (https://bank.com/)origin-when-cross-origin: 동일 origin엔 전체, 외부엔 origin만same-origin: 동일 origin에만 보냄strict-origin: origin만, downgrade 시 안 보냄strict-origin-when-cross-origin(현재 기본값, 2020년~): 동일 origin 전체, 외부 origin만, downgrade 안 보냄unsafe-url: 전부 보냄 (권장 안 함)
11. 추가 보안 헤더
11.1 X-Frame-Options → CSP frame-ancestors
X-Frame-Options: DENY
# 또는 (legacy)
X-Frame-Options: SAMEORIGIN
# 현대:
Content-Security-Policy: frame-ancestors 'none'
Clickjacking 방어.
11.2 X-Content-Type-Options
X-Content-Type-Options: nosniff
브라우저의 content type sniffing 비활성화. Content-Type: text/plain으로 보낸 파일을 브라우저가 "HTML 같은데?"로 해석해 XSS 당하는 걸 막음.
11.3 Permissions-Policy (구 Feature-Policy)
Permissions-Policy: geolocation=(), camera=(), microphone=()
브라우저 API 사용 제한. iframe에도 상속.
11.4 Cross-Origin-* 헤더
Cross-Origin-Opener-Policy: same-origin: 새 탭이window.opener접근 차단Cross-Origin-Embedder-Policy: require-corp: 교차 출처 리소스는Cross-Origin-Resource-Policy헤더 있어야만 embedCross-Origin-Resource-Policy: same-origin: 이 리소스가 다른 origin에서 embed되는 걸 차단
SharedArrayBuffer, high-resolution timer 같은 강력 기능 쓰려면 COOP+COEP 필수(Spectre 방어).
11.5 Timing-Allow-Origin
Resource Timing API로 JS가 network timing 접근 시 CORS처럼 origin 체크.
12. 인증 — Basic, Bearer, Digest, Session
12.1 Basic
Authorization: Basic dXNlcjpwYXNzd29yZA==
user:password를 base64 인코딩. TLS 없으면 평문 수준. 내부 관리 페이지 외 실전 사용 지양.
12.2 Bearer (OAuth2, JWT)
Authorization: Bearer eyJhbGc...
"이 토큰 가진 사람은 누구든 접근" — 대부분의 현대 API. 토큰 유출 시 즉시 취소 가능해야 함.
12.3 Digest
RFC 7616. challenge-response + nonce로 평문 전송 없이 인증. 복잡해서 거의 안 쓰임(TLS가 이겼다).
12.4 Session (Cookie 기반)
서버가 세션 ID를 쿠키로 발급. 서버는 세션 스토어(Redis, DB)에 상태 보관. 전통적이고 강력. JWT의 단점(취소 어려움)을 해결.
12.5 JWT vs Session — 2025년의 합의
- 세션: 서버 상태 필요, 즉시 취소 가능, 중앙 인증 불리
- JWT: stateless, 분산 친화, 짧은 유효기간 + refresh token 패턴 권장
혼합: Access token은 JWT, Refresh token은 DB 저장 → 장점 결합.
13. WebSocket 업그레이드
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
이후 같은 TCP 연결이 WebSocket 프레임으로 전환. HTTP/2/3에서는 다른 negotiation 메커니즘.
14. 에러와 재시도 — Retry-After, 429
14.1 429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Retry-After: 60
# 또는
Retry-After: Wed, 15 Apr 2026 10:00:00 GMT
클라이언트는 Retry-After만큼 기다린 뒤 재시도. 무조건적 exponential backoff보다 서버 협조적.
14.2 503 + Retry-After
서비스 점검 시 503 + Retry-After로 "5분 뒤에 와" 표시. 검색 엔진도 이해.
14.3 idempotency key
POST /payments HTTP/1.1
Idempotency-Key: d8f3a1b2-...
HTTP/1.1 200 OK ← 1차 성공
네트워크 끊김으로 다시 같은 키로 보내면 서버는 첫 응답을 반환(중복 결제 방지). Stripe, AWS가 대중화.
15. Content-Type — 오해의 근원
15.1 Charset
Content-Type: text/html; charset=utf-8
UTF-8 명시 필수. 생략 시 브라우저가 추측하다가 한글 깨짐. JSON은 RFC 8259에서 UTF-8 고정이라 charset 불필요.
15.2 형식 구분
application/json: REST API 표준application/x-www-form-urlencoded: HTML form 기본multipart/form-data: 파일 업로드text/html; charset=utf-8: 웹 페이지application/octet-stream: 바이너리application/pdf,image/png,video/mp4: 미디어application/problem+json(RFC 7807): 에러 응답 표준
15.3 Boundary in multipart
Content-Type: multipart/form-data; boundary=----abcd1234
본문에서 --abcd1234로 각 파트 구분. 프레임워크가 자동 처리하지만 로그에서 보면 이해됨.
16. HTTP/2 / HTTP/3 세맨틱 차이점
16.1 헤더는 compressed
- HTTP/2: HPACK (정적 + 동적 테이블)
- HTTP/3: QPACK (유사, 순서 의존성 완화)
같은 Cookie 값이 반복 요청에서 1~2 byte로 압축. 첫 요청은 전체 전송이라 여전히 쿠키 크기 중요.
16.2 대소문자
HTTP/2+에선 헤더 이름은 소문자 필수. HTTP/1.1은 대소문자 무시지만 관용으로 Content-Type 스타일.
16.3 Trailers (HTTP/2+에서 유용)
응답 body 끝에 추가되는 헤더. gRPC가 status를 trailer로 보냄.
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Trailer: Grpc-Status
...chunks...
Grpc-Status: 0
16.4 Server Push (HTTP/2) — 이미 deprecated
서버가 요청 없이 리소스 push. 실전 효율이 103 Early Hints보다 떨어져 Chrome이 2022년 제거. HTTP/3는 아예 스펙에서 뺌.
17. 실전 체크리스트
API 설계
- RESTful 메서드 의미 정확히 사용
- 상태 코드 적절한 범위 선택 (2xx/4xx/5xx)
- POST는 생성 + 201 Created + Location, 업데이트는 PUT/PATCH
- idempotency-key 헤더 지원 (결제 등 중요)
- 에러 응답
application/problem+json고려 - 페이지네이션은 Link 헤더 또는 cursor
- Rate limit: 429 + X-RateLimit-* 헤더
캐싱
- 정적 자산 hash URL +
max-age=31536000, immutable - HTML은
no-cache(revalidate) - API는
private, max-age=short -
stale-while-revalidate적극 활용 -
Vary헤더 정확히 (너무 많으면 캐시 히트 폭락)
쿠키 & 인증
- HttpOnly + Secure + SameSite=Lax(또는 Strict) 기본
-
__Host-/__Secure-prefix - 크기 주의 (JWT 너무 크면 헤더 오버헤드)
- Logout = 서버 세션 무효화 + 쿠키 삭제 지시
보안 헤더
- CSP
script-srcnonce + strict-dynamic (점진 도입) - HSTS
max-age=31536000; includeSubDomains -
X-Content-Type-Options: nosniff -
Referrer-Policy: strict-origin-when-cross-origin(기본) -
frame-ancestors 'none'또는'self' - Permissions-Policy로 불필요한 브라우저 API 차단
CORS
- 구체적 origin 허용 (credentials 있으면
*불가) -
Access-Control-Max-Age설정 (preflight 캐시) - 필요한 헤더만 expose
- 서버 간 요청은 CORS 아닌 내부 인증 (mTLS 등)
18. 디버깅 도구
18.1 curl
$ curl -v https://api.example.com/users
$ curl -X POST -H 'Content-Type: application/json' -d '{"a":1}' ...
$ curl -I ... # HEAD
-v로 모든 헤더 표시. --http3, --http2 옵션도 지원.
18.2 httpie
사람이 읽기 쉬운 CLI:
$ http GET api.example.com/users token==abc
18.3 Browser DevTools — Network tab
- Status: 200/304/3xx/4xx/5xx
- Response Headers / Request Headers
- Timing: DNS, TLS, waiting, downloading
- Priority: HTTP/2 priority hint
- Protocol: h2, h3, http/1.1 구분
18.4 Security headers 스캐너
- securityheaders.com: 사이트 URL 입력 시 헤더 검증 + A~F 등급
- Mozilla Observatory: 더 엄격
18.5 CORS tester
Chrome DevTools Console:
fetch('https://api.example.com/data', { credentials: 'include' })
.then(r => console.log(r.status))
.catch(console.error)
에러 메시지에 어느 헤더가 빠졌는지 정확히 나옴.
마무리 — HTTP는 계속 진화한다
2025년의 HTTP는 1996년의 HTTP/1.0과 세맨틱은 비슷하지만 운영 복잡도는 수십 배다. 메서드와 상태 코드는 그대로지만, 보안 헤더만 해도 CSP/HSTS/COOP/COEP/CORP/Permissions-Policy/Referrer-Policy가 각자 다른 문제를 해결한다. 쿠키는 SameSite로 재정의됐고, 서드파티 쿠키는 사라지고 있다. CORS는 여전히 오해 1위고, CSP는 적용 실패율 1위다.
교훈 5가지:
- 메서드와 상태 코드는 표준을 따라라 — 창의력 발휘하지 말 것
- 캐시는 설계 초반에 — 나중에 Cache-Control 추가는 항상 후회
- 보안 헤더는 deny-by-default —
default-src 'self'부터 시작해 허용을 추가 - 쿠키 속성 완전체 — HttpOnly + Secure + SameSite +
__Host-prefix - HTTP 에러는 정보를 담아라 —
application/problem+json+ 고유 에러 코드
HTTP는 웹의 공용어다. 이 공용어를 정확히 쓸 줄 아는 개발자가 만든 API는 다른 팀이 6개월 뒤에도 이해할 수 있고, 그 위에 새 피처를 쌓기 쉽고, 보안 감사에서 수월하다. 반대로 "대충 POST 쓰고 200 리턴" 스타일의 API는 기술 부채가 헤더마다 쌓인다.
다음 글에서는 브라우저 보안 모델 — Origin, Site, Process isolation, Sandboxing, Spectre/Meltdown 방어를 깊이 다룬다. HTTP 헤더가 서버와 브라우저 사이의 계약이라면, 브라우저 내부에서 그 계약을 어떻게 실행하는지가 그 다음 층이다. CSP가 말하는 "script-src"가 실제로 어떤 프로세스에서 어떻게 격리되는지까지 파고든다.
HTTP 1.0에서 3.0까지 30년. 그동안 메서드는 PATCH 하나가 추가됐고, 상태 코드는 몇 개 늘었다. 하지만 그 뒤의 보안 헤더 생태계는 매년 새 표준이 나온다. 웹은 계속 복잡해지고, 그만큼 HTTP의 세맨틱을 아는 개발자의 가치도 계속 커진다.
현재 단락 (1/325)
GET /api/users/42 HTTP/1.1