디바운싱(debouncing)과 쓰로틀링(throttling)

liinyeye·2024년 11월 21일
0

Project

목록 보기
42/44

🛠 요구 사항

실시간 검색 기능과 버튼에서 연속된 동작으로 인한 불필요한 네트워크 요청을 방지해야하는 상황

👩‍🏫 검색 기능에는 debouncing을, 버튼에는 throttling을 적용

Debounce란?

특정 시간 이후에 한 번만 실행하기

Debounce 는 여러 번 발생하는 이벤트에서, 가장 마지막 이벤트만을 실행되도록 만드는 개념이다.
아래 예시 그럼처럼 순차적 호출을 하나로 그룹화할 수 있다.

적용 예시

  • 타이핑 이벤트의 결과 표시(검색 결과 목록 표시)
  • 블로그 글쓰기 에디터의 자동저장 기능
  • resize 이벤트

Throttle이란?

일정한 간격으로 한 번만 실행하기

Throttle 는 여러 번 발생하는 이벤트를 일정 시간동안, 한 번만 실행되도록 만드는 개념이다.
예를 들어, Throttle 의 설정시간으로 3ms 를 주게되면 해당 이벤트는 3ms 동안 최대 한번만 발생하게 된다.

적용 예시

  • scroll 이벤트 최적화 -> 무한 스크롤링 페이지
  • 버튼 중복 클릭 방지

Debounce와 Throttle의 차이

이벤트를 언제 발생 시킬지의 시점 차이

Debounce 는 마지막 요청이 끝날 때까지 무한으로 기다리지만, Throttle 는 요청이 시작되면 일정 주기로 계속 실행한다.

Debounce 의 시간을 짧게 가져간다면 Throttle 와 비슷한 효과가 날 수 있지만, 그럼에도 시점에서 차이가 날 수 있기 때문에 결과물의 성격에 따라 사용 방법이 달라질 수 있다.

대표적인 예시로, 자동완성을 만들 때 각 기능별 차이는 아래와 같다.

  • Throttle -> 사용자 경험 측면(검색 되는 경험)에서 유리
  • Debounce -> 성능 측면(1번만 호출)에서 유리

이때 유료 API라면 요청 하나하나가 모두 돈이기 때문에 디바운싱이 더 적절한 선택이다.

📌 왜 이런 의사결정을 했는가?

1. 검색 기능에 Debounce 적용

사용자가 검색어를 입력할 때마다 API를 호출하면 성능에 영향을 미치므로, 입력이 끝난 후 일정 시간 동안 대기하여 최종 입력값으로만 API 호출을 수행하도록 결정.

기대 효과: 불필요한 API 호출 제거, 성능 최적화

useDebounce 커스텀훅

import { useEffect, useState } from "react";

const useDebounce = <T>(value: T, delay: number): T => {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
};

export default useDebounce;

컴포넌트 내 사용

const [searchTerm, setSearchTerm] = useState(initialSearchQuery);
  const debouncedSearchTerm = useDebounce(searchTerm, 200);

2. 버튼 클릭에 Throttle 적용

버튼을 빠르게 연속 클릭할 경우 중복 동작이 발생하거나 서버 부하를 유발할 수 있으므로, 지정된 시간 간격으로 한 번만 이벤트가 실행되도록 제한

기대 효과: 버튼 동작 안정성 보장, 중복 요청 방지

useThrottle 커스텀훅

import { useCallback, useEffect, useRef } from "react";

export function useThrottle() {
  // 마지막으로 함수가 실행된 시간을 지정하는 ref -> ref를 사용하여 리랜더링 간에 값을 유지
  const lastRun = useRef<number>(Date.now());
  // 현재 대기 중인 타이머를 추적하는 ref
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  // 컴포넌트 언마운트 시 타이머 정리 -> 메모리 누수 방지
  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  return useCallback((callback: () => Promise<void>, delay: number) => {
    const execute = () => {
      callback();
      lastRun.current = Date.now();
    };

    // 마지막 실행 이후 경과된 시간 계산
    const timeElapsed = Date.now() - lastRun.current;

    if (timeElapsed >= delay) {
      execute();
    } else {
      // 이전에 예약된 타이머가 있다면 취소
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
      // 새로운 타이머 설정
      timeoutRef.current = setTimeout(() => {
        execute();
      }, delay - timeElapsed);
    }
  }, []);
}

컴포넌트 내 사용

  const handleButtonClick = useCallback(
    (action: () => Promise<void>) => {
      throttle(async () => {
        if (isBtnDisabled) return;
        setIsBtnDisabled(true);
        try {
          await action();
        } finally {
          setIsBtnDisabled(false);
        }
      }, 2000);
    },
    [isBtnDisabled, throttle]
  );

🎯 결과

  • 검색 기능: 사용자 입력이 완료된 후에만 API 요청을 보내 불필요한 서버 부하 방지
  • 버튼 동작: 연속 클릭에도 중복 동작 없이, 한 번만 처리되는 안정적인 UI 제공


참고 자료

profile
웹 프론트엔드 UXUI

0개의 댓글