본문 바로가기
C.W.K.
Stream
Lesson 02 of 07 · published

요청과 응답 해부하기 — 네 부분으로 읽는 법

~11 min · foundations, anatomy, request, response

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"HTTP 메시지는 요청이든 응답이든 같은 기본 구조를 공유해. 이 구조를 한 번 익히면 낯선 메시지도 차근차근 읽을 수 있어."

한 가지 기본 구조

HTTP/1.1 메시지는 다음 네 부분을 순서대로 살펴보면 돼:

  1. 시작 줄어떤 종류의 메시지인지 알려 주는 첫 줄.
  2. 헤더 — 0개 이상의 이름과 값 쌍.
  3. 빈 줄 — CRLF 하나로 이루어진 빈 줄이며, "헤더가 끝났다"는 경계 표시.
  4. 본문 — 선택적 페이로드. 비어 있을 수도 있고 JSON, 양식 데이터, 이미지, 이벤트 스트림처럼 다양한 형식을 담을 수도 있어.

HTTP/1.1 메시지를 읽을 때는 이 네 부분이 기본 틀이야. HTTP/2와 HTTP/3는 같은 의미를 이진 프레임으로 표현하므로 전송선의 모양은 달라지지만, 애플리케이션이 다루는 요청과 응답의 개념은 이어져. 먼저 이 틀을 익혀 두면 이후 내용은 구체적인 변주로 보일 거야.

시작 줄 — 요청과 응답을 가르는 첫 단서

HTTP/1.1에서 요청과 응답의 구조적 차이가 가장 선명하게 드러나는 곳이 첫 줄이야.

요청 시작 줄은 토큰 세 개로 이루어져: METHOD SP URI SP HTTP-VERSION. 예를 들어 POST /api/chat HTTP/1.1"HTTP/1.1을 사용해 /api/chat에 POST 요청을 보낸다"는 뜻이야.

응답 시작 줄도 토큰 세 개지만 구성은 달라: HTTP-VERSION SP STATUS-CODE SP REASON-PHRASE. 예를 들어 HTTP/1.1 201 Created"HTTP/1.1 응답이며 상태 코드는 201, 이유 문구는 Created"라는 뜻이야. 이유 문구(reason phrase)는 사람이 읽기 위한 설명이므로, 클라이언트의 분기 기준은 숫자 상태 코드여야 해.

헤더 — 메시지를 설명하는 메타데이터

헤더는 메시지의 성격과 처리 방법을 설명해. 필드 이름은 전송선에서 대소문자를 구분하지 않아 (Content-Type, content-type, CONTENT-TYPE은 모두 같은 필드야). 필드 값의 문법은 헤더마다 다르며, 쉼표로 여러 값을 결합할 수 있는지도 각 필드의 정의를 따라야 해.

처음에는 두 범주가 특히 중요해:

  • 표현 메타데이터 — 본문이 무엇인지 설명해. Content-Type, Content-Length, Content-Encoding이 대표적이며, 본문을 담은 요청과 응답 모두에 나타날 수 있어.
  • 처리 제어 정보 — 메시지를 어떻게 다룰지 알려 줘. 요청에서는 Host, Authorization, Accept, User-Agent를, 응답에서는 Server, Set-Cookie, Cache-Control을 자주 만나.

빈 줄 — 단순하지만 꼭 필요한 경계

HTTP/1.1에서는 빈 줄 하나가 헤더 영역의 끝을 표시해. 이 줄을 빠뜨리면 서버는 뒤에 오는 데이터를 본문이 아니라 잘못된 헤더 줄로 해석할 수 있어. 대부분의 HTTP 클라이언트가 자동으로 넣어 주지만, TCP 연결에 요청을 직접 작성할 때는 자주 놓치는 부분이야.

헤더는 본문을 어떻게 읽을지 알려 주고, 본문은 실제 데이터를 담아. 클라이언트가 본문 해석에 실패하면 먼저 응답의 Content-Type을 확인해. 본문의 형식을 알려 주는 계약이 바로 그 헤더에 있어. text/html 본문에 response.json()을 호출해 놓고 API가 잘못된 데이터를 보냈다고 오해하는 일이 아주 흔해.

본문 — 선택적이며, 경계와 형식이 따로 정해져

본문은 선택 사항이야. GET 요청에는 보통 본문이 없고, DELETE 요청도 본문 없이 보내는 경우가 많아. HTTP/1.1에서는 Content-LengthTransfer-Encoding: chunked 같은 장치가 본문의 경계를 알려 주며, 연결 종료가 경계가 되는 경우도 있어. HTTP/2와 HTTP/3에서는 프레임이 그 역할을 맡아. cwkPippa의 SSE 응답처럼 Transfer-Encoding: chunked를 사용하는 HTTP/1.1 응답도 기본 구조는 같고, 본문 데이터가 열린 연결을 통해 조금씩 도착한다는 점만 달라.

전체 구조를 눈에 익히기

이 네 부분을 전송된 순서대로 구분할 수 있으면, 처음 보는 HTTP/1.1 메시지도 침착하게 읽어낼 수 있어.

Code

주석을 단 요청 — 위에서 아래로 읽는 네 부분·http
POST /api/chat HTTP/1.1                       <- start line
Host: localhost:8000                          <- header
Content-Type: application/json                <- header
Content-Length: 47                            <- header
Authorization: Bearer abc.def.ghi             <- header
                                              <- blank line (CRLF)
{"conversation_id":"xyz","message":"Hi"}     <- body
주석을 단 응답 — 같은 네 부분, 다른 시작 줄·http
HTTP/1.1 201 Created                          <- start line
Content-Type: application/json                <- header (body 에 대한)
Content-Length: 89                            <- header (body 에 대한)
Location: /api/chat/messages/m_42             <- header (control)
Date: Sun, 25 May 2026 03:46:50 GMT           <- header (control)
                                              <- blank line (CRLF)
{"id":"m_42","role":"assistant","content":"Hi 아빠."}  <- body
같은 구조를 Python httpx로 확인하기·python
import httpx

resp = httpx.post(
    'http://localhost:8000/api/chat',
    json={'conversation_id': 'xyz', 'message': 'Hi'},
    headers={'Authorization': 'Bearer abc.def.ghi'},
)

# Start line 조각들
print(resp.http_version)      # 'HTTP/1.1'
print(resp.status_code)       # 201
print(resp.reason_phrase)     # 'Created' (여기서 분기 마)

# Header — 대소문자 무관 dict
print(resp.headers['content-type'])  # 'application/json'
print(resp.headers['Content-Length'])  # 이것도 동작

# Body — header 가 어떻게 parse 할지 알려줘
if resp.headers.get('content-type', '').startswith('application/json'):
    print(resp.json())
else:
    print(resp.text)  # raw text 로 fallback
스트리밍 본문 — 같은 구조에서 청크로 전송하기·python
# Streaming body — cwkPippa SSE response 가 chunked transfer 써.
# 네 부분 anatomy 똑같음; body 만 시간 지나면서 조각으로 도착.
with httpx.stream('POST', 'http://localhost:8000/api/chat',
                  json={'message': '이야기 하나 해줘'}) as resp:
    print(resp.headers.get('content-type'))  # 'text/event-stream'
    print(resp.headers.get('transfer-encoding'))  # 'chunked'
    for chunk in resp.iter_text():
        print(chunk, end='', flush=True)
    # Body 는 Content-Length 없음 — server 가 미리 몰라

External links

Exercise

익숙한 API 하나를 골라 요청을 curl -v나 DevTools로 캡처해. 요청과 응답의 네 부분을 찾아 직접 주석을 달아 보자. 그런 다음 JSON 본문은 그대로 두고 Content-Typetext/plain으로 바꾼 요청을 보내 봐. 어떤 상태 코드가 돌아왔는지, 서버가 본문을 어떻게 해석했는지 기록해.
Hint
Content-Type이 본문과 맞지 않을 때의 결과는 서버 구현에 따라 달라. 415 Unsupported Media Type을 반환할 수도 있고, 검증 단계에서 400이나 422를 반환할 수도 있으며, 본문을 무시한 채 처리를 이어 갈 수도 있어. 이 차이를 관찰하면 헤더가 단순한 장식이 아니라 해석 계약이라는 점이 선명해져.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.