38일차 Refactor Express

2021. 9. 11. 14:15
반응형

2021. 09. 09 목요일

1. Today's key Points!🔑

  • Node.js Express
  • Middleware

2. 정리해보자!🧹

Express.js는 Node.js 환경에서 웹 서버, 또는 API 서버를 제작하기 위해 사용되는 인기 있는 프레임워크이다. http 모듈로 작성한 서버와 다른 점은 미들웨어 추가가 편리하고, 자체 라우터를 제공한다.

라우팅이 무엇인가? 라우팅은 URI(또는 경로) 및 특정 HTTP 요청 방법(GET, POST 등)인 특정 끝점에 대한 클라이언트 요청에 애플리케이션이 응답하는 방식을 결정하는 것을 말한다. 즉, 메소드와 URL(/lower, /upper 등)로 분기점을 만드는 것이다. 각 라우트는 하나 이상의 핸들러 함수를 가질 수 있고, 이러한 함수는 라우트가 일치할 때 실행된다. 라우트 정의에는 다음과 같은 구조가 필요하다.

app.METHOD(PATH, HANDLER)

*클라이언트는 특정한 HTTP 요청 메소드(GET, POST 등)나 서버의 특정 URI(또는 경로)로 HTTP요청을 보낸다. 라우팅은 클라이언트의 요청에 해당하는 메소드와 Endpoint에 따라 서버가 응답하는 방법을 결정하는 것이다.*

미들웨어가 무엇인가? 미들웨어 함수는 응용 프로그램 요청-응답 사이클에서 요청 객체(req), 응답 객체(res) 및 다음 미들함수에 액세스할 수 있는 함수이다. next 함수는 익스프레스 라우터의 함수이며, 호출되면 현재 미들웨어의 뒤를 잇는 미들웨어를 실행한다. 라고 공식문서에 나와있다. 그래서 내가 이해한 미들웨어는 요청과 응답 프로세스 중간에 관여해서 특정한 일들을 처리해줄때 쓰이는 함수라고 이해했다.

미들웨어를 사용하는 상황은 다음과 같다.

  1. 모든 요청에 대해 url이나 메소드를 확인할 때
  2. POST 요청 등에 포함된 body(payload)를 구조화할 때(쉽게 얻어내고자 할때)
  3. 모든 요청/응답에 CORS 헤더를 붙여야 할 때
  4. 요청 헤더에 사용자 인증 정보가 담겨있는지 확인할 때

CASE 1. 모든 요청에 대해 url이나 메소드를 확인할 때

수많은 미들웨어가 있지만, 가장 단순한 미들웨어 로거를 예로 들 수 있다. 로거는 디버깅이나, 서버 관리에 도움이 되기 위해 console.log로 적절한 데이터나 정보를 출력한다. 데이터가 여러 미들웨어를 거치는 동안 응답할 결과를 만들어야 한다면, 미들웨어 사이사이에 로거를 삽입해서 현재 데이터를 확인하거나, 디버깅에 사용할 수 있다. 이런 미들웨어는 일반적으로 다음과 같은 구성을 가진다.

위의 그림은 endpoint가 / 이면서, 클라이언트로 부터 GET 요청을 받았을 때 적용되는 미들웨어이다. 위의 미들웨어 내부에서는 아무런 작업을 하고 있지 않고, 특정 endpoint에 적용하고있다. 그럼 모든 요청에 동일한 미들웨어를 적용하는 방법을 알아보자. app.use를 이용하면 된다.

const express = require('express');
const app = express();
const port = 3000;

const myLogger = function (req, res, next) {
  console.log(`http request method is ${req.method}, url is ${req.url}`); // 이 부분을 req, res 객체를 이용해 고치면, 모든 요청에 대한 로그를 찍을 수 있다.
  next();
};

app.use(myLogger);

app.get('/', function (req, res) {
  res.send('Hello World!');
});
app.get('/favicon.ico', function (req, res) {
  res.send('GET favicon.ico');
});
app.get('/upper', function (req, res) {
  res.send('GET upper');
});
app.options('/upper', function(req, res){
  res.send('OPTIONS upper');
})
app.post('/upper', function(req, res){
  res.send('POST upper');
})


app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

이렇게 만들어놓고 하나씩 요청하면 그에 맞는 method와 url이 콘솔에 찍힐 것이다.

맨 위의 get 부터 하나씩 요청한 결과

CASE 2. POST 요청 등에 포함된 body(payload)를 구조화할 때

순수 node.js로 HTTP body(payload)를 받을 때에는 Buffer를 조합해서 다소 복잡한 방식으로 body를 얻을 수 있다.

let body = [];
request.on('data', (chunk) => {
  body.push(chunk);
}).on('end', () => {
  body = Buffer.concat(body).toString();
  // body 변수에는 문자열 형태로 payload가 담겨져 있다.
});

body-parser 미들웨어를 사용하면 이 과정을 간단하게 처리할 수 있다.

const bodyParser = require('body-parser')
const jsonParser = bodyParser.json()

// 생략
app.post('/api/users', jsonParser, function (req, res) {
  // req.body에는 JSON의 형태로 payload가 담겨져 있다.
})

그런데 지금은 저 body-parser 미들웨어가 기본적으로 내장되어 있다고 한다. 그래서 아래처럼 사용하면 자동 파싱을 해준다.

app.use(express.json())
app.use(express.json({strict : false})) 
//첫번째 줄의 파싱은 배열과 객체만 파싱을 해주는데, 두번째 줄의 파싱은 모든 것을 파싱해준다

CASE 3. 모든 요청/응답에 CORS 헤더를 붙일 때

순수 node.js 코드에 CORS 헤더를 붙이려면, 응답 객체의 writeHead 메소드 등을 이용해야한다. 이런 메소드를 이용하더라도 Access-Control-Allow-Origin 헤더를 매번 재정의 해야한다. 그뿐이 아니라 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
};

//생략
if (req.method === 'OPTIONS') {
  res.writeHead(201, defaultCorsHeader);
  res.end()
}
if(req.post === 'POST' && req.url === '/upper'){
  res.writeHead(200, defalutCorsHeader);
  res.end()
}

이런식으로 매 메소드마다 writeHead를 이용해야한다. post나 다른 메소드를 요청할 때에도 'Access-Contriol-Allow-Origin'헤더가 필요하기 때문이다. 그런데 이것을 cors 미들웨어를 사용하면 이 과정을 간단하게 처리할 수 있다. npm install cors를 터미널창에 입력해서 다운을 받고

const cors = require('cors')

// 생략
app.use(cors()) // 모든 요청에 대해 CORS 허용

//----------------------------------------------------------------------------------

const cors = require('cors')

// 생략
// 특정 요청에 대해 CORS 허용
app.get('/products/:id', cors(), function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for a Single Route'})
})

CASE 4. 요청 헤더에 사용자 인증 정보가 담겨있는지 확인할 때

HTTP 요청에서 토큰이 있는지 여부를 판단해서 이미 로그인한 사용자일 경우 성공, 아닐 경우 에러를 보내는 미들웨어 예제이다.

app.use((req, res, next) => {
  // 토큰 있니? 없으면 받아줄 수 없어!
  if(req.headers.token){
    req.isLoggedIn = true;
    next()
  } else {
    res.status(400).send('invalid user')
  }
})

로그인 없이 웹사이트에 접근을 시도했을 때, 로그인 창 등으로 되돌려 보내는 등 서버에서는 요청에 포함된 데이터를 통해 미들웨어가 요구하는 조건에 맞지 않으면 돌려보내도록 구현할 수 있다. 아직 토큰에 대해서 배우지는 않은 상태라 그냥 이렇게 사용할 수 있구나 라는 것만 알고 넘어가겠다.

3. Sprint 복기!🧐

그럼 express와 middleware에 대해 알았으니 mini-node-server를 리팩토링 해보자.

우선 npm install express --save 로 express를 설치하고, npm install cors도 설치해주자.

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

const app = express(); //express 사용
app.use(cors()); // 모든 요청/응답에 CORS 처리
app.use(express.json({strict: false})) // 자동 바디 파싱

const PORT = 5000;
const ip = 'localhost';


app.post('/upper', function (req, res){
  let result = req.body;
  result = result.toUpperCase();
  res.json(result); // 다시 데이터를 보내줄 때 json화 해서 보내주는 메소드
})
app.post('/lower', function (req, res){
  let result = req.body;
  result = result.toLowerCase();
  res.json(result);
})

app.use(function(req, res, next) { // 400에러 즉, 클라이언트의 잘못으로 인한 에러는 인자에 err를 넣지 않는다.
  res.status(404).send('Something broke! 404');
});

app.use(function(err, req, res, next) { //에러 처리
  console.error(err.stack);
  res.status(500).send('Something broke! 500');
});

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

이렇게 express를 사용하니 바디 데이터를 매우 간단하게 처리할 수 있다. 그리고 반복적으로 처리해주어야할 부분을 미들웨어로 한번에 처리해서 번거로움을 없앨 수 있었다. 아래는 순수 node.js로 서버를 구현한 것이다. 위와 비교해보면 확실히 반복적으로 처리해 주어야할 부분을 하나로 처리해 줄 수 있고, 코드도 매우 간단해 지는 것을 볼 수 있다.

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

const server = http.createServer((request, response) => {
  if (request.method === 'OPTIONS') {
    response.writeHead(200, defaultCorsHeader);
    response.end();
    return;
  } 
  let body = [];
  if (request.method = "POST" && request.url === '/lower') {
  request.on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString().toLowerCase();
    response.writeHead(200, defaultCorsHeader);
    response.end(body);
    return;
  });
  } 
  else if (request.method = "POST" && request.url === '/upper') {
    request.on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString().toUpperCase();
    response.writeHead(200, defaultCorsHeader);
    response.end(body);
    return;
  });
  }
  else {
    response.statusCode = 404;
    response.end();
  } 
}).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
};

마무리 Comment.. 😳

이렇게 express와 middleware를 사용해서 mini-node-server를 리팩토링 해보았다. 이제 점점 서버에 익숙해지는 느낌이 든다.

반응형
LIST

BELATED ARTICLES

more