Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Q의 denodeify와 nodeify

Q Promise 라이브러리에는 denodeify()nodeify()함수가 존재한다. 이름에서 알 수 있듯이 denodeify()는 node.js의 Callback 형식을 Promise 형식으로 바꾸는 함수이고 nodeify()는 Promise 스타일을 Callback 스타일로 바꾸는 함수이다.

Q.denodeify

Q.denodeify는 작년 발표에서 간단히 소개하기도 했지만, 위에서 말한 대로 Node.js의 표준 Callback을 Promise 변환하므로 Callback만 지원하는 라이브러리랑 Promise를 섞어서 사용할 때 유용하다.

var fs = require('fs');

var read = function(path, callback) {
  fs.readFile(path, function(err, data) {
    callback(err, data);
  });
}

위와 같은 간단한 함수가 있다고 해보자. 단순히 fs.readFile를 호출해서 결과를 콜백으로 다시 돌려주는 함수이다. 이 read함수를 Promise로 사용하려면 Promise 형식으로 바꾸어야 하는데 보통 Q.defer를 사용해서 다음과 같이 바꾼다.

var fs = require('fs');
var Q = require('q');

var read = function(path) {
  var deferred = Q.defer();
  fs.readFile(path, function(err, data) {
    if (err) { return deferred.reject(err); }
    deferred.resolve(data);
  });
  return deferred.promise;
}

이제 Promise 형식이 되었으므로 read('./test.txt').then(function(data) {}).catch(function(error) {});와 같이 사용할 수 있다. Q.defer에는 makeNodeResolver같은 함수도 있지만 Q.denodeify를 사용하면 다음과 같이 간단히 바꿀 수 있다.

var fs = require('fs');
var Q = require('q');

var read = Q.denodeify(fs.readFile);

Node.js의 표준 콜백 형식인 첫 파라미터는 error이고 두 번째 파라미터가 결과 값인 function(err, data) {}이면 위 한 줄로 Promise로 바꿀 수 있다. 물론 비슷한 역할을 하는 Q.nfbind, Q.nbind, Q.nfapply, Q.nfcall등도 있어서 상황에 맞게 사용할 수 있다.

promise.nodeify

promise.nodeify는 반대로 Promise 함수를 콜백 형식으로 변환해 주는 역할을 한다. Q.nodeify가 아니라 promise.nodeify라고 쓴 이유는 Q 객체의 함수가 아니라 Q에서 반환한 Promise 객체의 함수라는 의미이다.

var fs = require('fs');
var Q = require('q');

var read = function(path, callback) {
  var promise = Q.denodeify(fs.readFile);
  return promise(path);
}

위와 같은 Promise 함수가 있다고 해보자. 아까와는 반대로 Promise 함수를 Callback 형식으로 사용하려면 다음과 같이 변환할 수 있다.

var fs = require('fs');
var Q = require('q');

var read = function(path, callback) {
  var promise = Q.denodeify(fs.readFile);
  return promise(path)
    .then(function(data) {
      callback(null, data);
    })
    .catch(function(err) {
      callback(err);
    });
}

read('./test.txt', function(err, data) {
  if (err) { return console.error(err); }
  console.log(dta);
});

이렇게 콜백으로 강제로 변환하면 실제로는 잘 동작하는 것 같지만, 오류처리에 문제가 좀 있다.(Promise를 많이 사용하는 편은 아니라서 이 부분에서 고생을 좀 했다.) Promise의 then()에서 정상 콜백을 호출하고 catch()에서는 오류 콜백을 호출하면 일반 콜백처럼 인터페이스를 유지할 수 있지만, 문제는 호출된 콜백에서 오류가 생기게 되면 다시 원래의 Promise에서 catch 부분이 받아서 다시 콜백을 호출하게 된다. 그래서 콜백이 결과적으로는 2번 호출되게 된다.

var fs = require('fs');
var Q = require('q');

var read = function(path, callback) {
  var promise = Q.denodeify(fs.readFile);
  return promise(path)
    .then(function(data) {
      return data;
    })
    .catch(function(err) {
      return err;
    })
    .nodeify(callback);
}

read('./test.txt', function(err, data) {
  if (err) { return console.error(err); }
  console.log(data);
});

.nodeify(callback)는 Promise 객체에서 사용할 수 있으므로 Promise의 체이닝에 바로 연결할 수 있다. 체이닝에 nodeify()함수를 연결하고 인자로 호출할 콜백을 전달하면 된다. 그리고 앞에 then()이나 catch()가 있는 경우에는 값을 retrun해주어야 콜백에서 정상적으로 받을 수 있다. 리턴하지 않으면 다음 체이닝으로 연결되는 값이 없으므로 콜백에 인자도 전달되지 않는다. 이렇게 nodeify()를 사용했을 경우에는 콜백에서 오류가 발견한다고 다시 Promise로 돌아가지 않는다.

그리고 이 경우에는 Promise 객체를 리턴했으므로 호출하는 쪽에서 Promise 형식과 Callback 형식을 동시에 사용할 수 있다. 그래서 위처럼 read()함수를 콜백형태로 사용하는 대신 다음과 같이 Promise 형식으로 사용해도 아무런 문제가 없다.

read('./test.txt')
  .then(function(data) {})
  .catch(function(error) {});
2015/03/30 23:57 2015/03/30 23:57