왜 koa를 쓸까요?

멘토분께서 koa로 개발을 하자고 해서 서버를 koa로 구현한 적이 있어요.
그런데 왜 koa를 쓸까에 대한 의문은 있었어요.
koa... 열심히 구글링 해본 결과
es6 async await이 지원이 된대요. 이게 뭔말이에요?
express도 async await 쓸 수 있는데요??

좀더 자세히 찾아본 결과
support async await을 정확히 짚어준 블로그를 찾았고요.
직접 눈으로 확인해보기 위해서 다음과 같이 express로 서버를 만들고 koa로도 서버를 만들어서 비교를 해봤습니다.

express 준비

mkdir expressExample && cd expressExample
yarn init -y
{
  "name": "expressexample",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

여기에 npm 사용자 정의 명령어를 추가해줍니다.

"script"{
    "babel": "babel-node app.js"
},

사용방법
터미널에서 다음과 같이 명령어를 입력해줍니다.

npm run babel

특별한 명령어도 있어요.
start, test 등은 npm start, npm test 처럼 run을 안붙여도 되요.

패키지 설치

yarn add express @babel/core @babel/node @babel/preset-env

body-parser는 express에 내장되었습니다.

프로젝트 디렉토리

app.js
babel.config.js
package.json

결과적으로 package.json은 다음과 같습니다.

{
  "name": "expressexample",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "babel": "babel-node app.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  },
  "devDependencies": {
    "@babel/core": "^7.6.4",
    "@babel/node": "^7.6.3",
    "@babel/preset-env": "^7.6.3"
  }
}

babel.config.js도 루트디렉토리에 만들었어요.

module.exports = {
    "presets": [
        "@babel/preset-env"
    ]
}

이제 서버를 구현해볼게요.
app.js를 루트 디레토리에 생성합니다.

import express from "express";

export const app = express();

const PORT = 8000;

app.get("/", async (req, res, next) => {

    const delay = (duration) =>
        new Promise(resolve => setTimeout(resolve, duration));

    const asyncFunc = async () => {
        throw new Error("error occured");
        return await delay(1000);
    };
});

app.use((req, res) => {
    res.status(404).send("Not Found");
});


app.use((err, req, res, next) => {
    res.status(500).send("Internal Server Error");
});

app.listen(PORT, () => {
    console.log(`8000번 포트에서 서버 실행중`);
})

이제 서버를 실행해놓고요.

npm run babel

postman으로 요청을 보낼게요.

express는 async 내부에서 발생하는 에러를 핸들링하지 못해요.

image.png

npm run babel

> babel-node app.js

8000번 포트에서 서버 실행중
(node:96931) UnhandledPromiseRejectionWarning: Error: error occured
...

만약 이렇게 처리가 되지 않은 Promise reject때문에 서버 앱이 종료되면 매우 위험해집니다.

try catch로 잡아볼게요.
라우터 부분만 바꿨습니다.

app.get("/", async (req, res, next) => {

    const delay = (duration) =>
        new Promise(resolve => setTimeout(resolve, duration));

    const asyncFunc = async () => {
        throw new Error("error occured");
        return await delay(1000);
    };

    try {
        await asyncFunc();
        res.send(`hello express`);
    } catch (error) {
        next(error);
    }

});

try catch 적용후

에러를 잡습니다.

image.png

express에서는 async 내부에서 발생한 에러를 try catch로 잡아야합니다.


koa 준비

이제 koa로 서버를 만들어볼게요.

koa는 router, body-parser 까지 모듈로 쓴다는 것도 express와 차이점으로 얘기할 수 있겠네요.

mkdir koaExample && cd koaExample
yarn init -y
yarn add koa koa-router
yarn add -D nodemon @babel/cli @babel/core @babel/preset-env

프로젝트 구조는 다음과 같습니다.

-dist
-src
    -app.js
package.json
babel.config.js

package.json

{
  "name": "koaexample",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "babel src -d dist",
    "start": "nodemon dist/app.js"
  },
  "dependencies": {
    "koa": "^2.10.0",
    "koa-router": "^7.4.0"
  },
  "devDependencies": {
    "@babel/cli": "^7.6.4",
    "@babel/core": "^7.6.4",
    "@babel/plugin-transform-runtime": "^7.6.2",
    "@babel/preset-env": "^7.6.3",
    "@babel/runtime": "^7.6.3",
    "nodemon": "^1.19.4"
  }
}

babel.config.js

module.exports = {
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [
        ["@babel/transform-runtime"]
    ]
}

koa로 할때는요. 에러가 발생하는거에요.
image.png

그래서 stackoverflow에서 방법을 찾아서 해결했습니다. babel 7버전이 문제라고 하네요.

yarn add @babel/runtime 
yarn add -D @babel/plugin-transform-runtime

app.js를 만들었어요.

import koa from "koa";
import Router from "koa-router";

export const app = new koa();
const router = new Router();

const PORT = 8000;

router.get("/", async ctx => {

    const delay = (duration) =>
        new Promise(resolve => setTimeout(resolve, duration));

    const asyncFunc = async () => {
        throw new Error("error occured");
        return await delay(1000);
    };
    await asyncFunc();
    ctx.body = 'Hello World';
});

app.use(router.routes());
app.use(router.allowedMethods());

app.use((ctx, next) => {
    ctx.status = 404;
    next();
});

app.on('error', async (err, ctx) => {

    if (ctx.status === 404) {
        ctx.body = "Not Found";
    }
    ctx.body = "Internal Error";
});

app.listen(PORT, () => {
    console.log("8000번 포트를 실행중입니다.");
});

서버를 실행해놓고

npm run build

postman으로 요청을 날려볼게요.

koa는요 core도 async await으로 구현이 되어있대요.
express와 달리 async 내에서 발생하는 에러에 대해서
에러를 잡을 수가 있어요. try catch문이 없어도요.

image.png

레퍼런스 : from express to koa - HISWEPEDIA
레퍼런스 : Express 라우트에서 Async Await를 사용하려면? - Changjoo Park
레퍼런스 : ReferenceError: regeneratorRuntime is not defined - stackoverflow
레퍼런스 : koa를 사용하여 웹서버 만들기 - velopert
레퍼런스 : Why is Koa better than Express when async await is enabled for both? - reddit
레퍼런스 : Error - MDN
레퍼런스 : How to choose the best Node.js framework: Express.js, Koa.js or Sails.js - Cleveroad

정리

사실 express, koa 둘다 오래 써보지 않았습니다.
그러나 제가 경험한 것만 말씀 드리자면

  1. koa는 core에서부터 async await이 지원이 되기 때문에요 async 내부에서 발생하는 에러도 try catch가 없이 에러 핸들링 할 수 있습니다.

  2. 그리고 koa는 express에 비해 모듈화 되어있어요. 기본적인 것도 포함되지 않아요. 심지어, router, body-parser도요.
    https://github.com/koajs/koa/wiki 모듈은 여기서 골라서 사용하시면 됩니다.
    제가 사용했던것은 koa-routerkoa-body였습니다.