Promise and Bluebird
Daniel Ku (http://danielku.com/)
PromiseJavascript 코드 가독성을 높이기 위한 위한 베스트 프랙티스
Promise 스펙: https://promisesaplus.com/
Bluebird가장 유명한 Promise 구현체 중 하나
https://github.com/petkaantonov/bluebird
Node.js 사용 시
이렇게 선언하게 한다.
var Promise = require('bluebird');
나는 보통 다음과 같이 선언한다.
var Q = require('bluebird');
이유: 짧으니까
맛보기
이러한 코드가 있다고 하자.
User.findOne({email: email}, function(err, user) {
if (err) {
console.error(err);
} else {
Group.find({owner: user.id}, function(err, groups) {
if (err) {
console.error(err);
} else {
try {
// do something with groups
console.log('success');
} catch (err) {
console.error(err);
}
}
});
});
문제점 1. 중첩된 콜백과 if절 등으로 가독성이 떨어진다.
User.findOne({email: email}, function(err, user) {
if (err) {
console.error(err);
} else {
Group.find({owner: user.id}, function(err, groups) {
if (err) {
console.error(err);
} else {
try {
// do something with groups
console.log('success');
} catch (err) {
console.error(err);
}
}
});
});
문제점 2. 동일한 에러 처리 로직이 중복되어 사용되었다.
User.findOne({email: email}, function(err, user) {
if (err) {
console.error(err);
} else {
Group.find({owner: user.id}, function(err, groups) {
if (err) {
console.error(err);
} else {
try {
// do something with groups
console.log('success');
} catch (err) {
console.error(err);
}
}
});
});
Promise를 이용해 바꾸어 보자. 더 길어지긴 했지만,
new Promise(function(resolve, reject) {
User.findOne({email: email}, function(err, user) {
if (err) reject(err);
else resolve(user);
});
})
.then(function(user) {
return new Promise(function(resolve, reject) {
Group.find({owner: user.id}, function(err, groups) {
if (err) reject(err);
else resolve(groups);
});
});
})
.then(function(groups) {
// do something with groups
})
.then(function() {
console.log('success');
})
.catch(function(err) {
console.error(err);
});
개선점 1. 작업의 단계가 명확히 구분된다.
new Promise(function(resolve, reject) {
User.findOne({email: email}, function(err, user) {
if (err) reject(err);
else resolve(user);
});
})
.then(function(user) {
return new Promise(function(resolve, reject) {
Group.find({owner: user.id}, function(err, groups) {
if (err) reject(err);
else resolve(groups);
});
});
})
.then(function(groups) {
// do something with groups
})
.then(function() {
console.log('success');
})
.catch(function(err) {
console.error(err);
});
개선점 2. 에러도 한군데에서 다 처리한다.
new Promise(function(resolve, reject) {
User.findOne({email: email}, function(err, user) {
if (err) reject(err);
else resolve(user);
});
})
.then(function(user) {
return new Promise(function(resolve, reject) {
Group.find({owner: user.id}, function(err, groups) {
if (err) reject(err);
else resolve(groups);
});
});
})
.then(function(groups) {
// do something with groups
})
.then(function() {
console.log('success');
})
.catch(function(err) {
console.error(err);
});
더 개선해 보자. 사용자를 찾는 부분을..
new Promise(function(resolve, reject) {
User.findOne({email: email}, function(err, user) {
if (err) reject(err);
else resolve(user);
});
});
다음과 같이 Wrapper 형태로 뽑아냈다고 가정해보자.
var UserWrapper = {
findOne: function(query) {
return new Promise(function(resolve, reject) {
User.findOne(query, function(err, user) {
if (err) reject(err);
else resolve(user);
});
});
}
};
그룹들을 찾는 부분도 마찬가지로..
var GroupWrapper = {
find: function(query) {
return new Promise(function(resolve, reject) {
Group.find(query, function(err, groups) {
if (err) reject(err);
else resolve(groups);
});
});
}
};
그러면 이런 식으로 바꿀 수 있다. 가독성이 훨씬 좋아졌다.
UserWrapper.findOne({ email: email })
.then(function(user) {
return GroupWrapper.find({ owner: user.id });
})
.then(function(groups) {
// do something with groups
})
.then(function() {
console.log('success');
})
.catch(function(err) {
console.error(err);
});
참고로 이러한 변환 과정을 Promisification이라고 하며,
Bluebird에서 제공하는 Promise.promisify() 함수를 이용하면
더 쉽게 할 수 있다.
극단적으로는 다음과도 같이 할 수 있다.
findUserByEmail(email)
.then(findOwningGroups)
.then(doSomethingWithGroups)
.then(whenSuccess)
.catch(whenFail);
주석이 필요없지 않은가? ㅋㅋ
시작, 중간 그리고 끝
시작: Promise Chain을 시작하는 방법
가장 쉬운 방법 중 하나는 Promise.defer() 함수를 이용하는 방
법이지만 deprecated되었다.
function dont_do_like_this() {
var deferred = Promise.defer();
doSomethingAsync(function(err, result) {
if (err) return deferred.reject(err);
deferred.resolve(result);
});
return deferred.promise;
}
그러므로 다음과 같이 Constructor를 이용하는 것이 더 바람직하
다.
function ok() {
return new Promise(function (resolve, reject) {
doSomethingAsync(function(err, result) {
if (err) return reject(err);
resolve(result);
});
});
}
Callback이 없다면 Promise.try() 함수를 이용할 수도 있다.
function checkSomething(something) {
return Promise.try(function () {
if (is_not_valid(something)) {
throw new Error('Something is not valid.');
}
return something;
});
}
로직 없이 주어진 값이나 오브젝트로 바로 Promise를 생성할 수도
있다.
function startWithValue(value) {
return Promise.resolve(value);
}
심지어 에러로 Promise를 생성할 수도 있다. 쓸 일이 있을까?
function startWithError(error) {
return Promise.reject(error);
}
중간: Promise Chain을 연결시키는 방법
Promise는 다음과 같이 .then() 함수와 .catch() 함수로 이어
나갈 수 있다.
ajaxGetAsync(url)
.then(function(result) {
return parseValueAsync(result);
})
.catch(function(error) {
console.error('error:', error);
});
직전 Promise의 결과 값과 상관 없이 특정 값으로 Promise
Chain을 이어가려면..
somethingAsync()
.return(value);
위는 아래와 같다.
somethingAsync()
.then(function() {
return value;
});
직전 Promise의 결과 값과 상관 없이 특정 에러로 Promise
Chain을 이어가려면..
somethingAsync()
.throw(reason);
위는 아래와 같다.
somethingAsync()
.then(function() {
throw reason;
});
.finally() 함수도 있다.
Promise.resolve('test')
.catch(console.error.bind(console))
.finally(function() {
return 'finally';
})
.then(function(result) {
console.log(result); // result is 'test'
});
끝: Promise Chain을 끝내는 방법
가장 간단한 방법은 더 이상 Promise Chain을 연결하지 않는 것
이다.
하지만 명시적으로 .done() 함수를 쓸 수 있다.
somethingAsync()
.done();
Alias
Promise.try() === Promise.attemp()
.catch() === .caught()
.return() === .thenReturn()
.throw() === .thenThrow()
.finally() === .lastly()
몇가지 안티 패턴
이렇게 하지 않도록 주의하라. (X)
somethingAsync()
.then(function(result) {
return anotherAsync(result);
}, function(err) {
handleError(err);
});
이렇게 해야 한다. (O)
somethingAsync()
.then(function(result) {
return anotherAsync(result);
})
.catch(function(err) {
handleError(err);
});
이렇게 하지 않도록 주의하라. (X)
var promise = somethingAsync();
if (is_another_necessary) {
promise.then(function(result) {
return anotherAsync(result);
});
}
promise.then(function(result) {
doSomethingWithResult(result);
});
이렇게 해야 한다. (O)
var promise = somethingAsync();
if (is_another_necessary) {
promise = promise.then(function(result) {
return anotherAsync(result);
});
}
promise.then(function(result) {
doSomethingWithResult(result);
});
편리한 유틸리티 함수들
.all()
.any()
.some()
.spread()
설명이 필요 없다.
Promise
.all([
pingAsync("ns1.example.com"),
pingAsync("ns2.example.com"),
pingAsync("ns3.example.com"),
pingAsync("ns4.example.com")
]).spread(function(first, second, third, fourth) {
console.log(first, second, third, fourth);
});
설명이 필요 없다.
Promise
.any([
pingAsync("ns1.example.com"),
pingAsync("ns2.example.com"),
pingAsync("ns3.example.com"),
pingAsync("ns4.example.com")
]).spread(function(first) {
console.log(first);
});
설명이 필요 없다.
Promise
.some([
pingAsync("ns1.example.com"),
pingAsync("ns2.example.com"),
pingAsync("ns3.example.com"),
pingAsync("ns4.example.com")
], 2).spread(function(first, second) {
console.log(first, second);
});
.map()
.reduce()
.filter()
.each()
설명이 필요 없다.
Group.listAsync({user: user.id})
.map(function(group) {
return group.removeAsync(user.id);
})
.then(function() {
return user.removeAsync();
});
설명이 필요 없다.
Group.listAsync({user: user.id})
.filter(function(group) {
return group.owner_id === user.id;
});
.map() 혹은 .filter()에서의 concurrency 제한에 대하여
Group.listAsync({user: user.id})
.map(function(group) {
return group.removeAsync(user.id);
}, {concurrency: 1});
참고로 하나씩 처리하려면 이렇게 할 수도 있다.
Group.listAsync({user: user.id})
.reduce(function(promise, group) {
return promise.then(function(groups) {
return group.removeAsync(user.id)
.then(function(group) {
groups.push(group);
})
.return(groups);
});
}, []);
(확인은 안 해봤지만 아마도 틀리지 않을 듯;;)
.delay()
설명이 필요 없다.
Promise
.delay(500)
.then(function() {
return 'Hello world';
})
.delay(500)
.then(function(result) {
console.log(result);
});
.tap()
Promise Chain을 연결하긴 하지만 결과를 넘겨주진 않음..
somethingAsync()
.tap(function(result_of_somethingAsync) {
return anotherAsync(result_of_somethingAsync);
})
.then(function(result_of_somethingAsync) {
console.log(result_of_somethingAsync);
});
.bind()
이런 식으로 앞의 결과를 나중에 써야할 경우엔 콜백에서처럼 가독
성이 떨어지는데..
somethingAsync()
.spread(function (a, b) {
return anotherAsync(a, b)
.then(function (c) {
return a + b + c;
});
});
이런 식으로도 해결할 수 있지만..
var scope = {};
somethingAsync()
.spread(function (a, b) {
scope.a = a;
scope.b = b;
return anotherAsync(a, b);
})
.then(function (c) {
return scope.a + scope.b + c;
});
이게 더 깔끔하다.
somethingAsync()
.bind({})
.spread(function (a, b) {
this.a = a;
this.b = b;
return anotherAsync(a, b);
})
.then(function (c) {
return this.a + this.b + c;
});
고급 예제
10초 마다 어떤 작업을 반복하도록 하려면?
10초 마다 어떤 작업을 반복하도록 하려면?
var loop = function() {
Worker.executeAsync()
.catch(function(err) {
console.error('err:', err);
})
.delay(10000)
.then(loop);
};
loop();
매우 긴 작업 목록을 10개씩 끊어서 처리하려면?
매우 긴 작업 목록을 10개씩 끊어서 처리하려면?
var traverseJobs = function(handleTenJobsAsync) {
var limit = 10;
var loop = function(skip) {
return listJobsAsync(limit, skip)
.then(function(jobs) {
if (jobs.length) {
return handleTenJobsAsync(jobs)
.then(function() {
return loop(skip + limit);
});
}
});
};
return loop(0);
};
못다한 고급 주제
— Resource Management
— Cancellation & .timeout()
— Built-in Errors
— Error Management configuration
— Generators
https://github.com/petkaantonov/bluebird/blob/
master/API.md
참고
— Bluebird
— Bluebird API Reference
— Promise Anti-patterns
THE END

Promise and Bluebird

  • 1.
    Promise and Bluebird DanielKu (http://danielku.com/)
  • 2.
    PromiseJavascript 코드 가독성을높이기 위한 위한 베스트 프랙티스 Promise 스펙: https://promisesaplus.com/
  • 3.
    Bluebird가장 유명한 Promise구현체 중 하나 https://github.com/petkaantonov/bluebird
  • 4.
    Node.js 사용 시 이렇게선언하게 한다. var Promise = require('bluebird'); 나는 보통 다음과 같이 선언한다. var Q = require('bluebird'); 이유: 짧으니까
  • 5.
  • 6.
    이러한 코드가 있다고하자. User.findOne({email: email}, function(err, user) { if (err) { console.error(err); } else { Group.find({owner: user.id}, function(err, groups) { if (err) { console.error(err); } else { try { // do something with groups console.log('success'); } catch (err) { console.error(err); } } }); });
  • 7.
    문제점 1. 중첩된콜백과 if절 등으로 가독성이 떨어진다. User.findOne({email: email}, function(err, user) { if (err) { console.error(err); } else { Group.find({owner: user.id}, function(err, groups) { if (err) { console.error(err); } else { try { // do something with groups console.log('success'); } catch (err) { console.error(err); } } }); });
  • 8.
    문제점 2. 동일한에러 처리 로직이 중복되어 사용되었다. User.findOne({email: email}, function(err, user) { if (err) { console.error(err); } else { Group.find({owner: user.id}, function(err, groups) { if (err) { console.error(err); } else { try { // do something with groups console.log('success'); } catch (err) { console.error(err); } } }); });
  • 9.
    Promise를 이용해 바꾸어보자. 더 길어지긴 했지만, new Promise(function(resolve, reject) { User.findOne({email: email}, function(err, user) { if (err) reject(err); else resolve(user); }); }) .then(function(user) { return new Promise(function(resolve, reject) { Group.find({owner: user.id}, function(err, groups) { if (err) reject(err); else resolve(groups); }); }); }) .then(function(groups) { // do something with groups }) .then(function() { console.log('success'); }) .catch(function(err) { console.error(err); });
  • 10.
    개선점 1. 작업의단계가 명확히 구분된다. new Promise(function(resolve, reject) { User.findOne({email: email}, function(err, user) { if (err) reject(err); else resolve(user); }); }) .then(function(user) { return new Promise(function(resolve, reject) { Group.find({owner: user.id}, function(err, groups) { if (err) reject(err); else resolve(groups); }); }); }) .then(function(groups) { // do something with groups }) .then(function() { console.log('success'); }) .catch(function(err) { console.error(err); });
  • 11.
    개선점 2. 에러도한군데에서 다 처리한다. new Promise(function(resolve, reject) { User.findOne({email: email}, function(err, user) { if (err) reject(err); else resolve(user); }); }) .then(function(user) { return new Promise(function(resolve, reject) { Group.find({owner: user.id}, function(err, groups) { if (err) reject(err); else resolve(groups); }); }); }) .then(function(groups) { // do something with groups }) .then(function() { console.log('success'); }) .catch(function(err) { console.error(err); });
  • 12.
    더 개선해 보자.사용자를 찾는 부분을.. new Promise(function(resolve, reject) { User.findOne({email: email}, function(err, user) { if (err) reject(err); else resolve(user); }); });
  • 13.
    다음과 같이 Wrapper형태로 뽑아냈다고 가정해보자. var UserWrapper = { findOne: function(query) { return new Promise(function(resolve, reject) { User.findOne(query, function(err, user) { if (err) reject(err); else resolve(user); }); }); } };
  • 14.
    그룹들을 찾는 부분도마찬가지로.. var GroupWrapper = { find: function(query) { return new Promise(function(resolve, reject) { Group.find(query, function(err, groups) { if (err) reject(err); else resolve(groups); }); }); } };
  • 15.
    그러면 이런 식으로바꿀 수 있다. 가독성이 훨씬 좋아졌다. UserWrapper.findOne({ email: email }) .then(function(user) { return GroupWrapper.find({ owner: user.id }); }) .then(function(groups) { // do something with groups }) .then(function() { console.log('success'); }) .catch(function(err) { console.error(err); });
  • 16.
    참고로 이러한 변환과정을 Promisification이라고 하며, Bluebird에서 제공하는 Promise.promisify() 함수를 이용하면 더 쉽게 할 수 있다.
  • 17.
    극단적으로는 다음과도 같이할 수 있다. findUserByEmail(email) .then(findOwningGroups) .then(doSomethingWithGroups) .then(whenSuccess) .catch(whenFail); 주석이 필요없지 않은가? ㅋㅋ
  • 18.
  • 19.
    시작: Promise Chain을시작하는 방법 가장 쉬운 방법 중 하나는 Promise.defer() 함수를 이용하는 방 법이지만 deprecated되었다. function dont_do_like_this() { var deferred = Promise.defer(); doSomethingAsync(function(err, result) { if (err) return deferred.reject(err); deferred.resolve(result); }); return deferred.promise; }
  • 20.
    그러므로 다음과 같이Constructor를 이용하는 것이 더 바람직하 다. function ok() { return new Promise(function (resolve, reject) { doSomethingAsync(function(err, result) { if (err) return reject(err); resolve(result); }); }); }
  • 21.
    Callback이 없다면 Promise.try()함수를 이용할 수도 있다. function checkSomething(something) { return Promise.try(function () { if (is_not_valid(something)) { throw new Error('Something is not valid.'); } return something; }); }
  • 22.
    로직 없이 주어진값이나 오브젝트로 바로 Promise를 생성할 수도 있다. function startWithValue(value) { return Promise.resolve(value); } 심지어 에러로 Promise를 생성할 수도 있다. 쓸 일이 있을까? function startWithError(error) { return Promise.reject(error); }
  • 23.
    중간: Promise Chain을연결시키는 방법 Promise는 다음과 같이 .then() 함수와 .catch() 함수로 이어 나갈 수 있다. ajaxGetAsync(url) .then(function(result) { return parseValueAsync(result); }) .catch(function(error) { console.error('error:', error); });
  • 24.
    직전 Promise의 결과값과 상관 없이 특정 값으로 Promise Chain을 이어가려면.. somethingAsync() .return(value); 위는 아래와 같다. somethingAsync() .then(function() { return value; });
  • 25.
    직전 Promise의 결과값과 상관 없이 특정 에러로 Promise Chain을 이어가려면.. somethingAsync() .throw(reason); 위는 아래와 같다. somethingAsync() .then(function() { throw reason; });
  • 26.
    .finally() 함수도 있다. Promise.resolve('test') .catch(console.error.bind(console)) .finally(function(){ return 'finally'; }) .then(function(result) { console.log(result); // result is 'test' });
  • 27.
    끝: Promise Chain을끝내는 방법 가장 간단한 방법은 더 이상 Promise Chain을 연결하지 않는 것 이다.
  • 28.
    하지만 명시적으로 .done()함수를 쓸 수 있다. somethingAsync() .done();
  • 29.
    Alias Promise.try() === Promise.attemp() .catch()=== .caught() .return() === .thenReturn() .throw() === .thenThrow() .finally() === .lastly()
  • 30.
  • 31.
    이렇게 하지 않도록주의하라. (X) somethingAsync() .then(function(result) { return anotherAsync(result); }, function(err) { handleError(err); });
  • 32.
    이렇게 해야 한다.(O) somethingAsync() .then(function(result) { return anotherAsync(result); }) .catch(function(err) { handleError(err); });
  • 33.
    이렇게 하지 않도록주의하라. (X) var promise = somethingAsync(); if (is_another_necessary) { promise.then(function(result) { return anotherAsync(result); }); } promise.then(function(result) { doSomethingWithResult(result); });
  • 34.
    이렇게 해야 한다.(O) var promise = somethingAsync(); if (is_another_necessary) { promise = promise.then(function(result) { return anotherAsync(result); }); } promise.then(function(result) { doSomethingWithResult(result); });
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
    설명이 필요 없다. Group.listAsync({user:user.id}) .map(function(group) { return group.removeAsync(user.id); }) .then(function() { return user.removeAsync(); });
  • 42.
    설명이 필요 없다. Group.listAsync({user:user.id}) .filter(function(group) { return group.owner_id === user.id; });
  • 43.
    .map() 혹은 .filter()에서의concurrency 제한에 대하여 Group.listAsync({user: user.id}) .map(function(group) { return group.removeAsync(user.id); }, {concurrency: 1});
  • 44.
    참고로 하나씩 처리하려면이렇게 할 수도 있다. Group.listAsync({user: user.id}) .reduce(function(promise, group) { return promise.then(function(groups) { return group.removeAsync(user.id) .then(function(group) { groups.push(group); }) .return(groups); }); }, []); (확인은 안 해봤지만 아마도 틀리지 않을 듯;;)
  • 45.
  • 46.
    설명이 필요 없다. Promise .delay(500) .then(function(){ return 'Hello world'; }) .delay(500) .then(function(result) { console.log(result); });
  • 47.
  • 48.
    Promise Chain을 연결하긴하지만 결과를 넘겨주진 않음.. somethingAsync() .tap(function(result_of_somethingAsync) { return anotherAsync(result_of_somethingAsync); }) .then(function(result_of_somethingAsync) { console.log(result_of_somethingAsync); });
  • 49.
  • 50.
    이런 식으로 앞의결과를 나중에 써야할 경우엔 콜백에서처럼 가독 성이 떨어지는데.. somethingAsync() .spread(function (a, b) { return anotherAsync(a, b) .then(function (c) { return a + b + c; }); });
  • 51.
    이런 식으로도 해결할수 있지만.. var scope = {}; somethingAsync() .spread(function (a, b) { scope.a = a; scope.b = b; return anotherAsync(a, b); }) .then(function (c) { return scope.a + scope.b + c; });
  • 52.
    이게 더 깔끔하다. somethingAsync() .bind({}) .spread(function(a, b) { this.a = a; this.b = b; return anotherAsync(a, b); }) .then(function (c) { return this.a + this.b + c; });
  • 53.
  • 54.
    10초 마다 어떤작업을 반복하도록 하려면?
  • 55.
    10초 마다 어떤작업을 반복하도록 하려면? var loop = function() { Worker.executeAsync() .catch(function(err) { console.error('err:', err); }) .delay(10000) .then(loop); }; loop();
  • 56.
    매우 긴 작업목록을 10개씩 끊어서 처리하려면?
  • 57.
    매우 긴 작업목록을 10개씩 끊어서 처리하려면? var traverseJobs = function(handleTenJobsAsync) { var limit = 10; var loop = function(skip) { return listJobsAsync(limit, skip) .then(function(jobs) { if (jobs.length) { return handleTenJobsAsync(jobs) .then(function() { return loop(skip + limit); }); } }); }; return loop(0); };
  • 58.
  • 59.
    — Resource Management —Cancellation & .timeout() — Built-in Errors — Error Management configuration — Generators https://github.com/petkaantonov/bluebird/blob/ master/API.md
  • 60.
    참고 — Bluebird — BluebirdAPI Reference — Promise Anti-patterns
  • 61.