[자바스크립트] 비동기 처리를 위한 Promise 활용법
자바스크립트 개발을 하다보면 만나는 가장 골치아픈 것이 바로 시점 문제다. 자바스크립트는 대부분의 작업이 비동기로 처리되기 때문에 일단 스크립트가 실행되면 물흐르듯 쭉~ 흘러가버린다. 결과적으로 내가 의도했던 순서대로 코드가 실행되지 않게 되는 것이다.
그래서 콜백이 활용된다. 예를들어, 'A작업이 끝나는 시점에 콜백 함수를 호출해서 B작업을 수행'하는 식이다. 근데 이렇게 콜백을 활용하다보면 또 다른 문제를 마주하게 된다. 이는 콜백지옥이라는 표현으로 익히 알려져있다.
var async1 = function(param, callback) {callback(param*2);}
var async2 = function(param, callback) {callback(param*3);}
var async3 = function(param, callback) {callback(param*4);}
var start = 1;
async1(start, result => {
async2(result, result => {
async3(result, result => {
console.log(result); // 24
});
});
});
위 예제를 보자. 2 * 3 * 4를 차례대로 수행하기 위해 함수를 중첩하였다. 3개의 함수가 중첩되어 있을뿐이지만 이미 읽기가 힘들다. 콜백이 몇번 더 중첩되면 손쓸 수 없는 지경에 이르게 되는데, 이를 콜백지옥이라 부르는 것이다.
❙ Promise
따라서 이 문제를 해결하기 위해 Promise가 등장하게 된다. 처음에는 Q나 when 같은 library에서 제공하기 시작했으나 es6부터는 언어 수준에서 공식적으로 제공되고 있다.
Promise는 의미 그대로 '약속'이다. '지금은 없지만 이따가 줄게!' 라는 약속인 것이다. 먼저, 위에서 보았던 예를 promise 패턴으로 재구성해보면 이런식이다. 전체적으로 어떻게 흘러가는지 파악해보도록 하자.
function async1 (param) {
return new Promise(function(resolve, reject) {
if(param)
resolve(param*2);
else
reject(Error("param이 없습니다."));
});
}
function async2 (param) {
return new Promise(function(resolve, reject) {
if(param)
resolve(param*3);
else
reject(Error("param이 없습니다."));
});
}
function async3 (param) {
return new Promise(function(resolve, reject) {
if(param)
resolve(param*4);
else
reject(Error("param이 없습니다."));
});
}
function fail(){
console.log("함수 호출에 실패했습니다.");
}
var start = 1;
async1(start)
.then(async2, fail)
.then(async3, fail)
.then(result => {
console.log(result); // 24
}, fail);
뭔지는 모르겠지만 훨씬 깔끔하고 보기좋은 코드가 되었다. 차례차례 살펴보자.
먼저, 각 async 함수를 보면 Promise 객체를 리턴하고 있다. 이 Promise 객체의 생성자는 resolve와 reject라는 두개의 파라미터를 가진 콜백함수를 인자로 취한다. 콜백 내에서 비동기 작업이 순조롭게 수행됐다면 resolve가, 그렇지 않다면 reject가 호출된다.
var start = 1;
async1(start)
.then(async2, fail)
.then(async3, fail)
.then(result => {
console.log(result); // 24
}, fail);
Promise 함수를 호출하는 부분을 보자. 이때, 실제로 Promise 함수의 수행이 완료되면 호출되는 then이라는 메서드가 실행되게 된다. then 메서드는 역시 콜백함수 두개를 인자로 받는데, 각각은 성공시, 그리고 실패시 호출된다. 처음 호출한 async1 함수가 성공적으로 수행됐을때 이어서 async2 함수가 수행되는 것이다.
조금 더 간단한 예제를 보자.
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
Promise 객체를 생성했고 성공시에는 resolve, 실패시 reject 함수를 호출한다. 12라인에서 promise 함수를 호출하는데, 성공시에는 그대로 resolve 콜백안에 담긴 인자가 콘솔에 찍힐 것이며, 실패시에는 reject 콜백안에 담긴 Error가 찍힌다.
다만 이 예제는 위에서 본것과 약간의 차이가 있다. 눈치챘는지는 모르겠지만 promise 객체를 리턴하지 않고 바로 생성해서 사용하고 있다. 둘다 결과적으로는 promise 객체가 얻어지겠지만 몇몇 차이는 있다. 먼저, 바로 객체를 생성할 경우 다음과 같이 사용이 가능하다.
new Promise(function(resolve, reject) {
// 50프로 확률로 resolve
if (+new Date()%2 === 0) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
}).then(console.log("success!");
return이 아니라 바로 객체를 생성하므로 then 메서드 또한 바로 붙여놓을 수 있게 되는 것이다. 또한 한 가지 차이가 더 있다.
❙ Promise.all
Promise.all은 여러개의 비동기 작업들이 모두 완료되고나서 작업을 진행하기 위해 사용된다. 다음의 예제를 살펴보면 쉽게 이해할 수 있을 것이다.
var promise1 = new Promise(function (resolve, reject) {
// 비동기를 표현하기 위해 setTimeout 함수를 사용
window.setTimeout(function () {
console.log("첫번째 Promise 완료");
resolve("promise1");
}, 2000);
});
var promise2 = new Promise(function (resolve, reject) {
// 비동기를 표현하기 위해 setTimeout 함수를 사용
window.setTimeout(function () {
console.log("두번째 Promise 완료");
resolve("promise2");
}, 1000);
});
Promise.all([promise1, promise2]).then(function (values) {
console.log("모두 완료됨", values);
});
코드를 보면 promise1이 2초, promise1이 1초의 시간을 두고 수행된다. 그런데 Promise.all 메서드를 사용하면 이 두 함수의 수행이 모두 완료된 이후에야 then의 콜백함수(23라인)가 수행된다. 마지막 작업이 모두 수행될때까지 기다렸다가 한번에 리턴을 주는 아름다운 우정인 셈이다.
이 Promise.all 메서드는 인자로 배열을 받는데, 배열의 원소로는 반드시 Promise 객체가 들어가야 한다. 즉, Promise 객체 자체가 아닌 수행시에 Promise 객체가 리턴되는 함수가 인자로 들어가서는 안된다는 뜻이다. 따라서 처음 살펴본 Promise 예제처럼 Promise 객체를 리턴하는 형태의 함수는 Promise.all 에서 사용할 수 없다.
❙ Promise.catch
Promise 함수의 체이닝 중간에 에러가 발생할 위험은 없을까? 그래서 제공되는 것이 Promise.catch 메서드이다. 함수 체이닝의 중간에 발생한 에러를 잡아준다. 다음 예제만 살펴봐도 쉽게 이해할 수 있을 것이다.
asyncThing1()
.then(function() { return asyncThing2();})
.then(function() { return asyncThing3();})
.catch(function(err) { return asyncRecovery1();})
.then(function() { return asyncThing4();}
,function(err) { return asyncRecovery2(); })
.catch(function(err) { console.log("Don't worry about it");})
.then(function() { console.log("All done!");
});
- 끝 -
참고
https://developers.google.com/web/fundamentals/primers/promises?hl=ko
http://programmingsummaries.tistory.com/325
http://han41858.tistory.com/11