상세 컨텐츠

본문 제목

Node JS - Mini Node Server

Web/NodeJS

by Yongari 2023. 1. 4. 19:32

본문

 

Mini Node Server를 구축하기 전에 HTTP 트랜잭션을 분석하고 공부하는 시간을 가지려고 합니다.

 

 

 

서버 생성

모든 Node 웹서버 앱은 웹서버 객체를 만들어야하고 이 때 createServer를 사용합니다.

코드를 보면 http를 require로 가져오고 const server 변수에다가 http모듈의 createServer함수를 담는 것으로 보입니다. 

const http = require('http');

const server = http.createServer((request, response) => {
  // 여기서 작업이 진행됩니다!
});

 

그렇다면 function createServer 코드는 어디서 찾아야할까요?

우선  다음의 순서대로 코드를 확인하는 것이 좋겠습니다.

  1. http 모듈 
  2. createServer 함수 
  3. createServer가 반환하는 server 객체 
  4. server 객체가 EventEmitter인지 확인

 

 

1. Http 모듈

더보기
/ Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

const {
  ArrayPrototypeSlice,
  ArrayPrototypeSort,
  ObjectDefineProperty,
} = primordials;

const httpAgent = require('_http_agent');
const { ClientRequest } = require('_http_client');
const { methods } = require('_http_common');
const { IncomingMessage } = require('_http_incoming');
const {
  validateHeaderName,
  validateHeaderValue,
  OutgoingMessage
} = require('_http_outgoing');
const {
  _connectionListener,
  STATUS_CODES,
  Server,
  ServerResponse
} = require('_http_server');
let maxHeaderSize;

/**
 * Returns a new instance of `http.Server`.
 * @param {{
 *   IncomingMessage?: IncomingMessage;
 *   ServerResponse?: ServerResponse;
 *   insecureHTTPParser?: boolean;
 *   maxHeaderSize?: number;
 *   }} [opts]
 * @param {Function} [requestListener]
 * @returns {Server}
 */
function createServer(opts, requestListener) {
  return new Server(opts, requestListener);
}

/**
 * @typedef {object} HTTPRequestOptions
 * @property {httpAgent.Agent | boolean} [agent]
 * @property {string} [auth]
 * @property {Function} [createConnection]
 * @property {number} [defaultPort]
 * @property {number} [family]
 * @property {object} [headers]
 * @property {number} [hints]
 * @property {string} [host]
 * @property {string} [hostname]
 * @property {boolean} [insecureHTTPParser]
 * @property {string} [localAddress]
 * @property {number} [localPort]
 * @property {Function} [lookup]
 * @property {number} [maxHeaderSize]
 * @property {string} [method]
 * @property {string} [path]
 * @property {number} [port]
 * @property {string} [protocol]
 * @property {boolean} [setHost]
 * @property {string} [socketPath]
 * @property {number} [timeout]
 * @property {AbortSignal} [signal]
 */

/**
 * Makes an HTTP request.
 * @param {string | URL} url
 * @param {HTTPRequestOptions} [options]
 * @param {Function} [cb]
 * @returns {ClientRequest}
 */
function request(url, options, cb) {
  return new ClientRequest(url, options, cb);
}

/**
 * Makes a `GET` HTTP request.
 * @param {string | URL} url
 * @param {HTTPRequestOptions} [options]
 * @param {Function} [cb]
 * @returns {ClientRequest}
 */
function get(url, options, cb) {
  const req = request(url, options, cb);
  req.end();
  return req;
}

module.exports = {
  _connectionListener,
  METHODS: ArrayPrototypeSort(ArrayPrototypeSlice(methods)),
  STATUS_CODES,
  Agent: httpAgent.Agent,
  ClientRequest,
  IncomingMessage,
  OutgoingMessage,
  Server,
  ServerResponse,
  createServer,
  validateHeaderName,
  validateHeaderValue,
  get,
  request
};

ObjectDefineProperty(module.exports, 'maxHeaderSize', {
  configurable: true,
  enumerable: true,
  get() {
    if (maxHeaderSize === undefined) {
      const { getOptionValue } = require('internal/options');
      maxHeaderSize = getOptionValue('--max-http-header-size');
    }

    return maxHeaderSize;
  }
});

ObjectDefineProperty(module.exports, 'globalAgent', {
  configurable: true,
  enumerable: true,
  get() {
    return httpAgent.globalAgent;
  },
  set(value) {
    httpAgent.globalAgent = value;
  }
});

 

 

2. Function createServer

그 중 위 코드에 있는 Function createServer가 보이시죠 ? 해당 코드의 내용입니다.

opts, requestListener를 인자로 받고 받은 인자와 함께 Server 객체를 return(반환)해주고 있습니다. 

function createServer(opts, requestListener) {
  return new Server(opts, requestListener);
}


여기서는 function createServer의 라이브러리 내부 내용을 보면 다음과 같습니다.

살펴보면 Request와 Response를 인자로 받고 서버를 생성해서 리턴해주는 것으로 보입니다. 

    * @since v0.1.13

    function createServer<
        Request extends typeof IncomingMessage = typeof IncomingMessage,
        Response extends typeof ServerResponse = typeof ServerResponse,
    >(requestListener?: RequestListener<Request, Response>): Server<Request, Response>;
    function createServer<
        Request extends typeof IncomingMessage = typeof IncomingMessage,
        Response extends typeof ServerResponse = typeof ServerResponse,
    >(
        options: ServerOptions<Request, Response>,
        requestListener?: RequestListener<Request, Response>,
    ): Server<Request, Response>;

 

 

3. createServer가 반환하는 server 객체

 

그렇다면 여기서 반환해주는 Server객체는 무엇일까요? 

createServer가 반환한 Server 객체는 EventEmitter이고 EventEmitter란 개발자가 실제로 이벤트를 만들고 이벤트를 발생시킬 수 있는 기술로 알고계시면 됩니다. 

EventEmitter 객체는 다음과 같은 메소드를 가지고 있습니다.

 

emitter.addListener(event, listener): 이벤트를 생성하는 메소드입니다. on() 메소드와 같습니다. 
emitter.on(event, listener): 이벤트를 생성하는 메소드입니다. addListener()과 동일합니다. 
emitter.once(event, listener): 이벤트를 한 번만 연결한 후 제거합니다.
emitter.removelistener(event, listener): 
특정 이벤트의  핸들러를 제거합니다. 
이 메소드를 이용해 리스너를 삭제하면 리스너 배열의 인덱스가 갱신되니 주의해야 합니다.
emitter.removeAllListeners([event]): 모든 이벤트 핸들러를 제거합니다.
emitter.setMaxListeners(n): 
n으로 한 이벤트에 최대허용 개수를 지정합니다. 
node.js는 기본값으로 한 이벤트에 10개의 이벤트 핸들러를 작성할 수 있는데,
11개 이상을 사용하고 싶다면 n값을 넘겨주면 됩니다. 
n값으로 0을 넘겨주면 연결 개수 제한이 사라집니다.
emitter.emit(eventName[, ...args]): 이벤트를 발생시킵니다.

 

 

Server 객체

아래 코드를 통해 Server 객체가 EventEmitter가 맞는지 확인할 수 있다.

더보기
class Server<
  Request extends typeof IncomingMessage = typeof IncomingMessage,
  Response extends typeof ServerResponse = typeof ServerResponse
> extends NetServer {
  constructor(requestListener?: RequestListener<Request, Response>);
  constructor(
    options: ServerOptions<Request, Response>,
    requestListener?: RequestListener<Request, Response>
  );
  setTimeout(msecs?: number, callback?: () => void): this;
  setTimeout(callback: () => void): this;
  maxHeadersCount: number | null;
  maxRequestsPerSocket: number | null;
  timeout: number;
  headersTimeout: number;
  keepAliveTimeout: number;
  requestTimeout: number;
  closeAllConnections(): void;
  closeIdleConnections(): void;
  addListener(event: string, listener: (...args: any[]) => void): this;
  addListener(event: "close", listener: () => void): this;
  addListener(event: "connection", listener: (socket: Socket) => void): this;
  addListener(event: "error", listener: (err: Error) => void): this;
  addListener(event: "listening", listener: () => void): this;
  addListener(
    event: "checkContinue",
    listener: RequestListener<Request, Response>
  ): this;
  addListener(
    event: "checkExpectation",
    listener: RequestListener<Request, Response>
  ): this;
  addListener(
    event: "clientError",
    listener: (err: Error, socket: stream.Duplex) => void
  ): this;
  addListener(
    event: "connect",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  addListener(
    event: "request",
    listener: RequestListener<Request, Response>
  ): this;
  addListener(
    event: "upgrade",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  emit(event: string, ...args: any[]): boolean;
  emit(event: "close"): boolean;
  emit(event: "connection", socket: Socket): boolean;
  emit(event: "error", err: Error): boolean;
  emit(event: "listening"): boolean;
  emit(
    event: "checkContinue",
    req: InstanceType<Request>,
    res: InstanceType<Response> & { req: InstanceType<Request> }
  ): boolean;
  emit(
    event: "checkExpectation",
    req: InstanceType<Request>,
    res: InstanceType<Response> & { req: InstanceType<Request> }
  ): boolean;
  emit(event: "clientError", err: Error, socket: stream.Duplex): boolean;
  emit(
    event: "connect",
    req: InstanceType<Request>,
    socket: stream.Duplex,
    head: Buffer
  ): boolean;
  emit(
    event: "request",
    req: InstanceType<Request>,
    res: InstanceType<Response> & { req: InstanceType<Request> }
  ): boolean;
  emit(
    event: "upgrade",
    req: InstanceType<Request>,
    socket: stream.Duplex,
    head: Buffer
  ): boolean;
  on(event: string, listener: (...args: any[]) => void): this;
  on(event: "close", listener: () => void): this;
  on(event: "connection", listener: (socket: Socket) => void): this;
  on(event: "error", listener: (err: Error) => void): this;
  on(event: "listening", listener: () => void): this;
  on(
    event: "checkContinue",
    listener: RequestListener<Request, Response>
  ): this;
  on(
    event: "checkExpectation",
    listener: RequestListener<Request, Response>
  ): this;
  on(
    event: "clientError",
    listener: (err: Error, socket: stream.Duplex) => void
  ): this;
  on(
    event: "connect",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  on(event: "request", listener: RequestListener<Request, Response>): this;
  on(
    event: "upgrade",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  once(event: string, listener: (...args: any[]) => void): this;
  once(event: "close", listener: () => void): this;
  once(event: "connection", listener: (socket: Socket) => void): this;
  once(event: "error", listener: (err: Error) => void): this;
  once(event: "listening", listener: () => void): this;
  once(
    event: "checkContinue",
    listener: RequestListener<Request, Response>
  ): this;
  once(
    event: "checkExpectation",
    listener: RequestListener<Request, Response>
  ): this;
  once(
    event: "clientError",
    listener: (err: Error, socket: stream.Duplex) => void
  ): this;
  once(
    event: "connect",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  once(event: "request", listener: RequestListener<Request, Response>): this;
  once(
    event: "upgrade",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  prependListener(event: string, listener: (...args: any[]) => void): this;
  prependListener(event: "close", listener: () => void): this;
  prependListener(
    event: "connection",
    listener: (socket: Socket) => void
  ): this;
  prependListener(event: "error", listener: (err: Error) => void): this;
  prependListener(event: "listening", listener: () => void): this;
  prependListener(
    event: "checkContinue",
    listener: RequestListener<Request, Response>
  ): this;
  prependListener(
    event: "checkExpectation",
    listener: RequestListener<Request, Response>
  ): this;
  prependListener(
    event: "clientError",
    listener: (err: Error, socket: stream.Duplex) => void
  ): this;
  prependListener(
    event: "connect",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  prependListener(
    event: "request",
    listener: RequestListener<Request, Response>
  ): this;
  prependListener(
    event: "upgrade",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  prependOnceListener(event: string, listener: (...args: any[]) => void): this;
  prependOnceListener(event: "close", listener: () => void): this;
  prependOnceListener(
    event: "connection",
    listener: (socket: Socket) => void
  ): this;
  prependOnceListener(event: "error", listener: (err: Error) => void): this;
  prependOnceListener(event: "listening", listener: () => void): this;
  prependOnceListener(
    event: "checkContinue",
    listener: RequestListener<Request, Response>
  ): this;
  prependOnceListener(
    event: "checkExpectation",
    listener: RequestListener<Request, Response>
  ): this;
  prependOnceListener(
    event: "clientError",
    listener: (err: Error, socket: stream.Duplex) => void
  ): this;
  prependOnceListener(
    event: "connect",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
  prependOnceListener(
    event: "request",
    listener: RequestListener<Request, Response>
  ): this;
  prependOnceListener(
    event: "upgrade",
    listener: (
      req: InstanceType<Request>,
      socket: stream.Duplex,
      head: Buffer
    ) => void
  ): this;
}

코드를  전체적으로 살펴보니 addListener와 emit, on, once등이 있습니다. 이것을 보니 위의 createServer로 만든 server객체는 EventEmitter 객체가 맞는 것 같습니다.

 

 

그럼 위의 전체적인 큰 그림을 살펴봤구요 

method와 url, headers, requerst, response같은 전문 용어를 알아보겠습니다. 

용어에 대한 설명은 다음과 같습니다.

request객체ReadableStream , EventEmitter이다 (request객체는 일반적으로 우리가 네이버로그인 요청을 할 때 버튼클릭을 하거나 어떤  요청을 할 때 그 요청 정보를 담고 있는 정보라고 생각하시면 됩니다.)

response객체는   WritableStream , EventEmitter이다 (서버란 즉 서빙, 서비스를 하는 대상입니다. 위에 있는 request로 요청을 받은 정보에 대해 그것을 읽은 뒤 다시 응답해주는 것을 말합니다.)

method : HTTP 메소드 / 동사 (GET, POST ,PUT)
(직관적으로 GET은 가져와서 조회하는 것이고 POST는 무언가를 만드는 것입니다. 지금 블로그에 글을 쓰는 것을 포스팅이라고 하듯이요, PUT도 글자 그대로 넣는 것입니다. )

url : 전체 URL에서 서버, 프로토콜, 포트를 제외한  것, 세번째 슬래시 이후의 나머지 전부 


headers : request에 있는 headers 전용객체에는 캐시설정, 데이터 타입설정 등 

 

그리고 위에서 알게된 내용으로 사용자가 서버에 보낸 데이터를 다시 보내는 서버를 만들어보았습니다.

코드는 다음과 같습니다.

 

const http = require('http');

http.createServer((request, response) => {
  const { headers, method, url } = request;
  let body = [];
  request.on('error', (err) => {
    console.error(err);
  }).on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString();
    // 여기서부터 새로운 부분입니다.

    response.on('error', (err) => {
      console.error(err);
    });

    response.statusCode = 200;
    response.setHeader('Content-Type', 'application/json');
    // 주의: 위 두 줄은 다음 한 줄로 대체할 수도 있습니다.
    // response.writeHead(200, {'Content-Type': 'application/json'})

    const responseBody = { headers, method, url, body };

    response.write(JSON.stringify(responseBody));
    response.end();
    // 주의: 위 두 줄은 다음 한 줄로 대체할 수도 있습니다.
    // response.end(JSON.stringify(responseBody))

    // 새로운 부분이 끝났습니다.
  });
}).listen(8080);


코드 흐름과 설명은 다음과 같습니다.

  1. 요청하는 정보인 request정보를 const 변수로 headers, method, url을 선언했다.
  2. let 변수로 body에 빈 배열을 선언했다. 
  3. request로 온 정보를 분석한 뒤 에러가 있으면 console.error를 통해 에러를 반환한다.
  4. 에러가 아닐 경우 data 이벤트에서 발생시킨 chunk를 body라는 빈 배열에 넣어준다. 
  5. 위에서 발생한 chunk인 Buffer를 배열에 수집한 다음 end 이벤트에서 이어붙인다음 문자열로 만들어서 body에 저장한다.
  6. response에 에러가 있으면 에러를 반환한다. 
  7. response의 상태코드를 200으로 설정한다. 200은 보통 성공 상태다.
  8. response에서 헤더를 설정하고 콘텐츠 타입은 application/json이다.
  9. responsebody에 헤더와 메소드, url, body를 담는다.
  10. response 메시지에 JSON.stringify를 사용해서 responsebody를 작성합니다. 
  11. 이후 end를 이용해 response 데이터를 마무리합니다. 
  12. listen을 통해 8080포트로 서버가 열려있다는 것을 알 수 있습니다.

Mini Node Server 구현

 

그렇다면 위에서 배운 HTTP 트랜잭션 내용을 통해  Mini Node Server를 구현한다면 어떻게 나타낼 수 있을까요?? 

 

소문자로 요청이 오면 소문자로 응답을 하고 대문자로 요청이 오면 대문자로 응답을 하는 서버 소스입니다.

코드는 다음과 같습니다.

const http = require("http");

const PORT = 4999;

const ip = "localhost";

const server = http.createServer((request, response) => {
  // console.log(
  //   `http request method is ${request.method}, url is ${request.url}`
  // );
  console.log(request);

  const { method, url, headers } = request;
  //request 객체는 IncomingMessage의 인스턴스입니다.
  // >> request(요청)에서 메서드와 url, 헤더스를 가져온것 같다.

  let body = [];

  request
    .on("data", (chunk) => {
      //리퀘스트에 요청이 왔을 때 바디에 push를 이용해서 chunk를 삽입한다.
      body.push(chunk);
    })
    .on("end", () => {
      //end 요청이 끝날 때 버퍼를 바디에 합쳐서 문자열로 바꿔준다.
      body = Buffer.concat(body).toString();
      console.log("body", body);
      if (method === "POST" && url === "/upper") {
        response.writeHead(200, defaultCorsHeader);
        response.end(body.toUpperCase());
        // response.statusCode = 200;
        // response.setHeader("Content-Type", "application/json");
        // response.setHeader("X-Powered-By", "bacon");
        // response.userAgent;
      } else if (request.method === "POST" && request.url === "/lower") {
        response.writeHead(200, defaultCorsHeader);
        response.end(body.toLowerCase());
        response.statusCode = 201;
      } else if (request.method === "OPTIONS") {
        response.writeHead(200, defaultCorsHeader);
        response.end();
      } else {
        response.writeHead(404, defaultCorsHeader);
        response.end("잘못된 요청");
      }
    });
});

server.listen(PORT, ip, () => {
  console.log(`http server listen on ${ip}:${PORT}`);
});

const defaultCorsHeader = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Accept",
  "Access-Control-Max-Age": 10,
};

 

위의 미니노드 서버 구조는 다음과 같습니다.

그리고 코드 흐름과 설명은 다음 내용을 보시면 알 수 있습니다. 

 

  1. http 모듈 가져오기
  2. port 설정하기
  3. ip 설정하기
  4. http.createServer를 이용해서 서버객체 만들기
  5. method, url, server에 리퀘스트 프로퍼티 설정하기 
  6. body에 빈 배열 선언 
  7. request가 오면 chunk를 body에 넣고 요청이 끝나면 body에 한가지 데이터로 만들어준다.
  8. method가 POST이면서 url이 /upper면 200 상태 코드와 defaultCorsHeader를 헤더에 설정
  9. respose의 마지막 데이터에 요청받은 body데이터를 대문자로 만들어서 반환
  10. method가 POST이면서 url이 /lower이면 201 상태 코드와 defaultCorsHeader를 헤더에 설정
  11. respose의 마지막 데이터에 요청받은 body데이터를 소문자로 만들어서 반환
  12. request의 method가 "OPTIONS"면 defaultCorsHeader를 헤더에 설정한 뒤 response로 반환 
  13. 이외의 요청에 대해서는 404 상태코드와 defaultCorsHeader를 헤더에 설정하고 "잘못된 요청"이라는 데이터를 response의 마지막데이터에 넣은 뒤 반환하기 

 


 

참조 

http.js
https://github.com/nodejs/node/blob/v18.0.0/lib/http.js 

HTTP 트랜잭션 해부

참조 : 링크

'Web > NodeJS' 카테고리의 다른 글

Node JS - EventEmitters  (0) 2023.01.04
nvm & node.js  (0) 2023.01.01
Javascript 런타임에 대하여  (0) 2022.12.23

관련글 더보기