Dev/SW Engineering

20. Server & Node - Express

HJChung 2020. 7. 21. 12:57

이전 Sprint에서는 Node.js를 이용한 서버 구축을 해보았습니다. 그러나 코드가 복잡해지면 이렇게 계속 if문이 계속 되는 코드가 매우 힘들어질 것입니다. 

이런 상황에서 코드의 복잡성을 낮춰주고, 웹 애플리케이션을 구현하는 과정에서 공통적으로 요구되는 일들을 대신해주는 것이 프래임워크라고 합니다. 

https://wikibook.co.kr/article/what-is-expressjs/에서는 
Node.js의 핵심 모듈만 이용해서 중요 앱을 작성한다면 

HTTP 요청 본문 파싱
쿠키 파싱
세션 관리
URL 경로와 HTTP 요청 메서드를 기반으로 한 복잡한 if 조건을 통해 라우팅을 구성
데이터 타입을 토대로 한 적절한 응답 헤더 결정
하는 과정을 지속적으로 반복해야 하는 반면, 

Express.js는 이러한 문제를 비롯해 다른 여러 문제를 해결함과 동시에 웹 앱에 MVC 형태의 구조를 제공한다. 이 같은 앱은 백엔드만 갖춘 REST API에서부터 온갖 기능을 제공하는 고도로 확장 가능한 풀스택 실시간 웹 앱에 이르기까지 다양하다. 고 합니다. 

이번에는 Node.js를 이용한 서버 구축을 Node.js의 핵심 모듈인 http와 Connect 컴포넌트를 기반으로하는 Node.js 대표 웹 프래임워크인 Express를 이용해서 리펙토링해보았고, 그 과정에서 Express에 대해 배운 것을 정리하고자 합니다. 

 

1. Express 설치

http://expressjs.com/en/starter/installing.html

 

Installing Express

Installing Assuming you’ve already installed Node.js, create a directory to hold your application, and make that your working directory. $ mkdir myapp $ cd myapp Use the npm init command to create a package.json file for your application. For more inform

expressjs.com

설치를 하면 node_modules안에 express가 추가된 것을 볼 수 있습니다. 

 

2. Express - Getting Started - Hello world example

여기서 Express의 가장 기본이 되는 구조를 살펴보면 다음과 같습니다. 

http://expressjs.com/en/starter/hello-world.html

const express = require('express') 
const app = express()

app.get('/', (req, res) => {
  res.send('Hello World!')
})

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

이렇게 치고 저장 후 서버를 구동시키면 화면에 'Hello World!'가 찍혀있는 것을 볼 수 있다. 

각 줄의 코드가 무슨 의미이길래?!

 

① express 모듈과 Application이라는 객체 생성

const express = require('express') //'express'모듈을 가져와서
const app = express() //express를 함수처럼 호출한다. 그리고 그 함수의 리턴값은 app이라는 변수에 담겨있다.

app에 담긴 리턴값은 뭐냐면, Application이라는 객체이다. 

출처: Express 공식 홈페이지 http://expressjs.com/en/4x/api.html

Express 공식 홈페이지에 가면 이 Application이라는 객체의 생성과 그 객체에 대한 설명, method들이 잘 나와있다. 

 

② Application의 method 중 하나인 get

app.get('/', (req, res) => {
  res.send('Hello World!')
})

출처: Express 공식 홈페이지 http://expressjs.com/en/4x/api.html

get method 사용법을 보니 app.get(경로, 그 경로가 호출되었을 때(그 경로로 접속했을 때) 호출될 callback함수)로 되어있다. 

 

※ '경로' 가 나왔다. 여기서 Routing(Route) 의 개념이 필요하다. 

사용자들이 각각의 Path에서 어떤 요청을 보냈을 때 그 Path에 맞는 응답을 해 주어야하기 때문에 '경로'인자가 들어가는 것이고, 이에 맞게 상이한 알맞은 callback함수를 넣어주는 것이다. 

※ Express를 사용하지 않았을 때는 if 문을 통해서 어떤 경로일 때 무슨 callback이 실행되어야하는지 적어두었었다.

const requestHandler = function (request, response) {
  if(request.method === 'GET'){
    if(request.url === '/messages'){ 
      response.writeHead(200, headers); 
      response.end(JSON.stringify(results));
    }
    else{
      response.writeHead(404, headers);
      response.end();
    }
  }
};

그러나

※ Express를 사용 했을 때는 아래와 같이 쓰면 된다. 

/*
express.Router()의 의미
: 이를 이용해서 router로 분리할 수 있다. (이전처럼 if문을 복잡하게 쓰지 않아도 된다.)\
이렇게 분리할 수 있게 만든 router는 module.export를 통해 모듈로 만들어지고,
이를 통해 다른 파일에서 require하여 사용할 수 있다. 
*/
const router = express.Router()
router.get('/messages', cors(corsOptions), (req, res) => {
  res.send(results.results)
})

if 의 사용 유무 외에도 또 차이점은 

response 순서가 

※ Express를 사용하지 않았을 때

response.writeHead(200, headers); 
response.end(JSON.stringify(results));

※ Express를 사용 했을 때

 

response.send(results.results)

한 줄로 된다는 것이다. response.send(object)로 코드를 실행했을 때 함수의 실행 순서는 response.send(object)-> response.json(object)->response.send(string)이다. 그래서 굳이 JSON.stringify를 해주지 않아도 되는 것이다.

 

③  웹서버 실행

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

 


3. Routing

Routing은 특정URI 및 특정HTTP 요청 메소드(GET, POST 등) 에 대해 애플리케이션이 응답하는 방법을 결정하는 것을 말한다. 

 

위의

const express = require('express') 
const app = express()

app.get('/', (req, res) => {
  res.send('Hello World!')
})

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

이 코드는 app.get('/', 콜백) 이기 때문에  'http:localhost:8080/' 즉 home일때 실행되는 코드이다. 

그런데 내가 만든 client는 메뉴가 다양하고 그 메뉴를 눌러서 해당 페이지에 들어가면 ''http:localhost:8080/about' 이나 ''http:localhost:8080/project'..등 다양한 경로가 있다면??

 

그때 필요한 것이 Route Parameter이다. 

내 상세 경로(메뉴 중 하나를 클릭해서 들어간 창의 경로)가 들어있는 request.params을 잘 이용한다. 

예를 들어 메뉴 중 하나를 클릭해서 들어간 창의 경로가 http:localhost:8080/project라면

아래의 코드의 menuId는 project이고 request.params.menuId도 project이다. 

app.get('/:menuId', (request, response) => {
  //로 쓰면 되고, 
  //이 menuId 값을 request.params.menuId에 있다. 
})

4. get요청과 post 요청

위에서 살펴본 

app.get('/', (req, res) => {
  res.send('Hello World!')
})

형식과 동일하다.

또한 if 문을 사용하지 않고도 

app.get('/', (req, res) => {
  res.send('This is Get!')
})
app.post('/', (req, res) => {
  res.send('This is Post!')
})

같은 'http:localhost:8080/' 경로더라도 get요청을 하면 res.send('This is Get') 이 실행될 것이고, post 요청을 하면 res.send('This is Post') 가 실행된다.

※ Express를 사용하지 않았을 때

if (request.method === 'POST') {
    if (request.url === '/messages') {
      response.writeHead(201, headers)
      let message = ''
      request
        .on('data', (chunk) => {
          message += chunk
        })
        .on('end', () => {
          var post = JSON.parse(message) 
          results.results.push(post)
          response.end(JSON.stringify(message))
        })
    } else {
      response.writeHead(404, headers)
      response.end()
    }
  } else if (request.method === 'GET') {
    if (request.url === '/messages') {
      fs.readFile('data', (err, data) => {
        if (err) {
          console.log(err)
          response.writeHead(404, headers)
        }
        response.end(JSON.stringify(data))
        response.writeHead(200, headers)
      })
    }
  } else {
    response.writeHead(404, headers)
    response.end()
  }

※ Express를 사용 했을 때

router.get('/messages', cors(corsOptions), (req, res) => {
  res.send(results.results)
})

router.post('/messages', cors(corsOptions), (req, res) => {
  let post = req.body 
  results.results.push(post)
  res.send(results.results) 
})

router.use(function (req, res) {
  res.status(404).send('Sorry cant find that!')
})

5. Express Middleware

1) Middleware의 사용

http://expressjs.com/en/guide/using-middleware.html#middleware.third-party

 

Using Express middleware

Using middleware Express is a routing and middleware web framework that has minimal functionality of its own: An Express application is essentially a series of middleware function calls. Middleware functions are functions that have access to the request ob

expressjs.com

Express 공식홈페이지에 나온 설명에 따르면 Express는 기본적으로 일련의 미들웨어 함수의 호출로 구성되어있고, 

미들웨어는 요청 객체(req), 응답 객체(res), 다음 미들웨어 함수에 대한 접근권한을 가지는 next함수(그래서 이 next자리엔 그 다음에 호출되어야 할 함수 이름이 담겨있다.) 로 구성되어있다. 

출처: https://medium.com/aha-official/%EC%95%84%ED%95%98-rest-api-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C-3-daa2cce9d844

즉 미들웨어는 중간 처리 역할을 수행한는 소프트웨어를 의미합니다. 

 

Express에는 use the following types of middleware: 아래의 타입들의 미들웨어가 있다. 

이중 특히 Third-party middleware의 body parser middleware을 사용해보면, body는 웹브라우저제에서 요청한 정보의 본체를 말하고, 이 본체를 설명하는 데이터를 header라고 한다. 

이전에 이 정보인 body를 parser해서 우리가 필요한 형태로 가공하기 위해서(parsing(parse)는 가지고 있는 데이터를 내가 원하는 형태의 데이터로 '가공'하는 과정을 말한다.) body-Parser가 특정 문자를 기준으로 파싱하여파싱한 결과 body에 객체 형태로 데이터가 담기고, 그러면 req.body에 이 객체를 저장한다. 그래서 클라이언트 측에서 { name: 'yejinh', job: ...} 와 같은 JSON 형식의 바디를 보내면 서버 측에서 req.body 혹은 req.body.name, req.body.job 등으로 해당 데이터에 곧바로 접근할 수 있게 된다

 

※ Express를 사용하지 않았을 때

저 네모친 부분이 

※ Express에서 body parser middleware를 사용 했을 때

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

router.post('/messages', cors(corsOptions), (req, res) => {
  let post = req.body //var post = JSON.parse(message);
  results.results.push(post)
  res.send(results.results)
})

이렇게 간결하게 코딩할 수 있다. 

 

2) Middleware의 생성

http://expressjs.com/en/guide/writing-middleware.html

 

Writing middleware for use in Express apps

Writing middleware for use in Express apps Overview Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle. The next function is a func

expressjs.com

위의 문서에는 Middleware를 만드는 방법이 나와있고, myLogger라는 미들웨어를 만드는 것을 예시로 보여준다. 

아래 코드를 보면, 

// Middleware function myLogger
// Here is a simple example of a middleware function called “myLogger”

var express = require('express')
var app = express()

var myLogger = function (req, res, next) {
  console.log('LOGGED')
  next()
}

app.use(myLogger)

app.get('/', function (req, res) {
  res.send('Hello World!')
})

app.listen(3000)

app.use를 사용하고 그 인자로 어떤 변수(myLogger)을 준다. 

그 변수(myLogger)는 함수네요. var myLogger = function (req, res, next) {

그리고 그 함수의 req, res, next를 인자를 가지고 있다. 

 

여거서 알 수 있듯이 Express에서는 myLogger와 같은 함수형태를 가진 것을  middleware라고 한다. 그리고 그 함수를 어떻게 구현하느냐에 따라서 다른 기능을 하는 Third party middleware가 되는 것이다.

 

3) Middleware의 실행 순서

미들웨어는 Node와 다르게 순차적으로 실행되기때문에 코드 순서가 매우 중요하다. 

여러 middelware 타입 중

var express = require('express')
var app = express()

app.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
})

app.use에 함수를 인자로 넣어주게 되면 이 함수는 middelware로써 등록되고,

middleware의 핵심은 request와 response 객체를 받아서 그것을 변형할 수 있고,

next를 호출함으로써 그 다음에 실행되어야 할 middleware의 실행 유무를 결정한한다. 

뿐만아니라

 

app.use('/user/:id', function (req, res, next) {
  console.log('Request Type:', req.method)
  next()
})

이처럼 app.use에 특정 경로를 인자로 줌으로써 특정 경로일때만 해당 middleware을 실행하게 할 수도 있다.  

그리고

 

app.get('/user/:id', function (req, res, next) {
  res.send('USER')
})

요청받은 method가 get 방식일때만 해당 middleware을 실행하게 할 수도 있다. 

 

middleware를 여러개

middleware를 여러개 붙일 수도 있다. 여기서 next()를 호출하면 그 다음의 함수를 실행하라는 것과 마찬가지.

즉 순서대로 ↓ 진행되는 것이다. 

④-1

app.use('/user/:id', function (req, res, next) {
  console.log('Request URL:', req.originalUrl)
  next()
}, function (req, res, next) {
  console.log('Request Type:', req.method)
  next()
})

④-2

app.get('/user/:id', function (req, res, next) {
  console.log('ID:', req.params.id)
  next()
}, function (req, res, next) {
  res.send('User Info')
})

// handler for the /user/:id path, which prints the user ID
app.get('/user/:id', function (req, res, next) {
  res.end(req.params.id)
})

둘다 app.get 메서드에 경로도 '/user/:id'로 같다. 

순서대로 실행되기 때문에 제일 먼저

function (req, res, next) {
  console.log('ID:', req.params.id)

가 실행되고

next()가 되면서

function (req, res, next) {
  res.send('User Info')
})

가 실행되고 여기선 아무런 next함수 실행 여부가 적혀있지 않기 때문에 두번째 app.get은 실행되지 않고 여기서 끝난다. 

반면

④-3

이 경우에는 조건문을 사용해서

app.get('/user/:id', function (req, res, next) {
  // if the user ID is 0, skip to the next route
  if (req.params.id === '0') next('route')
  // otherwise pass the control to the next middleware function in this stack
  else next()
}, function (req, res, next) {
  // send a regular response
  res.send('regular')
})

// handler for the /user/:id path, which sends a special response
app.get('/user/:id', function (req, res, next) {
  res.send('special')
})
if (req.params.id === '0') next('route')

 

이 경우이면 next('route') 즉 다음 라우트의 미들웨어를 실행시키라는 의미이므로

// handler for the /user/:id path, which sends a special response
app.get('/user/:id', function (req, res, next) {
  res.send('special')
})

이게 실행된다. 

반면 

else next()

이 경우이면 바로 다음 미들웨어를 실행시키라는 것이므로 

function (req, res, next) {
  // send a regular response
  res.send('regular')
})

이게 실행된다. 


6. 에러처리 

http://expressjs.com/en/guide/error-handling.html

 

Express error handling

Error Handling Error Handling refers to how Express catches and processes errors that occur both synchronously and asynchronously. Express comes with a default error handler so you don’t need to write your own to get started. Catching Errors It’s impor

expressjs.com

① 위에 있는 라우터 경로에 하나도 해당이 안되는 경우 

router.use(function (req, res) {
  res.status(404).send('Sorry cant find that!')
})
//+ 맨뒤에 에러처리 middleware을 붙여준다. 왜냐하면 middleware는
//순차적으로 처리하는데 여기까지 내려왔다는건 처리를 못하고 에러가 났다는 거니까

② 아니면 중간에 에러처리를 해주고 싶다면 조건문을 사용해서 

if 에러가 있으면 next(err) 라고 next의 인자로 err을 준다. 그러면 express내에 있는 에러처리 미들웨어를 사용하게된다. . 

router.get('/messages', cors(corsOptions), (req, res) => {
     if(err){
    	next(err)
     }
     else {
     	res.send(results.results)
     }
})

에러 발생시 나오는 문구 등 error handlers을 작성하고 싶다면, 코드 맨 끝에 

아래와 같은 형식으로 적어준다. 

Node.js는 function(err, req, res, next) 처럼 인자가 4개가 등록되어있는 error-handling function으로 정의하기 때문이다. 

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})


7. CORS

https://github.com/expressjs/cors

 

expressjs/cors

Node.js CORS middleware. Contribute to expressjs/cors development by creating an account on GitHub.

github.com

여기에 매우 잘 정리되어 있다. 

express에서는

  • origin: Access-Control-Allow-Origin CORS 헤더를 구성.
  • methods: Access-Control-Allow-Methods CORS 헤더를 구성. 쉼표로 구분 된 문자열 (예 : 'GET, PUT, POST') 또는 배열 (예 :)이 필요 ['GET', 'PUT', 'POST']한다.
  • allowedHeaders: Access-Control-Allow-Headers CORS 헤더를 구성.
  • credentials: Access-Control-Allow-Credentials CORS 헤더를 구성.
  • maxAge: Access-Control-Max-Age CORS 헤더를 구성.

그리고 이들은 배열이나 쉼표로 구분된 문자열로 지정해주면 된다.

※ Express를 사용하지 않았을 때

const headers = defaultCorsHeaders;
const defaultCorsHeaders = {
  "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 // Seconds.
};

※ Express를 사용 했을 때

const cors = require('cors');
server.use(cors);

var corsOptions = {
  origin: '*' ,
  methods: 'GET, PUT, POST',
  allowedHeaders: 'Content-Type, Accept',
  maxAge: 10
}

 

Reference

https://wikibook.co.kr/article/what-is-expressjs/

 

Express.js란 무엇인가? | 위키북스

1장 Express.js란 무엇인가? Express.js는 Node.js의 핵심 모듈인 http와 Connect 컴포넌트를 기반으로 하는 웹 프레임워크다. 그러한 컴포넌트를 미들웨어(middleware)라고 하며, 설정보다는 관례(convention over c

wikibook.co.kr