React

React debounce in useEffect

리액트에서 useEffect 내부 debounce 를 구현하는 패턴에 대해서 학습하고 정리|2 months ago

React debounce in useEffect

useEffect 내부에서 클린업 함수를 실행함과 동시에 다음 이펙트에 대한 처리를 함으로써 debounce 기능과 동일한 효과를 보도록 구현할 수 있다.

import { useEffect, useState } from 'react'

function ChildInput({ onChange }) {
  const [input, setInput] = useState('')

  useEffect(() => {
    const timer = setTimeout(() => {
      onChange(input)
    }, 500)

    return () => clearTimeout(timer)
  }, [inpuㅃ onChange])

  return (
    <input type="text" value={input} onChange={e => setInput(e.target.value)} />
  )
}

위 예시를 보면 effect 내부 setTimeout 함수로 넘긴 콜백 함수는 이펙트가 여러 번 발생해도 마지막 이벤트가 발생한 후 500ms가 지나면 한번만 실행된다. 원리에 대해서 짚고 넘어가면 좋을 것 같다.

  • setTimeout 함수의 인자로 콜백 함수를 넘긴다. (effect 내부 실행)
  • (상태 업데이트) 다음 effect 내부가 실행되기 전 클린업 함수가 먼저 실행
  • 기등록된 setTimeout 예약 종료
  • 다음 effect 에서 반복..

따라서 debounce 를 사용한 것과 동일한 효과를 볼 수 있다.

번외) 비동기(Promise) 경쟁

꽤 많이 쓰이는 패턴인 것 같다. effect 내부 비동기 함수 호출이 있는 경우 상태가 변경되어 새 effect 가 실행되기 전 비동기 호출이 아직 종료되지 않았다면 그리고 effect 내부 또 다른 상태에 대한 업데이트가 동반되는 경우, 이전 effect 의 비동기 함수 호출보다 새 effect 비동기 함수 호출이 먼저 종료되어 이전 상태가 더 늦게 반영이 되어 업데이트되는 경우가 종종 일어날 수 있다.

useEffect(() => {
  let ignore = false // 플래그 설정

  const fetchData = async () => {
    // 디바운스된 값으로 API 호출
    const result = await api.search(debouncedValue)

    // 응답이 왔을 때, 이미 새로운 요청이 시작되었다면(ignore가 true면)
    // 상태를 업데이트하지 않음
    if (!ignore) {
      setResults(result)
    }
  }

  fetchData()

  return () => {
    ignore = true // 다음 렌더링(또는 언마운트) 시 플래그를 true로 변경
  }
}, [debouncedValue])

플래그를 하나 두고 플래그 상태에 따라 처리 방법을 달리한다. 플래그는 클린업 함수에서 초기화한다.

Copyright © 2026 imkh.dev All rights reserved.