잘못 이해하고 썼던 시간을 반성하며 제대로 사용하는 방법을 공유합니다. 누군가는 같은 실수를 하지 않길 바라면서..

# 그동안 오해해서 미안해

N개의 비동기 처리를 동시 실행하고 다 완료되면 반영하는 작업이 필요할 때 큰 고민 없이 Promise.all 을 썼는데요. 요청들 중에서 하나라도 reject 되거나 exception이 발생하면 모두 실패한 걸로 처리된다는 걸 이제야 알았네요. (털썩)

async getListData() {
	...
}

async getExtraData() {
	...
}

async getAllData() {
	try {
		const [
			listData,
			extraData,
		] = await Promise.all([this.getListData(), this.getExtraData()]);
	
		this.drawListData(listData);
		this.drawExtraData(extraData);
	} catch (e) {
		console.error(e);
	}
}

위 예제에서 getExtraData()를 처리하던 중 문제가 발생하면 drawListData()도 호출되지 못했던 거죠. 이런 상황에 아무것도 출력되지 않는 것보다는 성공한 부분은 보여주는 것이 사용성에 좋습니다. "어떻게 해야 할까?" 고민하며 찾아보니 모두 수행하고 요청 별로 성공과 실패를 전달하는 Promise.allSettled()가 있더군요.

const [
	listData,
	extraData,
] = await Promise.allSettled([this.getListData(), this.getExtraData()]);

// listData = { status: 'fulfilled', value: ... };
// extraData = { status: 'rejected', reason: ... };

스펙 문서를 보면 어떻게 동작하는지 쉽게 이해가 되실 겁니다. 요청 별로 처리 결과(status)를 알 수 있고 실제 반환된 값은 value에 전달됩니다. 문제는 tc39 stage4에 2019년 7월에 추가된 따끈따끈한 기능이라 모든 자바스크립트 엔진에서 지원하지 못하고 RN에서도 사용할 수 없는 점입니다. 하지만 여러 멋쟁이들이 만든 Polyfill들이 많고 자체 구현도 어렵지 않답니다.

export function allPromisesSettled(promises) {
  const onFulfilled = value => ({
    status: 'fulfilled',
    value,
  });

  const onRejected = reason => ({
    status: 'rejected',
    reason,
  });

  const settledPromises = promises.map(
		promise => Promise.resolve(promise).then(onFulfilled, onRejected),
	);

  return Promise.all(settledPromises);
}

간단하게 구현해서

async getAllData() {
	const [
		listData,
		extraData,
	] = await allPromisesSettled([this.getListData(), this.getExtraData()]);

	if (listData?.status === 'fulfilled') {
		this.drawListData(listData.value);
	}

	if (extraData?.status === 'fulfilled') {
		this.drawExtraData(extraData.value);
	}
}

결과 상태를 체크해서 처리되도록 수정하면 의도한 대로 동작합니다.

# 교훈

안다고 자만하지 말고 봤다고 지나치지 말고 스펙 문서는 꼼꼼하게 읽는 습관을 가지자!

@ifsnow