Node.js의 net 모듈로 웹 서버 만들기

dyeon-dev·2025년 9월 17일
post-thumbnail

목차
1. Node.js는 서버가 아니다. 그럼 어떻게 서버를 만들 수 있는걸까?
2. net 모듈 - 웹 서버 만드는 도구
3. 서버 객체 생성하기
4. HTTP 응답 형식 직접 작성해보기
5. HTTP 요청 구조에서 method, url 추출
6. 결론 및 향후 목표

1. Node.js는 서버가 아니다. 그럼 어떻게 서버를 만들 수 있는걸까?

Node.js 자체는 웹 서버가 아니다.
Node.js는 구글의 V8 엔진으로 자바스크립트 런타임 환경을 제공하는 프로그램일 뿐이다.
웹 브라우저말고도 자바스크립트를 실행할 수 있도록 해준다.
서버는 특정 포트를 열고 클라이언트의 요청을 기다리는 역할을 한다.
Node.js는 이러한 서버 기능을 구현할 수 있는 API를 제공하기 때문에, 직접 코드를 작성해서 서버를 만들 수 있다.

node.js의 웹 서버 프로그래밍에는 2가지 방식이 있다.

  • node.js에서 기본으로 제공해 주는 모듈을 이용
    • net 모듈 사용
    • http 모듈 사용
  • Express 등의 웹 프레임워크 사용

웹 서버 방식의 이해도를 높이기 위해 net 모듈을 사용해서 웹 서버를 만들어보자.

2. net 모듈 - 웹 서버 만드는 도구

Node.js의 net 모듈은 TCP 통신을 위한 기본적인 도구이다.
이 모듈을 사용하여 소켓 통신을 구현하고, 서버와 클라이언트 간의 데이터 스트림을 다룰 수 있다.
net 모듈로 특정 포트에서 연결을 기다리고, 연결이 들어오면 데이터를 주고받는 Socket 객체를 생성한다.
이 Socket 객체를 통해 들어오는 요청을 처리한다.

3. 서버 객체 생성하기

net 모듈을 불러오고 createServer() 메서드를 통해 TCP 서버 객체를 생성한다.
서버 객체를 생성한 후 특정 포트번호로 listen() 메서드를 호출하면 해당 포트로 웹 서버 통신이 시작된다.

import * as net from "node:net";
const PORT = process.env.PORT || 3000;

const server = net.createServer((socket)=> {
	// 클라이언트로부터 데이터를 수신할 때 
	socket.on("data", (data) => {
		console.log(data.toString());
		// 클라이언트에게 데이터 반환
		socket.write(`서버가 보낸 응답: ${data}`); 
	}
})

server.listen(PORT, () => {
	console.log(`TCP server (net) listening at http://localhost:${PORT}/index.html`);
});
  • 브라우저에 http://localhost:3000/ 을 입력했을 때 socket.on() 메서드를 통해 클라이언트로부터 온 데이터를 수신한다.
  • data 정보는 Buffer 형식으로 들어오고, 이를 문자열로 바꿔서 확인해보면 다음과 같은 정보가 담겨있다.
GET / HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: x_auth=eyJhbGciOiJIUzI1NiJ9.NjZjOTZkOTQ3NDllOGQ4ZTUyZWM3ZDQ3.PCwoG67FlL4g66sRfkvwJVgjS-NLIY9q-yzb9qq5tgY

Buffer란?

  • 바이너리 데이터를 다루는 객체이다.
  • Node.js는 데이터를 주고받을 때 Buffer 객체 형태로 처리한다.
  • Buffer는 메모리에 직접 할당된 고정 크기 버퍼로, I/O 작업(파일 읽기, 네트워크 통신 등)의 효율을 높여준다.
  • toString() 메서드를 사용하여 Buffer의 내용을 문자열로 변환할 수 있다.

4. HTTP 응답 형식 직접 작성해보기

브라우저에 http://localhost:3000/ 을 입력했을 때, 위의 코드에서 socket.write()가 작동하지 않고, 값이 없거나 오류가 출력된다.

그 이유는 다음과 같다.
net 서버는 단순히 TCP Socket을 통한 원시 데이터(Raw Data)만 주고받을 수 있다.
브라우저는 HTTP 프로토콜만 사용하기 때문에 net 서버가 브라우저의 HTTP 요청을 처리할 수 없는 것이다.
이를 해결하기 위해 브라우저 규약에 맞게 HTTP 응답 형식을 직접 작성해서 클라이언트가 정상 파싱하도록 해야 한다.

  • 공식문서: Node.js HTTP response.writeHead(), response.write() 섹션 참고.

그럼 HTTP 응답 형식부터 자세히 알아보자! (이게 핵심)
HTTP(Hypertext Transfer Protocol)는 텍스트 기반 프로토콜로, 규칙에 맞춰 개발해서 서로 정보를 교환할 수 있도록 한다.
클라이언트와 서버는 상태 줄, header, 빈 줄(CRLFCRLF), body 구조 순서로 통신한다.

header의 역할(메타데이터)

  • Content-Type: body를 어떤 형식으로 해석할지 지정
  • Content-Length / Transfer-Encoding: 응답 body 길이나 전송 방식(예: chunked)을 알려, 클라이언트가 어디까지를 body로 읽을지 판단하게 함. 둘 중 하나는 필수. 없으면 클라이언트는 응답 경계를 알 수 없음.
  • Connection: keep-alive/close 여부를 지정
  • 그 외 캐시, 쿠키, CORS 등 클라이언트 동작을 결정하는 다양한 정책 전달.

body의 역할(실제 데이터)

  • HTML, JSON, 이미지 등 클라이언트가 최종적으로 사용하는 페이로드
  • 일부 상태코드(예: 204, 304)나 요청 메서드(HEAD)에서는 응답에 body가 포함되지 않는다.
구분204 No Content304 Not Modified
목적요청에 대한 응답으로 보낼 새로운 콘텐츠가 없을 때 사용한다.클라이언트의 캐시된 리소스가 최신 상태임을 알린다.
사용 시점PUTDELETEPOST 등 서버의 리소스를 변경하는 요청이 성공했을 때 주로 사용한다.캐시된 리소스를 확인하기 위한 조건부 GETHEAD 요청에 대한 응답으로 사용한다.
캐시기본적으로 캐시될 수 있다. ETag 헤더가 포함될 수 있다.캐시를 활용하기 위한 상태 코드다.
클라이언트 동작클라이언트는 현재 페이지를 유지하거나, 서버의 성공 응답을 바탕으로 다음 작업을 진행한다.클라이언트는 가지고 있는 캐시된 복사본을 그대로 사용한다.

빈 줄(CRLFCRLF)

  • 헤더들은 \r\n으로 줄바꿈하며, 헤더 종료 후 반드시 빈 줄 \r\n을 넣는다.
  • 그 다음이 body. 이 경계가 없으면 클라이언트는 파싱할 수 없다.

그럼 HTTP 응답 형식을 직접 작성해보자.

const server = net.createServer((socket)=> {
  socket.on("data", (data) => {
    try {
      const body = `<h1>HTTP 응답 바디</h1>`;
      const header = 
      `HTTP/1.1 200 OK\r\n` +
      `Content-Type: text/html; charset=UTF-8\r\n` +
      `Content-Length: ${Buffer.byteLength(body)}\r\n` + 
      `\r\n`;
      const response = header + body;
      socket.write(response);
      socket.end();
    } catch (error) {
        console.error();
    } 
  })

  socket.on("error", (err) => {
    console.error("Socket error:", err.message);
    try { socket.destroy(); } catch {}
  });
})

브라우저에 접속하면 body로 선언한 부분이 표시된다.
Response Header에도 적용한 부분이 적용되어 있다.

5. HTTP 요청 구조에서 method, url 추출

HTTP 요청은 GET / HTTP/1.1과 같이 시작하는 Request Line과 헤더로 구성된다.
이 Request Line에서 URI를 추출하고 각 URI에 맞는 HTML 페이지를 응답하도록 분기처리를 해준다.

// ["GET", "/", "HTTP/1.1"]
    const [method, url] = data.toString().split(" ");

    const serverSideRendering = (url) => {
      switch (url) {
        case "/":
          return fs.readFileSync("./views/index.html");
        case "/login":
          return fs.readFileSync("./views/login.html");
        case "/register":
          return fs.readFileSync("./views/register.html");
        default:
          return `<h1>not found page</h1>`;
      }
    }

위 부분을 body 부분에 넣어주면 서버사이드렌더링이 적용된다!

const body = serverSideRendering(url);


6. 결론 및 향후 목표

이렇게 net 모듈을 사용해서 Node.js 서버의 기초를 직접 만들어보았다.
HTTP 요청과 응답 형식을 알아보고 HTTP/1.1 헤더를 수동으로 작성해보았다.
Node.js 서버의 기초이기 때문에 Express를 활용하기 위한 기본기를 다지는 경험이었다.
또한, 서버가 정적 HTML로 응답해주는 방식으로 MPA 구조와 SSR 방식의 기초를 다졌다.

하지만 지금까지 구현한 소스 코드는 stylesheet 와 파비콘 등을 지원하지 못하고 있다.
이후 더 다양한 컨텐츠 타입을 지원하도록 개선해 볼 것이다. MIME 타입을 적용하고 서버 코드를 확장하는 방식으로 진행할 것이다.

0개의 댓글