빠른 흐름
import { createServer } from "node:http";
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (req.method === "GET" && url.pathname === "/status") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === "POST" && url.pathname === "/echo") {
let body = "";
for await (const chunk of req) body += chunk;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ received: body }));
return;
}
res.writeHead(404);
res.end("Not Found");
});
server.listen(3000, () => console.log("http://localhost:3000"));서버 기본형
먼저 바로 떠올려야 하는 기본형은 아래입니다.
- 경로/메서드 분기:
req.method+url.pathname - JSON 응답:
res.writeHead(...); res.end(JSON.stringify(...)) - 본문 읽기:
for await (const chunk of req) - 분기 종료:
res.end()뒤return - 본문 크기 제한과 파싱 위치 한 곳으로 모으기
createServer에서 직접 책임져야 하는 것
프레임워크를 쓰지 않으면 아래를 전부 직접 결정합니다.
- 어떤 경로와 메서드를 받을지
- 본문을 읽을지 말지
- 상태 코드와 헤더를 무엇으로 보낼지
- 어느 분기에서 응답을 끝낼지
이 카드의 핵심은 "Node HTTP 서버는 라우터가 아니라 req와 res를 직접 다루는 함수"라는 점입니다.
req.url은 바로 비교하지 말고 먼저 파싱한다
쿼리스트링이 붙으면 req.url === "/status" 비교가 쉽게 깨집니다. 경로 기준 분기면 먼저 URL로 파싱하는 편이 안전합니다.
const url = new URL(req.url ?? "/", "http://localhost");
if (req.method === "GET" && url.pathname === "/users") {
// 처리
}본문이 필요할 때만 읽는다
GET 상태 확인처럼 본문이 필요 없는 요청까지 읽을 필요는 없습니다. 본문은 POST, PUT, PATCH처럼 실제 데이터가 들어오는 분기에서만 읽는 편이 흐름이 선명합니다.
let body = "";
for await (const chunk of req) body += chunk;req는 스트림이라 한 번 소비하면 끝납니다. 로깅과 파싱을 둘 다 하고 싶다면 읽는 위치를 한 군데로 모아야 합니다.
for await (const chunk of req) body += chunk;
// 같은 req를 아래에서 다시 읽을 수는 없다본문 크기 제한도 직접 챙겨야 한다
기본 node:http 서버는 JSON body limit 같은 보호막을 자동으로 주지 않습니다.
작은 예제는 그냥 읽어도 되지만, 실전에서는 크기 제한과 파싱 실패 처리를 같이 넣는 편이 맞습니다.
기본 HTTP 서버는 "미들웨어 없는 프레임워크"가 아니다
Express/Fastify 감각으로 들어오면 req.body, 라우터, 에러 핸들러가 있을 것 같지만, node:http는 그걸 전부 직접 짜는 쪽에 가깝습니다.
그래서 카드 핵심도 "무엇을 자동으로 해주지 않는가"를 빨리 아는 데 있습니다.
응답은 헤더와 종료를 한 흐름으로 묶는다
Node 기본 서버에서 가장 흔한 실수는 분기 하나에서 res.end()를 보냈는데 아래 코드가 계속 진행되는 경우입니다. 상태 코드, 헤더, res.end()는 한 묶음으로 보고 각 분기 끝에서 바로 return하는 편이 안전합니다.
if (req.method === "GET" && url.pathname === "/health") {
res.end("ok");
return;
}
res.statusCode = 404;
res.end("Not Found");반대로 return이 빠지면 같은 요청에 응답을 두 번 쓰려 하면서 오류가 날 수 있습니다.
언제 직접 만들까
체크포인트
- 최소 라우팅:
req.method+new URL(req.url, base).pathname - JSON 응답:
Content-Type: application/json+JSON.stringify - 본문 읽기:
for await (const chunk of req) - 크기 제한과 파싱 위치를 한 곳에 모은다
- 분기 종료:
res.end()뒤return req.body같은 추상화는 없으니 직접 책임 범위를 정한다
주의할 점
res.end() 이후에도 함수 흐름은 자동으로 멈추지 않는다. 분기 끝에서 return을 빼면 같은 요청에 헤더를 두 번 쓰거나, 404가 뒤늦게 덮어쓰려 하면서 ERR_HTTP_HEADERS_SENT가 납니다.
참고 링크
1 sources