본문 바로가기
TYLER SONGBlog
← Blog index
CS학습7 min read

HTTP Response: 상태 코드, 헤더, 본문으로 구성된 서버의 응답 메시지

HTTP 응답(HTTP Response)은 클라이언트의 요청(Request)에 대해 서버가 돌려주는 메시지로, 상태 줄(Status Line), 헤더(Header), 본문(Body)으로 구성된다. 상태 코드로 처리 결과를 알리고, 헤더로 본문의 해석 방법과 캐시·쿠키·보안 정책을 전달한다. 이 글에서는 메시지 구조와 바이트 수준의 동작 원리, Node.js

By 송민성

1. 개념

HTTP 응답(HTTP Response)은 클라이언트가 보낸 요청에 대해 서버가 반환하는 메시지다. HTTP는 요청-응답(Request-Response) 모델을 따르며, 요청 하나에 대해 최종 응답이 하나 돌아온다. (100 Continue 같은 1xx 임시 응답이 먼저 올 수 있다.)

HTTP/1.1 기준 응답 메시지는 세 부분으로 구성된다.

text
HTTP/1.1 200 OK                      ← 상태 줄(Status Line)
Content-Type: application/json       ← 헤더(Header)
Content-Length: 17
                                     ← 빈 줄(CRLF): 헤더의 끝
{"message":"ok"}                     ← 본문(Body)
  • 상태 줄: HTTP 버전, 상태 코드(Status Code), 사유 구절(Reason Phrase)
  • 헤더: 이름: 값 형태의 메타데이터. 이름은 대소문자를 구분하지 않는다.
  • 본문: 실제 전송 데이터. HTML, JSON, 이미지 등이며 없을 수도 있다(예: 204, 304).

상태 코드 분류

범위의미예
1xx정보(Informational)101 Switching Protocols
2xx성공(Successful)200 OK, 201 Created, 204 No Content
3xx리다이렉션(Redirection)301, 302, 304 Not Modified
4xx클라이언트 오류400, 401, 403, 404, 429
5xx서버 오류500, 502, 503, 504

2. 왜 사용하는가

HTTP는 무상태(Stateless) 프로토콜이다. 서버는 이전 요청을 기억하지 않으므로, 응답 메시지 자체가 처리 결과와 해석 방법을 모두 담아야 한다.

  • 결과 전달: 상태 코드로 성공·실패·재시도 가능 여부를 기계가 판단할 수 있게 한다.
  • 본문 해석 정보: Content-Type, Content-Encoding, Content-Length로 클라이언트가 바이트를 올바르게 해석하게 한다.
  • 부가 기능 제어: 캐시(Cache-Control, ETag), 상태 유지(Set-Cookie), 보안(Content-Security-Policy), 교차 출처 허용(Access-Control-Allow-Origin) 등을 헤더로 지시한다.

3. 동작 원리

3.1 전송 형태

HTTP/1.1에서 응답은 텍스트 기반이며 줄바꿈은 CRLF(\r\n)이다. 헤더와 본문은 빈 줄로 구분된다. 클라이언트는 본문의 끝을 다음 방식 중 하나로 판단한다.

  1. Content-Length: N: 본문이 정확히 N바이트
  2. Transfer-Encoding: chunked: 청크(Chunk) 단위로 전송하고 크기 0인 청크로 종료
  3. 연결 종료(Connection close): 서버가 연결을 닫는 시점

HTTP/2와 HTTP/3는 텍스트가 아니라 바이너리 프레임(Frame)으로 전송하며, 헤더는 HPACK(HTTP/2), QPACK(HTTP/3)으로 압축한다. 상태 줄이 없고 :status 의사 헤더(Pseudo-header)로 상태 코드를 전달하며, 사유 구절은 존재하지 않는다. 의미(Semantics)는 버전이 달라도 동일하다.

3.2 주요 헤더

헤더역할
Content-Type본문의 미디어 타입과 문자 인코딩
Content-Length본문 바이트 수
Cache-Control캐시 정책 (max-age, no-store 등)
ETag / Last-Modified조건부 요청용 검증자(Validator)
Set-Cookie쿠키 설정
Location리다이렉트 대상, 201 생성 리소스 위치
Retry-After429, 503에서 재시도 시점 안내

3.3 조건부 요청과 304

클라이언트가 If-None-Match: "abc"를 보내고 서버의 현재 ETag가 같으면, 서버는 본문 없이 304 Not Modified를 반환한다. 클라이언트는 캐시된 본문을 재사용하므로 전송량이 줄어든다.

4. 코드 예제

4.1 Node.js로 응답을 직접 구성하기 (TypeScript)

외부 라이브러리 없이 Node.js 내장 http 모듈만 사용한다. 실행하려면 @types/node와 tsx 같은 실행기가 필요하다.

typescript
import { createServer, IncomingMessage, ServerResponse } from "node:http"; import { createHash } from "node:crypto"; function sendJson( req: IncomingMessage, res: ServerResponse, status: number, data: unknown ): void { const body = JSON.stringify(data); const etag = `"${createHash("sha1").update(body).digest("hex")}"`; // 조건부 요청: 변경이 없으면 본문 없이 304 if (req.headers["if-none-match"] === etag) { res.writeHead(304, { ETag: etag }); res.end(); return; } res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Content-Length": Buffer.byteLength(body), // 문자 수가 아닌 바이트 수 "Cache-Control": "no-cache", // 저장은 가능, 사용 전 재검증 필수 ETag: etag, }); res.end(body); } const server = createServer((req, res) => { if (req.url === "/users/1" && req.method === "GET") { sendJson(req, res, 200, { id: 1, name: "김개발" }); } else if (req.url === "/old") { res.writeHead(301, { Location: "/users/1" }); res.end(); } else { sendJson(req, res, 404, { error: "Not Found" }); } }); server.listen(3000, () => console.log("http://localhost:3000"));

4.2 curl로 응답 확인하기 (Shell)

shell
# 헤더만 확인 (-i: 응답 헤더 포함, -s: 진행 표시 숨김) curl -si http://localhost:3000/users/1 # ETag를 이용한 조건부 요청 → 304 확인 ETAG=$(curl -si http://localhost:3000/users/1 | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r') curl -si -H "If-None-Match: $ETAG" http://localhost:3000/users/1 # 리다이렉트를 따라가지 않고 301 확인 curl -si http://localhost:3000/old

4.3 클라이언트에서 응답 읽기 (TypeScript)

typescript
async function getUser(id: number) { const res = await fetch(`http://localhost:3000/users/${id}`); // fetch는 404, 500에도 reject하지 않는다. 직접 확인해야 한다. if (!res.ok) { throw new Error(`HTTP ${res.status} ${res.statusText}`); } console.log(res.headers.get("content-type")); return (await res.json()) as { id: number; name: string }; } getUser(1).then(console.log).catch(console.error);

5. 시간 복잡도 또는 성능 특성

응답은 알고리즘 복잡도보다 전송 비용이 성능을 좌우한다.

  • 본문 크기: 전송 시간은 대략 본문 크기에 비례한다. Content-Encoding: gzip 또는 br(Brotli)로 텍스트 계열 본문을 크게 줄일 수 있다. 이미 압축된 JPEG, PNG 등에는 효과가 작다.
  • 캐시 적중: max-age 안에서는 요청 자체가 발생하지 않고, 304는 헤더만 오가므로 본문 전송 비용이 사라진다.
  • 청크 전송: 전체 길이를 모르는 상태에서도 응답을 시작할 수 있어 첫 바이트까지의 시간(TTFB)을 줄이는 데 유리하다. 스트리밍(SSE, 대용량 파일)에 쓴다.
  • 헤더 오버헤드: HTTP/1.1은 헤더를 매번 평문으로 보내지만, HTTP/2의 HPACK은 반복 헤더를 압축해 비용을 줄인다.
  • HTTP/1.1 Keep-Alive: 연결을 재사용해 TCP·TLS 핸드셰이크 비용을 반복하지 않는다.

6. 실무 사용 사례

  • REST API 상태 코드 설계: 생성은 201 Created + Location, 본문 없는 성공은 204, 검증 실패는 400 또는 422, 인증 없음은 401, 권한 없음은 403, 중복 충돌은 409.
  • 정적 자산 캐싱: 해시가 파일명에 포함된 JS/CSS는 Cache-Control: public, max-age=31536000, immutable로 장기 캐시하고, HTML은 no-cache로 항상 재검증한다.
  • 쿠키 기반 세션: Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax; Path=/로 XSS·CSRF 위험을 낮춘다.
  • CORS: 브라우저가 다른 출처의 응답을 JS에 노출할지 Access-Control-Allow-Origin 등으로 결정한다.
  • 장애 대응: 과부하 시 503 + Retry-After, 속도 제한 시 429 + Retry-After로 클라이언트의 재시도 간격을 제어한다.
  • 파일 다운로드: Content-Disposition: attachment; filename="report.csv"로 저장 동작을 지시한다.

7. 주의할 점

  • 200 OK에 에러 담기 금지: 200에 {"success": false}를 담으면 캐시, 모니터링, 재시도 로직이 오작동한다. 오류는 적절한 4xx/5xx로 반환한다.
  • 401과 403 혼동: 401은 "인증되지 않음", 403은 "인증됐지만 권한 없음"이다.
  • 301과 302: 301은 영구 이동으로 브라우저가 적극적으로 캐시한다. 임시 이동에 301을 쓰면 되돌리기 어렵다. 메서드 유지가 필요하면 307(임시), 308(영구)을 쓴다.
  • Content-Length 오계산: 문자열 길이(str.length)가 아니라 바이트 길이(Buffer.byteLength)를 써야 한다. 한글은 UTF-8에서 문자당 3바이트다. 값이 틀리면 본문이 잘리거나 연결이 멈출 수 있다.
  • 헤더 전송 후 변경 불가: Node.js에서 res.writeHead나 첫 res.write 이후 헤더를 바꾸면 ERR_HTTP_HEADERS_SENT 오류가 발생한다.
  • no-cache와 no-store의 차이: no-cache는 저장은 하되 재검증을 강제하고, no-store는 저장 자체를 금지한다. 민감 정보에는 no-store를 쓴다.
  • fetch의 오류 처리: fetch는 네트워크 실패에만 reject한다. res.ok나 res.status를 직접 검사해야 한다.
  • 보안 헤더: 서버 버전을 노출하는 헤더는 줄이고, Strict-Transport-Security, X-Content-Type-Options: nosniff, Content-Security-Policy 등을 적용한다.

8. 핵심 정리

  • HTTP 응답은 상태 줄 + 헤더 + (선택) 본문으로 구성되며, 헤더와 본문은 빈 줄로 구분된다.
  • 상태 코드는 1xx~5xx로 분류하며, 결과를 정확히 반영해야 캐시·재시도·모니터링이 올바르게 동작한다.
  • 본문의 끝은 Content-Length, 청크 전송(chunked), 연결 종료 중 하나로 결정된다.
  • ETag와 If-None-Match를 조합한 304 응답은 본문 전송을 생략해 대역폭을 절약한다.
  • HTTP/2, HTTP/3는 표현 방식(바이너리 프레임, 헤더 압축)만 다를 뿐 응답의 의미는 같다.
  • 실무에서는 올바른 상태 코드 선택, 캐시 정책, 쿠키·CORS·보안 헤더 설정이 응답 설계의 핵심이다.