express 와 async await 그리고 error 처리

반응형

express 는 listen 으로 서버를 시작한다.

그리고 request, response 를 가진 http 요청을 받는 http 처리 서버가 된다.

그리고 기본 서버에 다양한 middleware 가 추가되어 요청을 원하는 방식으로 처리한다.

express 의 미들웨어는 일반적인 미들웨어와 맨 앞에 err 를 받는 에러 미들웨어 두개가 존재한다.

// 일반적인 미들웨어
function commonMiddleware(req, res, next) {
  console.log('common middleware')
  next(new Error('error occurred'))
}
// 에러 미들웨어
function errorMiddleware(err, req, res, next) {
  console.log(err.message)
  // 에러처리
  next()
}

github.com/expressjs/express/blob/master/lib/router/layer.js#L77-L99

 

expressjs/express

Fast, unopinionated, minimalist web framework for node. - expressjs/express

github.com

이러한 미들웨어의 동작방식을 들여다 보면 다음과 같다. 일반적인 next() 가 호출되면 그 다음 미들웨어가 호출되고 next(err) 와 같이 호출되면 에러 미들웨어가 호출되며 미들웨어 함수 로직에 에러가 발생하면 또한 에러 미들웨어가 호출된다. 아래 로직은 expressjs 기본 미들웨어인 router 미들웨어 로직 중의 layer 의 일부인데 일반적인 미들웨어 함수 구현부이다. fn.length 에서 함수의 length 는 arguments 의 갯수를 말하며 따라서 fn(req, res, next) 와 같이 세개의 파라미터가 아니면 일반적인 미들웨어 함수가 아니라서 그냥 다음 미들웨어로 넘어가는 것을 보여주며 실제 수행부는 try / catch 로 감싸져 있는 것을 볼 수 있다. handle_error 는 err, req, res, next 네개의 파라미터를 갖는 함수로 구현되어 있다. 실제 next 자체의 구현은 router/index.js 의 proto.handle 안에 function next(err) 로 되어 있다. next 의 파라미터로 err 를 넣어주면 에러 처리용 미들웨어를 호출하는 것이다. expressjs 의 미들웨어의 동작방식은 koa 나 redux-saga 의 비동기 처리를 기본으로한 미들웨어와 조금 다르다. 오히려 더 단순하고 직관적이다. koa 나 redux-saga 의 방식은 next, next 되는 방식은 유사하지만 해당 next 를 수행하고 리턴되어 다시 복귀하는 구조를 가진다. 좀 더 자세히 말하면 Promise 나 generator 의 로직을 수행하고 그것을 리턴하는 방식이다. expressjs 처럼 단순히 next, next 그리고 마지막 next 되면 끝나는 형태의 구조를 가지면 비동기 함수를 미들웨어 방식으로 처리할 수가 없다. 미들웨어의 마지막 단에 비동기 함수 로직이 들어가서 해당 함수를 async 로 붙여줄 경우 정도가 가능할 것이다. koa 나 redux-saga 같은 경우 비동기 함수 로직을 계속 추가되는 순차적인 로직의 형태인 미들웨어 형태로 구현할 수 있도록 미들웨어가 Promise 나 generator 객체를 리턴하도록 되어 있다. 하지만 직관적인 측면에서 expressjs 를 사용하는 것이 오히려 나은 것으로 보인다. expressjs 의 마지막 router / controller 단의 구현부는 async 로 해주고 그 앞의 부분의 미들웨어들의 구현은 이런 async 방식을 적용하지 않는 방식이 될 것 같다.

그리고 expressjs 의 wrapper 이후 즉 async 가 붙은 이후 부터의 next 의 앞에도 await 를 붙여주어야 한다. 하지만 마지막 router 의 controller 에 async 가 붙으니 그 이후 next 를 호출하여 다음 미들웨어를 호출할 일은 없을 듯 하다.

이런걸 보면 오히려 koa 를 사용하는게 더 좋을려나? expressjs 는 마지막 publishing 이 1년도 넘은 것 같고 koa 는 2달정도인 걸 보면 ... 하지만 보편적인 측면에서 expressjs 가 더 나은 것 같긴한데. TDD 까지 고려해서 다시 한 번 생각해보자.

/**
 * Handle the request for the layer.
 *
 * @param {Request} req
 * @param {Response} res
 * @param {function} next
 * @api private
 */

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

async 함수는 Promise 를 리턴하고 해당 에러처리를 하려면 async 함수 내에서 try / catch 되어야 하지만 function handle(req, res, next) 은 async 함수가 아니기 때문에 router 와 같은 미들웨어에서 async 로 된 컨트롤러 로직이 있을 경우 해당 비동기 방식의 함수 로직의 에러 처리를 expressjs 의 기본 에러처리 방식에서 처리하지 못한다. 이를 위해 데코레이터 패턴을 사용하여 wrapper 함수를 만들어 사용하거나 async 가 붙은 각 컨트롤러 부분에 직접 try catch 로 감싸주면 문제가 해결된다. koa 나 nest 등 다양한 라이브러리들이 있지만 express 가 가장 널리 쓰이고 관련 지원 라이브러리들도 많아 약간의 이러한 태생적 문제점은 있지만 이렇게 수정하여 사용하는 것이 현재로서는 가장 좋은 방법으로 보인다.

// wrapper 사용 (데코레이터 패턴)
const wrap = asyncFn => {
  return (async (req, res, next) => {
    try {
      return await asyncFn(req, res, next)
    } catch (error) {
      return next(error)
    }
  })  
}

router.get('/', wrap(async (req, res, next) => {
  const result = await foo();
  res.send(result);
}))
// wrapper 사용 (데코레이터 패턴)
fn => async (req, res, next) => await fn(req, res, next).catch(next);
// express-asyncify 라이브러리 사용
const express = require('express');
const asyncify = require('express-asyncify');
 
const app = express();
const router = asyncify(express.Router());
 
// ...
 
router.get('/', async (req, res) => {
    const result = await foo();
    res.send(result);
});

 

아래는 참고

 

programmingsummaries.tistory.com/399

 

Express.js 라우팅 핸들러에 async/await 을 적용할 수 있을까?

들어가며 지난 2017년 2월 22일, node.js 의 자바스크립트 엔진인 V8 이 5.5 버전으로 업그레이드되면서 특별한 옵션 없이도 바로 async/await 을 네이티브로 사용할 수 있게 되었다. 물론 이전 버전의 nod

programmingsummaries.tistory.com

sustainable-dev.tistory.com/79

 

Express 라우터에 async/await

프론트에서 유튜브 비디오 아이디가 넘어오면 백에서 진행되는 단계는 다음과 같다. 1. 비디오 아이디로 API 요청 후 댓글 가져오기 2. 특수문자, ㅠㅠ, ㅋㅋ, ㅎㅎ와 같은 한글, 숫자, 한글 이외의

sustainable-dev.tistory.com

kjwsx23.tistory.com/199

 

[Express] router에서 async await callback사용하기

안녕하세요, Einere입니다. (ADblock을 꺼주시면 감사하겠습니다.) 오늘은 Express의 router에서 async await를 활용한 callback을 사용하는 법을 알아보도록 하겠습니다. Async / Await const asyncFunction = (..

kjwsx23.tistory.com

m.blog.naver.com/n_jihyeon/221806066778

 

Node.js (Express.js) 에서 mysql async/await 로 사용하기, 예제

Express.js 에서 mysql 에 async/await 사용하기​안녕하세요.node.js 로 서버를 구성하는 모든 분들, 반...

blog.naver.com

 

정작 에러처리 미들웨어 적용하는 것에 대한 것을 적지 않아 다음과 같이 추가한다.

 

app.js

const express = require('express');
const path = require('path');
const dotenv = require('dotenv');

dotenv.config();
const apiRouter = require('./routes/api');

const app = express();

// 일반적인 미들웨어
function commonMiddleware(req, res, next) {
  console.log('common middleware');
  next(new Error('error occurred'));
}

app.use(express.static(path.join(__dirname, 'public')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use('/api', apiRouter);
// 다음과 같이 get * 부분이 있으면 아래의 404까지 안가고 모두 index.html 을 render 해준다.
// app.get('*', (req, res, next) => {
//   res.sendFile(path.join(__dirname, 'public', 'index.html'));
// });
app.use(function (req, res, next) {
  res.status(404).json({ message: 'Not Found' });
});

app.use(function (err, req, res, next) {
  if (res.headersSent) {
    // 이미 응답된 상황에서 오류가 발생시 (스트리밍과 같을 때) expressjs 의 기본 에러 처리기에 전달한다.
    // 연결을 닫고 요청 실패처리를 함.
    next(err);
    return;
  }
  res.status(500).json({ message: 'Error' });
});

module.exports = app;

 

server.js

#!/usr/bin/env node

const dotenv = require('dotenv');
const http = require('http');
const app = require('./app');

/**
 * 포트는 unix socket 인 파이프일 수도 있고 숫자로된 port 일 수도 있다. 또한 "3050" 같이 숫자 포트가 문자열로 들어올 수도 있다.
 * @param val
 * @returns {boolean|number|*}
 */
function normalizePort(val) {
  const port = parseInt(val, 10);

  if (Number.isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

dotenv.config();
const normalizedPort = normalizePort(process.env.PORT || 3050);
app.set('PORT', normalizedPort);

const { sequelize } = require('./models');

sequelize
  .sync({ force: true })
  .then(() => {
    console.log('DB 연결 성공');
  })
  .catch(console.error);

// 에러 처리 부분은 node http 상의 오류들이기 때문에 http.createServer 를 이용해 server 를 만들어주었다.
// app.listen 의 리턴값도 http.createServer(app) 과 같은 http 서버라서 app.listen 을 해도 된다.
// app.listen 을 사용하면 require('http') 를 생략할 수 있다.
// 하지만 http.createServer 와 달리 app.listen 을 하면 즉시 실행되므로 server.on('listening', ...) 과 같이
// 사용하려면 createServer 를 사용해야한다. 그런데 server.on 부분이 server.listen 아래에 있어도 원하는대로 동작한다.
// 그래도 server.listen 을 밑으로 빼주자.

// const server = app.listen(app.get('PORT'));
const server = http.createServer(app);

// 이 에러 처리기의 에러들은 expressjs 의 미들웨어 에러 처리기와는 다르다.
server.on('error', (error) => {
  if (error.syscall !== 'listen') {
    throw error;
  }

  const port = app.get('PORT');
  const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(`${bind} requires elevated privileges`);
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(`${bind} is already in use`);
      process.exit(1);
      break;
    default:
      throw error;
  }
});
server.on('listening', () => {
  const addr = server.address();
  const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
  console.log(`Listening on ${bind}`);
});

server.listen(app.get('PORT'));

// const server = app.listen(app.get('PORT'), () => {
//   const addr = server.address();
//   const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
//   console.log(`listening on ${bind}`);
// });

 

 

마지막 에러처리에서 next(err) 하거나 어떠한 에러처리 미들웨어도 등록되지 않았다면 expressjs 의 기본 에러처리에 따른다.

github.com/expressjs/express/blob/master/lib/application.js

final handler 부분의 onerror 부분을 보면 logerror 가 등록되어 있다.

 // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });
  
  function logerror(err) {
  /* istanbul ignore next */
  if (this.get('env') !== 'test') console.error(err.stack || err.toString());
}

 

expressjs 는 내장된 오류 핸들러와 함께 제공되며 내장 오류 핸들러는 앱에서 발생할 수 있는 모든 오류를 처리한다.

이러한 기본 오류 처리 미들웨어 함수는 미들웨어 함수 스택의 끝에 추가된다.

 

  next('test error'); // 이렇게 전달할 경우 err.stack 은 없게 되고 err.toString() 이 되어 그냥 test error 가 출력.

  next(new Error('test Error'))  // err.stack 이 있으며 stacktrace 가 출력.

  throw new Error('test Error'))  // next(new Error('test Error')) 와 동일함.

 

throw new Error 도 된다. 단 위에서 언급했듯이 async 함수인 미들웨어에서 throw new Error 를 했을 경우 try catch(error) 로 잡아서 명확히 next(error) 해주지 않으면 에러처리가 되지 않고 서버는 강제 종료되고 클라이언트는 응답을 받지 못하게 된다.

new Error 로 생성된 객체를 console.log 하면 err.stack 을 출력한다.

미들웨어 내에서 위와 같이 호출할 경우 오류로 간주되어 에러처리 미들웨어로 전달된다.  각 에러처리 미들웨어에서도 마지막까지 에러를  next 나 throw 할 경우  expressjs 기본 오류 처리 미들웨어까지 전달된다. 기본 오류 처리 미들웨어는 위의 코드에서 보면 onerror 에 있는 로깅하는 logerror 가 호출되며 또한 클라이언트에 상태코드와 함께 응답도 보내준다.

 

기본 오류 처리 미들웨어에서 오류를 응답하게 되면 다음과 같은 정보가 전달된다.

res.statusCode 에 err.status (또는 err.statusCode) 가 들어간다. 만약 이 값이 4xx 나 5xx 가 아니라면 500 으로 셋팅된다.

res.statusMessage 가 statusCode 값에 따라 들어간다. 

응답의 body 는 production 에서는 status code message 의 HTML 이 될 것이고 그밖의 경우에는 err.stack 이 될 것이다.

err.headers 객체에 적절한 헤더들도 셋팅된다.

 

응답을 이미 시작한 다음에 next(error) 가 호출될 경우 (예를 들어 streaming 응답 도중에 에러를 만날 경우) expressjs 의 기본 에러 처리기는 연결을 닫고 요청을 실패처리한다. 그러므로 직접 만든 에러 미들웨어를 추가했을 때에는 다음과 같이 이에 대해서 기본 expressjs 에러 처리기로 위임하는 것을 넣어주어야 한다.

 

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500)
  res.render('error', { error: err })
}

 

기본 expressjs 의 경우 없는 url 요청을 할 경우 404 응답을 내려주는데 Cannot GET xxxurl 과 같은 문자열을 render 해준다.

이러한 404 상황은 에러로 간주되지 않아 에러처리 미들웨어들로 전달되지 않는다.

expressjs 에서 404 응답은 오류로 인해 발생하는 결과가 아니며, 따라서 에러 핸들러 미들웨어는 이를 파악하지 않는다. 이렇게 작동하는 이유는 404 응답은 단순히 실행해야 할 추가적인 작업이 없다는 것, 즉 expressjs 는 모든 미들웨어 함수 및 라우트를 실행했으며 이들 중 어느 것도 응답하지 않았다는 것을 나타내기 때문이다. 이를 처리하려면 다음과 같이 404 응답을 처리하기 위한 미들웨어 함수를 스택의 가장 아래 (다른 모든 함수의 아래) 에 추가하기만 하면 된다. 이것을 넣지 않아도 마지막 까지 해당되지 않을 경우 404 상태코드와 Cannot Get xxxurl 이라는 응답을 html render 해준다.

 

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

 

다음과 같은 모양으로 에러 핸들러를 만들어 줄 수 있다.

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

 

다음과 같이 clientErrorHandler 를 정의할 수 있다. 이 경우 에러는 명확하게 다음으로 넘겨준다.

에러 핸들러에서 next 를 해주지 않을 경우 응답에 대해서는 알아서 책임져야 한다. 그렇지 않으면 응답은 hang 에 걸리고 garbage callection 에 수집되지 않을 것이다.

function clientErrorHandler (err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' })
  } else {
    next(err)
  }
}

 

모든 에러를 잡는 errorHandler 는 다음과 같이 구현될 수 있다.

function errorHandler (err, req, res, next) {
  res.status(500)
  res.render('error', { error: err })
}

 

위에서 next('test error') 처럼 문자열을 넣어줘도 에러 처리 핸들러로 넘어가지만 특이하게 next('route') 의 경우는 그대로 다음 일반 미들웨어로 넘어간다. 또한 이 next('route') 의 경우 좀 특이한 기능을 한다. 다음과 같이 get 을 사용할 경우 해당 url 에 대한 get 요청에 대해 미들웨어를 연속으로 계속 등록해줄 수 있는데 이 때 next('route') 라고 해줄 경우 그 바로 다음 미들웨어를 건너뛰고 그 이후의 미들웨어로 넘어간다. 따라서 get 이 아니라 use 일 경우에는 그냥 다음 미들웨어로 넘어가는 것. next 에  'route' 라는 문자열일 경우만 이렇다.

app.get('/a_route_behind_paywall',
  function checkIfPaidSubscriber (req, res, next) {
    if (!req.user.hasPaid) {
      // continue handling this request
      next('route')
    } else {
      next()
    }
  }, function getPaidContent (req, res, next) {
    PaidContent.find(function (err, doc) {
      if (err) return next(err)
      res.json(doc)
    })
  })

 

에러처리 미들웨어의 경우 express 단의 미들웨어에서 오류를 처리하는 방식이다.

다음과 같이 app.listen 의 리턴값은 노드의 http 서버이다. 

 

const app = express();

const server = app.listen(3050);

아래의 error.syscall, error.code EACCESS , EADDRINUSE 와 같은 것들은 노드의 http 서버 관련 오류이다. 이를 expressjs 의 에러처리 미들웨어로 처리하려고 하면 안된다. 에러처리 미들웨어는 요청이 오는 상황에서 에러를 다루는 것이고 http server on error 의 경우 서버 시작부터 발생될 수 있는 이미 사용중인 주소와 같은 에러까지 다루는 것이다. 서버가 실행되어야 미들웨어도 등록될 것이다.

// const server = app.listen(app.get('PORT'));
const server = http.createServer(app);

// 이 에러 처리기의 에러들은 expressjs 의 미들웨어 에러 처리기와는 다르다.
server.on('error', (error) => {
  if (error.syscall !== 'listen') {
    throw error;
  }

  const port = app.get('PORT');
  const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(`${bind} requires elevated privileges`);
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(`${bind} is already in use`);
      process.exit(1);
      break;
    default:
      throw error;
  }
});

 

 

 

 

 

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY