[KoaJS] 1. KoaJS 시작하기

2017. 8. 3. 15:03다시 하자 Back-End/Koa framework

반응형

1. Express is dead?



NodeJS 진영에서 가장 유명한 풀스택 웹 프레임워크로 알려져있는 Express가 작년부터 이상한 말들이 나오기 시작했다.





그림 1. Express is dead?



구글에서 "Express is dead" 를 검색하면 나오는 게시글 중에서 하나를 캡처왔다. 내용을 살펴보면 Express의 Github에 마지막 커밋이 한달 전에 있었고, 이전에도 죽어있었다 (활동이 없었다) 는 것을 언급하고 있다. 하지만 17년 현재 Express의 Github를 보면 올해에도 커밋된 내용이 있음을 알 수 있다. 캡처한 글에 달린 댓글들도 "Express is dead" 라는 말을 동의하지 못하고 있는데, 소규모의 패치는 지속적으로 이루어지고 있으며 현재 진행하는 개발 작업들은 대부분 안정화 작업 위주로 진행하고 있다는 내용들을 볼 수 있다.



사실 이러한 주장이 나온 이유는 Express를 소유하고 있는 스타트업 회사인 Strong Loop가 IBM에 인수 되었기 때문이다. Strong Loop가 Express를 먹었을때도 시끄러웠는데 (Express 주 개발자인 TJ가 Strong Loop로 Express를 팔아버렸다라는 의혹들이 나왔던 적이 있었다.) 엎친데 겹친격으로 IBM에게 인수됬다는 기사는 유저들에게 충격을 주기에 충분했다.



이 과정에서 Express를 사용하는 유저들이 IBM이 인수하면 유료화가 되는거 아닌가? 무료버전은 지원이 끊기는 것 아닌가? 이미 Express로 개발된 제품들은 IBM에게 라이센스 비용을 지불해야 되는 것 아닌가? 등의 여러가지 추측들이 인터넷 상에 떠돌아 다니게 되었고 급기아 많은 컨트리뷰터들이 이탈하는 사건이 벌어졌다. 아무래도 막장 드라마 같은 일들이 계속 벌어지다보니 불안감을 느끼기에 충분했을 것이다.



다행히도(?) IBM은 이후에 Express에 대해 유료화를 하거나 지원을 종료하지 않았으며, 2016년 2월에는 NodeJS 재단에서 새로운 인큐베이터 프로젝트로 추가함으로써 Express의 지원을 지속적으로 유지하기로 결정하였다. 아무래도 Express가 NodeJS에서 막대한 영향력이 있으며, NodeJS에 입문하는 개발자들이 Express로 접하는 빈도가 높다는 점 등의 이유로 Express를 유지하기로 결정한 듯하다. (솔찍히 Express로 되어있는 제품들이 얼마나 많은데 ...)



아래의 링크는 NodeJS 재단에서 Express 프레임워크를 새로운 인큐베이터 프로젝트로 등록했다는 내용이다.



출처 : https://nodejs.github.io/nodejs-ko/articles/2016/02/10/announcements-foundation-express-news/



아래의 기사를 보면 Strong Loop의 창업주가 왜 IBM에 몸을 담았는지 인터뷰 내용이 나와있으므로 한번 참고해서 보는 것도 좋을 것 같다. (큰 그림 느낌이 나긴 하지만 ...)



출처 : http://www.bloter.net/archives/268273



현재 진행되는 상황만 보면 "Express is dead" 는 오랜시간 후에 "벌어질 수도 있는" 일로 생각된다. 여러 사건이 있었음에도 불구하고 여전히 Express는 NodeJS 진영에서 대표적인 풀스택 프레임워크이며 그 영향력을 무시할 수 없기 때문이다. 하지만 많은 컨트리뷰터들이 떠나갔다는 점과 Express를 대체할 수 있는 프레임워크들이 계속해서 등장한다는 점 등 현 시점에서 Express가 앞으로 가야할 길은 험난하기만 하다.



2. KoaJS



Express가 국내 드라마의 막장급 스토리의 과정을 거치면서 이미지가 쭉쭉 내려가는 동안 NodeJS 진영에서는 많은 변화들이 있었다. 가장 큰 사건으로는 ECMA 2015 (ES6)의 등장이다. ES6는 내부적으로 많은 문법들이 추가 되었는데, 특히 Promise 객체, Generator 함수는 자바스크립트의 영역에서 가장 골치아팠던 Callback 지옥을 해결할 수 있는 새로운 문법으로 자바스크립트 유저들에게 큰 호응을 얻었다.



KoaJS는 ES6 이상의 최신 자바스크립트 문법을 가장 적극적으로 지원하는 프레임워크로써 Generator 함수를 사용하여 요청과 응답을 처리하는 것이 가장 큰 특징이다. 최근에는 지원하는 모듈의 수가 많아지면서 Express의 대안으로 각광받고 있다.





그림 2. KoaJS의 소개 글



KoaJS를 하다보면 "Express 하는 것 같은 느낌은 뭐지?" 라는 생각이 들 때가 있는데, 사실 Koa는 Express를 개발한 팀이 새롭게 개발하고 있는 프레임워크다. NodeJS 메인 개발자인 TJ가 지속적인 유지보수를 하겠다라고 밝힌 프레임워크가 KoaJS라고 알려져있다. 그래서인지 Express를 사용해본 바로는 제너레이터를 사용하는 차세대 Express라는 느낌이 강하다. 그러다 보니 Express를 이미 사용해 본 개발자라면 KoaJS를 적응하는데 그리 긴 시간이 들지 않을 것으로 예상한다.



Generator



KoaJS를 살펴보기 전에 ES6, 7, 8에서 가장 획기적인 기술로 유저들의 큰 호응을 얻고있는 Generator (이하 제너레이터) 에 대하여 알아보자. 



제너레이터는 자바스크립트의 특수한 함수이며 함수 호출을 시작하면 Return 될 때까지 제어 할 수 없는 일반 함수와 달리, yield 키워드를 사용하여 함수가 완전히 종료되지 않아도 제어권을 넘겨받을 수 있다. 예제코드를 살펴보자.



function* generator(flag) {


  yield "첫 번째 yield 리턴";

  yield "두 번째 yield 리턴";


  if(flag){


    yield "Flag 조건문 True 전달!! 세 번째 yield 리턴";


  }


  yield "마지막 yield 리턴";


  return "진짜 리턴 !!";


}


let gen_obj = generator(true);


console.log(gen_obj.next());

console.log(gen_obj.next());

console.log(gen_obj.next());

console.log(gen_obj.next());

console.log(gen_obj.next());

console.log(gen_obj.next());



실행 결과는 아래와 같다.





그림 3. 제너레이터 실행 결과



제너레이터 함수를 호출하면 내부적으로 제너레이터 객체를 리턴한다. 제너레이터 객체에서 next 메서드를 호출하면 제어권을 제너레이터에게 넘기며 내부에 yield를 만나면 메인으로 제어권을 넘겨준다. 위의 예제 코드의 실행 순서는 아래와 같다.



1) 첫 번째 next() 메서드를 호출하면 제너레이터 함수에게 제어권을 넘긴다.


2) 제너레이터는 첫번째 yield 키워드를 만나면 yield 키워드 오른쪽에 있는 내용을 { 'value' : '내용', done : 순환이 끝나면 true, 아니면 false } 형태로 객체를 생성하여 리턴하고 제어권을 메인에게 넘긴다.


3) 두 번째 next() 메서드를 호출하면 다시 제너레이터로 제어권을 넘긴다.


4) 두 번째 yield 키워드를 만나면 동일하게 yield 오른쪽에 있는 내용을 객체로 생성하여 리턴한다. 이런 작업들을 yield 키워드를 만나지 않거나 return을 만나기 전까지 반복한다.


5) 마지막 순회에서 yield 문이 없으면 { 'value' : undefined, done : true } 객체를 리턴한다. 만약, yield 전에 return 문이 먼저 있다면 return 오른쪽에 있는 값을 리턴하고 순회를 종료한다.



그림 3에서 { value : '진짜 리턴 !!', done : true }가 출력된 이유는 yield가 아닌 return 키워드로 순회를 완료했기 때문이다. 당연한 이야기지만 yield보다 return이 먼저 호출되면 제너레이터는 순회를 완료시키고 { value : 'undefined', done : true } 를 리턴한다.



일반 함수와 제너레이터의 차이점은 아래와 같다.



 

 일반 함수

제너레이터 

리턴 시점

return 키워드 실행 시

yield 키워드 실행 시 

 선언 방법

 function

function*

리턴 타입

모든 타입 리턴

제너레이터 객체 리턴 

호출 방법

 함수명 ()

제너레이터 객체의 next() 메서드 호출 



제너레이터에 대해 좀 더 알아보자. 제너레이터 함수는 yield와 next 메서드로 작업 권한을 조절하며, 제너레이터 함수 내부로 인자 값을 전달할 수도 있다.



function* generator(flag) {


  const num_first = yield "첫 번째 next 호출은 무시된다 !!";


  const num_second = yield num_first;


  yield num_second


  return num_first + num_second;


}


let gen_obj = generator(true);


console.log(gen_obj.next(99));

실행 결과 : { 'value' : '첫 번째 next 호출은 무시된다 !!', done : false }


console.log(gen_obj.next(10));

실행 결과 : { 'value' : 10, done : false }


console.log(gen_obj.next(20));

실행 결과 : { 'value' : 20, done : false }


console.log(gen_obj.next());

실행 결과 : { 'value' : 30, done : true }



최초에 next 메서드 호출 시 인자로 99를 전달했지만 무시되는 것을 볼 수 있다. 따라서 제너레이터에 인자값을 넘길 경우, 최초 한번은 인자를 넘기지 않는것을 기억하자. 


일단은 제너레이터가 제어권을 원하는 시점에 반납하고 호출할 수 있다는 것만 알아두자. 또한 yield 키워드로 제너레이터의 데이터를 리턴하고 인자도 받을 수 있다는 사실을 기억하자. 나중에 제너레이터의 여러가지 특징에 대해서는 [자바스크립트] 게시판에서 따로 다루도록 하겠다.


Generator를 활용한 비동기 코드


간단히 제너레이터에 대해 알아보았다. 그럼 이걸 어디에서 활용할 수 있을지 고민이 된다. 사실 제너레이터는 매우 강력한 문법 중 하나이며, 특히 비동기 처리에서 그 능력을 발휘한다. 어떤 방법으로 제너레이터를 이용하여 비동기적인 작업을 수행할 수 있는지 예제를 통해서 알아보자.

아래의 예제는 우리에게 친숙한 콜백처리 방식이다.


const Fs = require('fs');

let str_result = "";

Fs.readFile('./callback2.txt', 'utf-8', (err, data1) => {

  str_result += data1;

  Fs.readFile('./callback3.txt', 'utf-8', (err, data2) => {

    str_result += data2;

    Fs.readFile('./callback4.txt', 'utf-8', (err, data3) => {

      str_result += data3;

      console.log(str_result)

실행 결과 : "안녕하세요. ES6의 제너레이터는 이렇게 사용된답니다."

    })

  })

})


어디서 많이 본듯한 친숙한 스타일의 코드가 보인다. 파일 3개를 순차적으로 읽고 하나의 변수에 합치는 간단한 코드지만 외관상으로는 전혀 간단해 보이지 않는다. 파일 3개를 더 추가하면 말도 안되게 생긴 코드가 나올것이다.


이렇게 콜백 함수를 이용하여 비동기를 처리할 때, 인자 값으로 전달된 콜백 함수가 끝도 없이 깊어지는 현상을 "콜백지옥"이라고 한다. 콜백 지옥이 형성되면 코드의 가독성을 현저히 떨어뜨리며 프로그램의 동작 흐름을 제대로 파악할 수 없는 치명적인 문제가 발생한다.




그림 4. 콜백 지옥



ES6에서는 콜백 지옥을 효과적으로 처리할 수 있는 두 가지 문법을 제공한다. 바로 Promise제너레이터이다. Promise 객체를 이용하면 콜백 함수를 사용할 때 처리하기 어려웠던 예외나 에러를 간단하게 처리할 수 있으며, Promise.all 메서드를 이용하여 프로그램의 정상적인 동작을 보장한다. 나중에 자바스크립트 쪽에서 자세하게 다루도록 하고, 제너레이터를 이용하여 콜백 지옥을 해결할 수 있는 예제를 보도록 하자.



function* readAllFile(arr_filepath, resume){


  let fir_read = yield Fs.readFile(arr_filepath[0], 'utf-8', resume);

  let sec_read = yield Fs.readFile(arr_filepath[1], 'utf-8', resume);

  let thr_read = yield Fs.readFile(arr_filepath[2], 'utf-8', resume);


  console.log(fir_read + sec_read + thr_read);

          

  실행 결과 : "안녕하세요. ES6의 제너레이터는 이렇게 사용된답니다."


}


function autoRun(arr_filepath, genFunc){


  const obj_gen = genFunc(arr_filepath, resume);


  function resume(err, data) {


    return obj_gen.next(data);


  }


  obj_gen.next();


}


autoRun(['./callback2.txt', './callback3.txt', './callback4.txt'], readAllFile);



코드를 분석하면 readAllFile 이라는 제너레이터 함수를 선언한다. 이 함수는 첫 번째 인자로 읽을 파일의 경로를 전달하고, 두 번째 인자는 NodeJS 파일 시스템 모듈의 readFile 이라는 메서드의 콜백 함수를 전달한다. 특이한 점은 readFile 메서드의 콜백 함수인 resume 함수 내부에 제너레이터 객체의 next 함수를 호출 한다는 것이다.



function resume(err, data) {


return obj_gen.next(data);


}



그 이유는 간단하다. 제너레이터 함수는 호출해도 자동으로 실행되지 않기 떄문이다. 제너레이터 함수를 호출하면 비어있는 제너레이터 객체를 리턴하는데, 이 객체에서 next 메서드를 호출해야만 제너레이터 함수 내부로 제어권이 넘어가며 실행된다.



제너레이터 내부에서 yield 문을 만나면 메인 쓰레드로 제어권이 넘어가며, 이후에 제너레이터에게 제어권을 넘기려면 next 메서드의 호출이 필요하다. 코드에서 next 메서드를 호출하지 않고 return만 해보면 쉽게 알 수 있다.



function* readAllFile(arr_filepath, resume){


  let fir_read = yield Fs.readFile(arr_filepath[0], 'utf-8', resume);

  let sec_read = yield Fs.readFile(arr_filepath[1], 'utf-8', resume);

  let thr_read = yield Fs.readFile(arr_filepath[2], 'utf-8', resume);


  console.log(fir_read + sec_read + thr_read);

          

  실행 결과 : 아무런 문자열도 출력되지 않는다. 첫 번째 yield에서 멈춰있는 상태이기 때문이다.


}

 

function autoRun(arr_filepath, genFunc){


  const obj_gen = genFunc(arr_filepath, resume);


  function resume(err, data) {


    return data;


  }


  obj_gen.next();


}


autoRun(['./callback2.txt', './callback3.txt', './callback4.txt'], readAllFile);



우리가 의도한 기능은 순차적으로 파일을 읽고 그 내용을 하나로 합쳐서 출력하는 기능이지만, next 메서드의 호출이 없기 때문에 첫 번째 이후의 코드는 실행되지 않는다. 따라서 첫 번째 파일만 읽고 프로그램이 종료되는 것이다. 콜백 함수에 next 메서드를 호출하면 파일을 읽고 난 후, next 메서드가 자동으로 실행되기 때문에 우리가 의도한대로 프로그램이 동작하는 것을 확인할 수 있다.



다시 코드를 살펴보자. 분명히 비동기 작업을 수행하지만 코드의 형태는 마치 동기식 코드처럼 보인다. 이러한 코드는 가독성이 높고 프로그램의 흐름을 한눈에 파악할 수 있는 장점을 가진다.



let fir_read = yield Fs.readFile(arr_filepath[0], 'utf-8', resume);

let sec_read = yield Fs.readFile(arr_filepath[1], 'utf-8', resume);

let thr_read = yield Fs.readFile(arr_filepath[2], 'utf-8', resume);



위에 있는 콜백 방식의 코드와 비교해보자. 어떤 코드가 파악하기 쉬운가? 아무리 봐도 제너레이터를 이용한 쪽이 코드도 간단하고 프로그램 흐름도 이해하기 쉬워보인다.



Promise 객체를 사용하면 더욱 간단한 코드로 바꿀 수 있다. (이왕 언급된거 Promise도 맛보기로 잠깐 써보자)



Fs.readFileAsync = function(filename) {


    return new Promise(function(resolve, reject) {


        Fs.readFile(filename, 'utf-8', function(err, data){


            if (err)

                reject(err);

            else

                resolve(data);


        });


    });


};


function* readAllFile(arr_filepath){


  let fir_promise = Fs.readFileAsync(arr_filepath[0]);

  let sec_promise = Fs.readFileAsync(arr_filepath[1]);

  let thr_promise = Fs.readFileAsync(arr_filepath[2]);


  yield Promise.all([fir_promise, sec_promise, thr_promise]).then((data) => {


    console.log("Data -> ", data);


  });


}


(() => {


  const obj_gen = readAllFile(['./callback2.txt', './callback3.txt', './callback4.txt']);


  obj_gen.next();


})();



실행 결과 : ['안녕하세요.', 'ES6의 제너레이터는', '이렇게 사용된답니다.']



Promise 객체를 활용하기 위해 FS 모듈에 readFileAsync 메서드를 추가하였다. 이 메서드는 내부적으로 Promise 객체를 생성하여 리턴하는데 파일을 읽는데 성공하면 resolve를, 실패하면 reject를 호출한다. Promise 객체들은 then 메서드를 호출하기 전까지 Pending 상태로 유지된다. 이후에 then 메서드를 호출하면 상태가 변경되어 작업을 수행한다.



코드에서 제너레이터로 제어권을 넘기면 3개의 Promise를 전달 받는다. 이후에 Promise.all 메서드를 이용하여 Resolve 작업을 수행한다. 이 때 하나의 에러라도 있으면 모든 결과값을 버리고 Reject 작업을 수행한다. Resolve 작업이 완료되면 then 메서드의 인자 값으로 결과값을 전달한다.



아까보다 더 이해하기 쉽고 간단한 코드가 나왔다. Promise와 제너레이터를 이용하면 원하는 시점에 언제 어디서든지 비동기 작업을 수행할 수 있다.



KoaJS와 제너레이터



위에서 살펴본 제너레이터와 Promise를 이용한 비동기 작업은 한가지 문제를 가지고 있다. 비동기 작업을 수행할 때마다 제너레이터를 생성하고 실행하는 부분을 만들어줘야 한다. 위의 예제는 단순히 파일을 읽는 작업을 수행하지만 실제 코딩에서는 더욱 복잡한 코드들이 들어간다. 그때마다 실행 부분을 코딩해야 한다는 점은 여간 귀찮은 일이 아니다.



그래서 ES7에서 async / await 라는 새로운 문법이 추가되었다. 쉽게 말하면 위에서 본 Promise와 제너레이터를 합친 문법이다. 위의 예제코드를 async / await 문법을 이용하면 아래와 같이 심플한 코드로 바꿀 수 있다.



async function readAllFile(arr_filepath){


  let fir_promise = await Fs.readFileAsync(arr_filepath[0]);

  let sec_promise = await Fs.readFileAsync(arr_filepath[1]);

  let thr_promise = await Fs.readFileAsync(arr_filepath[2]);


  console.log(fir_promise + sec_promise + thr_promise);


}


readAllFile(['./callback2.txt', './callback3.txt', './callback4.txt']);


실행 결과 : "안녕하세요. ES6의 제너레이터는 이렇게 사용된답니다."



KoaJS의 비동기 처리는 async / await 문법을 지원한다. (Callback 방식도 지원하지만, KoaJS 2.0 부터는 async / await가 기본으로 적용된다.) 따라서 KoaJS와 제너레이터의 관계는 매우 밀접하다.





그림 5. KoaJS는 NodeJS 7.6.0 버전 이상을 요구한다.



주의할 점은 async / await가 최근에 나오다보니 NodeJS의 최신 버전을 요구한다는 것이다. (공식 적용은 ECMA 2017 ES8) 만약 그 이전버전의 NodeJS를 사용해야 한다면 NPM에서 async를 지원하는 추가 모듈을 사용해야만 한다.



다음 포스팅에서는 KoaJS를 설치하고 비동기 작업을 수행하는 방법에 대해서 알아보도록 하자.

반응형

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

[KoaJS] 4. Mongoose (1)  (0) 2017.10.13
[KoaJS] 3. Router  (0) 2017.09.04
[KoaJS] 2. KoaJS 개발을 위한 환경설정  (0) 2017.08.09