[Nodejs] express와 koa의 미들웨어 차이점

HANJIN·2020년 9월 15일
2

Nodejs

목록 보기
1/1
post-thumbnail

시작하며

안녕하세요🙌
오랜만의 포스팅입니다. 취직한뒤로 게으름을 부리며 블로그포스팅을 게을리했음을 반성합니다...😢

최근에 express에서 koa로의 프레임워크 전환을 시도하면서, 단순히 express와 koa가 비슷한 flow를 가지고있고 미들웨어의 사용법 또한 동일하다고 생각하면서 코드를 작성하였으나, 생각대로 동작하지 않아 당황한 경험이 있습니다. 해당 이슈를 해결한 기념으로 해당 포스팅을 작성하려고합니다!


배경

먼저 문제의 상황은 이러했습니다.
express내에서 미들웨어를 사용하여 request general한 로깅을 해둔 부분이 있었는데,
koa에서 해당 코드를 그대로 사용하니 요청이 완료되지 않는 겁니다!!

express에서의 기본적인 미들웨어는 req,res,next를 인자로 받으며 next()가 호출이 되면 다음 미들웨어로 넘어가는 구조입니다.

예제 코드입니다.

const app = express()

app.use((req, res, next) => {
  console.log('요청을 받았습니다')
  next()
})

저는 지금까지 koa에서는 req,res대신 ctx(koajs context)를 인자로 받는다는 점 외에는 동일하다고 알고 있었습니다.
그래서 koa에서는 아래와 같은 코드로 바꿔서 사용해주었지요

const app = new Koa()

app.use((ctx, next) => {
  console.log('요청을 받았습니다')
  next()
})

그런데 이게 왠걸, 저 코드가 있기전에는 기본 라우팅이 동작했었는데 갑자기 404 응답을 주기 시작했습니다.

그래서 몇번 코드를 바꿔서 시도해봐도 방법을 찾을 수 없었기 때문에 koa의 코드를 들여다보기 시작했습니다.

해결과정

아래는 koa에서 미들웨어를 등록하고 요청을 받았을때 처리하는 핵심적인 3개의 함수입니다.

  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

위의 함수에 대한 설명을 잠깐해보자면,
use(fn)함수는 새로운 함수를 인자로 받아 this.middleware이라는 Array타입의 변수에 push합니다.
callback()함수는 미들웨어를 compose라는 함수에 미들웨어(함수)들의 배열을 집어넣은 결과값을 fn이라는 변수에 저장합니다. 그리고 새로운 ctx를 만들어서 handleRequest함수에 fn과 함께 넘겨주네요.
handleRequest함수는 미들웨어가 전부 resolve되면 response를 전송하는 정도로 보입니다.
특별히 눈에띄는 부분은 보이지 않네요. 그럼 블랙박스인 부분은 compose함수가 되겠네요. 같이 보실까요?

compose함수는 다음과 같은 코드로 이루어져 있습니다.

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

compose함수는 미들웨어의 구조로 된 새로운 함수를 리턴합니다. 코드를 분석해볼까요?
ctx와 next를 인자로 받고, dispatch(0)을 리턴합니다.
dispatch(i) 함수는 인덱스를 따라올라가면서 미들웨어들을 엮어줍니다.
n번째 미들웨어에 ctx와 n+1번째 미들웨어를 인자로주고 실행합니다.
즉 이런느낌인거죠

middleware0(ctx,middleware1(ctx,middleware2(ctx,middleware3(....middlewareN))

결과적으로 모든 미들웨어를 실행하고나면 dispatch(0)이 resolve되는것이지요

koa 미들웨어의 동작 방식

위의 구조를 보고 유추할 수 있는 점은 이렇습니다.
1. next()를 호출하지않으면 다음 미들웨어는 동작하지 않는다.
2. 미들웨어가 asynchronous할 경우(비동기적일 경우) return next() 혹은 await next()로 프로미스 체이닝을 이어가줘야만 미들웨어를 처리한 이후 response를 보낸다.
3. 미들웨어 내에서 await 키워드가 사용되지 않거나, 프로미스체이닝으로 이어지지 않을경우, 요청이 끝난후에 동작한다.
3-1. 즉 미들웨어에서 await next()를 하는 부분이 순차적 실행으로 이어지기 때문에, 미들웨어 내에서 await next() 이후에 작성된 코드는 프로미스체인이 끝나면(응답이 완료되면) 동작하게 됩니다.
(2,3번 부분에서 왜인지 이해가 되지 않으신다면 js의 promise에 대해서 찾아보시는 것을 추천드립니다.)

여기서 제 경우는 이러했습니다. 제가 사용한 코드 이후에 오는 미들웨어가 비동기코드였기때문에 체이닝이 이어지지않아, 응답 값(status, body)이 set되기 전에 response가 이루어지게되면서 기본 값인 404가 응답으로 보내지게 되었던 것입니다.

꽤 많은 시간을 소비했지만, 좋은 내용에 대해 알게되어 유익한 경험이었습니다!
다음에도 유익한 정보와 함께 돌아오겠습니다. 감사합니다🙇‍♂️

내용에 대한 피드백은 항상 감사히받겠습니다.

profile
소프트웨어 엔지니어.

0개의 댓글