React 비동기 재시도 가이드
재시도는 비동기 작업이 실패한 후 다시 실행합니다. 일시적인 실패가 사용자에게 덜 드러나게 할 수 있지만 부수 효과를 반복하고 비정상적인 서비스의 부하를 늘릴 수도 있습니다.
[!NOTE]
AsyncRetryer는 알파 API이며 1.0 이전에 변경될 수 있습니다. 현재 설계는 Pacer의 다른 비동기 유틸리티 내부에서도 재시도 동작을 지원합니다.
재시도는 이 프레임워크 가이드 가운데 예외입니다. TanStack Pacer는 React 전용 재시도 프리미티브를 제공하지 않습니다. 어댑터가 공개 asyncRetry 함수와 AsyncRetryer 클래스를 다시 내보내므로 이 가이드에서는 해당 API를 사용하고 장기 유지되는 인스턴스를 React 수명 주기에 연결합니다.
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/react-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)
}
기본값은 총 세 번의 시도, 1000밀리초부터 시작하는 지수 백오프, 최대 지연 없음, 지터 없음, throwOnError: 'last'입니다.
반환된 함수는 순차적으로 재사용할 수 있습니다. 하나의 AsyncRetryer를 소유하므로 이전 호출이 활성 상태일 때 새 호출을 시작하면 이전 재시도 흐름이 중단됩니다. 호출이 겹칠 수 있다면 호출마다 재시도기를 만들거나 동시 실행을 관리하는 다른 유틸리티를 사용합니다.
import { AsyncRetryer } from '@tanstack/react-pacer'
async function loadOneUser(id: string) {
const retryer = new AsyncRetryer(loadUser, { maxAttempts: 3 })
return retryer.execute(id)
}
컴포넌트마다 재시도기 하나 만들기
import { useEffect, useMemo } from 'react'
import { AsyncRetryer } from '@tanstack/react-pacer'
function UserPanel({ id }: { id: string }) {
const retryer = useMemo(
() =>
new AsyncRetryer(loadUser, {
maxAttempts: 3,
baseWait: 1000,
jitter: 0.2,
backoff: 'exponential',
maxWait: 5000,
onRetry: (attempt, error) => {
console.log(`Attempt ${attempt} failed; retrying`, error)
},
onLastError: (error) => {
console.error('Attempts exhausted:', error)
},
}),
[],
)
useEffect(() => () => retryer.abort(), [retryer])
return <button onClick={() => void retryer.execute(id)}>Reload</button>
}
함수가 변경되는 props나 상태를 클로저로 캡처한다면 retryer.fn을 업데이트합니다. 같은 인스턴스에서 두 번째 execute()를 시작하면 이전 재시도 흐름이 중단됩니다. 실행이 겹칠 수 있다면 별도의 인스턴스를 만듭니다.
콜백, 상태, 옵션 변경, 수동 중단 제어가 필요하다면 장기 유지되는 AsyncRetryer를 사용합니다. maxAttempts에는 첫 번째 호출이 포함됩니다. 값을 1로 설정하면 결과, 오류, 타임아웃, 콜백, 중단 동작은 유지하면서 재시도를 비활성화합니다.
시도와 백오프
첫 번째 시도는 즉시 시작됩니다. 추가 시도가 남아 있는 시도가 실패한 후에만 지연 시간을 계산합니다.
baseWait: 1000일 때 명목상 지연 시간은 다음과 같습니다.
| 실패한 시도 | 지수 | 선형 | 고정 |
|---|---|---|---|
| 1 | 1000 ms | 1000 ms | 1000 ms |
| 2 | 2000 ms | 2000 ms | 1000 ms |
| 3 | 4000 ms | 3000 ms | 1000 ms |
| 4 | 8000 ms | 4000 ms | 1000 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는 취소로 처리됩니다. undefined로 이행되며 추가 시도를 소비하거나 onAbort를 자동으로 호출하지 않습니다.
타임아웃
서로 독립적인 두 옵션이 재시도 작업을 제한합니다.
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: 개별 시도가 아니라 성공한 최상위 실행 횟수입니다.lastExecutionTime,totalExecutionTime: 가장 최근의 성공에 기록된 타이밍입니다.status:'disabled','idle','executing','retrying'중 하나입니다.
AsyncRetryer는 React 상태 셀렉터를 노출하지 않습니다. 콜백을 사용해 뷰에 필요한 필드를 React 상태에 복사합니다. retryer.store를 직접 구독한다면 재시도기를 중단하는 동일한 컴포넌트 또는 소유자 정리 과정에 구독 해제 함수를 등록합니다.
정확한 시그니처는 asyncRetry 함수 레퍼런스, AsyncRetryer 클래스 레퍼런스, AsyncRetryerOptions 레퍼런스를 참고합니다.