JavaScript의 특별한 Promise 메소드들!

YeongWoooo·2021년 8월 30일
0

우리는 자바스크립트를 사용하며 동기/비동기에 관한 문제에 부딪힙니다. 하지만 대부분 Promiseasync/await 문법을 사용해 슬기롭게 해결할 수 있죠! 그렇다고 Promiseasync/await문법만 사용하면 모든게 다 해결될까요? 아니면 더 스마트하게 사용하는 방법은 없을까요?

Promise.all()

정의

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"]

이렇게 ArrayPromise함수들을 묶어서 넣어서 모든 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.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"

이렇게 promise1promise2가 매개변수 배열로 Promise.rase()를 실행할 때, promise1이 더 먼저 실행되지만, promise2가 더 일찍 완료되기 때문에, promise2의 리턴 값인 'two'를 리턴해주는 모습을 볼 수 있습니다. 결과적으로는 promise1promise2모두 실행됩니다.

비동기성

// 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()또한 기본적으로 비동기로 실행 된다는 것을 알 수 있습니다. 동기적으로 사용하려면 꼭 비동기 처리를 해야합니다!

Promise.prototype.finally()

정의

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 콜백함수에서 처리하는게 더 깔끔해보이네요!

마치며

물론 위와 같은 메소드들을 무조건 알고있어야만 하는 것은 아닙니다! 없어도 충분히 구현할 수 있습니다. 하지만 비동기 처리와 코드 가독성, 유지보수적 측면에서 이런 다양한 기능을 하는 메소드들이 더 가치있는 개발자로 만들어주는 것 같습니다.


참고자료

MDN - Promise.prototype.finally()

profile
개발 재밌다!

0개의 댓글