Promise.all 및 Promise.allSettled를 사용하여 여러 API 요청 처리하기
James Reed
Infrastructure Engineer · Leapcell

소개
현대의 웹 개발에서는 종종 여러 API 호출이 데이터를 가져오거나, 리소스를 업데이트하거나, 복잡한 작업을 수행해야 하는 등 다양한 백엔드 서비스와 애플리케이션이 상호 작용하는 것이 일반적입니다.
이러한 API 요청 일괄 처리를 다룰 때 효율적이고 강력한 처리가 중요합니다. 두 가지 강력한 JavaScript Promise 조합기인 Promise.all
과 Promise.allSettled
는 이러한 동시 작업을 관리하는 고유한 접근 방식을 제공합니다.
개발자가 작업을 위한 올바른 도구를 선택하고 애플리케이션 안정성과 최적의 사용자 경험을 보장하려면 그 미묘한 차이를 이해하는 것이 중요합니다. 이 문서는 이러한 메서드의 실용적인 응용 프로그램을 자세히 살펴보고, 핵심 기능, 차이점을 탐구하며, 효과적인 일괄 API 요청 처리를 위해 각 메서드를 언제 사용해야 하는지에 대한 지침을 제공합니다.
핵심 개념 및 실용적인 응용 프로그램
세부 사항으로 들어가기 전에 JavaScript, 특히 Promises의 비동기 작업과 관련된 핵심 개념에 대한 공통된 이해를 확립해 봅시다.
Promises: Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 세 가지 상태가 있습니다.
- Pending: 초기 상태로, 아직 이행되지도 거부되지도 않은 상태.
- Fulfilled: 작업이 성공적으로 완료되었음을 의미합니다.
- Rejected: 작업이 실패했음을 의미합니다.
이제 Promise.all
과 Promise.allSettled
를 살펴보겠습니다.
Promise.all 설명
Promise.all
은 Promise의 이터러블(예: Promise 배열)을 입력받아 단일 Promise를 반환합니다. 이 반환된 Promise는 입력 Promise가 모두 이행되면 이행되며, 입력 Promise와 동일한 순서로 해당 이행 값 배열과 함께 이행됩니다. 입력 Promise 중 하나라도 거부되면 즉시 첫 번째 거부된 Promise의 이유와 함께 거부됩니다.
원칙: "전부 아니면 전부." 요청 하나라도 실패하면 전체 일괄 작업은 실패한 것으로 간주됩니다. 이는 모든 요청의 성공이 상호의존적인 경우에 이상적입니다.
사용 사례:
전자 상거래 애플리케이션을 상상해 보세요. 제품 페이지를 표시하기 전에 제품 세부 정보와 사용자 리뷰를 동시에 가져와야 합니다. 이 API 호출 중 하나라도 실패하면 불완전한 제품 페이지를 표시하는 것은 도움이 되지 않거나 심지어 오해의 소지가 있을 수 있습니다. 이 시나리오에서는 Promise.all
을 사용하여 두 데이터 조각이 모두 성공적으로 검색되었는지 확인하고 싶을 것입니다.
구현 예시:
// API 호출 시뮬레이션 const fetchProductDetails = (productId) => { return new Promise((resolve, reject) => { setTimeout(() => { if (productId === 'p123') { resolve({ id: 'p123', name: 'Premium Widget', price: 29.99 }); } else { reject(new Error(`Product ${productId} not found`)); } }, 1000); }); }; const fetchUserReviews = (productId) => { return new Promise((resolve, reject) => { setTimeout(() => { if (productId === 'p123') { resolve([ { user: 'Alice', rating: 5, comment: 'Great product!' }, { user: 'Bob', rating: 4, comment: 'Good value.' } ]); } else if (productId === 'p456') { // 실패하는 리뷰 요청 시뮬레이션 reject(new Error('Failed to fetch reviews for product p456')); } else { resolve([]); // 다른 제품에는 리뷰가 없음 } }, 800); }); }; const productId = 'p123'; // 예시: 성공 시나리오 // const productId = 'p456'; // 예시: 요청 하나 실패 Promise.all([ fetchProductDetails(productId), fetchUserReviews(productId) ]) .then(([productDetails, userReviews]) => { console.log("Promise.all Success:"); console.log("Product Details:", productDetails); console.log("User Reviews:", userReviews); // 두 데이터 조각으로 제품 페이지 렌더링 }) .catch(error => { console.error("Promise.all Failed:", error.message); // 사용자에게 오류 메시지 표시 또는 재시도 }); // 실패하는 요청이 있는 예시 (테스트하려면 주석 해제) /* const failingProductId = 'p456'; Promise.all([ fetchProductDetails(failingProductId), // 이것은 성공할 수 있음 fetchUserReviews(failingProductId) // 이것은 거부될 것임 ]) .then(([productDetails, userReviews]) => { console.log("Promise.all Success (여기에 도달하지 않아야 함):", productDetails, userReviews); }) .catch(error => { console.error("Promise.all with failure:", error.message); // 출력: Failed to fetch reviews for product p456 // 하나의 요청이 실패했기 때문에 전체 일괄 처리가 실패했습니다. }); */
Promise.allSettled 설명
Promise.allSettled
도 Promise의 이터러블을 입력받아 단일 Promise를 반환합니다. 이 반환된 Promise는 모든 입력 Promise가 이행되거나 거부될 때 이행됩니다. 이행된 각 Promise의 결과를 설명하는 객체 배열과 함께 이행됩니다. 각 객체에는 status
속성( 'fulfilled' 또는 'rejected')과 value
(이행된 경우) 또는 reason
(거부된 경우)이 포함됩니다.
원칙: "모든 결과를 얻되, 개별 성공 또는 실패에 관계없이." 배치 내의 모든 작업 결과를 알고 싶을 때 유용합니다. 일부 작업이 실패하더라도 성공한 작업을 계속 진행할 수 있습니다.
사용 사례: 사용자가 여러 이미지를 동시에 업로드하려는 소셜 미디어 애플리케이션을 생각해 보세요. 일부 업로드는 성공할 수 있고, 일부는 네트워크 문제 또는 잘못된 파일 형식으로 인해 실패할 수 있습니다. 애플리케이션은 이미지 하나가 처리되지 않았다고 해서 전체 작업을 실패시키는 대신 각 업로드의 상태(예: "이미지 1 업로드됨", "이미지 2 실패")를 사용자에게 알려야 합니다.
구현 예시:
// API 호출 시뮬레이션 const uploadImage = (fileName, artificialFailure = false) => { return new Promise((resolve, reject) => { setTimeout(() => { if (artificialFailure || fileName.includes('failed')) { reject(new Error(`Failed to upload ${fileName}`)); } else { resolve({ fileName, url: `https://example.com/images/${fileName}` }); } }, Math.random() * 1000 + 500); // 가변 업로드 시간 시뮬레이션 }); }; const imagesToUpload = [ 'photo1.jpg', 'avatar.png', 'corrupted_image.failed.gif', // 이것은 실패할 것임 'document.pdf' ]; const uploadPromises = imagesToUpload.map(img => uploadImage(img)); Promise.allSettled(uploadPromises) .then(results => { console.log("Promise.allSettled Results:"); const successfulUploads = []; const failedUploads = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Image ${imagesToUpload[index]} uploaded successfully:`, result.value.url); successfulUploads.push(result.value); } else { console.error(`Image ${imagesToUpload[index]} failed to upload:`, result.reason.message); failedUploads.push({ fileName: imagesToUpload[index], reason: result.reason.message }); } }); console.log("\nSummary:"); console.log("Successfully uploaded:", successfulUploads); console.log("Failed uploads:", failedUploads); // 개별 결과에 따라 UI 업데이트, 예를 들어 각 항목에 대한 진행률 표시 }) .catch(error => { // 이 catch 블록은 일반적으로 Promise.allSettled에서 도달하지 않습니다. // 입력 이터러블 자체가 유효하지 않거나 동기 오류가 발생하는 경우가 아니고서는. console.error("Unexpected error with Promise.allSettled:", error); });
Promise.all과 Promise.allSettled 중 선택
결정은 주로 하나 이상의 API 요청이 실패했을 때 원하는 동작에 달려 있습니다.
-
Promise.all
을 사용하는 경우:- 모든 작업이 중요하고 서로 의존적인 경우. 하나가 실패하면 전체 트랜잭션이 롤백되거나 불완전한 것으로 간주되어야 합니다.
- 모든 요청의 집단적인 성공만 신경 쓰는 경우.
- 빠른 실패가 허용 가능하거나 바람직한 경우.
-
Promise.allSettled
를 사용하는 경우:- 개별적인 성공 또는 실패에 관계없이 모든 요청에 대한 결과를 처리해야 하는 경우.
- 작업이 대체로 독립적이고, 하나가 실패한다고 해서 전체 일괄 처리가 실패할 필요는 없는 경우.
- 성공한 작업은 계속 처리하면서 오류를 정상적으로 처리하려는 경우.
- 사용자에게 어떤 특정 작업이 성공했고 어떤 작업이 실패했는지 자세한 피드백을 제공하려는 경우.
결론
Promise.all
과 Promise.allSettled
모두 여러 비동기 작업을 조정하는 데 매우 유용한 도구입니다. 특히 일괄 API 요청 맥락에서 그렇습니다. Promise.all
은 집단적인 성공이 가장 중요한 상호 의존적인 작업에 적합한 "전부 아니면 전부" 계약을 강제합니다. 반대로 Promise.allSettled
는 더 탄력적인 접근 방식을 제공하여 각 요청의 결과를 확인할 수 있게 해주므로 부분적인 성공이 허용되고 개별 상태 보고가 중요한 시나리오에 이상적입니다. 고유한 동작을 이해하고 적절하게 적용함으로써 개발자는 더 강력하고 내결함성이 있으며 사용자 친화적인 JavaScript 애플리케이션을 구축할 수 있습니다. 중요한 상호 의존적인 작업에는 Promise.all
을, 개별 결과가 중요한 탄력적인 처리를 위해서는 Promise.allSettled
를 선택하세요.