Skip to content

✍️ 필사 모드: HTTP 세맨틱 완전 해설 — 메서드, 상태 코드, 쿠키, CORS, CSP, HSTS, SameSite 끝장 가이드 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — HTTP는 "단순한 텍스트 프로토콜"이 아니다

GET /api/users/42 HTTP/1.1
Host: api.example.com

이 네 줄이 HTTP의 모든 것인 것처럼 보이지만, 현대 웹은 수십 개의 헤더와 세맨틱이 얽힌 복잡한 협약 시스템이다. POSTPUT의 차이, 200204의 차이, 302307의 차이, Cache-Control의 10가지 지시어, Content-TypeAccept의 협상 규칙, 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.1GET /foo (HTTP/2 HEADERS 프레임) + GET /foo (HTTP/3 QPACK)은 전송 방식만 다를 뿐 의미는 같다. 이 글은 세맨틱에 집중.


2. 메서드 — 9개지만 실전은 6개

2.1 RFC 9110 표준 메서드

메서드SafeIdempotentCacheable대표 용도
GET리소스 조회
HEADGET의 헤더만
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 헤더에 URL
  • 202 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: 요청 자체가 malformed
  • 401 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 Large
  • 415 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, 사용 전 반드시 revalidate
  • private: 공용 캐시(프록시/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-src
  • object-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 헤더 있어야만 embed
  • Cross-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가 이겼다).

서버가 세션 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-src nonce + 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가지:

  1. 메서드와 상태 코드는 표준을 따라라 — 창의력 발휘하지 말 것
  2. 캐시는 설계 초반에 — 나중에 Cache-Control 추가는 항상 후회
  3. 보안 헤더는 deny-by-defaultdefault-src 'self'부터 시작해 허용을 추가
  4. 쿠키 속성 완전체 — HttpOnly + Secure + SameSite + __Host- prefix
  5. 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

작성 글자: 0원문 글자: 16,537작성 단락: 0/325