우리는 자바스크립트를 사용하며 동기/비동기에 관한 문제에 부딪힙니다. 하지만 대부분 Promise
와 async/await
문법을 사용해 슬기롭게 해결할 수 있죠! 그렇다고 Promise
와 async/await
문법만 사용하면 모든게 다 해결될까요? 아니면 더 스마트하게 사용하는 방법은 없을까요?
Promise.all() 메서드는 순회 가능한 객체에 주어진 모든 프로미스가 이행한 후, 혹은 프로미스가 주어지지 않았을 때 이행하는 Promise를 반환합니다. 주어진 프로미스 중 하나가 거부하는 경우, 첫 번째로 거절한 프로미스의 이유를 사용해 자신도 거부합니다.
- MDN Web Docs
말이 어렵습니다! '강영우'식으로 말하자면, "여러 **Promise
전체가 완료될 때 한번에 묶어서 리턴받자!"**입니다. 코드를 보겠습니다.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
이렇게 Array
에 Promise
함수들을 묶어서 넣어서 모든 Promise
가 정상적으로 종료될 때까지 기다립니다. 그리고, 각 Promise
들의 결과를 배열 순서에 맞춰 정리해서 리턴해줍니다. 그렇게 어렵지는 않네요! 만약 Promise
중 하나가 오류가 나면 어떻게 될까요?
// 매개변수 배열이 빈 것과 동일하게 취급하므로 이행함
var p = Promise.all([1,2,3]);
// 444로 이행하는 프로미스 하나만 제공한 것과 동일하게 취급하므로 이행함
var p2 = Promise.all([1,2,3, Promise.resolve(444)]);
// 555로 거부하는 프로미스 하나만 제공한 것과 동일하게 취급하므로 거부함
var p3 = Promise.all([1,2,3, Promise.reject(555)]);
// setTimeout()을 사용해 스택이 빈 후에 출력할 수 있음
setTimeout(function() {
console.log(p);
console.log(p2);
console.log(p3);
});
// 출력
// Promise { <state>: "fulfilled", <value>: Array[3] }
// Promise { <state>: "fulfilled", <value>: Array[4] }
// Promise { <state>: "rejected", <reason>: 555 }
p
, p2
는 매개변수 배열에서 거부된 Promise
가 없으므로 정상적인 fulfilled
상태이고, 각 배열의 값의 결과를 순서에 맞춰 배열에 넣어 다시 넣어 리턴합니다. 하지만 p3
의 경우 매개변수 배열의 4번째 값에서 거부가 일어났습니다. 그래서 Promise.all()
의 결과 상태는 rejected
상태이고, 실패한 이유로 매개변수 배열의 4번째 거부 사유를 리턴합니다. 결국, 매개변수 Promise중, 오류가 나는 Promise의 error를 리턴하게 된다는 것을 알 수 있습니다.
var mixedPromisesArray = [Promise.resolve(33), Promise.reject(44)];
var p = Promise.all(mixedPromisesArray);
console.log(p);
setTimeout(function() {
console.log('the stack is now empty');
console.log(p);
});
// 출력
// Promise { <state>: "pending" }
// the stack is now empty
// Promise { <state>: "rejected", <reason>: 44 }
Promise.all()
은 기본적으로 비동기적으로 동작합니다. mixedPromisesArray
는 성공, 거부되는 Promise
가 각각 배열로 선언되어 있습니다. Promise.all()
로 실행하자마자 p
를 출력해보면 Promise
가 진행중인 "pending"상태가 출력됨을 알 수 있습니다. 그리고 시간이 지나고, 다시 p를 출력하면 mixedPromisesArray
의 2번째 Promise
가 실패했다는 결과가 나오게 됩니다. 만약 Promise.all()
을 동기적으로 사용하려면 꼭 비동기 처리를 해주셔야합니다!
앞에서 살펴보았듯이, Promise.all()
은 Promise
들을 실행 중에 하나라도 reject
가 발생하면 즉시 거부됩니다. 원래라면 결과가 실패한 사유 하나만 나오지만, 실패우선성을 이용해 사전처리를 하면 여러가지 Promise
들 각각 처리해서 결과를 리턴할 수 있습니다!
var p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('p1_지연_이행'), 1000);
});
var p2 = new Promise((resolve, reject) => {
reject(new Error('p2_즉시_거부'));
});
Promise.all([
p1.catch(error => { return error }),
p2.catch(error => { return error }),
]).then(values => {
console.log(values[0]) // "p1_지연_이행"
console.log(values[1]) // "Error: p2_즉시_거부"
})
Promise.race() 메소드는 Promise 객체를 반환합니다. 이 프로미스 객체는 iterable 안에 있는 프로미스 중에 가장 먼저 완료된 것의 결과값으로 그대로 이행하거나 거부합니다.
- MDN Web Docs
race는 '경주'라는 뜻이 있죠? Promise.all()
은 모든 Promise
가 완료되는 것을 기다린다고 생각한다면, Promise.race()
는 모든 Promise
중 가장 첫 번째로 완료되는 Promise
를 리턴합니다.
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
// Both resolve, but promise2 is faster
});
// expected output: "two"
이렇게 promise1
과 promise2
가 매개변수 배열로 Promise.rase()
를 실행할 때, promise1
이 더 먼저 실행되지만, promise2
가 더 일찍 완료되기 때문에, promise2
의 리턴 값인 'two'를 리턴해주는 모습을 볼 수 있습니다. 결과적으로는 promise1
과 promise2
모두 실행됩니다.
// Promise.race를 최대한 빨리 완료시키기 위해
// 이미 이행된 프로미스로 배열을 만들어 인자로 전달
var resolvedPromisesArray = [Promise.resolve(33), Promise.resolve(44)];
var p = Promise.race(resolvedPromisesArray);
// 실행 즉시 p의 값을 기록
console.log(p);
// 호출 스택을 비운 다음 실행하기 위해 setTimeout을 사용
setTimeout(function(){
console.log('the stack is now empty');
console.log(p);
});
// 로그 출력 결과 (순서대로):
// Promise { <state>: "pending" }
// the stack is now empty
// Promise { <state>: "fulfilled", <value>: 33 }
Promise.race()
를 실행하고 바로 결과를 출력하게 되면, 아직 진행중임을 확인할 수 있습니다. 이를 통해 Promise.race()
또한 기본적으로 비동기로 실행 된다는 것을 알 수 있습니다. 동기적으로 사용하려면 꼭 비동기 처리를 해야합니다!
try {
// ...logic
} catch (err) {
// error handler
} finally {
// 최종적으로 실행할 코드
}
위 코드가 익숙하지 않으신가요? try-catch
구문에서 종종 사용되는 finally
구문입니다! 성공 여부와 관계없이 메인로직이 종료될 때 실행시켜야 할 코드가 있을 때, try
구문과 catch
구문에 중복으로 들어가는 부분을 finally
에 작성해서 가독성을 높이는 방법이죠. Promise
에서도 이와 같은 방식으로 처리할 수 있는 Promise.prototype.finally()
메소드가 있습니다.
finally() 메소드는 Promise 객체를 반환합니다. Promise가 처리되면 충족되거나 거부되는지 여부에 관계없이 지정된 콜백 함수가 실행됩니다. 이것은 Promise가 성공적으로 수행 되었는지 거절되었는지에 관계없이 Promise가 처리 된 후에 코드가 무조건 한 번은 실행되는 것을 제공합니다.
- MDN Web Docs
new Promise((resolve, reject) => {
resolve('성공!') // or reject('실패 ㅠㅠ')
}).then((data) => {
console.log(data)
}).catch((error) => {
console.error(error)
}).finally(() => {
console.info('프로미스 끝!')
})
위 코드에서 프로미스 객체가 종료될 때, 로직에 성공 여부에 관계 없이 '프로미스 끝!'이라는 문자열을 출력하고 종료하게 됩니다. 그러면 어떤 곳에 활용할 수 있을까요?
let isLoading = true;
fetch(myRequest).then(function(response) {
var contentType = response.headers.get("content-type");
if(contentType && contentType.includes("application/json")) {
return response.json();
}
throw new TypeError("Oops, we haven't got JSON!");
})
.then(function(json) { /* process your JSON further */ })
.catch(function(error) { console.log(error); })
.finally(function() { isLoading = false; });
다음은 어떤 API에서 GET요청으로 정보를 받아오는 함수입니다. 만약 contentType이 없다면 타입에러가 나고, contentType이 JSON형식이라면 결과를 리턴하겠네요! 그리고 이 fetch가 어떤 결과가 되었던지, 종료되었다는 사실을 isLoading변수를 이용해 처리를 해야합니다. 이때 then과 catch 각각 콜백함수에 isLoading을 update시켜주는 것보다 finally 콜백함수에서 처리하는게 더 깔끔해보이네요!
물론 위와 같은 메소드들을 무조건 알고있어야만 하는 것은 아닙니다! 없어도 충분히 구현할 수 있습니다. 하지만 비동기 처리와 코드 가독성, 유지보수적 측면에서 이런 다양한 기능을 하는 메소드들이 더 가치있는 개발자로 만들어주는 것 같습니다.