[React] Debounce 직접 구현하기

Sara Jo·2024년 11월 4일
0
post-thumbnail

유저 입력에 따라 빈번하게 발생하는 이벤트(ex. 검색어 입력과 같이 실시간으로 변하는 데이터를 다룰 때)를 효율적으로 처리하기 위해 디바운스(Debounce)가 많이 사용된다. 그 동안 lodash 라이브러리의 debounce 함수를 사용했는데, 이번에는 라이브러리를 사용하지 않고 직접 debounce를 구현해보았다.


디바운스(Debounce)란?

디바운스는 특정 이벤트가 연속적으로 발생할 때, 마지막 이벤트가 발생한 후 일정 시간 동안 추가 이벤트가 발생하지 않으면 해당 이벤트를 실행하는 기법이다. 이를 통해 불필요한 함수 호출을 줄이고 성능을 최적화 할 수 있다.

주로 아래와 같은 상황에서 많이 사용된다.

  • 검색어 입력 처리: 사용자가 검색어를 입력할 때마다 API를 호출하면 과도한 요청이 발생할 수 있다. 디바운스를 사용하면 사용자가 입력을 완료한 후에 API를 호출할 수 있다.
  • 윈도우 리사이즈 이벤트: 창 크기가 변경될 때마다 레이아웃을 재계산하는 대신, 리사이즈가 완료된 후에 한 번만 처리할 수 있다.
  • 버튼 클릭 이벤트: 빠르게 여러 번 클릭하는 것을 방지하고, 마지막 클릭만 처리할 수 있다.

lodash의 debounce 함수 사용하기

먼저, lodash 라이브러리에서 제공하는 debounce 함수를 사용하는 방법을 살펴보자. 아래와 같이 아주 간편하게 debounce 처리가 가능하다.

import React, { useMemo, useState } from "react";
import { debounce } from "lodash";

function App() {
  const [searchKeyword, setSearchKeyword] = React.useState("");

  const debouncedSearch = useMemo(
    () => debounce((keyword: string) => setSearchKeyword(keyword), 500),
    []
  );

  const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    debouncedSearch(event.target.value);
  };

  return (
    <div>
      <input type="text" onChange={handleSearchChange} placeholder="검색어를 입력하세요" />
      <p>검색어: {searchKeyword}</p>
    </div>
  );
}

export default App;

lodashdebounce를 사용하여 handleSearchChange 함수가 호출될 때마다 0.5초 동안 대기한 후 setSearchKeyword를 실행하게 된다.


디바운스 직접 구현하기

lodash 라이브러리의 다른 기능들이 필요한 것이 아니라면, 번들 크기를 줄이고 의존성을 최소화하기 위해 라이브러리를 사용하지 않고 직접 디바운스를 구현할 수 있다. 구현 방법은 생각보다 정말 간단하다. React에서는 useRefuseCallback 훅을 활용해서 구현할 수 있다.

1. 커스텀 useDebounce 훅 만들기

먼저, 디바운스 로직을 재사용 할 수 있도록 useDebounce라는 커스텀 훅을 만들어주었다.

// hooks/useDebounce.ts
import { useRef, useEffect, useCallback } from "react";

function useDebounce<T extends (...args: any[]) => void>(callback: T, delay: number): T {
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const debouncedFunction = useCallback((...args: any[]) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    timeoutRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  }, [callback, delay]);

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

  return debouncedFunction as T;
}

export default useDebounce;

💡 timeout 참조에 useRef를 사용한 이유

위 코드에서 timeout을 참조하기 위해 useRef를 사용했다.
let timeout: ReturnType<typeof setTimeout>; 와 같은 형식의 단순 변수를 사용하지 않고 useRef로 할당한 이유는 뭘까?

React의 함수 컴포넌트는 렌더링될 때마다 함수가 다시 호출된다. 이 때, 단순 변수는 매번 새로운 값으로 초기화된다. 이는 이전 타이머를 참조할 수 없게 만들어 clearTimeout이 제대로 동작하지 않을 수 있다.

반면, useRef컴포넌트의 라이프사이클 동안 값을 유지할 수 있어 타이머를 안정적으로 관리가 가능하다. 이를 통해 타이머를 정확히 취소하고, 메모리 누수를 방지할 수 있다.


2. 컴포넌트에서 useDebounce 사용하기

이제 useDebounce 훅을 컴포넌트에 적용해보았다.

import React, { useState } from "react";
import useDebounce from "./hooks/useDebounce";

function App() {
  const [searchKeyword, setSearchKeyword] = React.useState("");

  const debouncedSearch = useDebounce((keyword: string) => {
    setSearchKeyword(keyword);
  }, 500);

  const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    debouncedSearch(event.target.value);
  };

  return (
    <div>
      <input type="text" onChange={handleSearchChange} placeholder="검색어를 입력하세요" />
      <p>검색어: {searchKeyword}</p>
    </div>
  );
}

export default App;

lodash 라이브러리를 사용했을 때와 동일하게 handleSearchChange 함수가 호출될 때마다 0.5초 동안 대기한 후 setSearchKeyword를 실행하게 된다.

0개의 댓글