본문 바로가기
Node.js

http 모듈로 서버 만들기

by Programmer.Junny 2025. 1. 11.

기본적으로 Node.js 를 써서 통신을 진행하기위해 좀 더 편리하게 사용할 수 있는 프레임워크인 Express.js를 사용하게 된다.

그러나 Express.js 도 내부적으로는 Node.js의 http 모듈을 구성해서 구현이 되어있다. 또한 가장 기본적인 http 통신에 대한 감을 익히기 위해선 http 모듈이 어떻게 구성되고 통신되는지 한 번정도는 테스트해볼 필요가 있다.

http

http 모듈은 웹 서버(HTTP 서버)를 구현할 수 있게 해주는 핵심 모듈이다. 기본적으로 HTTP 요청과 응답을 처리하는 기능을 제공하며, 이를 통해 간단한 웹 서버부터, 다른 라이브러리/프레임워크(Express 등) 위에서 동작하는 큰 프로젝트까지 다양하게 활용할 수 있다.

const http = require('http');

//server는 비동기이므로 on 사용가능
const server = http.createServer((req, res) => {
    //res는 스트림
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf8' });
    res.write('<h1>Hello Node!</h1>');
    res.write('<p>Hello Server!</p>');
    res.end('<h1>Hello JY!</h1>');
}).listen(8080);

server.on('listening', () => {
    console.log('8080 포트에서 서버 대기 중입니다.');
});
server.on('error', (error) => {
    console.log(error);
});

1. 서버 생성(createServer)

가장 기본적인 사용 방식은 http 모듈의 createServer() 함수를 이용해 서버 객체를 생성하는 것이다.

동작 과정

  1. createServer(callback): HTTP 서버 객체를 생성하고, 콜백 함수에서 **req(요청)**와 **res(응답)**를 다룬다.
  2. req 객체: 클라이언트에서 온 요청 정보(URL, 헤더, HTTP 메서드 등)가 들어있다.
  3. res 객체: 서버가 클라이언트에게 보낼 응답 정보를 설정하고, .end()로 전송을 마친다.
  4. server.listen(port, callback): 지정한 포트(여기서는 8080)에서 수신 대기.

2. 요청(req) 객체 살펴보기

  • req.method: HTTP 메서드 (예: GET, POST, PUT, DELETE)
  • req.url: 요청 URL 경로 (예: /, /users, /api/v1/books)
  • req.headers: 요청 헤더(key-value 객체)
  • req.on('data', chunk => {...}): 요청 본문(body)이 들어올 때 이벤트로 데이터 수신(특히 POST, PUT 등에 유용)
  • req.on('end', () => {...}): 요청 본문 수신이 끝났을 때 실행
http.createServer((req, res) => {
  console.log('Method:', req.method);
  console.log('URL:', req.url);

  let body = '';
  req.on('data', (chunk) => {
    body += chunk; // 요청 본문을 계속 이어붙임
  });
  req.on('end', () => {
    console.log('Request Body:', body);
    res.end('Request received');
  });
});

3. 응답(res) 객체 살펴보기

  • res.statusCode = 200; : 응답 상태 코드 설정 (기본은 200)
  • res.setHeader('Content-Type', 'text/html'); : 응답 헤더 설정
  • res.write() : 응답 본문을 여러 번 나눠서 전송 가능
  • res.end() : 응답 전송 완료(마무리). 인자를 넣으면 바로 본문을 마지막으로 써주고 종료
http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/html');
  
  // 응답 본문 나눠서 보내기
  res.write('<h1>Hello from Node.js!</h1>');
  res.write('<p>This is a paragraph.</p>');
  
  // 응답 종료
  res.end('<p>Goodbye!</p>');
});

4. 다양한 포인트

4.1 라우팅(Routing)

순수 http 모듈로 라우팅(경로별 처리)을 구현하려면 req.url과 req.method를 직접 분기해야 한다.

http.createServer((req, res) => {
  if (req.url === '/' && req.method === 'GET') {
    res.end('Hello from main page');
  } else if (req.url === '/about' && req.method === 'GET') {
    res.end('About Page');
  } else {
    res.statusCode = 404;
    res.end('Not Found');
  }
});

4.2 JSON 응답

API 서버를 만들 때는 JSON 응답을 자주 사용한다.

http.createServer((req, res) => {
  const data = { message: 'Hello', success: true };
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify(data));
});

4.3 POST 데이터 처리

http.createServer((req, res) => {
  if (req.method === 'POST') {
    let body = '';
    req.on('data', (chunk) => body += chunk);
    req.on('end', () => {
      // 예: JSON 파싱
      try {
        const parsed = JSON.parse(body);
        console.log('Parsed data:', parsed);
      } catch (err) {
        console.error('Invalid JSON');
      }
      res.end('Data received');
    });
  } else {
    res.end('Send a POST request');
  }
});

쿠키

쿠키는 웹 브라우저(클라이언트)와 서버 간의 상태 유지 를 위해 사용되는 헤더의 한 종류이다. 일반적으로 HTTP 는 무상태 프로토콜이기 때문에 사용자가 어떤 페이지에서 로그인을 했는지, 장바구니에 어떤 상품을 담았는지 등을 서버가 “기억”하기 위해 쿠키가 자주 활용된다.

HTTP 헤더 - 일반 헤더

 

HTTP 헤더 - 일반 헤더

참고로 표현헤더는 과거 Entity 헤더 부분에 속한다.표현 헤더 - Content-Type협상 (컨텐츠 네고시에이션)클라이언트가 선호하는 표현 요청만약 서버에서는 기본적용은 독일어이고 영어까지만 지원

stack501.tistory.com

1. 쿠키의 기초 개념

  1. 저장 위치
    • 쿠키는 클라이언트(브라우저) 쪽에 문자열 형태로 저장된다.
    • 브라우저는 동일한 도메인으로 요청을 보낼 때, 해당 도메인과 관련된 쿠키를 HTTP 헤더에 담아 자동으로 서버에 전송한다.
  2. 형식
    • 서버 → 클라이언트로 보낼 때: Set-Cookie 헤더 사용
    • 클라이언트 → 서버로 보낼 때: Cookie 헤더 사용
  3. 이름/값 쌍(Key-Value)
    • 쿠키는 단순히 “이름=값” 형태를 가지고 있으며, 추가로 옵션을 붙일 수 있다.

2. 쿠키 옵션

2.1 Path

  • 쿠키가 어떤 경로에서 유효한지 지정한다.
  • 예: Path=/ 이면 도메인 내 모든 페이지에서 쿠키를 사용할 수 있음.

2.2 Domain

  • 쿠키가 어떤 도메인(또는 서브도메인)에 유효한지 지정한다.
  • 보통은 example.com 또는 .example.com 형태로 설정.
  • 설정하지 않으면, 기본적으로 현재 호스트명이 사용된다.

2.3 Expires / Max-Age

  • 쿠키의 유효기간을 지정한다.
    • Expires: 특정 날짜·시간 (GMT 기준)
    • Max-Age: 초 단위로 유효기간 (예: Max-Age=3600 → 1시간 동안 유효)
  • 유효기간이 지나면 브라우저에서 쿠키 삭제.

2.4 HttpOnly

  • JavaScript에서 쿠키 접근이 불가능하게 만든다. (document.cookie로 읽기 방지)
  • 보안상 중요한 쿠키(세션쿠키 등)는 흔히 HttpOnly를 사용해 XSS 공격(관리자가 아닌 이가 웹페이지에 악성 스크립트를 삽입할 수 있는 공격)에 의한 탈취를 막는다.

2.5 Secure

  • HTTPS 연결에서만 쿠키가 전송되도록 제한한다.
  • 보안이 필요한 쿠키(로그인 세션)는 Secure 옵션이 권장된다.

2.6 SameSite

  • CSRF(신뢰할 수 있는 사용자를 사칭해 웹 사이트에 원하지 않는 명령을 보내는 공격) 방지를 위해, 쿠키를 크로스 사이트 요청에서 보내지 않도록 설정 가능하다.
    • SameSite=Lax (기본), Strict, 또는 None
    • None 설정 시 Secure 옵션도 같이 필요함(현대 브라우저 기준).

3. 쿠키의 종류

  1. 세션 쿠키(Session Cookie)
    • Expires/Max-Age가 없고, 브라우저 세션이 끝날 때(브라우저 닫을 때) 자동으로 삭제
    • 일반 로그인 세션 등에 많이 사용
  2. 영구 쿠키(Persistent Cookie)
    • 유효기간이 있어서 브라우저를 껐다 켜도 일정 기간 저장
    • 사용자 설정, 장바구니 정보 등을 오랫동안 기억할 때 사용

4. Node.js에서 쿠키 사용 예시

4.1 기본 예시 (http 모듈)

const http = require('http');

http
  .createServer((req, res) => {
    // 쿠키 읽기
    console.log('Request Cookie:', req.headers.cookie);

    // 쿠키 설정
    // 'sessionId=abc123; Path=/; HttpOnly; Max-Age=3600'
    res.setHeader('Set-Cookie', [
      'sessionId=abc123; Path=/; HttpOnly; Max-Age=3600',
      'theme=dark; Path=/; Max-Age=86400'
    ]);

    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello, World!');
  })
  .listen(3000, () => {
    console.log('Server running at http://localhost:3000/');
  });

 

  • 읽기: req.headers.cookie에서 문자열 형태로 가져옵니다. 직접 파싱해야 할 수도 있습니다.
  • 쓰기: res.setHeader('Set-Cookie', ...)로 쿠키 설정

4.2 Express.js에서 쿠키 사용

Express에서는 쿠키 파서 미들웨어나 session 미들웨어를 자주 사용합니다.

4.2.1 cookie-parser 미들웨어

const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser());  // 쿠키 파싱 미들웨어

app.get('/', (req, res) => {
  // 쿠키 읽기
  console.log('Cookies:', req.cookies);

  // 쿠키 설정
  // res.cookie('sessionId', 'abc123', { httpOnly: true, maxAge: 3600000 });
  res.cookie('sessionId', 'abc123', {
    httpOnly: true,
    maxAge: 3600000 // 1시간
  });
  res.send('Cookie set!');
});

app.listen(3000, () => console.log('Express server started'));

 

  • req.cookies를 통해 파싱된 쿠키 객체를 바로 사용 가능
  • res.cookie(name, value, options)를 통해 간단히 쿠키 설정 가능

4.2.2 express-session 사용

const session = require('express-session');

app.use(
  session({
    secret: 'secretkey',
    resave: false,
    saveUninitialized: true,
    cookie: {
      httpOnly: true,
      secure: false,
      maxAge: 3600000
    }
  })
);

세션 미들웨어는 내부적으로 쿠키(세션ID 보관) + 서버 측 저장소(세션 데이터)를 결합해서 로그인 세션 구현.

5. 쿠키 보안 이슈

  1. XSS 공격
    • JS에서 접근할 수 있는 쿠키(Non-HttpOnly 쿠키)는 스크립트로 탈취될 수 있으므로, 세션 쿠키에는 HttpOnly 옵션을 권장.
  2. 전송 구간 노출
    • Secure 옵션을 써서 HTTPS 연결에서만 쿠키가 오가게 하는 것이 안전.
  3. CSRF 공격
    • SameSite 옵션(Lax 또는 Strict)을 통해, 크로스 사이트 요청에 쿠키가 전송되지 않도록 설정 가능.

6. 쿠키 vs 세션(서버 세션)

  • 쿠키 자체에 유저 정보를 직접 담으면(예: 토큰, 사용자명), 클라이언트에 노출되어 보안 위험이 있을 수 있음.
  • 일반적으로는 **“쿠키에는 세션ID만 저장”**해두고, 실제 세션 데이터는 서버(또는 DB, Redis)에서 관리하는 편이 안전함.
  • 그렇지 않고, JWT 같은 토큰을 쿠키로 저장하여 인증을 처리하기도 함(Stateless 인증).

7. 세션과 쿠키의 관계

  • 쿠키: 클라이언트 측 저장
    • 쿠키는 sessionId라는 간단한 식별자만 저장.
    • 실질적 사용자 정보는 서버 세션 저장소에 있음.
  • 세션: 서버 측 저장
    • 세션은 유저별로 서버에 저장되므로, 쿠키 탈취에 의한 위험을 줄이고, 클라이언트에게 불필요한 민감 정보 노출을 막음.
    • 다만 세션 스토리지가 늘어날수록 서버·DB 자원 소모가 많아짐.

8. JWT vs 세션

최근에는 JWT(JSON Web Token) 방식도 사용된다. JWT는 서버에 세션을 저장하지 않고 클라이언트가 토큰을 저장하는 Stateless 인증 방법이다.

  • 전통 세션: 서버가 세션 저장(상태ful), 쿠키에는 세션ID
  • JWT: 클라이언트가 서명된 토큰 자체 보관(상태less), 서버는 토큰 검증만.

http2

HTTP/2 HTTP/1.1의 단점을 개선하여, 더 빠르고 효율적인 웹 통신을 가능하게 한 프로토콜이다. Google의 SPDY 프로토콜이 기반이 되었고, 병목현상을 줄이고 네트워크 리소스를 효율적으로 활용하는 다양한 기능을 제공하고 있다.

1. HTTP/2의 주요 특징

1.1 멀티플렉싱(Multiplexing)

  • 하나의 TCP 연결 위에서 여러 요청과 응답을 동시에 주고받을 수 있다.
  • HTTP/1.1에서는 동일 연결에서 직렬화된 방식(파이프라이닝 등)이어서 리소스를 동시에 로딩하려면 여러 연결을 열어야 했다.
  • HTTP/2는 단일 연결에서 병렬 스트림으로 요청과 응답을 송수신하므로, 브라우저가 여러 리소스를 동시에 받아올 때 속도가 빨라진다.

1.2 헤더 압축(HPACK)

  • HTTP/1.1의 헤더는 중복되는 필드들이 많아, 리소스 요청 시에도 매번 헤더를 전송해야 하므로 오버헤드가 컸다
  • HTTP/2는 HPACK을 통해 헤더 정보를 압축하고, 이전 요청과 같은 헤더는 재전송하지 않아 네트워크 낭비를 줄인다.

1.3 서버 푸시(Server Push)

  • 서버가 클라이언트 요청 없이도 추가 리소스(CSS, JS, 이미지 등)를 미리 보내줄 수 있다.
  • 브라우저가 HTML을 요청하면, 서버는 HTML과 함께 해당 페이지에 필요한 JS/CSS를 한꺼번에 전송해, **RTT(Round Trip Time)**를 절약할 수 있다.

1.4 바이너리 프로토콜

  • HTTP/2는 바이너리 형식으로 메시지를 주고받는다.
  • 텍스트 기반인 HTTP/1.x보다 처리 효율이 좋고, 오류율도 낮다.

2. Node.js에서 HTTP/2 사용하기

Node.js는 http2 모듈을 통해 HTTP/2 서버/클라이언트를 구현할 수 있다. (Node.js 8+ 버전에서 도입)

2.1 HTTPS 기반 HTTP/2 서버 (TLS)

가장 흔히 사용하는 방식은 TLS(HTTPS) 기반의 HTTP/2 서버이다.

const http2 = require('http2');
const fs = require('fs');

// 인증서 & 키
const serverOptions = {
  key: fs.readFileSync('server.key'),     // 개인키
  cert: fs.readFileSync('server.crt'),    // 인증서
  allowHTTP1: true, // HTTP/1.1을 지원할지 여부 (downgrade 용도)
};

const server = http2.createSecureServer(serverOptions);

server.on('stream', (stream, headers) => {
  // 'stream' 이벤트: 클라이언트 요청이 들어올 때마다 실행
  // headers: 요청 헤더 (':method', ':path' 등)
  console.log('Request headers:', headers);

  // 응답 설정
  stream.respond({
    ':status': 200,
    'content-type': 'text/plain',
  });

  // 응답 데이터 전송
  stream.end('Hello from HTTP/2!');
});

server.listen(8443, () => {
  console.log('HTTP/2 server is running at https://localhost:8443');
});

설명

  1. createSecureServer(serverOptions): TLS 인증서와 키를 사용해 HTTPS 기반의 HTTP/2 서버 생성.
  2. server.on('stream', callback): HTTP/1.x의 request 이벤트와 유사하며, HTTP/2 스트림이 생성될 때마다 호출.
  3. stream.respond(...): 응답 헤더 설정.
    • HTTP/2는 :status, :method, :path 등의 콜론(:)으로 시작하는 Pseudo-Header를 사용.
  4. stream.end(...): 응답 본문 전송 후 스트림 종료.

2.2 HTTP/2 클라이언트

Node.js에서는 HTTP/2 클라이언트를 테스트용으로 만들 수 있다.

const http2 = require('http2');

const client = http2.connect('https://localhost:8443', {
  rejectUnauthorized: false, // 테스트용. 실무에서는 인증서 신뢰 설정 필요
});

const req = client.request({ ':path': '/' });

req.on('response', (headers, flags) => {
  console.log('Response headers:', headers);
});

req.setEncoding('utf8');
req.on('data', (chunk) => {
  console.log('Body chunk:', chunk);
});

req.on('end', () => {
  console.log('No more data.');
  client.close();
});

req.end();

 

  • http2.connect()를 통해 HTTP/2 세션을 맺음.
  • client.request({ ':path': '/' })로 스트림을 생성하고, 서버에 요청.
  • req.on('data')로 응답 본문 수신, req.on('end')에서 종료.

3. HTTP/2의 장단점 및 주의사항

3.1 장점

  1. 멀티플렉싱으로 많은 리소스를 동시에 로드 가능 → 웹 페이지 로딩 성능 향상.
  2. 헤더 압축으로 네트워크 오버헤드 감소.
  3. 서버 푸시로 RTT 절약 및 빠른 리소스 로딩.
  4. 텍스트 기반 → 바이너리 기반 변환으로 처리 효율 상승.

3.2 단점 / 고려사항

  1. 서버 푸시는 잘못 사용하면 오히려 불필요한 데이터 전송으로 대역폭 낭비가 될 수도 있음.
  2. 오래 걸리는 단일 요청이 멀티플렉싱 연결을 막을 수도 있으므로, TCP 혼잡 제어와 HTTP/2 프로토콜 특성에 대한 이해가 필요.
  3. 여전히 브라우저·네트워크·캐싱 로직 등을 잘 고려해야, 기대만큼의 성능 향상을 얻을 수 있음.

cluster

cluster 모듈은 멀티코어 CPU 환경에서 하나의 Node.js 애플리케이션을 여러 프로세스로 복제(fork)하여, 부하 분산과 성능 향상을 노리는 기능을 제공한다. Node.js는 싱글 스레드 이벤트 루프를 사용하지만, cluster를 활용하면 CPU 코어 수만큼 워커(Worker) 프로세스를 생성해 동시에 요청을 처리할 수 있다.

1. 개념 및 동작 구조

  1. 마스터(Master) 프로세스
    • 애플리케이션을 실행하면, 첫 번째로 생성되는 프로세스가 “마스터” 역할을 한다.
    • 마스터는 여러 개의 “워커”를 생성(fork)하고, 워커들의 생명주기(시작·종료 등)를 관리한다.
  2. 워커(Worker) 프로세스
    • 마스터가 cluster.fork()를 호출하여 생성하는 자식 프로세스
    • 각 워커는 독립적인 Node.js 프로세스이며, 이벤트 루프를 별도로 가진다.
    • 동일한 포트(예: 3000)로 요청을 받을 수 있지만, 실제로는 내부적으로 net 모듈이 소켓을 공유하여, 요청을 라운드 로빈 방식 등으로 분배한다.
  3. 부하 분산(Load Balancing)
    • 여러 워커 프로세스가 하나의 서버처럼 동일 포트로 동시에 요청을 처리할 수 있으므로, CPU 코어를 고르게 활용할 수 있다.

2. 기본 예시 코드

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`마스터 프로세스 아이디: ${process.pid}`);
  // CPU 개수만큼 워커를 생산
  for (let i = 0; i < numCPUs; i += 1) {
    cluster.fork();
  }
  // 워커가 종료되었을 때
  cluster.on('exit', (worker, code, signal) => {
    console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
    console.log('code', code, 'signal', signal);
    cluster.fork();
  });
} else {
  // 워커들이 포트에서 대기
  http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.write('<h1>Hello Node!</h1>');
    res.end('<p>Hello Cluster!</p>');
    setTimeout(() => { // 워커 존재를 확인하기 위해 1초마다 강제 종료
      process.exit(1);
    }, 1000);
  }).listen(8086);

  console.log(`${process.pid}번 워커 실행`);
}

 

3. 주요 이벤트 및 메서드

  1. cluster.fork(env?)
    • 새 워커를 생성(fork)한다.
    • env 매개변수로 환경변수를 전달할 수도 있다.
  2. cluster.isMastercluster.isWorker
    • 현재 프로세스가 마스터인지 워커인지 판별할 수 있는 상수.
  3. cluster.on('exit', callback)
    • 워커 프로세스가 종료될 때 발생한다.
    • 예기치 않게 죽은 워커를 자동 복구하거나 로깅할 때 사용.
  4. worker.process
    • 워커 프로세스 객체에 접근할 수 있다.
  5. worker.id
    • 클러스터 내 워커 식별자(1,2,3...)를 나타낸다.

4. 실무 고려 사항

  1. 상태 공유
    • 각 워커는 별도 프로세스이므로, 메모리를 공유하지 않는다.
    • 세션, 캐시 등 공유 상태가 필요하다면 RedisDB메모리 캐시 서버 등 외부 저장소를 사용해야 한다.
  2. 그레이스풀 셧다운(Graceful shutdown)
    • 워커를 재시작하거나 종료할 때, 갑작스럽게 연결이 끊기지 않도록 우아한 종료를 구현해야 한다.(예: 새 요청은 받지 않고, 기존 요청 처리 마무리 후 종료).
  3. PM2 등의 프로세스 매니저
    • Node.js 내장 cluster 모듈 대신, PM2 같은 프로세스 매니저가 클러스터링과 프로세스 관리를 더 편하게 해준다.
    • PM2는 자동 재시작, 로깅, 로드밸런싱, 모니터링 등 부가 기능을 제공.
  4. 로드 밸런서
    • Node.js 내장 로드밸런싱은 (v0.12 이후) 라운드 로빈 방식으로 요청을 분배하지만, 고수준 트래픽 시 외부 로드밸런서(예: Nginx, HAProxy)와 결합해 확장성을 높이는 경우가 많다.
  5. CPU보다 많은 워커?
    • 보통 CPU 코어 수만큼 워커를 띄우는 것이 적절하다.
    • 워커가 너무 많으면 **문맥교환(Context Switching)**이 늘어나 오히려 성능이 떨어질 수 있다.
  6. 에러 처리
    • 워커에서 처리되지 않은 에러가 발생하여 프로세스가 죽을 수 있다.
    • 마스터에서 cluster.on('exit', ...)로 죽은 워커를 재생성(“self-healing”) 하는 패턴을 자주 사용한다.

5. fork() vs cluster

  • child_process.fork(): 단순히 새 Node.js 프로세스를 실행하고, 주어진 스크립트를 별개로 수행. IPC 채널(메시지)로만 통신.
  • cluster: child_process.fork()를 내부적으로 활용하지만, TCP 서버 소켓을 여러 워커가 공유할 수 있도록 해줌.
  • 즉, cluster는 서버 포트를 하나 두고 여러 프로세스가 동시에 listen할 수 있는 게 차이점.

최근댓글

최근글

skin by © 2024 ttuttak