[JS] Promise.all과 Promise.race의 차이점과 활용 예제

2022. 4. 12. 10:07Javascript

728x90

이번 포스트에서는 Promise 객체의 정적 메소드인 all 과 race에 대해서 알아보도록 하겠습니다.

두 메소드 모두 프로미스 배열을 인자로 받지만 동작하는 방법에는 큰 차이가 있기때문에 두 메소드의 차이점과 각각 어떤 상황에 유용하게 사용할수 있는지 예시와 함께 살펴보도록 하겠습니다.

 

프로미스(Promise)란?

자바스크립트에서 프로미스는 비동기 처리를 위한 패턴(객체 형식)중 하나로 기존 콜백 패턴의 단점을 보완해서 ES6에 도입되었습니다. 현재는 비동기 처리를 동기방식의 코드를 작성하듯이 사용가능한 async await 문법이 추가가 되었지만 이는 프로미스를 대체하는 패턴은 아니며 결국에 async 함수가 반환하는 값은 프로미스입니다.

 

프로미스는 pending(대기), reject(실패), fulfill(이행) 3가지 중 하나의 상태를 가질수 있는데 각 상태에 따른 처리 방식은 아래와 같습니다.

 

 

Promise flow - MDN

 

프로미스에 대해 좀 더 자세히 알고 싶다면 아래 링크를 한번 읽어보시는걸 추천합니다.

 

 

자바스크립트 Promise 쉽게 이해하기

(중급) 자바스크립트 입문자를 위한 Promise 설명. 쉽게 알아보는 자바스크립트 Promise 개념, 사용법, 예제 코드. 예제로 알아보는 then(), catch() 활용법

joshua1988.github.io

 

Promise.all

Promise.all(iterable);

Promise의 all 메소드는 프로미스로 구성된 배열을 인자로 받아서 병렬로 처리할때 유용하게 사용할수 있습니다. 이름에서도 유추할수 있듯이 프로미스 전체가 성공적으로 이행되어야 성공으로 간주되기 때문에 프로미스중 하나라도 실패하면 실패를 즉시 반환합니다. 이런 이유로 어떤 비동기 작업을 순서와 상관없이 병렬적으로 처리하되 한 작업이라도 오류가 있어서는 안되는 작업이 필요할때 유용하게 사용할수 있습니다. 

 

구체적인 예시를 한번 들어보겠습니다.

 

보통 간단한 crud 앱을 만들게 되면 선택한 로컬 이미지를 서버 저장소 (S3, Cloudinary, 파이어스로지등) 에 올리고 해당 이미지 url string을 다른 정보(제목, 날짜, 내용 등)과 함께 db에 올리는 경우가 많습니다.

 

아래 함수는 파일리스트의  로컬 data_url string을 배열로 받아 Promise.all을 통해 파이어스토리지(파일 저장소)에 저장한뒤 저장소 url을 불러오는 작업을 병렬적으로 처리한 코드입니다. 해당 코드는 파이어스토리지를 써보지 않으셨다면 이해하지 못하실수 있으니 Promise.all이 어떻게 사용되는지만 보시는걸 추천합니다.

 

const uploadImages = async (fileList, filePath) => {
  if (!fileList || fileList.length < 1) return;
  return await Promise.all(
    fileList.map(async (file) => {
      const imageRef = ref(storage, `${filePath}/${Date.now()}`); // 파이어스토어 ref 생성
      try {
        const snap = await uploadString(
          imageRef,
          file?.thumbUrl || file.originFileObj.thumbUrl,
          "data_url"
        ); // data_url string을 서버에 업로드
        return getDownloadURL(snap.ref); // 저장소 url 받아오기
      } catch (e) {
        console.log(e);
      }
    })
  );
};

 

만약 성공적으로 저장하고 url을 전부 불러왔다면 uploadImages 함수의 반환값은 파이어스토리지에 저장된 url이 담긴 배열이 되기 때문에 해당 배열을 db에 적절한 필드이름과 함께 저장하기만 하면 됩니다.  

 

만약 이를 Promise.all을 사용하지 않고 처리할 경우 하나의 이미지 업로드 시간과 url을 불러오는 시간이 3초가 걸린다고 하면 10개의 첨부 이미지를 처리할경우 30초가 소요됩니다. 하지만 Promise.all을 사용시 프로미스를 병렬로 처리하기 때문에 마지막 프로미스가 성공적으로 이행된 시점으로, 5초가 걸렸다고 하면 최종적으로 프로미스를 처리하는데 걸리는 시간은 5초입니다.

 

 

병렬 처리

 

 


Promise.race

Promise.race(iterable);

 

Promise의 race 메소드도 마찬가지로 프로미스로 구성된 배열을 인자로 받지만 all 과는 달리 모든 요청이 완료되기까지 기다리지 않고 가장 먼저 성공적으로 이행되거나 실패한 프로미스를 반환합니다. 필자는 실무에서 해당 메소드를 사용해야할 상황은 크게 없었기때문에 검색을 좀 해본 결과 서버로부터 일정시간 응답이 없을경우 수동으로 오류 처리를 하기 위해 많이 사용되고 있는걸 알게되었습니다. 

 

예를들어 db에서 특정 정보를 불러와서 화면에 랜더링하는데 10초가 걸린다면 로딩처리가 잘 되어있다 해도 사용자들은 불편함을 느낄것입니다. 그렇기 때문에 10초이내에 완료되는 timeout 프로미스와 db에서 데이터를 불러오는 로직을 Promise.race로 경합을 시키게 되면 먼저 완료되는 프로미스를 반환하게 됩니다.

 

const timeout = (s) => {
	return new Promise(resolve => setTimeout(resolve, s * 1000));
}



Promise.race([
	fetchData(),
	timeout(10).then(() => {
		throw new Error("10초 이내에 완료되지 않음")
	}),
	// 에러를 발생시켜도 background에서 데이터를 불러오는 작업은 계속되기 때문에 만약에 해당 작업을 완전 멈추고싶다면
    // AbortController를 사용해주시길 바랍니다 - https://stackoverflow.com/questions/46946380/fetch-api-request-timeout
])

 

Promise.race의 경우 Promise.all 보다 예매한 부분이 많기 때문에 공식문서나 관련문서를 꼭 확인후 사용하시는걸 추천합니다. 

예를 들어서 Promise.race의 인자로 빈 배열을 넣게 되면 완료 되거나 거부되기를 예상하지만 대기(pending) 상태로 계속 기다리게 됩니다.

 

const emptyPromise = () => {
return Promise.race([]).then((val) => console.log("완료", val))
.catch((err) => console.log("거부", err));
}

console.log(emptyPromise()) // Promise { <pending> }

 

또, 실제 경합(race)에서 한명이 낙오 되어도 경기는 계속 지속되는 반면 Promise의 race 메소드는 한 프로미스라도 실패하게 되면 그 즉시 바로 전체가 거부되기 때문에 이런 부분을 잘 주의하여 사용하셔야합니다. 이 부분에 대해 좀더 자세히 알고싶으신분들은  Promise me you won't use Promise.race 포스트(영문)를 한번 참고하시길 바랍니다.

 

 

요약

 

결과적으로 Promise.all 과 Promise.race 두 메소드 모두 하나 이상의 프로미스를 배열로 받아 해당 프로미스들을 이행합니다. 가장 큰 차이점은 전자는 모든 프로미스들의 결과값을 배열로 받지만 후자는 가장 빨리 응답을 받은 결과값만 resolve 한다는점입니다. Promise.all의 경우 이미지를 업로드하는 작업같이 순서가 보장되지 않아도 상관없는 비동기 태스크를 병렬적으로 처리할때 유용하게 사용할수 있고 Promise.race는 로딩 시간이 너무 짧거나 길때 최소 로딩시간과 최대 로딩시간을 설정할때 유용하게 사용할수 있습니다.