From a6a7b7286d3a96de7834a08eba0fbb45915ee399 Mon Sep 17 00:00:00 2001 From: hyemimi Date: Mon, 1 Jun 2026 14:42:39 +0900 Subject: [PATCH] =?UTF-8?q?[=EC=8B=A0=EA=B7=9C=20=EB=B2=88=EC=97=AD]=20pro?= =?UTF-8?q?mise-all-failure=20task.md,=20solution.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../04-promise-all-failure/solution.md | 113 ++++++++++++++++++ .../04-promise-all-failure/task.md | 79 ++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 1-js/11-async/08-async-await/04-promise-all-failure/solution.md create mode 100644 1-js/11-async/08-async-await/04-promise-all-failure/task.md diff --git a/1-js/11-async/08-async-await/04-promise-all-failure/solution.md b/1-js/11-async/08-async-await/04-promise-all-failure/solution.md new file mode 100644 index 0000000000..69129c7296 --- /dev/null +++ b/1-js/11-async/08-async-await/04-promise-all-failure/solution.md @@ -0,0 +1,113 @@ + +문제의 원인은 `Promise.all`이 프라미스 중 하나라도 거부되면 즉시 거부되지만, 나머지 프라미스를 취소하지는 않는다는 데 있습니다. + +위 예시에서는 두 번째 쿼리가 실패하므로 `Promise.all`이 거부되고, `try...catch` 블록이 이 에러를 잡습니다. 한편, 다른 프라미스는 *영향을 받지 않고 독립적으로 실행을 계속합니다*. 예시에서는 잠시 후 세 번째 쿼리가 자체적으로 에러를 던집니다. 이 에러는 어디에서도 잡히지 않으므로 콘솔에서 확인할 수 있습니다. + +이 문제는 Node.js 같은 서버 측 환경에서 특히 위험합니다. 잡히지 않은 에러로 인해 프로세스가 중단될 수 있기 때문입니다. + +어떻게 고칠 수 있을까요? + +가장 이상적인 해결책은 쿼리 중 하나가 실패했을 때 아직 끝나지 않은 쿼리를 모두 취소하는 것입니다. 이렇게 하면 잠재적인 에러를 피할 수 있습니다. + +하지만 안타깝게도 `database.query` 같은 서비스 호출은 취소 기능을 지원하지 않는 서드파티 라이브러리로 구현된 경우가 많습니다. 이런 경우 호출을 취소할 방법이 없습니다. + +대안으로 `Promise.all`을 감싸는 래퍼 함수를 직접 작성할 수 있습니다. 이 함수는 각 프라미스에 커스텀 `then/catch` 핸들러를 붙여 상태를 추적합니다. 결과를 모으다가 에러가 발생하면 그 이후 프라미스는 모두 무시합니다. + +```js +function customPromiseAll(promises) { + return new Promise((resolve, reject) => { + const results = []; + let resultsCount = 0; + let hasError = false; // 첫 번째 에러가 발생하면 true로 바꿉니다. + + promises.forEach((promise, index) => { + promise + .then(result => { + if (hasError) return; // 이미 에러가 발생했다면 이 프라미스를 무시합니다. + results[index] = result; + resultsCount++; + if (resultsCount === promises.length) { + resolve(results); // 모든 결과가 준비되면 성공입니다. + } + }) + .catch(error => { + if (hasError) return; // 이미 에러가 발생했다면 이 프라미스를 무시합니다. + hasError = true; // 에러가 발생했습니다. + reject(error); // 거부 상태로 실패 처리합니다. + }); + }); + }); +} +``` + +이 방식에도 문제가 있습니다. 쿼리가 아직 처리 중일 때 `disconnect()`를 호출하는 것은 대개 바람직하지 않습니다. + +특히 일부 쿼리가 중요한 업데이트를 처리한다면 모든 쿼리가 끝까지 완료되어야 할 수 있습니다. + +따라서 실행을 계속 진행하고 마지막에 연결을 끊기 전에 모든 프라미스가 처리될 때까지 기다려야 합니다. + +다른 구현을 살펴봅시다. 이 구현은 `Promise.all`과 비슷하게 동작합니다. 첫 번째 에러와 함께 거부되지만, 모든 프라미스가 처리될 때까지 기다립니다. + +```js +function customPromiseAllWait(promises) { + return new Promise((resolve, reject) => { + const results = new Array(promises.length); + let settledCount = 0; + let firstError = null; + + promises.forEach((promise, index) => { + Promise.resolve(promise) + .then(result => { + results[index] = result; + }) + .catch(error => { + if (firstError === null) { + firstError = error; + } + }) + .finally(() => { + settledCount++; + if (settledCount === promises.length) { + if (firstError !== null) { + reject(firstError); + } else { + resolve(results); + } + } + }); + }); + }); +} +``` + +이제 `await customPromiseAllWait(...)`는 모든 쿼리가 처리될 때까지 실행을 멈춥니다. + +실행 흐름을 예측할 수 있게 보장하므로 더 안정적인 방식입니다. + +마지막으로 모든 에러를 처리하고 싶다면 `Promise.allSettled`를 사용하거나, 모든 에러를 하나의 [AggregateError](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) 객체에 모아 그 객체로 거부하는 래퍼를 작성할 수 있습니다. + +```js +// 모든 프라미스가 처리될 때까지 기다립니다. +// 에러가 없으면 결과를 반환합니다. +// 에러가 하나라도 있으면 모든 에러가 담긴 AggregateError를 던집니다. +function allOrAggregateError(promises) { + return Promise.allSettled(promises).then(results => { + const errors = []; + const values = []; + + results.forEach((res, i) => { + if (res.status === 'fulfilled') { + values[i] = res.value; + } else { + errors.push(res.reason); + } + }); + + if (errors.length > 0) { + throw new AggregateError(errors, 'One or more promises failed'); + } + + return values; + }); +} +``` diff --git a/1-js/11-async/08-async-await/04-promise-all-failure/task.md b/1-js/11-async/08-async-await/04-promise-all-failure/task.md new file mode 100644 index 0000000000..d5ccd3c02a --- /dev/null +++ b/1-js/11-async/08-async-await/04-promise-all-failure/task.md @@ -0,0 +1,79 @@ + +# 위험한 Promise.all + +`Promise.all`은 여러 작업을 병렬로 처리할 때 아주 유용합니다. 여러 서비스에 병렬로 요청을 보내야 할 때 특히 빛을 발합니다. + +하지만 숨어 있는 위험이 있습니다. 이번 과제에서는 예시를 통해 어떤 위험이 있는지 살펴보고 이를 피하는 방법을 알아봅시다. + +데이터베이스 같은 원격 서비스에 연결한다고 가정해 봅시다. + +`connect()`와 `disconnect()`라는 두 함수가 있습니다. + +연결된 상태에서는 `database.query(...)`를 사용해 요청을 보낼 수 있습니다. `database.query(...)`는 보통 결과를 반환하지만, 에러를 던질 수도 있는 비동기 함수입니다. + +간단히 구현하면 다음과 같습니다. + +```js +let database; + +function connect() { + database = { + async query(isOk) { + if (!isOk) throw new Error('Query failed'); + } + }; +} + +function disconnect() { + database = null; +} + +// 사용법: +// connect() +// ... +// database.query(true)는 성공한 호출을 흉내 냅니다. +// database.query(false)는 실패한 호출을 흉내 냅니다. +// ... +// disconnect() +``` + +이제 문제가 되는 부분을 살펴봅시다. + +연결한 뒤 쿼리 세 개를 병렬로 보내고, 이후 연결을 끊는 코드를 작성했습니다. 각 쿼리는 100ms, 200ms, 300ms처럼 서로 다른 시간이 걸립니다. + +```js +// `ms` 밀리초 뒤에 비동기 함수 `fn`을 호출하는 헬퍼 함수 +function delay(fn, ms) { + return new Promise((resolve, reject) => { + setTimeout(() => fn().then(resolve, reject), ms); + }); +} + +async function run() { + connect(); + + try { + await Promise.all([ + // 병렬 작업 세 개는 각각 100ms, 200ms, 300ms로 서로 다른 시간이 걸립니다. + // 이 효과를 내기 위해 `delay` 헬퍼를 사용합니다. +*!* + delay(() => database.query(true), 100), + delay(() => database.query(false), 200), + delay(() => database.query(false), 300) +*/!* + ]); + } catch(error) { + console.log('에러를 처리했습니다. 정말 그럴까요?'); + } + + disconnect(); +} + +run(); +``` + +세 쿼리 중 두 개는 실패합니다. 그래도 `Promise.all` 호출을 `try..catch` 블록으로 감싸 두었으니 충분해 보입니다. + +하지만 이 방법으로는 부족합니다! 실제로 이 스크립트는 콘솔에 잡히지 않은 에러를 남깁니다. + +왜 이런 일이 생길까요? 어떻게 피할 수 있을까요? \ No newline at end of file