[Next.js] async/await, Promise.all, Promise.allSettled

질문Bot·2025년 5월 16일

Next.js

목록 보기
4/13

Nexon Open Api를 통해 프로젝트 개발을 한지 일주일이 넘었네요.

하나의 페이지에서 다양한 정보를 렌더링해야 하다 보니, 같은 페이지에서 여러 API를 동시에 호출해야 하는 상황이 자주 발생했습니다.

개발하면서 겪은 고민들과 학습한 내용을 이 글을 통해 정리해보려 합니다.


🍁 닉네임 페이지 예시

현재 개발 중인 캐릭터 검색 프로젝트의 닉네임 검색 페이지입니다.

예를 들어, 팡이요라는 닉네임을 검색하면 아래와 같이 다양한 데이터를 색상별로 구분하여 각각의 API 요청으로 받아와 렌더링하게 됩니다.


📌 Data Fetch에 대한 고민

📉기존 방식: 순차 처리

처음에는 아래처럼 async/await으로 순차 처리 방식으로 데이터를 가져오도록 구현했습니다.

const getCharacterInfo = await getCharInfo(ocid)
const popularity = await getPopularity(ocid)
const getStat = await getStatApi(ocid)
const cashCody = await getCashCody(ocid)

즉, 각 요청은 이전 요청이 끝난 후에야 시작되기 때문에 전체 요청 시간이 누적됩니다.

❗ 문제점

  • API 간 의존성이 없음에도 순차적으로 처리
  • 전체 응답 시간 = 모든 요청 시간의 합
  • API가 많아질수록 UX 저하 발생

🚀 개선 방안: Promise.all 사용

🔎 Promise.all이란?

Promise.all()은 여러 개의 프로미스를 병렬로 실행하고, 모든 프로미스가 완료될 때까지 기다린 후 결과를 반환합니다. 단, 하나라도 실패하면 전체가 실패(reject) 처리된다는 특징도 존재합니다.

즉, Promise 중 처리 시간이 가장 오래 걸리는 요청 기준으로 전체 완료 시간이 결정됩니다.

const [getCharacterInfo, popularity, getStat, cashCody] = await Promise.all([
  getCharInfo(ocid),     // 처리 시간 3초
  getPopularity(ocid),   // 처리 시간 5초
  getStatApi(ocid),      // 처리 시간 2초
  getCashCody(ocid),     // 처리 시간 6초
])

각 요청이 3초, 5초, 2초, 6초 걸린다면
=> 순차 처리 시 총 16초 소요
=> Promise.all()을 쓰면 가장 오래 걸리는 6초 안에 모든 요청 완료

✅ Promise.all() 장점 정리

  • 병렬 처리로 응답 시간 단축
  • 독립적인 요청일수록 효율 극대화

⚠️ 멱등성 주의 with Promise.all

Promise.all을 알아보다보면 멱등성과 비멱등성에 대한 언급이 자주 나옵니다.

Promise.all()은 병렬 요청 중 하나라도 실패하면 전체가 실패합니다.
이때, 실패한 요청을 다시 시도하거나 Promise.all() 전체를 다시 호출할 경우, 동일 요청이 여러 번 실행될 수 있습니다.
요청이 멱등하지 않으면 문제 발생 가능성이 있기 때문입니다.

✅ 멱등성(Idempotency)이란?

같은 요청을 여러 번 보내더라도, 결과나 시스템 상태가 변하지 않는 성질입니다.

요청 예시멱등성 여부
GET /sampleUser/123✅ O
POST /sampleUser❌ X

사실 이러는 이유는?
멱등한 API를 사용하는 것이 재시도나 병렬 호출에서 안정성을 높여줍니다.


🔥 또 다른 개선: Promise.allSettled

💡 일부 요청이 실패해도 가능한 데이터만 우선 보여주고 싶을 때

Promise.allSettled의 코드는 아래와 같습니다.

각 요청의 성공과 실패를 추적할수 있습니다. Promise.all과는 다르게 하나만 실패해도 모두 실패가 아닌 독립적으로 성공과 실패를 구별하기 때문에 일부는 실패해도 성공한것들은 정상적으로 실행할수있는 것입니다.

const results = await Promise.allSettled([
	getCharInfo(ocid),    
  getPopularity(ocid),  
  getStatApi(ocid),    
  getCashCody(ocid), 
])

const getCharacterInfo = results[0].status ==='fulfilled' ? results[0].value : null
const popularity = results[1].status === 'fulfilled' ? results[1].value : null
const getStat = results[2].status === 'fulfilled' ? results[2].value : null
const cashCody = results[3].status === 'fulfilled' ? results[3].value : null

✅ 언제 Promise.all은 사용해야 할까요?

모든 요청이 반드시 성공해야할때 사용하는것이 좋습니다.

  • 요청 중 하나라도 실패하면 전체 작업이 실패로 간주해야하는 상황
  • 모든 데이터가 정상적으로 있어야 UI를 렌더링할 수 있는 경우
  • 위 예시와 같이 검색 결과, 프로필 정보 등 전체 데이터가 의존 관계가 있을 때
  • 에러가 발생하면 catch로 전체 흐름을 중단시키고 처리하고 싶을 때

✅ 언제 Promise.allSettled는 사용해야 할까요?

일부 요청이 실패해도 가능한 것만 먼저 보여주고 싶을 때입니다.

  • 각 요청이 독립적이고, 일부만 성공해도 괜찮은 경우
  • 실패한 요청도 어떤 요청이 실패했는지 알고 싶을 때
  • 실패해도 UI 일부만 안 보이게 처리하고 싶을 때

⭐️ Promise.all vs Promise.allSettled 정리

항목Promise.allPromise.allSettled
성공 조건모든 요청이 성공해야 전체 성공요청 성공/실패와 무관하게 전체 완료
실패 처리 방식하나라도 실패하면 전체 reject각 요청별 fulfilled / rejected로 나뉘어 개별 확인 가능
적합한 상황모든 데이터가 필수적일 때일부 데이터만 있어도 괜찮을 때
유연성낮음높음
코드 복잡도간단하지만 실패 시 취약복잡하지만 견고한 fallback 가능

🍀 정리

이번 프로젝트를 진행하면서 데이터를 효율적으로 fetch하는 방법에 대해 많은 고민을 하게 되었습니다. 기존 CSR 환경에서는 React Query의 useQuery를 활용했기 때문에, 비동기 요청 처리에 대해 크게 신경 쓰지 않고도 편리하게 개발할 수 있었습니다.

하지만 서버 컴포넌트 환경에서는 data fetching을 고민할 때, 여러 API를 순차적으로 호출하는 방식이 성능에 큰 영향을 줄 수 있다는 점을 체감하게 되었습니다.

이를 통해 알게 된 것은, 여러 개의 API 요청이 서로 독립적이라면 Promise.all이나 Promise.allSettled를 활용해 병렬로 처리하는 것이 성능 면에서 훨씬 효율적이라는 점입니다. 상황에 맞는 적절한 병렬 처리를 통해 성능 개선을 해보았습니다.

profile
유용한 정보를 전달하는 사람이 되고자 노력합니다.

0개의 댓글