HTTP Request: 메서드·헤더·바디로 구성되는 클라이언트의 요청 메시지
HTTP 요청(HTTP Request)은 클라이언트가 서버에 자원 조작을 요청하기 위해 보내는 메시지로, 시작 줄(request line), 헤더(header), 바디(body)로 구성된다. 메서드의 의미론(안전성, 멱등성)과 헤더, 바디 인코딩 방식을 이해하면 API 설계와 디버깅이 쉬워진다. 이 글에서는 요청 메시지의 구조, 전송 과정, TypeScri
By 송민성
1. 개념
HTTP 요청(HTTP Request)은 클라이언트가 서버에 특정 자원(resource)에 대한 동작을 요구하기 위해 보내는 텍스트 기반(HTTP/1.1 기준) 메시지다. 요청은 다음 세 부분으로 구성된다.
- 시작 줄(request line):
메서드 요청대상 HTTP버전 - 헤더(headers):
이름: 값형태의 메타데이터 목록 - 바디(body): 선택적인 페이로드(payload)
HTTP/1.1 요청 메시지의 실제 모양은 다음과 같다.
POST /api/users?notify=true HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 27
Authorization: Bearer <token>
{"name":"kim","age":30}헤더와 바디 사이에는 빈 줄(CRLF)이 하나 들어간다. HTTP/2와 HTTP/3는 같은 의미론(semantics)을 유지하되 바이너리 프레이밍(framing)과 헤더 압축을 사용하므로 위와 같은 텍스트 형태로 전송되지는 않는다.
2. 왜 사용하는가
HTTP는 요청-응답(request-response) 모델의 무상태(stateless) 프로토콜이다. 요청 하나에 처리에 필요한 정보가 모두 담겨 있으므로 서버는 이전 요청을 기억하지 않아도 된다. 이 특성 덕분에 다음이 가능하다.
- 서버를 수평 확장(horizontal scaling)하고 로드 밸런서(load balancer)로 분산하기 쉽다.
- 프록시(proxy)와 CDN이 메시지만 보고 캐싱(caching)·라우팅을 결정할 수 있다.
- 메서드와 상태 코드라는 공통 어휘로 서로 다른 시스템이 상호 운용할 수 있다.
3. 동작 원리
3.1 요청이 서버에 도달하기까지
- URL을 파싱해 스킴(scheme), 호스트(host), 포트(port), 경로(path), 쿼리(query)를 분리한다.
- DNS로 호스트 이름을 IP 주소로 변환한다.
- TCP 연결을 맺는다(HTTP/3는 QUIC 위에서 UDP를 사용한다). HTTPS라면 TLS 핸드셰이크(handshake)가 추가된다.
- 요청 메시지를 전송하고 응답을 기다린다.
- 연결은 keep-alive(HTTP/1.1 기본)나 멀티플렉싱(HTTP/2)으로 재사용될 수 있다.
3.2 메서드의 의미론
| 메서드 | 안전(safe) | 멱등(idempotent) | 일반적 용도 |
|---|---|---|---|
| GET | O | O | 조회 |
| HEAD | O | O | 헤더만 조회 |
| OPTIONS | O | O | 지원 메서드·CORS preflight |
| PUT | X | O | 자원 전체 교체 |
| DELETE | X | O | 삭제 |
| POST | X | X | 생성, 처리 요청 |
| PATCH | X | 보장되지 않음 | 부분 수정 |
멱등성(idempotency)은 같은 요청을 여러 번 보내도 서버 상태가 한 번 보낸 것과 같다는 성질이다. 네트워크 오류 시 재시도(retry)해도 되는지 판단하는 기준이 된다.
3.3 주요 요청 헤더
Host: 대상 호스트. HTTP/1.1에서 필수다.Content-Type: 바디의 미디어 타입(예:application/json).Content-Length또는Transfer-Encoding: chunked: 바디의 경계를 알려준다.Accept: 클라이언트가 받을 수 있는 응답 타입.Authorization: 인증 정보.Cookie: 서버가 이전에 설정한 쿠키.If-None-Match/If-Modified-Since: 조건부 요청(conditional request).
3.4 바디 인코딩
application/json: JSON 문자열.application/x-www-form-urlencoded:key=value&key2=value2형태, 특수문자는 퍼센트 인코딩.multipart/form-data: 파일 업로드 등 여러 파트를 경계(boundary) 문자열로 구분.
4. 코드 예제
4.1 Node.js 내장 net으로 직접 요청 보내기
HTTP/1.1이 텍스트 메시지임을 확인하는 예제다. 평문 HTTP를 받는 서버가 필요하므로, 같은 파일에서 서버를 띄우고 요청한다. Node.js 18 이상, ts-node 또는 tsx로 실행할 수 있다(@types/node 필요).
import net from "node:net";
import http from "node:http";
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (c: Buffer) => chunks.push(c));
req.on("end", () => {
const body = Buffer.concat(chunks).toString("utf8");
console.log("[server] method :", req.method);
console.log("[server] url :", req.url);
console.log("[server] ctype :", req.headers["content-type"]);
console.log("[server] body :", body);
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
});
});
server.listen(0, "127.0.0.1", () => {
const { port } = server.address() as net.AddressInfo;
const payload = JSON.stringify({ name: "kim", age: 30 });
const raw =
`POST /api/users?notify=true HTTP/1.1\r\n` +
`Host: 127.0.0.1:${port}\r\n` +
`Content-Type: application/json\r\n` +
`Content-Length: ${Buffer.byteLength(payload)}\r\n` +
`Connection: close\r\n` +
`\r\n` +
payload;
const socket = net.connect(port, "127.0.0.1", () => socket.write(raw));
let response = "";
socket.on("data", (d) => (response += d.toString("utf8")));
socket.on("end", () => {
console.log("[client] raw response:\n" + response);
server.close();
});
});4.2 fetch로 요청 보내기 (타임아웃과 오류 처리 포함)
Node.js 18 이상 또는 브라우저에서 동작한다.
interface CreateUserBody {
name: string;
age: number;
}
async function createUser(baseUrl: string, body: CreateUserBody) {
const url = new URL("/api/users", baseUrl);
url.searchParams.set("notify", "true");
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(5000), // 5초 후 중단
});
// fetch는 4xx/5xx에서도 reject하지 않는다. 직접 확인해야 한다.
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
return res.json();
}
createUser("https://httpbin.org", { name: "kim", age: 30 })
.then((data) => console.log(data))
.catch((err) => console.error("요청 실패:", err));4.3 curl로 확인하기
curl -v -X POST "https://httpbin.org/post?notify=true" \
-H "Content-Type: application/json" \
-d '{"name":"kim","age":30}'-v 옵션을 주면 >로 시작하는 줄이 보낸 요청 메시지, <로 시작하는 줄이 받은 응답이다.
5. 시간 복잡도 또는 성능 특성
요청의 비용은 알고리즘 복잡도보다 네트워크 왕복 시간(RTT, Round-Trip Time) 단위로 따진다.
- 새 연결 비용: TCP 핸드셰이크는 1 RTT, TLS 1.3 핸드셰이크는 추가로 1 RTT가 든다(TLS 1.2는 보통 2 RTT). 연결을 재사용하면 이 비용이 사라진다.
- HTTP/1.1: 한 연결에서 응답을 순서대로 받으므로 선두 차단(HOL blocking, Head-of-Line blocking)이 생긴다. 브라우저는 호스트당 여러 연결을 열어 완화한다.
- HTTP/2: 한 연결에서 여러 스트림을 멀티플렉싱(multiplexing)하고 HPACK으로 헤더를 압축한다. 다만 TCP 수준의 선두 차단은 남는다.
- HTTP/3: QUIC 기반이라 스트림 간 패킷 손실 영향이 독립적이다.
- 헤더 크기: 쿠키와 토큰이 큰 경우 매 요청마다 전송되므로 대역폭과 지연에 영향을 준다.
6. 실무 사용 사례
- REST API 설계: 자원은 URL로, 동작은 메서드로 표현한다. 조회는 GET, 생성은 POST, 교체는 PUT, 부분 수정은 PATCH.
- 조건부 요청 캐싱:
If-None-Match에 ETag를 담아 보내고 서버가304 Not Modified로 응답하면 바디 전송을 줄인다. - CORS preflight: 브라우저가 교차 출처(cross-origin)에서 비단순 요청을 보내기 전에
OPTIONS요청을 먼저 보낸다. - 멱등성 키(Idempotency-Key): 결제처럼 POST를 안전하게 재시도해야 할 때 클라이언트가 고유 키를 헤더로 보내 서버가 중복 처리를 막는다. 표준 RFC가 확정된 헤더는 아니며 서비스별 관례로 쓰이는 경우가 많다.
- 디버깅: 브라우저 개발자 도구의 Network 탭,
curl -v, 프록시 도구로 실제 전송된 메시지를 확인한다.
7. 주의할 점
- GET에 민감 정보를 넣지 않는다. 쿼리 문자열은 서버 로그, 브라우저 기록, 프록시 로그에 남기 쉽다.
- GET 바디에 의존하지 않는다. 명세상 GET 바디에는 정의된 의미가 없고, 일부 구현이 무시하거나 거부한다.
- `fetch`는 HTTP 오류 상태에서 reject하지 않는다. 네트워크 실패일 때만 reject되므로
res.ok를 반드시 확인한다. - `Content-Type`과 실제 바디가 일치해야 한다. JSON을 보내면서 헤더를 빠뜨리면 서버 파서가 바디를 읽지 못할 수 있다.
- 타임아웃을 설정한다. 기본값이 없거나 매우 길면 장애 시 요청이 쌓인다.
- 자동 재시도는 멱등한 메서드에 한정한다. POST를 무작정 재시도하면 중복 생성이 일어날 수 있다.
- URL 인코딩: 쿼리 값에
&,=, 공백, 한글이 들어가면URLSearchParams등으로 인코딩한다. 직접 문자열을 이어 붙이지 않는다. - 요청 크기 제한: 서버와 프록시는 헤더·바디 크기에 상한을 두며, 초과하면
413,431등으로 거부한다.
8. 핵심 정리
- HTTP 요청은 시작 줄, 헤더, 빈 줄, 바디로 이루어진 메시지이며, HTTP/2·3에서도 의미론은 같고 전송 형식만 다르다.
- HTTP는 무상태 프로토콜이라 요청 하나에 처리에 필요한 정보가 담겨야 한다.
- 메서드는 안전성과 멱등성으로 구분하며, 재시도 가능 여부 판단의 기준이 된다.
Content-Type,Content-Length같은 헤더가 바디 해석 방법을 결정한다.- 성능은 RTT와 연결 재사용 여부에 크게 좌우되며, HTTP/2·3는 멀티플렉싱으로 이를 개선한다.
- 실무에서는 상태 코드 확인, 타임아웃, 멱등한 재시도, 올바른 인코딩을 기본으로 지켜야 한다.