[JS] 비동기 함수를 병렬로 호출하는 방법

jiny·2025년 1월 30일

기술 면접

목록 보기
46/78

🗣️ 여러 비동기 함수를 동시에 병렬로 호출하기 위해서는 어떻게 해야 할까요?

  • 의도: Promise에 대한 이해도를 확인하는 질문

  • 팁: Promise.all()을 언급하는 게 정답이다.

  • 나의 답안

    여러 비동기 작업을 병렬로 호출할 때는 Promise.all()을 사용합니다.
    각 비동기 함수를 먼저 시작만 해 두고, 그 프로미스들을 배열로 모아 한 번에 기다리면 네트워크 지연을 겹쳐서 처리 시간을 줄일 수 있습니다.
    이때 모든 작업이 성공해야만 전체가 성공으로 간주되고, 하나라도 실패하면 즉시 거부되므로 에러를 빠르게 감지할 수 있다는 장점이 있습니다.

    주의할 점은, 의존 관계가 없는 작업만 병렬화해야 합니다.
    왜냐하면 하나의 결과가 다른 비동기 함수의 입력으로 사용되는 경우,
    병렬로 실행하면 순서 보장이 깨져서 잘못된 데이터나 예기치 않은 동작이 발생할 수 있기 때문입니다.

  • 주어진 답안 (모범 답안)

    보통 async/await를 사용해 데이터를 불러오게 되면 순차적으로 데이터를 불러오기 마련입니다.
    이때 Promise.all() 함수를 사용한다면 함수의 인자로 넘겨준 비동기 함수를 병렬로 실행할 수 있습니다.

    예를 들어 3초의 실행 시간을 가진 비동기 함수 3개가 있다고 한다면, 기본적으로 순차적으로 호출할 시에 모든 함수가 끝나기까지 3*3=9초가 걸리게 됩니다.
    하지만 이걸 병렬로 호출하게 된다면 3개의 요청을 동시에 보내게 되어 3초만 걸리게 됩니다.


📝 개념 정리

🌟 비동기 함수란?

비동기 함수는 async 키워드로 정의되고, 항상 Promise 객체를 반환한다.
이는 작업이 비동기로 실행되며 완료 여부를 나중에 알 수 있다는 의미이다.

async function fetchData(url) {
  const response = await fetch(url);
  return response.json();
}

위 함수는 URL로부터 데이터를 가져오는 비동기 작업을 수행하며, 결과는 Promise로 반환된다.


🌟 병렬 호출이란?

병렬 호출은 여러 비동기 작업을 동시에 실행하여 각 작업이 독립적으로 완료되도록 하는 방식이다. 이를 통해 작업 시간이 단축될 수 있다.
자바스크립트에서 병렬 호출은 주로 다음 두 가지 메서드를 통해 이루어진다.

  • Promise.all()
  • Promise.allSettled()

🌟 병렬 호출의 주요 메서드

  1. Promise.all()
    Promise.all()주어진 모든 Promise가 성공해야만 결과를 반환한다.
    하나라도 실패하면 전체가 실패로 처리된다.

    • 사용 예시

      async function parallelFetch() {
        const urls = ["https://api.example.com/data1", "https://api.example.com/data2", "https://api.example.com/data3"];
        
        try {
          const results = await Promise.all(urls.map(url => fetch(url).then(res => res.json())));
          console.log("All data: ", results);
        } catch (error) {
          console.error("One of the requests failed: ", error);
        }
      }
      
      parallelFetch();

      1) urls.map()을 사용하여 각 URL에서 데이터를 가져오는 Promise를 생성한다.
      2) Promise.all()이 모든 Promise를 병렬로 실행하고, 모두 성공했을 때만 결과 배열을 반환한다.
      3) 하나라도 실패하면 catch 블록이 실행된다.

  1. Promise.allSettled()
    Promise.allSettled()는 모든 Promise가 완료될 때까지 기다린 후, 각각의 결과(fulfilled 또는 rejected)를 반환한다.
    실패한 작업이 있어도 나머지 작업이 영향을 받지 않는다.

    • 사용 예시

      async function parallelFetchWithAllSettled() {
        const urls = ["https://api.example.com/data1", "https://api.example.com/data2", "https://api.example.com/data3"];
        
        const results = await Promise.allSettled(urls.map(url => fetch(url).then(res => res.json())));
        results.forEach((result, index) => {
          if (result.status === "fulfilled") {
            console.log(`Request ${index + 1} succeeded: `, result.value);
          } else {
            console.error(`Request ${index + 1} failed: `, result.reason);
          }
        });
      }
      
      parallelFetchWithAllSettled();

      1) Promise.allSettled()는 모든 작업의 성공 여부와 상관없이 상태(fulfilled 또는 rejected)를 배열로 반환한다.
      2) 각각의 작업 결과를 별도로 처리할 수 있다.


🌟 병렬 호출과 순차 호출의 비교

특징병렬 호출(Promise.all)순차 호출(for-await-of 또는 await 반복)
속도여러 작업이 동시에 실행되므로 더 빠름작업이 순차적으로 실행되므로 느림
사용 용도작업 간에 독립적일 때 적합작업 간에 의존성이 있을 때 적합
에러 처리한 작업 실패 시 전체 실패개별 작업 실패 시 나머지 작업 계속 진행 가능

🌟 다양한 병렬 호출 예시

  1. Promise.race()로 가장 빠른 작업 선택하기
    Promise.race()는 가장 빨리 완료된 Promise를 반환한다.

    async function fastestResponse() {
      const urls = ["https://api.example.com/data1", "https://api.example.com/data2"];
      
      try {
        const fastest = await Promise.race(urls.map(url => fetch(url)));
        console.log("Fastest response received: ", await fastest.json());
      } catch (error) {
        console.error("Error: ", error);
      }
    }
    
    fastestResponse();
  1. Promise.any()로 첫 번째 성공 값 가져오기
    Promise.any()는 첫 번째 성공한 Promise 값을 반환하며, 모두 실패했을 경우 AggregateError를 발생시킨다.

    async function firstSuccessfulResponse() {
      const urls = ["https://api.example.com/data1", "https://api.example.com/data2"];
      
      try {
        const result = await Promise.any(urls.map(url => fetch(url).then(res => res.json())));
        console.log("First successful result: ", result);
      } catch (error) {
        console.error("All requests failed: ", error);
      }
    }
    
    firstSuccessfulResponse();

🌟 병렬 호출에서 주의할 점

  • API 호출 제한
    서버에 과도한 요청을 보내면 차단당할 수 있다. 이를 방지하려면 호출 수를 제한하거나 적절한 지연을 추가해야 한다.

  • 작업 의존성
    병렬 호출은 작업 간 의존성이 없는 경우에만 적합하다. 순서가 중요하다면 순차 호출을 사용해야 한다.

  • 에러 처리
    Promise.all은 하나라도 실패하면 전체가 중단된다. 안정성을 위해 Promise.allSettled를 사용하는 것이 더 나을 수도 있다.


🌟 병렬 호출을 효과적으로 활용하기 위한 추가 팁

Promise.all로 너무 많은 작업을 동시에 실행하면 브라우저나 서버가 과부하될 수 있다.
이를 방지하려면 병렬 호출 수를 제한하는 유틸리티를 사용하면 된다.

async function limitedParallelFetch(urls, limit) {
  // 1. 결과 저장 배열 및 실행 중인 요청 배열 초기화
  const results = []; // 모든 요청의 Promise를 저장하는 배열 (최종 결과 반환용)
  const executing = new Set(); // 현재 실행 중인 요청을 추적하는 Set (병렬 요청 개수 제한을 위해 사용)
  
  // 2. 반복문을 통한 요청 생성
  for (const url of urls) {
    const promise = fetch(url).then(res => res.json());
    results.push(promise); // 요청(Promise)을 results 배열에 추가 (최종 결과 저장용)
    executing.add(promise); // 현재 실행 중인 요청을 executing Set에 추가 (동시 요청 제한을 위해 추적)
    
    // 3. 병렬 요청 개수 제한 (limit 개수만큼 유지)
    if (executing.size >= limit) {
      // 실행 중인 요청이 limit을 초과하는 경우, 하나가 끝날 때까지 기다림
      await Promise.race(executing);
      // executing Set 내 요청 중 가장 먼저 완료되는 Promise가 끝날 때까지 기다림
      // 즉, limit 개수만큼 요청을 유지하면서 하나가 끝나면 새로운 요청을 시작할 수 있도록 만듦
    }
    promise.finally(() => executing.delete(promise));
      // 완료된 Promise를 executing Set에서 제거하여 새로운 요청을 실행할 수 있도록 함
  }
  
  // 4. 모든 요청이 끝난 후 결과 반환
  return Promise.all(results);
  // 모든 요청이 완료될 때까지 기다린 후 결과 배열을 반환함
}

limitedParallelFetch(["https://api.example.com/data1", "https://api.example.com/data2"], 2)
  .then(results => console.log(results));
  • 실제 동작 과정 (예제)
    5개의 요청을 limit = 2로 실행한다고 가정하자.
    1. 초기 상태
      • 실행 중: { Promise 1, Promise 2 } (최대 2개 실행)
      • 대기 중: [ Promise 3, Promise 4, Promise 5 ]
    2. Promise 1이 완료됨
      • 실행 중: { Promise 2 }
      • 새로운 요청 실행: [ Promise 2, Promise 3 ]
    3. Promise 2가 완료됨
      • 실행 중: { Promise 3 }
      • 새로운 요청 실행: [ Promise 3, Promise 4 ]
    4. Promise 3이 완료됨
      • 실행 중: { Promise 4 }
      • 새로운 요청 실행: [ Promise 4, Promise 5 ]
    5. Promise 4가 완료됨
      • 실행 중: { Promise 5 }
    6. 모든 요청 완료
      • 실행 중: {}
      • 최종 결과 반환: [ data1, data2, data3, data4, data5 ]
  • 이 코드의 장점
    1. 병렬 요청 개수 제한
      • 서버에 너무 많은 요청을 보내지 않도록 limit을 설정할 수 있다.
      • 네트워크 부하를 줄이면서도 여러 요청을 빠르게 처리할 수 있다.
    2. 효율적인 비동기 처리
      • Promise.race()를 활용하여 가장 먼저 끝나는 Promise를 감지하고 다음 요청을 실행한다.
    3. 유연한 확장성
      • limit 값을 조절하면 동시 실행 요청 수를 쉽게 변경할 수 있다.

0개의 댓글