[Javascript] Promise.all에서 에러 핸들링하기

Broccolism·2021년 7월 24일
10

dev-story

목록 보기
1/9
post-thumbnail
post-custom-banner

3줄 아니, 4줄 요약

1. Promise.all은 기본적으로 atomic하게 실행된다.
하지만.. 하지만 중간에 에러가 나도 계속 진행하고 싶어..!
2. map 함수를 써서 해결 가능!
3. 어라? 찾아보니 `Promise.allSettled` 라는 함수도 같은 기능을 한다고 적혀있음.
4. 그 내용이 한국어 MDN에는 안 적혀있어서 이슈 제보.

발단

Promise.all 함수에서 "에러가 난 promise는 건너뛰고 계속 진행하기" 기능이 필요했습니다.

어떻게 된 일이냐면.. ..졸업 프로젝트용 서버를 만들면서 외부 API를 사용하고 있었는데, 같은 API 내에 있는 함수인데도 데이터가 없는 경우가 생기는게 문제였습니다.

검색 페이지
사용자가 'lalaland'라는 키워드로 검색하면,
1. 'lalaland'와 관련있는 영화 목록과 함께
2. 'romance', 'musical', '엠마스톤'(철자..를..모르겠)처럼 키워드
를 검색바 아래에 보여주는 화면이죠.

이 때 보여지는 키워드를 가져오기 위해서는 1번의 '영화 목록' 안에 있는 각 영화마다 키워드를 가져오는 API 함수를 따로 호출해야 합니다. 그래서 영화 id 목록을 가져오는 API를 먼저 호출한 뒤 키워드를 불러오는 API를 따로 호출하려고 했습니다. 여기까지는 무난한 과정이죠.

그런데 분명 같은 API 안에 있는 함수임에도 불구하고 영화 목록 안에 있는 영화 id로 키워드를 가져오는 함수를 호출하면, 데이터가 없다고 뜨는 영화가 몇개 있었습니다. 이 모든 과정을 Promise.all로 처리하고 있던 저는 엄청 긴 에러 메세지와 함께 죽어버리는 서버를 어떻게든 살릴 방법을 찾아야 했습니다. 졸업은 해야 하니까요

🔥 키워드 검색 결과 데이터가 없는 영화는 건너뛰고, 나머지 데이터는 모두 불러올 수 있는 방법을 말이죠. 원래 Promise.all 함수는 "에러가 난 promise는 건너뛰기" 기능을 지원하지 않습니다.

Promise.all

Promise.all() will reject immediately upon any of the input promises rejecting
Promise.all() 함수는 주어진 promise를 실행하다가 reject 되는 것이 하나라도 있으면 즉시 실행을 멈춥니다.
- MDN Web Docs

그리고 에러를 내뿜죠.
죽은 서버
기존 코드에서는 Promise.all에서 reject가 일어나 에러가 발생하고 catch 문에 잡히게 됩니다. 결국 클라이언트에게 아무런 데이터도 주지 못하고 500 Server Error를 돌려주는거죠.

단비거야
에러 나도 다른 애들 결과값은 돌려달란말이야!

map 함수로 해결하기

  • 기존 코드
...
const tmdbIds: number[] = await TmdbApi.searchMovie(query);

const keywords: string[][] = await Promise.all(
  tmdbIds.map(async (tmdbId: number) =>
	await TmdbApi.getKeywordsById(tmdbId));
...
  • map 함수 사용해서 promise[] 분리한 코드
...
const tmdbIds: number[] = await TmdbApi.searchMovie(query);

const keywordPromises: Promise<string[]>[] = tmdbIds.map(
  async (tmdbId: number) =>
    await TmdbApi.getKeywordsById(tmdbId)
);

const keywords2D: string[][] = await Promise.all(
  keywordPromises.map((promise: Promise<string[]>) 
    => promise.catch((err) => null)));
...

가독성을 위해 keywordPromises라는 변수를 따로 만들어서 넘겨주고 있지만, 핵심은 Promise.all 함수의 파라미터로 promise 리스트를 넘겨줄 때, map 함수를 사용해서 각 promise마다 에러를 캐치하도록 만드는 것입니다.

이제 keywords2D 안에 들어있는 null값만 제거하면 됩니다!

... 라는 아이디어를 스택오버플로우에서 얻어서 해결하고, 신나게 블로그 글을 적다가 MDN에서 이런걸 발견했습니다.

Promise.allSettled 함수로 해결하기

The Promise.allSettled() method returns a promise that resolves after all of the given promises have either fulfilled or rejected, ...
Promise.allSettled는 주어진 promise의 성공/실패 여부에 관계 없이 모든 promise를 resolve한 결과를 리턴합니다.
- MDN Web Docs

왜 스택오버플로우에서 이 함수를 알려주지 않았나, 하고 살펴보니 비교적 최근에 도입된 함수라 그런 것 같았습니다. 특히 타입스크립트에 도입된지는 1년이 채 되지 않았기 때문에 아래와 같이 tsconfig.ts 파일에 따로 설정을 넣어주지 않으면 컴파일러 에러가 뜰 수도 있습니다.

// tsconfig.ts
{
  "compilerOptions": {
    ...
    "lib": ["ES2020.Promise"],
    ...
  },
  ...
}

아무튼 직접 사용해본 결과, map 함수를 썼을 때와 동일한 결과가 나왔습니다.
잘 나오는 결과

  • Promise.allSettled 함수 적용 코드
const tmdbIds: number[] = await TmdbApi.searchMovie(query);

const keywords2DResult: PromiseSettledResult<string[]>[] =
  await Promise.allSettled(
    tmdbIds.map(async (tmdbId: number) =>
      await TmdbApi.getKeywordsById(tmdbId))
  );

const keywords2D: string[][] = keywords2DResult
  .map((result) =>
    (result.status == "fulfilled" ? result.value : null))
  .filter((value) => value != null);

map 함수를 썼을 때와 비교해보면 status 값이 'fulfilled' 인지 'rejected' 인지 일일이 확인하는 코드가 추가되었습니다. 개인적으로는 map 함수를 적용했을 때의 코드가 더 깔끔해보여서 map 함수를 더 애용할 것 같습니다.

allsettled.. 좋은 경험이었다. 다신 만나지 말자!

그.. 함수가 한국어 문서에는 없는데요..

정확히 말하자면, Promise.all의 영어 문서에는 Promise.allSettled에 대한 언급이 있지만, 한국어 문서에는 없습니다. 그래서 한국어로 MDN을 보는 분들은 이 함수의 존재 조차 모를 수 있겠단 생각이 들었습니다.

한국어 문서 차이

MDN이 JS 공식 문서인줄로만 알고 있었던 저는 이 문서가 오픈소스라는 점을 처음 알게 되었습니다....... 그래서 이슈를 하나 올려놓았습니다.

바쁜 나 대신 누군가 고쳐주거나.. 한가해진 미래의 내가 고치겠지!

아무튼 시간이 나면 풀리퀘도 넣어보기로 하고, 오늘은 이쯤에서 마무리하기로 했습니다. 벌써 새벽 2시가 되어버렸네요 :D

profile
설계를 좋아합니다. 코드도 적고 그림도 그리고 글도 씁니다. 넓고 얕은 경험을 쌓고 있습니다.
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 8월 25일

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
지금 allSetteld 확인했는데 ㅋㅋㅋㅋ 한글문서를 작성자님께서 번역하셔서 올리셨군요

감사합니다

1개의 답글