[KoaJS] 3. Router

2017. 9. 4. 18:04다시 하자 Back-End/Koa framework

반응형

지난 포스팅에서는 KoaJS로 개발하기 위한 환경설정과 첫번째 KoaJS 예제를 만들어보았다. (만들었다고 하기에는 부끄러운 수준이지만 ...) 이번 포스팅에서는 Koa-router에 대해서 알아보자.



지난 포스팅 보기 : [KoaJS] 2. KoaJS 개발을 위한 환경설정



1. Router 기본



웹 서비스는 다양한 자원 (HTML, JS, IMG ...) 을 가지고 있다. 이 자원들은 웹 서비스 내부의 여러 경로에 존재한다. 클라이언트는 원하는 정보를 얻기 위해 URI를 이용하여 웹 서비스에게 자원을 요청한다. 웹 서비스는 클라이언트가 요청한 URI에 따라 기능을 분기하는데, 이러한 작업을 자동으로 수행하는 모듈 또는 기능을 Router라고 한다.



이전 포스팅에 돌렸던 첫 번째 예제를 한번 실행 해보자. 그리고 브라우저의 주소창에 아무런 주소나 입력해보자. 예를 들어 우리가 만든 웹 서비스의 주소가 localhost:7777 이라면 localhost:7777/about 과 같이 존재하지 않는 URI를 요청해보자.



그림 1. 없는 경로를 요청한 화면



에러없이 정상으로 실행되는 것을 볼 수 있다. 하지만 이 동작은 잘못된 것이다. 존재하지 않는 경로를 요청했기 때문에 웹 서비스에서는 HTTP 상태 코드를 404로 리턴하여 클라이언트에게 요청한 경로가 없다는 것을 알려야한다. /about 주소가 아닌 다른 경로를 요청해도 동일한 결과가 나온다. 즉, 우리가 만든 웹 서비스는 자신이 어떤 자원을 가지고 있는지 파악할 수 없다. 클라이언트에게 정확한 정보를 제공하는 것이 웹 서비스의 주목적이지만 결과를 보면 서비스라고 부르기 민망할 정도다.



위에서 본 것처럼 Koa 모듈 하나만 가지고는 경로를 알려줄 수 없다. KoaJS는 이전 포스팅에서 언급한 것처럼 모든 모듈이 분리되어 있기 때문에 Router 모듈을 사용하기 위해서는 koa-router 모듈을 설치해야만 한다. 아래의 명령어로 koa-router 모듈을 설치한다. 참고로 프로젝트의 Root 경로에서 아래의 명령어를 실행해야한다.



npm install --save koa-router



자 이제 웹 서비스의 Router 기능을 구현해보자. 우리는 아래의 5가지 경로를 생성할 것이다.



/ : 메인 페이지 출력

/login : login 페이지 출력

/about : About 페이지 출력

/todolist : Todolist 페이지 출력

/register : Register 페이지 출력



koa-router 모듈을 사용하는 것은 간단하다. 이전 포스팅에서 만들었던 src/index.js 파일을 아래와 같이 코딩해보자.



const Koa = require("koa");

const Router = require("koa-router");


const app = new Koa();

const router = new Router();


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


  ctx.body = `<h1>Root page</h1>

              <br>

              <p>Todolist - Root page</p>`;


});


router.get('/login', async ctx => {


  ctx.body = `<h1>Login page</h1>

              <br>

              <p>Todolist - Login page</p>`;


});


router.get('/about', async ctx => {


  ctx.body = `<h1>About page</h1>

              <br>

              <p>Todolist - About page</p>`;


});


router.get('/todolist', async ctx => {


  ctx.body = `<h1>Todolist page</h1>

              <br>

              <p>Todolist - Todolist page</p>`;


});


router.get('/register', async ctx => {


  ctx.body = `<h1>Register page</h1>

              <br>

              <p>Todolist - Register page</p>`;


});


app.use(router.routes());

app.use(router.allowedMethods());


app.on('error', error => {


  console.error(error);


})


app.listen(7777);



서비스를 재시작하고 우리가 만든 경로를 브라우저의 주소창에 입력해보자. 등 여러가지를 요청해보자. localhost:7777/about 을 요청한 결과는 아래와 같다.



그림 2. Router를 적용한 화면



이번에는 존재하지 않는 경로를 요청해보자.



그림 3. 존재하지 않는 경로를 요청한 화면



드디어 서비스에서 자신이 무엇을 가지고 있는지, 클라이언트의 요청을 어느 곳으로 어떻게 처리할지 파악할 수 있게 되었다. 브라우저의 네트워크 요청 목록을 보면 HTTP 상태 코드가 404로 리턴 되었음을 볼 수 있다. 처음과 같은 엉성한 모습이 조금은 사라진 느낌이다.



2. HTTP 메서드 지정



라우터를 설정하는 방법을 알았으니 이번에는 HTTP 메서드를 종류별로 사용하는 방법에 대해서 알아보자. HTTP 메서드란, 클라이언트가 자신이 원하는 정보를 얻기 위해 서버에 요청하는 방식을 말하며 GET, POST 외에도 여러가지 메서드가 존재한다. HTTP 메서드에 대한 설명은 아래의 링크를 참고하자.



모질라 개발자 커뮤니티에서 설명하는 HTTP 메서드



KoaJS에서 여러가지 HTTP 메서드를 사용해보자. 그 전에 Postman 프로그램을 설치하자. GET 방식은 URI에 파라미터를 붙이면 쉽게 테스트가 가능하지만, 나머지는 테스트하기 위해서 클라이언트에서 따로 AJAX를 요청하는 자바스크립트 코드가 필요하다. Postman은 추가적인 자바스크립트 코딩 없이도 다양한 HTTP 메서드를 쉽게 호출할 수 있도록 도와준다.



그림 4. 추가코드 없이 다양한 클라이언트 요청을 만들어주는 Postman



이 예제에서는 REST 아키텍처에서 사용하는 GET, PUT, POST, DELETE만 다루도록 한다. 위에서 만든 경로 중 /todolist 경로를 HTTP 메서드로 구분해보자. 아래의 코드를 작성하자.



...



router.get('/todolist', async ctx => {


  ctx.body = `<h1>Todolist 목록 출력</h1>

              <br>

              <p>파라미터가 없으면 모든 Todolist 출력</p>`;


});


router.get('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 출력</h1>

              <br>

              <p>번호에 해당하는 Todolist 출력</p>`;


});


router.post('/todolist', async ctx => {


  ctx.body = `<h1>Todolist 추가</h1>

              <br>

              <p>새로운 Todo 추가</p>`;


});


router.put('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 수정</h1>

              <br>

              <p>번호에 해당하는 Todolist 수정</p>`;


});


router.delete('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 삭제</h1>

              <br>

              <p>번호에 해당하는 Todolist 삭제</p>`;


});



...



Postman 프로그램을 실행하고, 아래의 그림과 같이 서비스에 요청해보자.



그림 5. Postman에서 todolist를 HTTP 메서드별로 호출



모두 정상으로 호출되는 것을 볼 수있다. 참고로 HTTP 메서드가 지정된 경로에서 다른 HTTP 메서드로 요청하게되면 405 에러가 리턴된다.




그림 6. HTTP 메서드가 지정된 경로에 다른 HTTP 메서드로 요청한 경우



HTTP 메서드로 경로를 등록할 때는 순서에 주의해야 한다. 아래의 코드를 todolist 예제코드 맨 위에 올려보자.



router.all('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Router All로 처리 중 ...</h1>

              <br>

              <p>${num_todo}는 All로 처리하는게 맞나요??</p>`;


});


...



router.all는 모든 HTTP 메서드 요청을 처리하기 때문에 서비스의 Route 설정에서 맨 위에 위치할 경우, 아래에 있는 HTTP 메서드를 사용하는 경로들을 모두 무시한다. router.all을 사용한다면 반드시 맨 아래에다 정의하여 경로들이 무시되지 않도록 주의하자.



그림 7. 무시된 DELETE 메서드



이제 우리 서비스는 클라이언트의 요청에 올바른 길을 찾아주는 기능이 추가되었다.



3. 파라미터 전달



위의 예제에서 GET 방식의 메서드를 사용할 때 파라미터로 id를 전달받는 것을 볼 수 있다.



router.get('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 출력</h1>

              <br>

              <p>번호에 해당하는 Todolist 출력</p>`;


});



경로를 지정하는 부분에 ":id"로 되어있는 부분을 확인할 수 있는데, 이 부분이 URI에서 파라미터를 받을 때 사용하는 이름이다. URI 파라미터는 ctx.params에 리터럴 객체 형태로 전달받는다. 아래의 방법으로 여러개의 파라미터를 전달받을 수 있다.



router.get('/todolist/:id/:pwd', async ctx => {


  console.log("Ctx param -> ", ctx.params);


});


...


요청 주소 : localhost:7777/todolist/kim1124/kim1124_pwd


출력 결과 : {id : "kim1124", pwd : "kim1124_pwd"}



만약 RESTFul 방식이 아닌 GET / POST 터널링 방식처럼 파라미터를 전달하는 경우에는 ctx.request.query로 파라미터를 전달 받을 수 있다.



...


router.get('/todolist', async ctx => {


  console.log("ctx -> ", ctx.request.query);


  ctx.body = `<h1>Todolist 목록 출력</h1>

              <br>

              <p>파라미터가 없으면 모든 Todolist 출력</p>`;


});


...


요청 주소 : (GET 호출 : localhost:7777/todolist?id="kim1124"&pwd="kim1124_pwdd")


출력 결과 : ctx ->  { id: '"kim1124"', pwd: '"kim1124_pwdd"' }



POST 같이 요청 정보를 Body에 포함하는 경우에는 요청 객체의 Body 부분을 파싱하는 모듈이 필요하다. 아래의 모듈을 설치하자.



npm install --save koa-bodyparser



다음 코드 상단 부분에 koa-bodyparser를 선언하고 app.use로 모듈 사용을 정의하자. 주의할 점은 Router 객체보다 먼저 선언이 되어야 한다.



const Koa = require("koa");

const Router = require("koa-router");

const BodyParser = require("koa-bodyparser");


const app = new Koa();


app.use(BodyParser());


const router = new Router();


...



다음 Postman에서 HTTP 메서드를 POST로 설정하고 아래의 Body 탭을 클릭하여 스크린 샷과 같이 파라미터를 JSON 형태로 지정하자.


그림 8. Postman으로 POST 파라미터 설정



다음 Router 객체의 POST 메서드 내부에 아래와 같이 console.log를 찍고 NodeJS를 재시작하자. 그리고 Send 버튼을 누르면 Postman에서 전달한 파라미터를 볼 수 있다.



router.post('/todolist', async ctx => {


  console.log("Ctx -> ", ctx.request.body);


  ctx.body = `<h1>Todolist 추가</h1>

              <br>

              <p>새로운 Todo 추가</p>`;


});


...


실행 결과 : Ctx ->  { post_id: 'kim1124', post_pw: 'kim1124_pwd' }



4. 메서드 체이닝



메서드 체이닝은 Koa-router 모듈의 기능이 아니라 자바스크립트 언어의 특징 중 하나이며, 하나의 객체에 메서드를 연속으로 호출하는 것과 같은 문법을 제공한다. 위의 예제 코드들은 아래의 코드로 변경할 수 있다.



...


// 메서드 호출 시 ;로 끊기지 않는 것에 주의하자.


router.get('/todolist', async ctx => {


  console.log("ctx -> ", ctx.request.query);


  ctx.body = `<h1>Todolist 목록 출력</h1>

              <br>

              <p>파라미터가 없으면 모든 Todolist 출력</p>`;


})

.get('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  console.log("ctx -> ", ctx.requests);


  ctx.body = `<h1>Todolist ${num_todo}번 출력</h1>

              <br>

              <p>번호에 해당하는 Todolist 출력</p>`;


})

.post('/todolist', async ctx => {


  console.log("Ctx -> ", ctx.request.body);


  ctx.body = `<h1>Todolist 추가</h1>

              <br>

              <p>새로운 Todo 추가</p>`;


})

.put('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 수정</h1>

              <br>

              <p>번호에 해당하는 Todolist 수정</p>`;


})

.delete('/todolist/:id', async ctx => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 삭제</h1>

              <br>

              <p>번호에 해당하는 Todolist 삭제</p>`;


});


...



Postman으로 URI를 호출하면 정상으로 동작하는 것을 볼 수 있다.



5. 다중 라우팅



지금까지 우리는 Koa-route 모듈을 가지고 서비스에서 경로를 관리하는 방법에 대해서 알아보았다. 하지만 웹 서비스는 우리가 여태까지 진행해 온 예제와 달리 수많은 경로들로 이루어져있다. 따라서 기능별로 경로를 분리하는 것이 개발하거나 관리하는데 이점이 있을 것이다. 위에서 /todolist 라는 경로를 코딩 해왔으니 todolist 경로를 독립적인 라우터 객체로 만들어 분리해보자.





그림 9. Todolist 라우터 모듈 분리



그림 9 와 같이 프로젝트 Root 경로 내부에 todolist 라는 디렉터리를 하나 생성하고, 내부에 index.js 와 restapi.js 파일을 생성한다. 먼저 index.js 파일먼저 작성해보자.



/todolist/index.js 파일


const todoRouter = require('koa-router');

const Router = todoRouter();


const todoFunc = require('./restapi');


Router.get('/todolist', todoFunc.getTodoList)

.get('/todolist/:id', todoFunc.getTodoList)

.post('/todolist', todoFunc.createTodolist)

.put('/todolist', todoFunc.modifyTodolist)

.delete('/todolist/:id', todoFunc.deleteTodolist);


module.exports = Router;



위에서 작성한 todolist를 index.js에 작성한 것과 동일한 코드인데, 각 HTTP 메서드의 콜백 함수를 restapi.js 파일로 따로 뺀 것이 차이점이다. module.exports는 NodeJS에서 사용하는 CommonJS의 문법이며, index.js 모듈을 require 키워드로 호출한 곳에서 Router 객체를 사용할 수 있도록 객체를 export 하는 것이다.



다음은 restapi.js 파일을 작성해보자.



/todolist/restapi.js 파일


exports.getTodoList = (ctx) => {


  let num_todo = ctx.params.id;


  if(num_todo){


    ctx.body = `<h1>Todolist ${num_todo}번 출력</h1>

                <br>

                <p>번호에 해당하는 Todolist 출력</p>`;


  }

  else{


    ctx.body = `<h1>Todolist 목록 출력</h1>

                <br>

                <p>파라미터가 없으면 모든 Todolist 출력</p>`;


  }


};


exports.createTodolist = (ctx) => {


  ctx.body = `<h1>Todolist 추가</h1>

              <br>

              <p>새로운 Todo 추가</p>`;


};


exports.modifyTodolist = (ctx) => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 수정</h1>

              <br>

              <p>번호에 해당하는 Todolist 수정</p>`;


};


exports.deleteTodolist = (ctx) => {


  let num_todo = ctx.params.id;


  ctx.body = `<h1>Todolist ${num_todo}번 삭제</h1>

              <br>

              <p>번호에 해당하는 Todolist 삭제</p>`;


};



async 키워드는 각 메서드 내부에서 비동기를 처리하는 로직이 없어 제외하였다. (나중에 엄청 많이 등장하지만 지금은 필요가 없으므로 제외한다.) exports 키워드는 위에서 살펴본 것과 같이 NodeJS CommonJS의 문법이며 해당 모듈을 사용 시, exports 객체의 멤버들을 외부에서 사용할 수 있도록 하는 문법이다. 


마지막으로 프로젝트 Root 경로의 index.js 부분에 todolist 라우터를 사용하도록 app.use() 메서드를 이용하여 등록한다.


index.js 파일


const Koa = require("koa");

const Router = require("koa-router");

const BodyParser = require("koa-bodyparser");


const TodolistRouter = require("./todolist");


const app = new Koa();


app.use(BodyParser());


const router = new Router();


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


  ctx.body = `<h1>Root page</h1>

              <br>

              <p>Todolist - Root page</p>`;


});


router.get('/login', async ctx => {


  ctx.body = `<h1>Login page</h1>

              <br>

              <p>Todolist - Login page</p>`;


});


router.get('/about', async ctx => {


  ctx.body = `<h1>About page</h1>

              <br>

              <p>Todolist - About page</p>`;


});


router.get('/register', async ctx => {


  ctx.body = `<h1>Register page</h1>

              <br>

              <p>Todolist - Register page</p>`;


});


router.use(TodolistRouter.routes());


app.use(router.routes());

app.use(router.allowedMethods());


app.on('error', error => {


  console.error(error);


})


app.listen(7777);



파란색으로 강조된 부분이 todolist 라우터를 프로젝트에서 사용할 수 있게 등록한 부분이다. 여기서 확인할 수 있는 것은 여러 개의 Router 객체를 동시에 사용할 수 있다는 것이다. 따라서 복잡한 웹 서비스라고 하더라도 여러개의 라우터 객체를 사용하여 기능을 분기할 수 있다.


todolist 라우터 객체에 /todolist 경로가 지저분하게 보인다면 아래와 같이 수정할 수도 있다.


// index.js 파일

...

router.use('/todolisr', TodolistRouter.routes());

...

/todolist/index.js 파일


const todoRouter = require('koa-router');

const Router = todoRouter();


const todoFunc = require('./restapi');


Router.get('/', todoFunc.getTodoList)

.get('/:id', todoFunc.getTodoList)

.post('/', todoFunc.createTodolist)

.put('/', todoFunc.modifyTodolist)

.delete('/:id', todoFunc.deleteTodolist);


module.exports = Router;



Postman을 이용하여 각 기능들을 테스트 해보자. 모든 기능이 잘 동작하는 것을 확인할 수 있다. 이제 웹 서비스가 클라이언트의 요청에 맞는 데이터를 찾아줄 수 있게 되었다. 시간이 되면 index.js에 지저분하게 되어있는 get 라우터들도 기능별로 분리해보면 좋겠다.


지금까지 한 내용은 이 파일에서 볼 수 있다 : First_koa.zip

다음 포스팅에서는 Koa-static 모듈을 이용하여 수정되지 않는 정적 자원들을 서비스하는 방법에 대해 알아보자.


반응형

'다시 하자 Back-End > Koa framework' 카테고리의 다른 글

[KoaJS] 4. Mongoose (1)  (0) 2017.10.13
[KoaJS] 2. KoaJS 개발을 위한 환경설정  (0) 2017.08.09
[KoaJS] 1. KoaJS 시작하기  (0) 2017.08.03