eunsoolib
비동기·서버

iterate-paginated

페이지네이션 API를 아이템 단위 async iterable로 평탄화

커서를 넘기며 페이지를 반복 요청하는 루프를 for await로 감춥니다. 페이지 결과를 한곳에 모으지 않고 아이템을 하나씩 흘려보내므로, 중간에 break하면 이후 페이지는 요청하지 않습니다.

설치

pnpm add @cbcruk/iterate-paginated

사용법

import { iteratePaginated, type PagedFetch } from '@cbcruk/iterate-paginated'

type User = { id: string; name: string }

const fetchUsers: PagedFetch<User, string> = async (cursor) => {
  const res = await fetch(`/api/users?cursor=${cursor ?? ''}`)
  const { data, nextCursor } = await res.json()

  // 마지막 페이지면 nextCursor가 null/undefined → 순회 종료
  return { items: data, nextState: nextCursor }
}

for await (const user of iteratePaginated(fetchUsers)) {
  console.log(user.name)
}

첫 호출에 넘길 커서가 있으면 두 번째 인자로 줍니다.

for await (const user of iteratePaginated(fetchUsers, 'cursor-from-last-run')) {
  // ...
}

API

iteratePaginated(fetcher, initialState?, options?)

AsyncGenerator<T>를 반환합니다.

  1. fetcher(initialState)를 호출합니다.
  2. 받은 items를 순서대로 yield합니다.
  3. nextStateundefinednull이면 끝냅니다.
  4. nextState가 방금 넘긴 커서와 같으면(Object.is) RepeatedCursorError를 던집니다.
  5. 그 밖에는 nextState로 다시 fetcher를 호출합니다.

fetcher는 최소 한 번 호출됩니다.

인자타입설명
fetcherPagedFetch<T, S>한 페이지를 가져오는 함수
initialStateS첫 호출에 넘길 커서 (기본 undefined)
optionsIteratePaginatedOptions순회 옵션

IteratePaginatedOptions

옵션타입설명
signalAbortSignal중단 신호. fetcher 호출 전과 아이템을 내보내기 전에 확인하고, 중단되면 signal.reason을 던짐
const controller = new AbortController()

for await (const user of iteratePaginated(fetchUsers, undefined, {
  signal: controller.signal,
})) {
  // controller.abort()를 호출하면 다음 아이템/페이지 전에 멈춤
}

signal은 진행 중인 fetcher 호출을 취소하지 않습니다. 요청까지 끊으려면 같은 signalfetcher 안의 fetch에도 넘기세요.

RepeatedCursorError

nextState가 직전 커서와 같아 순회를 멈췄을 때 던지는 에러입니다. 반복된 값은 cursor 속성에 담깁니다.

PagedFetch<T, S>

type PagedFetch<T, S> = (
  state?: S,
) => Promise<{ items: T[]; nextState?: S | null }>

주의

  • 종료 조건은 nextStateundefined 또는 null인 경우입니다. 빈 문자열이나 0 같은 값은 유효한 커서로 보고 계속 요청하므로, API가 이런 값으로 마지막 페이지를 표시하면 fetcher에서 null로 바꿔 반환하세요.
  • 반복 커서 감지는 직전 커서와의 Object.is 비교뿐입니다. 매번 새 객체를 커서로 쓰거나 커서가 A → B → A처럼 순환하면 감지하지 못합니다.

On this page