본문으로 건너뛰기

바닐라 비동기 재시도 가이드

재시도는 비동기 작업이 실패한 후 다시 실행합니다. 일시적 실패를 사용자에게 덜 드러나게 할 수 있지만 부수 효과를 반복하고 정상적이지 않은 서비스의 부하를 늘릴 수도 있습니다.

[!NOTE] AsyncRetryer는 알파 API이며 1.0 이전에 변경될 수 있습니다. 현재 설계는 Pacer의 다른 비동기 유틸리티 내부에서 이루어지는 재시도 동작도 지원합니다.

TanStack Query가 이미 요청을 소유하고 있다면 하나의 시스템에서 요청 상태와 취소를 제어하도록 TanStack Query의 재시도 지원을 사용합니다.

재시도의 안전성 판단하기

일시적인 네트워크 장애, 요청률 제한 응답, 일부 서버 오류처럼 나중에 성공할 가능성이 있는 오류만 재시도합니다. 검증, 인증, 권한과 대부분의 다른 클라이언트 오류는 일반적으로 코드나 사용자 입력을 변경해야 합니다.

AsyncRetryer는 발생한 모든 오류를 재시도합니다. shouldRetry 조건자는 제공하지 않습니다. 래핑된 함수가 재시도 가능한 결과에 대해서만 오류를 발생시키도록 합니다.

async function loadUser(id: string) {
const response = await fetch(`/api/users/${id}`)

if (response.status === 429 || response.status >= 500) {
throw new Error(`Temporary failure: ${response.status}`)
}

if (!response.ok) {
return { ok: false as const, status: response.status }
}

return { ok: true as const, user: await response.json() }
}

작업을 반복해도 멱등성이 있는지도 고려합니다. 읽기는 일반적으로 안전합니다. 서버가 작업을 완료한 후 첫 응답이 손실되면 쓰기로 인해 레코드, 청구, 메시지가 중복되거나 다른 부수 효과가 발생할 수 있습니다. 이러한 쓰기를 재시도하기 전에 멱등성 키나 서버 측 중복 제거를 사용합니다.

빠른 시작

asyncRetry는 재시도기 하나를 만들고 바인딩된 실행 함수를 반환합니다.

import { asyncRetry } from '@tanstack/pacer'

const loadUserWithRetry = asyncRetry(loadUser, {
maxAttempts: 3,
baseWait: 1000,
jitter: 0.2,
})

try {
const result = await loadUserWithRetry('123')
console.log(result)
} catch (error) {
console.error('All attempts failed:', error)
}

기본값은 총 3회 시도, 1000밀리초에서 시작하는 지수 백오프, 최대 지연 없음, 지터 없음, throwOnError: 'last'입니다.

반환된 함수는 순차적으로 재사용할 수 있습니다. 하나의 AsyncRetryer를 소유하므로 이전 호출이 활성 상태일 때 새 호출을 시작하면 이전 재시도 흐름이 중단됩니다. 호출이 겹칠 수 있다면 호출마다 재시도기를 만들거나 동시성을 관리하는 다른 유틸리티를 사용합니다.

import { AsyncRetryer } from '@tanstack/pacer'

async function loadOneUser(id: string) {
const retryer = new AsyncRetryer(loadUser, { maxAttempts: 3 })
return retryer.execute(id)
}

클래스 사용하기

콜백, 상태, 옵션 변경 또는 수동 중단 제어가 필요하다면 AsyncRetryer를 사용합니다.

import { AsyncRetryer } from '@tanstack/pacer'

const retryer = new AsyncRetryer(loadUser, {
maxAttempts: 4,
backoff: 'exponential',
baseWait: 500,
maxWait: 5000,
jitter: 0.2,
onRetry: (attempt, error) => {
console.log(`Attempt ${attempt} failed; retrying`, error)
},
onLastError: (error) => {
console.error('Attempts exhausted:', error)
},
})

const result = await retryer.execute('123')

maxAttempts에는 첫 호출이 포함됩니다. 값이 1이면 결과, 오류, 시간 제한, 콜백, 중단 동작은 유지하면서 재시도를 비활성화합니다.

시도 및 백오프

첫 시도는 즉시 시작됩니다. 다음 시도가 남아 있는 시도가 실패한 후에만 지연을 계산합니다.

baseWait: 1000일 때 명목상 지연은 다음과 같습니다.

실패한 시도지수선형고정
11000 ms1000 ms1000 ms
22000 ms2000 ms1000 ms
34000 ms3000 ms1000 ms
48000 ms4000 ms1000 ms

maxWait는 지터를 적용하기 전의 명목상 지연에 상한을 둡니다. baseWait, maxWait, maxAttempts에는 재시도기 인스턴스를 받는 함수를 지정할 수도 있습니다.

공유 서비스에 지터 추가하기

여러 클라이언트가 동시에 실패한 뒤 동기화된 흐름으로 재시도할 수 있습니다. jitter는 각 명목상 지연의 위아래로 임의의 변동을 추가합니다.

const retryer = new AsyncRetryer(loadUser, {
maxAttempts: 4,
baseWait: 1000,
maxWait: 10_000,
jitter: 0.25,
})

0에서 1 사이의 값을 사용합니다. 0.25는 최대 25%의 변동을 허용합니다. 지터는 maxWait 이후에 적용되므로 최종 무작위 지연이 maxWait보다 약간 클 수 있습니다.

오류 및 콜백

오류 콜백은 재시도 생명주기의 서로 다른 단계에서 동작합니다.

  • onError(error, args, retryer)는 실패한 모든 시도에서 실행됩니다.
  • onRetry(attempt, error, retryer)는 다음 시도가 남아 있을 때 시도가 실패한 후 실행됩니다. attempt는 방금 실패한 시도 번호입니다.
  • onLastError(error, retryer)는 모든 시도가 실패한 후 실행됩니다.
  • onSuccess(result, args, retryer)는 시도가 성공한 후 한 번 실행됩니다.
  • onSettled(args, retryer)는 시도가 완료될 때 실행됩니다.

throwOnError는 최종 실패 결과를 제어합니다.

  • 기본값인 'last'는 모든 시도가 실패한 후 최종 오류로 거부됩니다.
  • true도 설정된 시도를 실행한 다음 최종 오류로 거부됩니다.
  • false는 모든 시도가 실패한 후 undefined로 이행됩니다.

onError를 제공하면 throwOnError의 기본값이 false로 바뀝니다. 관찰과 거부가 모두 필요하다면 명시적으로 설정합니다.

const retryer = new AsyncRetryer(loadUser, {
onError: (error) => reportError(error),
throwOnError: 'last',
})

래핑된 함수에서 발생한 AbortError는 취소로 처리됩니다. 추가 시도를 소비하거나 onAbort를 자동으로 호출하지 않고 undefined로 이행됩니다.

시간 제한

서로 독립적인 두 옵션이 재시도 작업을 제한합니다.

  • maxExecutionTime은 시도 하나의 기한을 설정합니다.
  • maxTotalExecutionTime은 시도와 백오프 대기를 포함한 전체 호출의 기한을 설정합니다.
const retryer = new AsyncRetryer(loadUser, {
maxAttempts: 4,
maxExecutionTime: 5000,
maxTotalExecutionTime: 15_000,
onExecutionTimeout: () => console.warn('Attempt timed out'),
onTotalExecutionTimeout: () => console.warn('Retry operation timed out'),
})

시간 제한이 발생하면 재시도 흐름이 중단됩니다. onAbort'execution-timeout' 또는 'total-timeout'을 받습니다. 전체 시간 제한은 일반적으로 undefined로 이행됩니다. 시도별 시간 제한은 throwOnError가 활성화되어 있으면 최종 시간 제한 또는 중단 오류로 거부될 수 있으며 오류 발생이 비활성화되어 있으면 undefined로 이행됩니다.

JavaScript는 임의의 Promise를 강제로 중지할 수 없습니다. 시간 제한은 Pacer가 재시도 흐름을 계속하지 못하게 하지만 기반 작업은 중단 신호와 연동되는 경우에만 중지됩니다.

기반 작업 중단하기

래핑된 함수에서 getAbortSignal()을 호출하고 이를 지원하는 API에 신호를 전달합니다.

const retryer = new AsyncRetryer(
async (url: string) => {
const response = await fetch(url, {
signal: retryer.getAbortSignal() ?? undefined,
})
if (!response.ok) throw new Error(`Request failed: ${response.status}`)
return response.json()
},
{ maxAttempts: 3 },
)

const request = retryer.execute('/api/data')
retryer.abort()
await request // undefined

abort()는 활성 재시도 흐름과 대기 중인 백오프를 중지합니다. onAbort'manual'을 받습니다. 동일한 인스턴스에서 다른 execute()를 시작하면 이전 흐름이 'new-execution'으로 중단됩니다.

작업에 신호를 전달하지 않으면 abort()가 이후 재시도 작업은 막지만 활성 Promise는 백그라운드에서 계속 실행될 수 있습니다.

재설정 및 옵션 변경

setOptions()는 새 옵션을 현재 설정에 병합합니다. 활성 실행을 다시 시작하지는 않습니다.

reset()은 기본 상태만 복원합니다. 활성 작업을 중단하지는 않습니다.

retryer.abort()
retryer.reset()

재사용할 수 있고 타입 검사를 거친 옵션 객체를 정의하려면 asyncRetryerOptions()를 사용합니다.

const networkRetryOptions = asyncRetryerOptions({
maxAttempts: 3,
backoff: 'exponential',
baseWait: 1000,
jitter: 0.2,
})

상태

클래스는 retryer.store에 상태를 저장합니다.

애플리케이션이 유지한 일부 상태를 복원하려면 부분 스냅샷을 initialState로 전달합니다. 이 스냅샷은 기본값과 병합됩니다. 지속 가능한 필드만 복원합니다. 대기 중인 타이머와 활성 실행은 복원되지 않습니다.

일반적으로 유용한 속성은 다음과 같습니다.

  • currentAttempt: 현재 또는 가장 최근에 시작된 시도 번호입니다. 성공하거나 재설정하면 0으로 돌아갑니다.
  • isExecuting: 재시도 흐름이 활성 상태인지 나타냅니다.
  • lastError: 가장 최근에 실패한 시도의 오류입니다.
  • lastResult: 가장 최근의 성공 결과입니다.
  • executionCount: 개별 시도가 아닌 성공한 최상위 실행 횟수입니다.
  • lastExecutionTimetotalExecutionTime: 가장 최근 성공에 기록된 타이밍입니다.
  • status: 'disabled', 'idle', 'executing' 또는 'retrying'입니다.

AsyncRetryer는 코어 API이며 프레임워크별 훅이 없습니다. 반응형 상태가 필요하다면 retryer.store나 적절한 TanStack Store 어댑터를 통해 구독합니다.

정확한 시그니처는 asyncRetry 함수 레퍼런스, AsyncRetryer 클래스 레퍼런스, AsyncRetryerOptions 레퍼런스를 참고합니다.