면접자님, Promise.all 구현해보세요

dante Yoon·2022년 8월 14일
40

js/ts

목록 보기
1/14
post-thumbnail

영상으로 보고싶다면?

https://www.youtube.com/watch?v=WCI_FAmXGEc

구글 프론트엔드 면접을 유튜브에 검색해보았을 때 나오는 영상들

면접에서 여러분이 가장 당황하는 순간은 언제인가요?
저는 전혀 예상치 못한 질문이 날라왔을 때 입니다.
그리고 그게 내가 알긴하지만 적당히 아는 지식일 경우 불현듯 불안감과 함께 목소리에 자신이 없어지는 경험은 누구나 해보셨을 것입니다.

비동기 프로그래밍에 대해 말해보세요 라는 질문은 달달 외우지만
Promise api를 직접 구현해보신 적은 혹시 있으신지요?

오늘은 Promise.all, Promise.race, Promise.allSettled 삼형제를 구현해보겠습니다.

Promise.all

Promise.all()은 배열 내 요소 중 어느 하나라도 거부하면 즉시 거부합니다. 예를 들어, 일정 시간이 지난 이후 이행하는 네 개의 프로미스와, 즉시 거부하는 하나의 프로미스를 전달한다면 Promise.all()도 즉시 거부합니다.

구현할 때 유의해야 할 점은 무엇일까요?

Promise.all의 스펙을 만족하기 위해 reject되는 promise가 하나라도 발견되면, 먼저 이행된 promise의 존재여부와 상관없이 항상 error를 reject해야 합니다.

Promise.all은 배열 형식으로 인자를 받습니다. 배열에 담긴 인자 중 Promise가 아닌 값이 있다면 이를 Promise로 변환해주는 로직을 포함해야 합니다.

아래 코드에서 promises 배열에 forEach api를 사용하여 내부에 있는 ps 요소를 사용하기 전 항상 Promise.resolve로 래핑된 값을 사용하는 것을 눈여겨 보세요.

Promise.myPromiseAll = (promises = []) => {
  return new Promise((rs, rj) => {
    let count = promises.length;
    const returnArray = [];
    promises.forEach((ps, index) => {
      Promise.resolve(ps)
        .then((value) => {
          returnArray[index] = value;
          --count;
          !count && rs(returnArray);
        })
        .catch(rj);
    });
  });
};

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve(3000), 3000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve(2000), 2000);
});

const p3 = Promise.reject(1000);

Promise.myPromiseAll([p1, p2]).then(console.log); // [3000, 2000]
Promise.myPromiseAll([p1, p2, 3]).then(console.log); // [3000, 2000, 3]
console.log(Promise.myPromiseRace([p1, p2, p3])); // Error 1000

인자의 갯수를 함수 내부에서 카운트하고 모든 promise가 다 fulfilled 되었을때 array의 index에 resolved된 값을 넣고 Promise의 resolve된 값으로 리턴합니다.

Promise.race

race는 배열 인자 중 최초 resolved된 값만 resolve합니다.
Promise.all과 마찬가지로 배열의 요소가 Promise가 아닌 경우를 대비해 Promise.resolve로 감싼 값을 사용합니다.

Promise.myPromiseRace = (promises) => {
  return new Promise((rs, rj) => {
    let finish = false;

    promises.forEach((ps) => {
      Promise.resolve(ps)
        .then((value) => {
          if (!finish) {
            rs(value);
          }
        })
        .catch(rj);
    });
  });
};

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve(3000), 3000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve(2000), 2000);
});

const p3 = Promise.reject(1000);

Promise.myPromiseRace([p1, p2]).then(console.log); // 2000
Promise.myPromiseRace([p1, p2, 3]).then(console.log); // 3
Promise.myPromiseRace([p1, p2, p3]).then(console.log); // Error 1000

Promise.allSettled

Promise.allSettled가 어떻게 동작하는지 아래 코드를 통해 살펴봅시다.
배열 요소의 Promise가 fulfilled가 아닌 rejected가 된다고 하더라도 프로미스 전체가 에러 값으로 리턴되면 안됩니다. 그리고 응답 인터페이스가 다릅니다.

Promise.allSettled([
  Promise.resolve(33),
  new Promise(resolve => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error('an error'))
])
.then(values => console.log(values));

// [
//   {status: "fulfilled", value: 33},
//   {status: "fulfilled", value: 66},
//   {status: "fulfilled", value: 99},
//   {status: "rejected",  reason: Error: an error}
// ]
Promise.myPromiseAllSettled = (promises = []) => {
  return new Promise((rs) => {
    let count = promises.length;
    const returnArray = [];

    promises.forEach((ps, index) => {
      Promise.resolve(ps)
        .then((value) => {
          returnArray[index] = { status: "fulfilled", value };
          --count;
        })
        .catch((e) => {
          returnArray[index] = {
            status: "rejected",
            reason: e
          };
          --count;
        })
        .finally(() => !count && rs(returnArray));
    });
  });
};

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve(3000), 3000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve(2000), 2000);
});

const p3 = Promise.reject(1000);

Promise.myPromiseAllSettled([p1, p2]).then(console.log);
/**
 (2) [Object, Object]
0: Object
status: "fulfilled"
value: 3000
1: Object
status: "fulfilled"
value: 2000
 */
Promise.myPromiseAllSettled([p1, p2, 3]).then(console.log);
/**
(3) [Object, Object, Object]
0: Object
status: "fulfilled"
value: 3000
1: Object
status: "fulfilled"
value: 2000
2: Object
status: "fulfilled"
value: 3
 */
Promise.myPromiseAllSettled([p1, p2, p3]).then(console.log);
/**
 (3) [Object, Object, Object]
0: Object
status: "fulfilled"
value: 3000
1: Object
status: "fulfilled"
value: 2000
2: Object
status: "rejected"
reason: "Error1000"
 */

Promise.all, Promise.allSettled, Promise.race. 호출하기만 했지 구현해본적은 없지 않으신가요? 각 api의 스펙을 맞추기 위해 이런 저런 고민을 해보면 얻는 게 많습니다.
동시 진행은 어떻게 하지?
중간에 에러가 발생하면 어떻게 처리하지?
완료 시점을 어떻게 캐치해서 배열에 resolve된 값들만 넣지?

창을 옮기거나 다른 모니터에 codesandbox를 열고 제 코드를 보기 전에 실제로 한번 구현해보시는건 어떨까요?

읽어주셔서 감사합니다.

profile
성장을 향한 작은 몸부림의 흔적들

5개의 댓글

comment-user-thumbnail
2022년 8월 24일

직접 구현은 생각해보지도 못했는데 좋은 글이네요 잘 보고갑니다~

1개의 답글
comment-user-thumbnail
2022년 9월 19일

정말 유익한 글이네요 !! 잘 읽었습니다 :)

1개의 답글
comment-user-thumbnail
2023년 8월 23일

myPromiseRace 에서 finish 는 역할이 뭔가요 ??

Promise.myPromiseRace = promises => {
  return new Promise((rs, rj) => {
    promises.forEach(ps => {
      Promise.resolve(ps).then(rs).catch(rj);
    });
  });
};

이렇게 작성하는 경우와 차이가 없어보입니다.

답글 달기