Node.js callback hell 탈출하기(부재: Q모듈을 활용한 Promise 사용하기)

JavaScript에서 Asynchronous한 작업을 직관적인 코드로 작성하는 일은 매우 힘듭니다. 대부분 이런 작업을 위해서 callback 함수를 많이 사용하는데, 이런 callback 함수는 아래의 예시처럼 코드의 복잡성을 증가시키고 가독성을 떨어트리게 됩니다. (생각해보면 저도 JavaScript를 처음 공부할 때 callback 함수의 난해한 모습에 꽤 당황했었습니다.)

asyncFunction1(function(input, result1) {
asyncFunction2(function(result2) {
asyncFunction3(function(result3) {
asyncFunction4(function(result4) {
asyncFunction5(function(output) {
// finally, do something...
});
});
});
});
});

Callback 함수는 특정 시점에 호출할 함수를 의미합니다. JavaScript의 대부분 Callback 함수는 특정 함수의 파라미터로 전달됩니다. JavaScript의 함수는 First Object이기 때문에 가능한 일이죠. 함수의 흐름을 Synchronous하게 제어하기 위해서 연속적으로 Callback 함수를 활용하다보면 Callback Hell에 빠지게 됩니다. 아래 사이트는 Callback Hell에 대한 내용을 잘 정리한 글입니다.

위 사이트에서 Callback Hell에 대한 3가지 해결 전략은 아래와 같습니다.

  • Keep your code shallow
  • Modularize
  • Handle every single error

하지만 제 생각에는 위 방법은 Callback Hell 자체의 모습을 해결하는 전략은 아닌 것 같습니다. 마침 글 마지막 부분에는 Promise를 소개하네요.

Promises are a way to write async code that still appears as though it is executing in a top-down way, and handles more types of errors due to encouraged use of try/catch style error handling.

저는 JavaScript 개발을 Node.js 개발로 시작했기 때문에 callback hell의 모습을 만나는 일은 흔한 일이었습니다. 일부 코드를 Module로 만들고 항상 코드를 간결하게 하려해도 callback을 계속 호출해야하는 코드는 더럽게 느껴질 정도였습니다.

저는 Outsider님이 제안한 Promise 패턴을 적극적으로 활용해보기로 했고, Node.js에서 비교적 Promise를 쉽게 사용할 수 있게 도와주는 Q 모듈을 활용했습니다. 따라서 이를 바탕으로 Q 모듈을 활용한 Promise 사용 경험을 공유해볼까 합니다.


Promise 활용하기

Q 모듈은 다양한 기능이 존재하지만 일단 저는 세가지 기능에 대해서 소개하려 합니다. Q.defer(), Q.denodeify(), promise.nodeify() 입니다. 차례대로 살펴보겠습니다.

Q.defer()

이 기능은 callback 형태의 함수를 쉽게 promise 패턴으로 호출할 수 있게 도와줍니다. 코드로 설명해보겠습니다.

sample.file 존재유무를 callback 함수로 넘기는 asyncFunction 함수는 Q.defer() 기능을 활용해서 아래처럼 Promise 형태로 변형이 가능합니다. 쉽고 간단합니다.

이 Q.defer()를 사용하면 대부분의 Callback 패턴 함수들을 Promise 패턴으로 쉽게 변경이 가능합니다. 하지만 함수의 처음에 var deferred = Q.defer();를 선언하고 끝에 deferred.promise를 return 하는게 조금 귀찮고 지저분합니다.

Q.denodeify()

Q 모듈은 Node.js의 Callback 패턴 스타일의 함수를 Promise 패턴 함수로 인터페이싱 할 수 있는 Q.denodeify()를 제공합니다. Node.js Callback 패턴 스타일은 어떤 스타일을 말하는 걸까요. 바로 Error-First Callback 스타일을 말합니다. 쉽게 말하면 callback 함수의 첫 번째 파라미터로 error를 넘긴다는 의미입니다. 해당 내용은 understanding-error-first-callbacks-in-node-js 글을 참고하면 될 것 같습니다.

대부분의 Node.js의 함수들은 fs.readFile과 같은 모양을 가지고 있습니다. callback 함수의 첫 번째 파라미터로 err를 넘기게 됩니다. 이런 Node.js Callback 스타일의 함수들(Error-First Callback)은 Q.denodeify()로 감싸서 쉽게 Promise 패턴 함수로 변형이 가능합니다.

자, 그럼 위 두 가지를 응용 해보겠습니다.

위 코드는 아래처럼 변형이 가능합니다.

코드가 길어진 것 같은 이상한 기분이 들지만 예제가 이상한거라고 믿습니다… 실제로 우리는 Asynchronous 동작의 함수를 모듈로 생성해서 쓰기 때문에 Promise 패턴을 활용하면 코드의 흐름을 조금 더 명확하게 할 수 있고, 예외처리도 catch 안에서 통합 처리가 가능합니다.

promise.nodeify()

이 기능은 Callback 패턴과 Promise 패턴이 뒤섞인 상황에서 막강한 능력을 자랑합니다. 예를들어 아래와 같은 모듈이 있습니다.

이 모듈에 promise.nodeify()를 적용해보면 아래와 같습니다.

놀랍게도 nodeify()를 통해서 Model.Promise 패턴의 findAll 함수의 결과 객체와 error 객체를 callback 함수로 알아서 전달(Error-First Callback 형태)해줍니다. Model.findAll() 자체를 return 했기 때문에 something()를 호출하는 입장에서는 Callback 형태와 Promise 패턴 모두를 사용할 수 있습니다. 대박이죠. 쏠쏠합니다.


이렇게 Q 모듈의 세가지 기능을 활용한다면 Node.js를 개발하는 누구나 Promise 패턴을 쉽게 적용 가능합니다. 다음에는 ECMAScript의 Promise를 공부해서 포스팅해보겠습니다.