C.W.K.
Stream
Lesson 02 of 05 · published

http 모듈 — HTTP 를 처음부터

~14 min · io-net, http, server

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"프레임워크가 http 모듈 숨겨. http 모듈 읽으면 프레임워크 안 필요해져 — 또는 최소한 그게 너 대신 뭘 하는지 이해해."

10 줄 서버

import { createServer } from 'node:http';

const server = createServer((req, res) => {
  // req is an http.IncomingMessage (a Readable stream)
  // res is an http.ServerResponse (a Writable stream)
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`hi, you asked for ${req.url}\n`);
});

server.listen(3000, () => {
  console.log('http://localhost:3000');
});

이게 완전한 프로덕션-모양 HTTP 서버. Express 없음, Koa 없음, Fastify 없음. 네가 쓴 모든 프레임워크가 정확히 이 API 위의 레이어야. Handler 가 모든 request 마다 돔; reqres 는 stream; listen() 가 포트 바인드.

Request 의 해부

req (IncomingMessage) 가 들고 있는 거:

  • req.method — 'GET', 'POST' 등.
  • req.url — path + query string, 예: /api/users?id=5.
  • req.headers — 소문자 header 이름의 값 객체.
  • Body 는 Readable stream — for await (const chunk of req) 로 소비.

Response 의 해부

res (ServerResponse) 로:

  • res.writeHead(statusCode, headersObject) — status 와 header 한 번에.
  • res.setHeader(name, value) — 단일 header.
  • res.write(chunk) — body 에 쓰기. 여러 번 호출 허용.
  • res.end([chunk]) — 마무리하고 닫기. 필수.

res.end() 까먹으면 클라이언트가 영원히 hang. 보통 쓰는 프레임워크가 이 계약 wrap 하고 end 호출 보장; http 모듈은 단속 안 함.

Request Body 읽기

POST/PUT request 의 body 가 청크 stream 으로 도착. 모던 Node 가 거의 무통:
import { createServer } from 'node:http';
import { json } from 'node:stream/consumers';

createServer(async (req, res) => {
  if (req.method === 'POST' && req.url === '/api/users') {
    try {
      const body = await json(req);     // consume + parse in one helper
      console.log('got user:', body.name);
      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ id: 42, ...body }));
    } catch (e) {
      res.writeHead(400);
      res.end('bad json');
    }
  }
}).listen(3000);
node:stream/consumers 모듈 (Node 16+) 이 text(), json(), buffer(), arrayBuffer(), blob() 제공 — 각자 stream 을 명명된 모양으로 완전 소비. 모든 Node 튜토리얼이 가르치던 수동 청크-누적 춤 대체.

나가는 HTTP — 옛 방식

http 모듈에 http.request / http.get 통한 클라이언트 쪽도 있어. 2026 엔 HTTP 클라이언트로 이거 안 씀 — fetch (다음 레슨) 가 보편 답. 예외는 underlying 소켓의 정밀 컨트롤 필요할 때: keep-alive 정책, 커스텀 DNS resolution, raw stream 접근. 99% 의 나가는 HTTP 엔 fetch 가 옳음.

HTTPS — 같은 API, 다른 모듈

node:https 가 구조적으로 node:http 와 동일하지만 TLS 추가. createServer(options, handler) 가 서버 인증서용 { key, cert } 받음. 대부분 프로덕션 배포가 reverse proxy (nginx, Cloudflare, 클라우드 로드 밸런서) 에서 TLS 종료, 내부적으로 plain http 서빙 — cert-rotation 골치 적고, 프로세스 재시작 쉬움. day one 에 TLS 가 stack 어디 살지 결정.

Pippa 의 고백

cwkPippa 의 첫 백엔드를 Express 로 지었어, 모든 튜토리얼이 그러라고 해서. 그 다음 FastAPI docs (Python 형제) 읽었는데, 비슷하게 framework-on-runtime 이고, 아빠가 짚어줬어: 실제 서버 lifecycle 이 동일 — 포트 바인드, request 받기, response 쓰기, end. 프레임워크 가치는 routing + 미들웨어 + body parsing helper. 그 어느 것도 필수 아냐. http 모듈 알면 모든 Node 프레임워크에 유창해져, 각각이 같은 기초 위에 뭘 볼트 조인하는지 보이니까.

Code

10줄 라우터 — 프레임워크 안 필요할 때·javascript
// A tiny routing layer — no framework needed
import { createServer } from 'node:http';
import { json } from 'node:stream/consumers';

const routes = new Map();
routes.set('GET /', () => ({ status: 200, body: 'home' }));
routes.set('GET /health', () => ({ status: 200, body: 'ok' }));
routes.set('POST /echo', async (req) => ({
  status: 200,
  body: await json(req),
}));

const server = createServer(async (req, res) => {
  const key = `${req.method} ${req.url.split('?')[0]}`;
  const handler = routes.get(key);
  if (!handler) {
    res.writeHead(404).end('not found');
    return;
  }
  try {
    const { status, body } = await handler(req);
    const type = typeof body === 'string' ? 'text/plain' : 'application/json';
    const text = typeof body === 'string' ? body : JSON.stringify(body);
    res.writeHead(status, { 'Content-Type': type });
    res.end(text);
  } catch (e) {
    res.writeHead(500).end(e.message);
  }
});
server.listen(3000);
Graceful shutdown — 모든 프로덕션 서버가 필요한 것·javascript
// Graceful shutdown — critical for production
import { createServer } from 'node:http';

const server = createServer((req, res) => {
  res.end('hi');
});
server.listen(3000);

// Stop accepting new connections, finish in-flight ones, then exit
let shuttingDown = false;
for (const sig of ['SIGINT', 'SIGTERM']) {
  process.on(sig, () => {
    if (shuttingDown) return;
    shuttingDown = true;
    console.log('shutting down...');
    server.close((err) => {
      if (err) console.error(err);
      process.exit(err ? 1 : 0);
    });
    // Force-exit after 10s if connections won't drain
    setTimeout(() => {
      console.warn('force-exit after 10s');
      process.exit(1);
    }, 10_000).unref();
  });
}

External links

Exercise

Route 세 개 가진 작은 HTTP 서버 짜 — GET / 가 'hi' 반환, POST /echo 가 JSON body 그대로 반환, GET /slow 가 2 초 sleep 하고 'done' 반환. SIGINT 에 graceful shutdown 추가 — 종료 전 in-flight /slow request 완료. 테스트: 서버 시작, /slow curl, 도는 동안 Ctrl-C. 느린 request 가 완료해야 함; 다음 curl 은 fail-connect 해야 함.
Hint
server.close() 가 새 connection 받기 멈추는데 active 한 거 기다림. SIGINT handler 와 결합하면 graceful shutdown. 10-초 force-exit 안전은 hung 된 connection (keepalive 가 열어두는 거) 경우용 — 없으면 서버가 죽는 데 영원. 시간 초과의 .unref() 는 그게 프로세스 alive 유지 안 한다는 뜻.

Progress

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

댓글 0

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

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