블로그 목록
frontend

Array.fromAsync()로 비동기 이터러블을 배열로 모으기 — for await + push 반복문 그만

커서 기반 페이지네이션 API에서 전체 목록을 한 번에 긁어와야 할 일이 있었다. 응답에 nextCursor가 들어있고, 그게 없을 때까지 계속 요청하는 흔한 구조다. 보통 이렇게 짠다.

async function* fetchAllPages() {
  let cursor = null;
  do {
    const res = await fetch(`/api/items?cursor=${cursor ?? ''}`);
    const { items, nextCursor } = await res.json();
    yield* items;
    cursor = nextCursor;
  } while (cursor);
}

제너레이터까지는 깔끔한데, 이걸 실제 배열로 만들려면 결국 아래처럼 받아야 한다.

const all = [];
for await (const item of fetchAllPages()) {
  all.push(item);
}

for await 돌면서 push 하는 이 패턴, 쓸 때마다 번거로웠다. 동기 이터러블이면 Array.from()이나 스프레드([...]) 한 방인데, 비동기에는 스프레드가 안 먹는다. [...fetchAllPages()]는 그냥 터진다.

Array.fromAsync() 한 줄로 끝난다

이 경우에 쓰라고 나온 게 Array.fromAsync()다.

const all = await Array.fromAsync(fetchAllPages());

끝이다. 비동기 이터러블을 끝까지 소비해서 배열 하나로 묶어주고, Promise를 반환하니까 await만 붙이면 된다. 임시 배열과 for await 루프가 통째로 사라졌다.

Array.from()과 똑같이 두 번째 인자로 매핑 함수도 받는다. 모으면서 바로 변환할 수 있다.

const ids = await Array.fromAsync(
  fetchAllPages(),
  (item) => item.id
);

중요한 건 이 매핑 함수가 async여도 된다는 점이다. 각 값마다 비동기 작업을 순차적으로 돌리고 싶을 때 유용하다.

const details = await Array.fromAsync(
  idStream,
  async (id) => {
    const res = await fetch(`/api/items/${id}`);
    return res.json();
  }
);

동기 이터러블도 받는다

Array.fromAsync()는 비동기 이터러블뿐 아니라 일반 배열·유사 배열도 받는다. 안에 Promise가 들어있으면 알아서 풀어준다.

await Array.fromAsync([Promise.resolve(1), Promise.resolve(2)]);
// → [1, 2]

다만 Promise.all()과는 성격이 다르다. Promise.all()은 동시에 실행하지만, Array.fromAsync()는 하나씩 순차적으로 소비한다. 그래서 수백 개를 병렬로 빠르게 받아야 하는 상황이면 여전히 Promise.all()이 맞다. 반대로 페이지네이션처럼 앞 결과가 있어야 다음을 요청하는 순차 작업엔 Array.fromAsync()가 자연스럽다.


Array.fromAsync()는 이제 모든 최신 브라우저와 Node 22+에서 기본으로 쓸 수 있다. 비동기 제너레이터나 스트림 결과를 배열로 모을 일이 생기면, 빈 배열 만들고 for await부터 짜지 말고 이걸 먼저 떠올리자. 순차 소비라는 특성만 기억하면 쓸 데가 꽤 많다.