Node.js 교재 · 12편 · raw http

HTTP 서버 직접 만들기 — http 모듈 기초

Express 가 무엇을 감싸고 있는지, 한 번 직접 만들어보면 안다.

raw http 서버가 요청과 응답을 처리하는 컨셉 아이소메트릭 일러스트

13편부터 Express 로 들어가지만, 그 전에 Node 내장 http 모듈만으로 서버를 만들어 본다. 이유는 둘 — ① Express 가 결국 무엇을 자동화해주는지 이해, ② 작은 마이크로서비스·CLI 헬스체크 등 라이브러리 없이 한 파일로 끝내고 싶을 때 실용적.

20줄이면 HTTP 서버 한 대 완성. 그 뒤로 더 필요한 게 보이면 그게 Express 의 역할.

1. 가장 단순한 서버 — 10줄

// server.js import http from 'node:http'; const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('안녕, Node 서버!'); }); server.listen(3000, () => { console.log('http://localhost:3000'); });

node server.js 실행 후 브라우저에서 localhost:3000 — "안녕, Node 서버!" 가 뜬다. 끝.

핵심은 createServer 의 콜백 (req, res) => {...}. 요청이 올 때마다 호출되고 두 개를 받는다. req(IncomingMessage) 는 들어온 요청, res(ServerResponse) 는 보낼 응답. Express 의 req·res 도 결국 이걸 감싼 것.

2. req — 들어온 요청 읽기

req 객체에서 자주 쓰는 4가지.

const server = http.createServer((req, res) => { console.log(req.method); // 'GET' | 'POST' | ... console.log(req.url); // '/blog/hello?lang=ko' console.log(req.headers); // { host, 'user-agent', cookie, ... } console.log(req.httpVersion); // '1.1' // ... });

주의 — req.url경로+쿼리가 합쳐진 raw 문자열이다. 분리하려면 URL 객체 사용.

const url = new URL(req.url, `http://${req.headers.host}`); console.log(url.pathname); // '/blog/hello' console.log(url.searchParams.get('lang')); // 'ko'
왜 URL 인자에 host 가 필요?req.url 은 보통 절대 경로가 아니라 path 부터 시작 (/foo). URL 생성자는 절대 URL 을 원해서 host 를 두 번째 인자로 베이스 제공. 표준 보일러플레이트 한 줄.

3. res — 응답 보내기

응답은 3단계. 헤더 → 본문 → 끝.

// ① 헤더 res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', }); // ② 본문 (여러 번 write 가능) res.write('{"hello":'); res.write('"world"}'); // ③ 끝 (마지막 인자에 추가 데이터도 가능) res.end(); // 또는 한 번에: res.end('{"hello":"world"}');

일상에선 거의 res.writeHead + res.end(본문) 두 줄로 끝. write 를 여러 번 쓰는 건 Streaming 응답 (10편 참조)에서 의미가 있다.

4. 라우팅 — 직접 분기

Express 없이 라우팅은 if/switch 로 직접.

const server = http.createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); const route = `${req.method} ${url.pathname}`; switch (route) { case 'GET /': res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<h1>홈</h1>'); break; case 'GET /api/health': res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', ts: Date.now() })); break; case 'POST /api/echo': // 본문 파싱 (아래 섹션) break; default: res.writeHead(404); res.end('Not Found'); } });

3~5 라우트면 이렇게도 충분. 동적 세그먼트·미들웨어·에러 처리가 필요해지면 그제야 Express 가 가치 있다.

5. POST 본문 파싱 — chunk 모으기

req 는 사실 Readable Stream. POST 본문이 큰 경우 chunk 단위로 도착한다.

if (route === 'POST /api/echo') { let body = ''; req.on('data', (chunk) => { body += chunk; }); req.on('end', () => { try { const data = JSON.parse(body); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ received: data })); } catch { res.writeHead(400); res.end('Invalid JSON'); } }); return; }

또는 10편의 for await 패턴이 더 깔끔:

async function readBody(req) { const chunks = []; for await (const chunk of req) chunks.push(chunk); return Buffer.concat(chunks).toString('utf-8'); } // 라우터 안에서 const body = await readBody(req); const data = JSON.parse(body);
본문 크기 제한 — raw http 는 본문 크기를 알아서 제한하지 않는다. 악의적으로 1GB POST 를 보내면 메모리 폭발. 실전에선 누적 길이 체크해서 일정 이상이면 즉시 res.writeHead(413); res.end() 로 차단. Express 도 express.json({ limit: '100kb' }) 같은 옵션으로 같은 일을 한다.

여기까지 — Express 가 자동화하는 일이 정확히 보인다. 라우팅 트리, 본문 파싱, 미들웨어 체인, 에러 캐치, 200 응답 자동 — 13편부터 그걸 그대로 다룬다.

요약 — 12편 좌표

여기까지 정리. Node 내장 http 만으로 서버 — createServer((req, res) => ...) + listen(port). req 에서 method·url·headers, 본문은 stream chunk 로 모은다. res.writeHead(status, headers) + res.end(body) 가 응답 끝. 라우팅은 switch 로 직접, 본문 크기 제한도 직접. 작은 마이크로서비스·헬스체크 엔드포인트엔 충분. 다음 편부터 Express 가 이걸 어떻게 단순화하는지 본다.

다음 편 예고 — Express 입문

설치·기본 라우팅·응답 보내기. 13편.

© 2026 주나이테크(주) @JUNAITECH