본문으로 건너뛰기

React 디바운싱 가이드

디바운싱은 설정된 시간 동안 호출이 멈출 때까지 함수 실행을 지연합니다. 새 호출이 발생할 때마다 타이머가 다시 시작됩니다. 기본 설정에서는 가장 최근 호출의 인수를 사용해 해당 호출만 실행합니다.

중간 호출을 버려도 되고 최종 값이 중요할 때 디바운싱을 사용합니다. 검색 입력, 폼 검증, 자동 저장, 크기 변경 처리가 일반적인 예입니다.

디바운싱의 작동 방식

아래 타임라인은 호출이 버스트 형태로 들어오는 모습을 보여 줍니다. 모든 호출은 타이머를 재설정합니다. 각 버스트의 마지막 호출은 세 틱 동안 활동이 없으면 실행됩니다.

Debouncing (wait: 3 ticks)
Timeline: [1 second per tick]
Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
Executed: ❌ ❌ ❌ ❌ ❌ ❌ ❌ ❌ ⏳ -> ✅ ❌ ⏳ -> ✅
[================================================================]
^ Executes here after
3 ticks of no calls

[Burst of calls] [More calls] [Wait] [New burst]
No execution Resets timer Execute Reset and execute

각 버스트에서 가장 최근 호출만 실행됩니다. 이전의 모든 호출은 버려집니다.

디바운싱은 의도적으로 일부 호출을 버립니다. 모든 작업을 반드시 실행해야 한다면 큐잉을 사용합니다.

디바운싱을 사용해야 하는 경우

다음과 같은 경우 디바운싱을 선택합니다.

  • 활동이 멈출 때까지 기다리려는 경우
  • 가장 최근 인수만 중요한 경우
  • 모든 이벤트마다 작업을 반복하면 리소스가 낭비되는 경우
  • 짧은 지연을 허용할 수 있는 경우

다음과 같은 경우 다른 유틸리티를 선택합니다.

  • 활동이 계속되는 동안 작업을 일정한 간격으로 실행해야 하는 경우 스로틀링을 사용합니다.
  • 시간 창 안에서 정해진 수의 호출을 실행할 수 있어야 하는 경우 요청률 제한을 사용합니다.
  • 모든 작업을 결국 실행해야 하는 경우 큐잉을 사용합니다.
  • 여러 항목을 함께 처리해야 하는 경우 배칭을 사용합니다.
  • 결과를 기다리거나 오류를 처리하고 재시도하거나 진행 중인 작업을 중단해야 하는 경우 비동기 디바운싱을 사용합니다.

React에서 디바운싱 사용하기

React 어댑터는 세 가지 수준의 디바운싱 API를 제공합니다.

  • useDebouncedCallback은 안정적인 디바운스 이벤트 핸들러를 생성합니다.
  • useDebouncedStateuseDebouncedValue는 상태 또는 변경되는 값을 지연합니다.
  • useDebouncer는 수명 주기 메서드, 동적 옵션, 콜백, 선택한 상태를 노출합니다.

이 훅들은 컴포넌트 또는 다른 훅의 최상위 수준에서 호출합니다. 이 가이드에서 이후에 다루는 핵심 예제는 해당 컨텍스트에서 실행한다고 가정합니다.

디바운스 콜백

이벤트가 디바운스된 부수 효과를 호출해야 할 때 useDebouncedCallback을 사용합니다.

import { useDebouncedCallback } from '@tanstack/react-pacer'

function SearchBox() {
const search = useDebouncedCallback(
(query: string) => updateSearchResults(query),
{ wait: 500 },
)

return (
<input
onChange={(event) => search(event.currentTarget.value)}
placeholder="Search"
/>
)
}

콜백은 cancel()이나 flush()를 노출하지 않습니다. 컴포넌트에서 이러한 제어가 필요하다면 useDebouncer를 사용합니다.

디바운스 상태와 값

Pacer가 지연된 상태를 소유해야 한다면 useDebouncedState를, 값이 이미 다른 곳에서 변경되고 있다면 useDebouncedValue를 사용합니다.

import { useDebouncedValue } from '@tanstack/react-pacer'

function Results({ query }: { query: string }) {
const [debouncedQuery] = useDebouncedValue(query, { wait: 500 })

return <SearchResults query={debouncedQuery} />
}

인스턴스 API

import { useDebouncer } from '@tanstack/react-pacer'

function SaveControls() {
const debouncer = useDebouncer(saveDraft, { wait: 500 }, (state) => ({
isPending: state.isPending,
}))

return (
<button
disabled={!debouncer.state.isPending}
onClick={() => debouncer.flush()}
>
Save now
</button>
)
}

콜백과 maybeExecute()는 모두 void를 반환합니다. 동기 어댑터는 반환값을 보관하거나 오류를 포착하지 않습니다. 후행 콜백 안에서 오류를 처리하거나, 호출자에게 Promise 결과가 필요하다면 비동기 디바운싱을 사용합니다.

실행 타이밍

leadingtrailing 옵션은 대기 시간의 어느 시점에 실행할 수 있는지를 제어합니다.

leadingtrailing동작
falsetrue활동이 멈출 때까지 기다린 다음 가장 최근 호출을 실행합니다. 기본 동작입니다.
truefalse첫 호출을 즉시 실행합니다. 이후 호출은 실행하지 않고 대기 시간을 다시 시작합니다.
truetrue첫 호출을 즉시 실행합니다. 대기 시간 중에 다른 호출이 들어오면 가장 최근 호출을 후행 시점에 실행합니다.
falsefalse어떤 호출도 실행하지 않습니다.
const debouncer = useDebouncer(saveDraft, {
wait: 1000,
leading: true,
trailing: true,
})

debouncer.maybeExecute('first') // Executes immediately.
debouncer.maybeExecute('second')
debouncer.maybeExecute('latest') // Executes after 1 second of inactivity.

두 시점을 모두 활성화하면 단일 호출은 선행 시점에만 실행됩니다. 후행 실행은 대기 시간 중에 다른 호출이 들어올 때만 발생합니다.

최대 대기 시간 없음

useDebouncermaxWait 옵션을 제공하지 않습니다. 호출이 계속 들어오면 후행 실행이 무기한 미뤄질 수 있습니다. 호출이 계속 들어오는 동안에도 제한된 간격으로 작업을 계속해야 한다면 스로틀링을 사용합니다.

대기 작업 제어하기

인스턴스 API는 대기 작업의 실행, 취소, 재설정을 구분합니다.

플러시

flush()는 대기 중인 후행 호출을 가장 최근 인수로 즉시 실행합니다. 대기 중인 후행 호출이 없으면 아무 동작도 하지 않습니다.

const debouncer = useDebouncer(saveDraft, { wait: 1000 })

debouncer.maybeExecute('draft')
debouncer.flush() // Executes saveDraft('draft') now.

취소

cancel()은 함수를 실행하지 않고 대기 중인 타임아웃을 지웁니다. 또한 다음에 maybeExecute()를 호출할 때 선행 호출이 즉시 실행될 수 있게 합니다.

debouncer.maybeExecute('discarded draft')
debouncer.cancel()

재설정

reset()은 디바운서의 상태 카운터와 플래그를 기본값으로 복원합니다. 이미 예약된 타임아웃은 지우지 않습니다. 대기 작업을 버리고 상태를 재설정해야 한다면 먼저 cancel()을 호출합니다.

debouncer.cancel()
debouncer.reset()

런타임에 동작 설정하기

생성 후 옵션을 변경하려면 setOptions()를 사용합니다.

debouncer.setOptions({
wait: 1000,
leading: true,
trailing: false,
})

wait 값은 다음 호출에서 타임아웃을 예약할 때 적용됩니다. 이미 대기 중인 타임아웃을 다시 예약하지는 않습니다. maybeExecute()를 다시 호출하면 이전 타임아웃을 지우고 현재 옵션을 사용해 새 타임아웃을 예약합니다.

활성화 및 비활성화

실행을 막으려면 enabledfalse로 설정합니다. setOptions()로 디바운서를 비활성화하면 대기 중인 호출도 취소됩니다.

const debouncer = useDebouncer(saveDraft, {
wait: 500,
enabled: false,
})

debouncer.maybeExecute('ignored')
debouncer.setOptions({ enabled: true })
debouncer.maybeExecute('saved')

enabledwait 옵션에는 디바운서 인스턴스를 받는 함수를 사용할 수도 있습니다.

const debouncer = useDebouncer(saveDraft, {
enabled: (debouncer) => debouncer.store.state.executionCount < 10,
wait: (debouncer) => (debouncer.store.state.executionCount === 0 ? 300 : 500),
})

실행 관찰하기

래핑된 함수가 실행된 후 부수 효과를 수행하려면 onExecute를 사용합니다. 콜백에는 실행된 인수와 디바운서 인스턴스가 차례로 전달됩니다.

const debouncer = useDebouncer(saveDraft, {
wait: 500,
onExecute: (args, debouncer) => {
console.log('Saved arguments:', args)
console.log('Execution count:', debouncer.store.state.executionCount)
},
})

React 수명 주기

컴포넌트가 언마운트되면 어댑터가 대기 작업을 취소합니다. onUnmount를 제공하면 이 기본 정리 동작을 대체하므로 사용자 지정 콜백에서 필요한 모든 수명 주기 작업을 수행해야 합니다. 언마운트 중 플러시할 때는 컴포넌트 해제가 시작된 뒤 콜백이 실행될 수 있으므로 특히 중요합니다.

반응형 상태

어댑터는 세 번째 인수로 전달한 셀렉터를 통해서만 반응형 상태를 노출합니다. 셀렉터가 없으면 debouncer.state는 빈 객체입니다. 컴포넌트에서 사용하는 필드만 선택합니다.

const debouncer = useDebouncer(fn, options, (state) => ({
isPending: state.isPending,
executionCount: state.executionCount,
}))

console.log(debouncer.state.isPending, debouncer.state.executionCount)

옵션 함수와 수명 주기 콜백은 기반 공개 유틸리티 인스턴스를 받습니다. 위 예제처럼 해당 콜백 안에서 .store.state를 읽는 방식을 지원합니다. 컴포넌트 렌더링에서는 선택한 어댑터 상태를 사용해야 합니다.

앱에서 유지한 선택 상태를 복원하려면 initialState로 부분 스냅샷을 전달합니다. 이 스냅샷은 기본값과 병합됩니다. 지속 가능한 필드만 복원합니다. 대기 중인 타이머는 복원되지 않습니다.

  • isPending: 후행 실행이 대기 중인지 여부입니다.
  • executionCount: 래핑된 함수가 실행된 횟수입니다.
  • lastArgs: 후행 실행이 활성화된 가장 최근 호출에서 기록한 인수입니다. 대기 작업으로 간주하기 전에 isPending을 확인합니다.
  • status: 'disabled', 'idle', 'pending' 중 하나입니다.

어댑터 시그니처는 React API 레퍼런스를, 전체 옵션 및 상태 타입은 공개 코어 레퍼런스를 참고합니다.