[번역] 자바스크립트 반복문에서 async를 다시 생각해보자

Haz·2025년 11월 7일

개발여행기

목록 보기
33/34
post-thumbnail

await을 반복문 안에서 사용하는 건 코드가 쥐도새도 모르게 멈추거나 기대한 것보다 느리게 실행되는 걸 보기 전까지는 꽤나 직관적인 것처럼 느껴집니다. API 호출이 한번에 실행되는 대신에 하나하나 실행되거나 map() 함수와 await을 섞은 방법이 생각한 것처럼 잘 작동하지 않았다면 당장 여기 앉아보세요. 이야기해보자구요!

문제점: for 반복에서의 await

유저 리스트를 데이터를 하나씩 가져오는 상황이라고 가정해봅시다.

const users = [1, 2, 3];

for (const id of users) {
  const user = await fetchUser(id);
  console.log(user);
}

이건 작동하긴 하지만, 순차적으로 작동합니다. fetchUser(2)fetchUser(1)이 끝나기 전에는 시작되지 않죠. 순서가 중요하다면 괜찮겠지만, 독립적인 네트워크 호출에 있어서는 비효율적입니다.

의도한 게 아니라면 map() 안에선 await 하지 않기

map() 함수 안에서 await를 사용하는 경우, 그 결과로 생성되는 Promise 객체들을 올바르게 처리하지 않아 자주 혼란스럽기도 합니다.


const users = [1, 2, 3];

const results = users.map(async id => {
  const user = await fetchUser(id);
  return user;
});

console.log(results); // [Promise, Promise, Promise] – 실제 데이터 아님

이 코드는 문법상으로나 동작상으로는 올바르게 작동하지만(프로미스 배열을 반환), 기대한 결과는 아닙니다. 이건 Promise 객체가 resolve될 때까지 기다리지 않습니다.

병렬적으로 호출하려면 이렇게 해야합니다.


const results = await Promise.all(users.map(id => fetchUser(id)));

이제 모든 요청이 병렬적으로 처리되고, result는 불러온 실제 유저 데이터를 포함하게 됩니다.

Promise.all()은 호출 하나만 실패해도 빠르게 실패한다

Promise.all()을 사용할 때 하나만 거부되도 전체 동작이 실패하게 됩니다.


const results = await Promise.all(
  users.map(id => fetchUser(id)) // fetchUser(2)에서 에러 발생 가능
);

fetchUser(2)가 에러(404나 네트워크 에러)가 나면 Promise.all() 호출 전부 거부되고 결과로는 아무것도 반환되지 않아요(성공한 시도를 포함해서).

⚠️ Note: Promise.all()가 첫 에러를 발생시키면 다른 결과도 폐기됩니다. 나머지 Promise 객체가 작동하더라도 첫 에러만 전달됩니다. 각각 객체를 따로 다루지 않는다면 말이죠.

더 안전한 대안들

Promise.allSettled()를 사용하기

const results = await Promise.allSettled(
  users.map(id => fetchUser(id))
);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('✅ User:', result.value);
  } else {
    console.warn('❌ Error:', result.reason);
  }
});

이렇게 한다면 몇몇이 실패하더라도 모든 결과를 확인할 수 있습니다.

매핑 함수 안에서 에러를 다루기

const results = await Promise.all(
  users.map(async id => {
    try {
      return await fetchUser(id);
    } catch (err) {
      console.error(`Failed to fetch user ${id}`, err);
      return { id, name: 'Unknown User' }; // 실패했을 때 대체값
    }
  })
);

이 방법은 --unhandled-rejections=strict 설정이 된 Node.js 같은 더 엄격한 환경에서 경고나 충돌을 발생시킬 수 있는 다루지 못한 promise 거부도 미연에 방지합니다.

현대적인 해결법

for...of + await 사용(순차적 실행)

만약 다음 동작이 이전 수행 결과에 영향을 받거나 API 제한이 있는 상황이라면 이 방법을 사용해보세요.

for (const id of users) {
  const user = await fetchUser(id);
  console.log(user);
}

async 함수 컨텍스트를 사용하지 않았다면 이렇게 해보세요.

(async () => {
  for (const id of users) {
    const user = await fetchUser(id);
    console.log(user);
  }
})();
  • 순서 유지
  • 처리율 제한 및 배칭(Batching)에 용이
  • 독립적인 요청 속도는 느려짐

Promise.all + map() 사용(병렬적 실행)

동작이 독립적이거나 동시에 수행 가능할 때는 이렇게 해보세요.

const usersData = await Promise.all(users.map(id => fetchUser(id)));
  • 네트워크 사용량이 많거나 CPU를 독립적으로 사용하는 작업이 훨씬 빨라짐
  • 한 번의 거부(rejection)이 전체 배칭을 실패하게 만듦(처리되지 않는 한)

더 안전한 배칭 작업을 위해선 Promise.allSettled()try/catch 문을 사용하세요.

요컨대 CPU-bound 작업, 병렬 작업이 별로 눈에 띄는 차이는 없을지도 모릅니다. 그러나 API 호출처럼 I/O가 많은 작업이라면 병렬 작업이 눈에 띄게 총 수행 시간을 줄여줄 수 있어요.

조절된 병렬 처리(Throttled parallelism, 조절된 동시성)

빠른 작업 속도가 필요하지만, API 제한을 반드시 준수해야한다면 p-limit처럼 스로틀링 도구를 사용해보세요.

import pLimit from 'p-limit';

const limit = pLimit(2); // 2개의 패칭을 한번에
const limitedFetches = users.map(id => limit(() => fetchUser(id)));

const results = await Promise.all(limitedFetches);
  • 동시성과 제어 간 균형
  • 외부 서비스 오버로딩 방지
  • 독립성 부여

💡 톺아보기
await이 함수 밖에서 어떻게 작동하는지 보고 싶다면, ES modules에서 상위 레벨 await을 사용하는 법에 대한 포스트를 살펴보세요.

동시성 레벨

목표패턴동시성
순서 준수, 하나씩 실행for...of + await1
동시 실행, 순서 없음Promise.all + map()∞(튕기지 않을 경우)
제한된 동시성p-limit, PromisePoolN(설정에 따라)

마지막 팁: forEach()에서 절대 await 쓰지 않기

이건 아주 흔한 함정이에요.

users.forEach(async id => {
  const user = await fetchUser(id);
  console.log(user); // ❌ await 작동 X
});

forEach() 반복문은 async 함수를 기다려주지 않아요. 정해진 타이밍이나 순서에 대한 보장 없이 작동하게 됩니다.

대신에, 이렇게 해보세요.

  • 순차적인 로직: for...of + await
  • 병렬적인 로직: Promise.all + map()

돌아보기

JavaScript의 async 모델은 강력하지만 await을 반복문 안에서 쓰는 건 의도가 필요합니다. 당신의 필요에 따라서 async 로직을 구성하세요.

  • 순서 → for...of
  • 속도 → Promise.all
  • 안전성 → allSettled() / try-catch
  • 균형 → p-limit




원문 출처: Rethinking async loops in JavaScript

profile
나도 재밌고, 남들도 재밌는 서비스 만들어보고 싶다😎

0개의 댓글