본문 바로가기
React

[React/TypeScript] Debounce, 일정 시간동안 발생한 이벤트 중 마지막만 실행

by LasBe 2023. 3. 8.
반응형

Debounce란


Debounce란 일정 시간 동안 연속적으로 발생했던 이벤트들 중

마지막만 실행시켜 과다한 호출이나 렌더를 막아 최적화하는 기술입니다.

 

예를 들어 이번 개인 프로젝트 중 입력 시 중복체크를 위해 API를 호출하는데
한 글자 타이핑할 때마다 API를 호출하는 것이 부담되어 Debounce를 도입하여 해결했습니다.

const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
  let timeout: ReturnType<typeof setTimeout>;

  return (...args: Parameters<T>): ReturnType<T> => {
    let result: any;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      result = fn(...args);
    }, delay);
    return result;
  };
};

Typescript로 구현한 debounce함수입니다.

여러 타입 때문에 복잡해 보이지만 클로저만 알고 있다면 간단하게 해석할 수 있습니다.

 

설정한 delay 시간 내에 함수를 재실행하게 된다면

내부 함수에 의해 살아있는 timeout함수가 clear 되어 무효화됩니다.

 

만약 시간 내 아무런 호출이 없었다면 가장 마지막으로 호출된 setTimeout 내부의 함수가 실행될 것입니다.


📌 React에서 Debounce 구현하기

특히나 상태 변화에 의해 렌더링이 발생하는 React에서는 Debounce가 굉장히 유용합니다.

const [state, setState] = useState('');
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setState(e.target.value);
}
console.log(state);

return <input onChange={onChange} />

이렇게 간단한 입력에도 수많은 렌더링이 발생하는 리액트에서는
특히나 onChange 같은 함수에 Debounce를 사용하면 더욱더 효율적입니다.

 

import { useState } from 'react';

const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
  let timeout: ReturnType<typeof setTimeout>;

  return (...args: Parameters<T>): ReturnType<T> => {
    let result: any;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      result = fn(...args);
    }, delay);
    return result;
  };
};

// API를 호출해 중복 체크하는 예시
const promiseFun = async (value: string) => {
  console.log('Call');
  await new Promise((resolve) => setTimeout(resolve, 300));
  return 'abcd' === value;
};

export default function App() {
  const [check, setCheck] = useState(false);
  const [debounceState, setDebounceState] = useState('');

  const onChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const result = await promiseFun(e.target.value);
    setDebounceState(e.target.value);
    setCheck(result);
  };

  const debouncedOnChange = debounce<typeof onChange>(onChange, 500);

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
        minHeight: '100vh',
      }}
    >
      <h2>중복 문자열 "abcd"</h2>
      <h3>state : {debounceState}</h3>
      <h3>check : {check ? 'true' : 'false'}</h3>
      <input type="text" onChange={debouncedOnChange} />
      <hr />
      <button disabled={check}>BUTTON</button>
    </div>
  );
}

API 호출을 가정한 promise 함수를 불러와 중복 체크하는 기능을 구현한 코드입니다.

 

직접 입력해 보면 debounce에 의해 마지막 입력까지 렌더와 함수 호출이 없는 것을 확인할 수 있습니다.

 

참고사항


📌 lodash 라이브러리 Debounce

$ npm install @types/lodash

// -----------------------------------------

import { debounce } from 'lodash';
...
const debouncedOnChange = debounce(onChange, 500);

유틸 라이브러리로 유명한 lodash에도 debounce 기능이 내장되어 있어

별도 코드 작성 없이도 기능을 구현할 수 있습니다.

 

내부 코드도 위에서 소개한 것과 같은 원리로 작동됩니다.


📌 useCallback() warning

다른 상태에 의해 리랜더링이 일어나 debounce 함수 객체를 재생성해 timer가 초기화될 위험이 있습니다.

 

이럴 때 주로 useCallback()으로 함수를 메모이제이션 합니다.

const debouncedOnChange = useCallback(
    debounce((...) => {...},500)
,[])

하지만 이렇게 사용하게 된다면 다음과 같은 warning을 볼 수 있습니다.

React Hook useCallback received a function whose dependencies are unknown.
Pass an inline function instead. (react-hooks/exhaustive-deps)

 

이 경고는 debounce로 감싼 함수에서 종속성을 감지하지 못해 발생합니다.

const debouncedOnChange = useMemo(
    () => debounce((...) => {...},500)
,[])

이는 useMemo를 통해 debounce 함수를 반환함으로써 해결할 수 있습니다.

반응형

댓글


오픈 채팅