Mini node Server 구축하기

Node.js 공식 문서의 HTTP 트랜잭션 해부 와 express 공식문서 시작하기를 참고하여 작성하면 쉽게 만들 수 있다

 

HTTP 트랜잭션 해부
https://nodejs.org/ko/docs/guides/anatomy-of-an-http-transaction

 

HTTP 트랜잭션 해부 | Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

express 공식문서 시작하기
https://expressjs.com/ko/starter/installing.html

 

Express 설치

설치 Node.js가 이미 설치되었다고 가정한 상태에서, 애플리케이션을 보관할 디렉토리를 작성하고 그 디렉토리를 작업 디렉토리로 설정하십시오. $ mkdir myapp $ cd myapp npm init 명령을 이용하여 애플

expressjs.com

1. 서버 생성을 위한 빈 폴더 생성
mkdir 생성할폴더이름
cd 생성할폴더이름

2. package.json 파일 생성하기
npm init
// default 설정값으로 만들면 된다(enter~)

3. express 설치
npm install express --save
// --save 옵션을 통해 package.json에 종속 항목 목록에 추가(dependencies 에)

이렇게 하면 기본적인 뼈대가 될 폴더와 파일들이 생성된다.


만약 내가 이 폴더와 파일들의 구조를 보고 싶다면?
-> tree를 설치하면 터미널에서 구조 파악하는데 좋다

* tree 설치 및 버전 확인
sudo apt install tree
tree --version

// tree 설치 후 버전이 정상적으로 나오면 정상적으로 설치된 것

tree --help 를 통해 각종 옵션을 써서 원하는 구조를 출력 가능

ex) tree -df
// 어느 경로에서 tree를 쓰느냐에 따라서 보이는 구조가 다르므로 이를 이용해 구조 출력해보자

웹 서버 생성의 구조 파악

기본적인 Node.js 명령어로 작성하는 것도 좋지만, express를 이용하면 다양한 미들웨어를 통해 좀더 직관적이면서
효율적인 코딩이 가능하므로, express를 이용하여 작성해 보자

 

기본적으로 웹 서버를 생성할 때, 순서를 생각해 보자

1. 웹 서버 객체를 생성하고 서버 생성
2. 메서드, url, 헤더 생성하기
3. 요청바디 만들기
4. 응답 헤더 설정 및 HTTP 상태 코드 부여
5. 응답 바디 만들기 및 전송

 

기본적인 서버 생성 문법은 다음과 같다

// app.js 파일을 생성 후 코드 작성을 한 예시

const express = require('express')    //express 모듈을 가져옴
const app = express()    // express 인스턴스를 생성
const port = 3000    // 사용할 포트

app.get('/', (req, res) => {    //get 메서드를 사용, 루트(/) 라우트    <- 요청
  res.send('Hello World!')    // <- 응답
})

app.listen(port, () => {    //서버를 listen 상태로 만들면서 포트와 매칭 
  console.log(`Example app listening at http://localhost:${port}`)
})

기본 node.js 로 서버 구현

만약, express를 사용하지 않고 node.js로 서버 구현을 한다고 생각해 보자

 

먼저 메소드 부분에 대한 수도코드를 생각해 보면,

// 프리플라이트 처리
   // if(메소드가 OPTIONS 이면){
   //   CORS 설정을 돌려줘야 한다
   // }
   //upper, lower의 엔드포인트를 구현해야함!
   // 이거만 만들고 나머지는 error처리하면 됨
   // if(메소드가 POST 이고, url이 /upper 면){
   //   대문자로 응답해줘야 한다
   // }
   // else if(메소드가 POST dlrh url이 /lower 면){
   //   소문자로 응답해줘야 한다
   // }
   // else{
   //   에러로 처리. bad request 처리
   // }  

이런 구조를 내부에서 짜야 한다

 

이 부분을 생각하여 코드를 작성하면 다음과 같다(server.js)

// server/basic-server.js 파일


const http = require('http');
const PORT = 5000;
const ip = 'localhost';

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

const {method, url, headers } = request;  // 구조 분해 할당

request.on('error', (err) => {
    console.log(err);
     response.writeHead(404, defaultCorsHeader);
     response.end("에러 입니다")
   });
   response.on('error', (err) => {
     console.log(err);
   })
   if(method==='OPTIONS'){
     console.log("옵션 메서드 입니다")
     response.writeHead(200, defaultCorsHeader); // 200의 상태 코드(status code)와 defaultCorsHeader 라는 헤더 설정을 돌려줌
     response.end(); //이 상태로 끝냄  응답 바디가 필요한건 아니니깐
   }
   if(method==='POST' && url==='/upper'){
     let body=[];  // 빈 배열에 넣어 주는 방식을 이용
     request.on('data', (chunk)=> {  //요청에 data가 왔을때, callback함수를 실행해라~  //이벤트리스너 생각
       body.push(chunk);
     }).on('end', ()=>{
       body=Buffer.concat(body).toString();  //body.toString() 과 똑같음
   // 여기에 요청바디가 문자열로 담겨있다.
       response.writeHead(201, defaultCorsHeader);
       console.log(body);  
       response.end(body.toUpperCase());
     })
   }
   else if(method==='POST' && url==='/lower'){

     let body=[];
     request.on('data', (chunk)=> {
       body.push(chunk);
       // console.log(body);
     }).on('end', ()=>{
       body=Buffer.concat(body).toString();
       response.writeHead(201, defaultCorsHeader);
       console.log(body);
       response.end(body.toLowerCase());
     })
   }
   else{
     // 배드 리퀘스트 처리
     response.writeHead(400, defaultCorsHeader);
     response.end("잘못된 메시지입니다");
   }

   console.log(
     `http request method is ${method}, url is ${url}` // 구조 분해 할당을 했으므로 request.method -> method로 쓰면됨
   );
   response.writeHead(200, defaultCorsHeader);
   response.end('hello mini-server sprints');


 });

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


// 만약에 cors 미들웨어를 사용하지 않은 경우 아래서처럼 디폴트 헤더를 만들고 options 메서드도 구현해야 할 것이다
 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  //응답 헤더는 결과를 브라우저 캐시에 캐시 할 수있는 기간을 나타냅니다.
 };

참고: 브라우저를 사용한 프리플라이트 캐싱


프리 플라이트 캐시는 다른 캐싱 메커니즘과 유사하게 작동합니다.
브라우저가 프리 플라이트 요청을 할 때마다 먼저 프리 플라이트 캐시에서 해당 요청에 대한 응답이 있는지 확인합니다.
브라우저가 응답을 찾으면 서버에 Preflight 요청을 보내지 않고 대신 캐시 된 응답을 사용합니다.
브라우저는 프리 플라이트 캐시에 응답이없는 경우에만 프리 플라이트 요청을 보냅니다.

express를 활용한 서버 구현(리팩토링)

그런데 위의 코드는 너무 복잡하고 코드도 간결하지 않다.


아래는 이러한 부분을 express를 사용하여 basic-server.js 파일을 리팩토링한 것이다

// server/basic-server.js 파일

const http = require('http');
const express = require('express');
const cors=require('cors');

const PORT = 5000;
const ip = 'localhost';
const server = express();

server.use(express.json({strict: false}));    // primitive data type 도 parsing 해주도록 설정
//! 해당 부분에 대한 설정 ({strict: false}) 을 해주지 않으면 데이터 내용이 보이지 않는 증상이 발생할 수 있다!
//! 즉 .body 부분의 내용이 {} [] 이런식으로 껍데기만 보일 수 있다. json 내용을 파싱하려면 반드시 false 설정해줘야함
//! 만약 json 형식이 아닌 일반 텍스트(text/plain)를 요청, 응답 받는 경우
//! express.text()    메서드를 사용하자

server.use(cors());    // 모든 요청에 대해 CORS 를 적용

server.get('/', (req, res) => {
  console.log("get 메서드 실행됨")
  res.send('hello mini-server sprints');
})

server.post('/upper', (req, res)=> {
  console.log('upper 테스트');
  res.json((req.body).toUpperCase());
})

server.post('/lower', (req, res) => {
  console.log('lower 테스트');
  res.json((req.body).toLowerCase());
})

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

 

다음은 클라이언트 부분의 코드이다(App.js)

// 해당 부분은 서버 부분이 아닌 클라이언트 부분이므로 기본적인 node.js 문법으로 작성
// client/App.js

class App {
  init() {
    document
      .querySelector('#to-upper-case')
      .addEventListener('click', this.toUpperCase.bind(this));
    document
      .querySelector('#to-lower-case')
      .addEventListener('click', this.toLowerCase.bind(this));
  }
  post(path, body) {
    fetch(`http://localhost:5000/${path}`, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: {
        'Content-Type': 'application/json',
        // 'Content-Type': 'application/xml'    //컨텐츠 타입이나
        // 'Accept':  'application/json'        //Accept 부분을 바꾸어 여러 테스트를 해봐도 된다
      }
    })
      .then(res => res.json())
      .then(res => {
        this.render(res);
      });
  }
  toLowerCase() {
    const text = document.querySelector('.input-text').value;
    this.post('lower', text);
  }
  toUpperCase() {
    const text = document.querySelector('.input-text').value;
    this.post('upper', text);
  }
  render(response) {
    const resultWrapper = document.querySelector('#response-wrapper');
    document.querySelector('.input-text').value = '';
    resultWrapper.innerHTML = response;
  }
}

const app = new App();
app.init();

서버와 관련한 보충 내용 및 tip

  • spread(...) 연산자를 잘 활용하자
  • ... 연산자를 통해 배열의 요소들을 꺼내올 수 있다는 점을 생각!
const arr=[el1, el2, el3, el4];

cosole.log([...arr, el5, el6])    // [el1,el2,el3,el4,el5,el6]
  • 쿼리 파라미터를 이용해 요청(request)를 보낸 경우, 이 쿼리 내용은 어디에 담겨있는가?

인자명.query에 해당 쿼리 내용이 담겨 있다

  • req.param() 또는 req.params
    req.param(name [, defaultValue])

특정 파라미터에 대한 값을 가져오는 듯?

다음의 형태로 사용할 수 있다

// ?name=tobi
req.param('name')
// => "tobi"

// POST name=tobi
req.param('name')
// => "tobi"

// /user/tobi for /user/:name
req.param('name')
// => "tobi"

자세한 내용은 express 공식문서를 참고

http://expressjs.com/ko/api.html#req.param

 

Express 4.x - API 참조

Express 4.x API express() Creates an Express application. The express() function is a top-level function exported by the express module. var express = require('express') var app = express() Methods express.json([options]) This middleware is available in Ex

expressjs.com

 

  • 구조 분해 할당의 다양한 응용
// 구조 분해 할당의 응용 예시 1
...생략

update: (req, res) => {
    let data;

    const list = flights.filter((flight) => {
      return flight.uuid===req.param('id')
    });
    [data]=list;  // 구조 분해 할당

    Object.assign(data, req.body);
return res.status(200).json(data);

...생략

// 구조 분해 할당의 응용 예시 2

create: (req, res) => {

    const {flight_uuid , name, phone} =req.body    // 구조 분해 할당
    booking.push({
      flight_uuid,
      name,
      phone
    })
    res.location(`/book/${flight_uuid}`)
    return res.status(201).json({book_id: flight_uuid});
  },
복사했습니다!