[React] useDebounce와 useThrottle 완벽 가이드: 성능 최적화의 핵심

ByEunwoo
react

React에서 성능 최적화는 매우 중요한 주제이다. 특히, 사용자 경험을 향상시키기 위해 입력 이벤트에 대한 반응성을 조절하는 것이 필요하다. 이를 위해 useDebounceuseThrottle 훅을 사용하여 성능을 최적화할 수 있다. 이 글에서는 이 두 가지 훅의 개념과 사용법을 자세히 알아보겠다.

이 글에서는 이 두 개념을 React 훅 형태로 직접 구현하고, 어떤 상황에서 어떤 훅을 사용하는 것이 좋은지, 그리고 각 훅의 사용 예제장단점까지 상세하게 설명해보려고 한다.

useDebounce란?

연속된 이벤트 중 마지막 이벤트만 처리하는 방식이다.
예를 들어, 사용자가 검색어를 입력할 때 입력이 멈춘 뒤 일정 시간 후에만 API 요청을 보내는 패턴이 바로 Debounce이다.

추가로 본인은 왜 Hook 이름이 useDebounce인지 궁금했는데, bounce 즉, 공이 튕기는 것을, De- 의 접두사가 붙어서 공이 튕겨져 나오는 것을 방지하는 것에서 유래되었다고 한다. 즉, 연속된 이벤트 중 마지막 이벤트만 처리하여 불필요한 작업을 줄이는 것이다.

사용 예시

  • 검색어 자동 완성
  • 입력값에 대한 실시간 검증
  • 무분별한 API 호출 방지

useDebounce Hook 구현

import { useCallback, useRef } from 'react';
 
const useDebounce = (callback: () => void, term: number) => {
  const timer = useRef<ReturnType<typeof setTimeout>>(null);
 
  const dispatchDebounce = useCallback(() => {
    if (timer.current) {
      clearTimeout(timer.current);
    }
    const newTimer = setTimeout(() => {
      callback();
    }, term);
    timer.current = newTimer;
  }, [callback, term]);
 
  return dispatchDebounce;
};
 
export default useDebounce;

useDebounce 훅은 callback 함수와 term 즉 delay시간을 인자로 받는다.
반환값으로는 callbackterm 밀리초 후에 실행하는 함수를 반환한다.

useRef는 컴포넌트가 리렌더링되어도 값이 유지되므로, 타이머 추적에 적합하다.
(값이 매번 바뀔때마다 리랜더링 되는 것을 방지한다.)

if (timer.current) {
  clearTimeout(timer.current);
}

위 코드를 살펴보면, 이전에 실행 예정이었던 타이머가 있다면, 그것을 취소한다. 이를 통해 연속된 이벤트 중 마지막 이벤트만 처리할 수 있다.
이것이 debouncing의 핵심이다.

const newTimer = setTimeout(() => {
  callback();
}, term);
timer.current = newTimer;

이후, 새로운 타이머를 설정하여 term 밀리초 후에 callback()을 실행한다.
새 타이머의 ID를 timer.current에 저장하여 다음에 또 취소할 수 있게 한다.

사용 예시

'use client';
 
import { useEffect, useState } from 'react';
import useDebounce from './useDebounce';
 
function SearchInput() {
  const [input, setInput] = useState('');
  const debouncedInput = useDebounce(input, 500);
 
  useEffect(() => {
    if (debouncedInput) {
      // API 호출 등
      console.log('API 요청:', debouncedInput);
    }
  }, [debouncedInput]);
 
  return <input value={input} onChange={(e) => setInput(e.target.value)} />;
}

위 예시에서 사용자가 입력할 때마다 setInput이 호출되지만, useDebounce 훅을 통해 마지막 입력 후 500ms가 지나야 API 요청이 발생한다.
즉, 사용자가 빠르게 입력해도 마지막 입력 후에만 console.log()가 실행된다.

장점

  • API 호출 최소화 (server 부하 감소)
  • UX 개선 (사용자가 입력 중일 때 불필요한 응답 방지)

단점

  • 마지막 입력만 처리됨 (중간 이벤트는 무시)
  • delay가 짧으면 debounce 효과가 약함

useThrottle란?

지정한 시간 동안 최대 한 번만 실행되도록 제한하는 방식이다.
스크롤 이벤트나 마우스 움직임처럼 짧은 시간에 반복 호출되는 이벤트에 적합하다.

ThrottleDebounce와는 달리, 이벤트가 발생할 때마다 즉시 반응하지만, 지정한 시간 간격으로만 실행된다. 즉, 연속된 이벤트를 일정 주기로 나누어 처리하는 것이다.
예를 들어, 사용자가 스크롤을 할 때, 매번 스크롤 이벤트가 발생하는 것이 아니라, 100ms마다 한 번씩만 처리하는 방식이다.
이런 방식은 성능을 최적화하면서도 사용자 경험을 해치지 않도록 도와준다.

throttle는 기계적 용어로, 어떤 작업을 일정한 속도로 제한하는 것을 의미한다. 즉, 연속된 이벤트를 일정 간격으로 나누어 처리하는 것이다.

사용 예시

  • 스크롤 위치 기반 애니메이션
  • 버튼 반복 클릭 방지
  • 실시간 입력 반응 (하지만 너무 자주 실행되면 비효율적)

useThrottle Hook 구현

import { useCallback, useState } from 'react';
 
const useThrottle = () => {
  const [timeoutId, setTimeoutId] = useState<ReturnType<typeof setTimeout> | null>(null);
 
  const makeThrottle = useCallback(
    (callback: () => void, throttleTime: number) => () => {
      if (timeoutId) return;
      const newTimeoutId = setTimeout(() => {
        callback();
        setTimeoutId(null);
      }, throttleTime);
      setTimeoutId(newTimeoutId);
    },
    [timeoutId],
  );
 
  const cleanup = useCallback(() => {
    if (!timeoutId) return;
    clearTimeout(timeoutId);
  }, [timeoutId]);
 
  return { makeThrottle, cleanup };
};
 
export default useThrottle;

makeThrottle(callback, throttleTime) 함수를 호출하면, 지정한 throttleTime 동안 callback이 한 번만 실행되도록 제한한다.
useThrottle 훅은 내부적으로 timeoutId 상태를 관리하여, 현재 실행 중인 타이머가 있는지 확인한다.

if (timeoutId) return;

위 코드는 현재 throttle 타이머가 실행 중이면 callback() 실행하지 않음을 의미한다 → throttle 효과

const newTimeoutId = setTimeout(() => {
  callback();
  setTimeoutId(null); // 일정 시간 후 다시 실행 가능하도록 초기화
}, throttleTime);
setTimeoutId(newTimeoutId);

callback()throttleTime 밀리초 후에 실행하고 타이머 종료 후에는 timeoutId를 null로 초기화한다. → 다음 실행 허용
타이머가 끝나기 전까지는 timeoutId가 유지되므로, 같은 함수는 실행되지 않는다.

즉, 이 훅은 어떤 함수든 "지금 실행하면, 일정 시간동안 다시 못 실행하게 만드는" throttle 로직을 생성합니다.

사용 예시

import { useState } from 'react';
import useThrottle from './useThrottle';
 
function ThrottleButton() {
  const [count, setCount] = useState(0);
  const { makeThrottle } = useThrottle();
 
  const handleClick = makeThrottle(() => {
    setCount((prev) => prev + 1);
  }, 1000);
 
  return <button onClick={handleClick}>Click Me ({count})</button>;
}

위 예시에서 버튼을 클릭할 때마다 setCount가 호출되지만, useThrottle 훅을 통해 1초에 한 번만 카운트가 증가한다.

장점

  • 연속 이벤트를 제한하면서도 반응성 유지
  • 성능 최적화에 적합

단점

  • 마지막 이벤트가 무시될 수 있음
  • debounce보다 UX가 거칠게 느껴질 수 있음

useDebounce vs useThrottle 비교

항목DebounceThrottle
실행 타이밍마지막 이벤트 후 일정 시간일정 간격마다 한 번씩 실행
대표 예시입력 필드 검색, API 호출스크롤, 버튼 클릭 등 빠른 반복 이벤트
UX 측면사용자 입력이 끝났을 때만 응답중간에도 응답 가능 (일정 주기마다)
데이터 처리가장 마지막 값이 중요할 때 적합중간 과정의 반응이 필요한 경우 적합

마무리

useDebounce는 최소한의 호출로 정확한 데이터를 원할 때 유용하다.
useThrottle는 빈번한 이벤트에 대응하면서도 과도한 처리 방지에 적합하다.

둘 다 사용자 경험과 성능 사이의 균형을 잡는 데 핵심적인 도구이다.
상황에 맞는 도구를 적절히 사용하면 React 앱의 성능과 반응성을 모두 개선할 수 있다.

Posted inreact
Written byEunwoo