[Web Server] 기초 (2)

선정·2022년 6월 17일
0

JavaScript의 런타임인 node.js를 앞서 조금 다뤄보기는 했지만 서버 코드는 처음 작성해봐서 굉장히 생소하고 낯설었다. 첫번째 과제는 거의 공식문서만 쥐어주고 과제를 진행하라고 해서 체감상 역대급으로 힘들었다. 대략이나마 이해하고 난 지금 보면 그렇게 어렵지 않은 수준인데 공식문서부터 들여다보니 처음보는 개념들이 너무 많아서 더 복잡하고 골치 아프게 느껴졌던 것 같다. 필요한 함수를 사용해보려고 두 세줄 읽으면 모르는 개념들이 나오고 또 그걸 알기 위해서 들어가면 또 모르는 개념이..(무한반복)

공식문서를 많이 봐야한다지만 처음 접하는 입장에서 공식문서로 개념을 이해하고 활용한다는 건 너무너무 어려웠다..🤯 그래도 검색을 통해, 함께 공부하는 동료분들의 도움도 받으면서 이해해보려고 노력했다. 어쨌거나 지금은 어떻게든 이해해서 활용할 수 있도록 익히는 게 가장 중요하니까.

오늘까지 완료한 node.js를 이용해 서버를 구축하는 과제는 2개로, 먼저 node.js의 기본 모듈인 http 모듈을 사용해 서버를 구현하는 과제를 했다. 다음으로 첫번째 과제를 express 프레임워크를 사용해 리팩토링 했다.



과제 1 - Mini Node Server

참고 자료 : HTTP 트랜잭션 해부(Anatomy of an HTTP Transaction)

우선 clone한 과제를 열고 서버를 실행해야 하는데, 그냥 터미널에서 node로 실행하면 서버 코드를 수정할 때마다 매번 서버를 다시 실행시켜야 하는 번거로움이 있다. 이 번거로운 재실행 작업을 생략하기 위해 nodemon이라는 툴을 이용한다.

  1. nodemon 설치 : npm install nodemon
  2. package.json의 "scripts" 아래에 따로 "start": "nodemon [파일명]" 코드 추가
  3. npm start로 서버 실행

혹은 nodemon을 설치하지 않고 아래의 명령어로 실행할 수도 있다.

npx nodemon [파일명]

이미 작성되어있는 클라이언트의 실행을 위해 html 파일을 열어 웹 브라우저에서 실행하는데, 특정 포트를 클라이언트로 실행하고 싶다면, serve를 이용할 수 있다.

npx serve -l 포트번호 client/


과제에 이미 작성돼 있는 코드들과 그 코드들이 하는 일들을 주석으로 간단히 작성해봤다. 코드 작성란에 구현해야 할 내용을 참고해서 코드를 작성해보자!

const http = require('http') // http 모듈 호출
const PORT = 4999;
const ip = 'localhost';

// 웹 서버 객체를 만들기 위해 createServer를 사용한다.
// (모든 node 웹 서버 애플리케이션은 웹 서버 객체를 만들어야 한다.)
// 이 서버로 오는 HTTP 요청마다 createServer에 전달된 함수가 한 번씩 호출한다.
const server = http.createServer((request, response) => {
  // 코드를 작성하세요.
}

// 서버가 사용하고자 하는 포트 번호를 listen에 전달한다.
// listen() 메서드를 사용해야 서버가 제대로 실행된다.
server.listen(PORT, ip, () => {
  console.log(`http server listen on ${ip}:${PORT}`);
});

// 프리플라이트 요청을 위한 CORS 설정
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
};

과제에서 구현할 웹 서버의 기능은, 클라이언트의 버튼(toUpperCase, toLowerCase) 클릭 액션에 따라 각각 다른 HTTP 요청을 서버로 보내고, HTTP 요청에 담아 보낸 단어를 소문자 또는 대문자의 응답을 받아 화면에 표시하는 것이다.

  • Endpoint에 따른 메서드와 기능
Endpoint(URL)Method기능
/lowerPOST문자열을 소문자로 만들어 응답한다
/upperPOST문자열을 대문자로 만들어 응답한다
  • CORS 관련 헤더를 OPTIONS 응답에 적용해야 한다.
  • 클라이언트의 preflight request에 대한 응답을 돌려줘야한다.


코드 (1)

서버로 오는 HTTP 요청마다 createServer에 전달된 함수가 한 번씩 호출되므로, 각각의 요청에 대한 적절한 응답을 보낼 수 있도록 조건문으로 분기하여 코드를 작성했다.

const server = http.createServer((request, response) => {
  
  if (request.method === 'OPTIONS') { 
    // 클라이언트의 preflight 요청에 대한 응답을 돌려준다.
    response.writeHead(200, defaultCorsHeader); // 헤더 응답
    response.end(); // OPTIONS 메서드는 body를 응답할 필요 없다.
  }

  if (request.method === 'POST' && request.url === '/upper') {
    let body = [];
    request.on('data', (chunk) => {
      body.push(chunk); // data 
    }).on('end', () => {
      body = Buffer.concat(body).toString();
      response.writeHead(201, defaultCorsHeader); // 헤더 응답
      response.end(body.toUpperCase()); // 바디 응답
    })
  } else if (request.method === 'POST' && request.url === '/lower') {
    let body = [];
    request.on('data', (chunk) => {
      body.push(chunk);
    }).on('end', () => {
      body = Buffer.concat(body).toString();
      response.writeHead(201, defaultCorsHeader);
      response.end(body.toLowerCase());
    })
  } else { // 과제에서 요구되는 것 외 나머지 요청들
    response.writeHead(404, defaultCorsHeader);
    reponse.end('Bad Request');
  }
})

위의 코드 중 가장 이해하기 난해했던 부분들을 따로 정리해보자. 공식문서만으로는 이해가 어려워 세션과 검색 등을 통해 내가 이해한 방식으로 정리했다. 그렇기 때문에 틀린 부분이 있을 수 있다..
let body = [];
request.on('data', (chunk) => { // POST 요청이 'HELLO'일 때,
  // console.log(chunk) => <Buffer 22 48 45 4c 4c 4f 22>
  body.push(chunk)
  // console.log(body) => [ <Buffer 22 48 45 4c 4c 4f 22> ]
}).on('end', () => {
  // console.log(Buffer.concat(body)) => [ <Buffer 22 48 45 4c 4c 4f 22> ]
  body = Buffer.concat(body).toString();
  // console.log(body) => 'HELLO'

위의 코드를 콘솔을 찍은 결과, 브라우저에서 string 타입으로 요청을 보낸 데이터가 Buffer 형식으로 전달되었다. 그리고 <Buffer> -> [ <Buffer> ] -> string 과정을 통해, 최종적으로 변수 body에 string 타입의 데이터가 할당됐다. 결론적으로 5줄의 코드는 요청받은 데이터를 문자열로 변환해주기 위한 것임을 알 수 있다. (문자열 메서드인 toUpperCase(), toLowerCase()를 적용하기 위해서)

Buffer.concat(body)로 콘솔을 찍어보면 body는 해당 메서드를 통해 [ <Buffer> ] -> <Buffer> 로 바뀌었다. Buffer.concat() 메서드는 배열에 담긴 모든 버퍼 객체를 하나의 버퍼 객체로 결합한다. 데이터의 조각 조각으로, 여러 개의 버퍼 객체로 전달된 것을 이 메서드를 통해 하나의 객체로 결합시키는 기능을 하는 것으로 이해했다.

  • request.on() => emitter.on(eventName, listener)

    • eventName는 이벤트의 이름
    • listenereventName에 대한 리스너 함수로, 콜백 함수

    on()은 이벤트를 연결하는 함수이고 위의 코드에서는 request에 on() 함수를 사용해서 data라는 이벤트를 연결하고 콜백함수를 실행한다.(chunk는 매개변수로, 관행적으로 chunk라고 쓰는듯하다.) 그런 다음 또 다시 on() 함수로 end 이벤트를 연결하고 그에 해당하는 콜백함수를 실행시킨다.
    EventTarget.addEventListener()와 쓰임이 아주 비슷해서, 해당 함수의 용법을 대입해보니 이해하기 쉬웠다.

  • data 이벤트
    데이터 덩어리(the chunk of data)를 활용할 수 있게 하는 이벤트
    위의 코드에서는 요청(request)으로 데이터가 전달되면 data 이벤트가 실행된다.

  • end 이벤트
    스트림에서 사용할 데이터가 더 이상 없을 때 end 이벤트가 발생한다.
    위의 코드에서는 요청으로부터 전달받은, 조각 형태의 데이터(Buffer)가 빈배열 body로 전부 push되어서 스트림에서 사용할 데이터가 없어졌을 때, end 이벤트가 실행된다.

response.writeHead(201, defaultCorsHeader);
response.end(body.toUpperCase());
  • response.writeHead() => response.writeHead(statusCode[, statusMessage][, headers])
    응답 스트림에 명시적으로 상태 코드와 헤더를 작성한다.

  • response.end() => response.end([data[, encoding]][, callback])
    모든 응답 헤더와 본문이 전송되었음을 서버에 알린다. 각 응답에 대해 response.end() 메서드를 호출해야 한다(MUST).
    위의 코드 (1)을 보면, 모든 응답 코드의 마지막줄에 response.end() 메서드를 호출하고 있다. 그리고 보통 end()에는 body를 실어보내는 듯하다. POST 요청에는 body를 담아 응답하고 body가 필요 없는 OPTIONS 요청에는 end() 메서드에 아무것도 담지 않는다.



코드 (2) : 중복 코드 제거

코드 (1)은 제대로 작동하지만, 중복되는 코드들(let body = [] ~ body = Buffer.concat(body).toString())이 있어 좋은 코드라고 하기 어렵다. 코드 (1)의 중복을 제거해 코드를 개선해보자.

const server = http.createServer((request, response) => {

  let body = [];
  request.on('data', (chunk) => {
    body.push(chunk)
  }).on('end', () => {
    body = Buffer.concat(body).toString();

    if (request.method === 'OPTIONS') {
      response.writeHead(200, defaultCorsHeader);
      reponse.end();
    } else if (request.method === 'POST' && request.url === '/upper') {
      response.writeHead(201, defaultCorsHeader);
      response.end(body.toUpperCase());
    } else if (request.method === 'POST' && request.url === '/lower') {
      response.writeHead(201, defaultCorsHeader);
      response.end(body.toLowerCase());
    } else {
      response.writeHead(404, defaultCorsHeader);
      reponse.end('Bad Request');
    }
  })
})


코드 (3) : request 객체의 프로퍼티 이용하기

request에서 구조분해할당을 통해 필요한 프로퍼티를 가져온다. 아래 코드의 경우, 기존 request.method, request.url으로 표기했던 코드를 method, url로 줄여서 사용 가능해진다.

const { method, url } = request;

const server = http.createServer((request, response) => {

  let body = [];
  request.on('data', (chunk) => {
    body.push(chunk)
  }).on('end', () => {
    body = Buffer.concat(body).toString();

    if (method === 'OPTIONS') {
      response.writeHead(200, defaultCorsHeader);
      reponse.end();
    } else if (method === 'POST' && url === '/upper') {
      response.writeHead(201, defaultCorsHeader);
      response.end(body.toUpperCase());
    } else if (method === 'POST' && url === '/lower') {
      response.writeHead(201, defaultCorsHeader);
      response.end(body.toLowerCase());
    } else {
      response.writeHead(404, defaultCorsHeader);
      reponse.end('Bad Request');
    }
  })
})


~ 정리해야 됨 ~

express

expresss는 Node.js 웹 애플리케이션 프레임워크로, Node.js의 http 모듈과 Connect 컴포넌트??를 기반으로 한다. 그러한 컴포넌트를 미들웨어라고

(쉽게말하면, Node.js 개발시 개발을 빠르고 손쉽게 할 수 있도록 도와주는 역할을 합니다. 이것은, 미들웨어(Middleware) 구조 때문에 가능한 것입니다. 자바스크립트코드로 작성된 다양한 기능의 미들웨어는 개발자가 필요한 것만 선택하여 익스프레스와 결합해 사용할 수 있습니다.)

express는 요청을 받아들일 서버 구축을 돕고, 텍스트로 들어온 HTTP 요청을 JavaScript 객체로 파싱한다. 즉, 정보를 요청으로 변환하고 요청을 객체로 변환한다. 또한 요청에 대한 응답(상태코드, 헤더 등)을 설정하는 것에도 도움을 준다. <= http 모듈로도 할 수 있는데 코드를 더 간결하게 할 수 있는!

http 모듈 대신 express 프레임워크로 서버를 생성할 때 장점

내장 모듈인 http와 달리 프레임워크이기 때문에 npm으로 설치가 필요
Express 공식 문서에서 Express 설치부터 여러가지 사용 방법과 예제들을 확인할 수 있다.


과제 2 - Refactor Express

profile
starter

0개의 댓글