React) useDebounce와 useThrottle 만들기

2ast·2023년 8월 26일
1

Debounce와 Throttle

debouncethrottle은 사용성과 성능을 끌어올릴 수 있는 최적화 방법 중 가장 유명하지 않을까 생각한다. 간단하게만 설명하자면, debounce는 일정 시간 동안 연속적으로 이벤트가 들어왔을 때 가장 마지막 이벤트만 실행하도록 해주는 반면, throttle은 일정 시간 동안 연속적으로 이벤트가 들어왔을 때 첫번째 이벤트만 실행하도록 해준다.
예를 들어, 눌렀을 때 A라는 api를 호출하는 버튼에 각각 1000ms로 debounce와 throttle을 적용하고, 800ms 주기로 버튼을 눌렀다고 상정해보자. debounce의 경우 각 이벤트 사이의 간격이 1000ms를 넘지 않으므로 이벤트를 유예하고 있다가, 마지막 세번 째 버튼을 누른 시점으로부터 1000ms가 경과했을 때 api를 호출하게 된다. 반면 throttle은 가장 먼저 입력된 이벤트를 수용하여 api를 호출하고, api 호출 시점으로부터 1000ms를 세어, 그 사이에 입력된 이벤트를 무시하게 된다. 결과적으로 debounce를 적용한 버튼은 최초 이벤트가 발생하고 2600ms 이후 api를 호출하지만, throttle이 적용된 버튼은 0ms 시점에 api를 호출한 뒤, 두번 째 이벤트를 건너뛰고, 1600ms 이후 세번 째 api를 호출한다.

useDebounce와 useThrottle hook 만들어 적용하기

useDebounce

import {useCallback, useRef} from 'react';

export const useDebounce = () => {
  //timeout을 clear 해주기 위해 ref에 Timer를 저장
  const schedule = useRef<NodeJS.Timer>();
  
  //함수를 받아 새로운 함수를 반환해주는 currying 구조의 함수를 반환하고 있다.
  return useCallback(
    (callback: (...arg: any) => void, delay: number /*ms*/) =>
      (...arg: any) => {
        //함수가 호출되면 기존 timeout을 초기화하고 새로운 timeout을 생성한다.
        clearTimeout(schedule.current);
        schedule.current = setTimeout(() => callback(...arg), delay);
      },
    [],
  );
};
const App = () =>{
    const onClickButton = () =>{
    	...
    }
	const debounce = useDebounce();
    const onClickButtonWithDebounce = debounce(onClickButton,500);
    
    return <div>
      <button onClick={onClickButtonWithDebounce}/>
    </div>
}

useDebounce의 반환 타입을 보면 callback을 인자로 받아 새로운 함수를 반환하는 currying 구조를 띠고 있다(커링에 대한 보다 자세한 개념은 여기로). 사실 조금 더 쉬운 구조로 정의하자면 useDebounce가 함수를 받아 함수를 반환하는 debounce 함수를 반환할게 아니라, useDebounce 자체적으로 function과 delay를 받아 즉시 debounced function을 반환해줄 수도 있었다.

const onclickButtonWithDebounce = useDebounce(onClickButton,500);

하지만 이런 구조는 동일한 timeout을 공유하는 다수의 function을 만드는데 제약이 있다. 만약 goBack함수가 호출되고, 1000ms가 지나기 전에 goForward함수가 호출되면 goBack을 무시하고 goForward를 실행해야하는 요구조건이 있다면 어떻게 해야할까? useDebounce가 직접 callback을 받는 구조에서 이 기능을 구현하려면 오히려 코드가 더 까다로워 질 것이다. 이런 이유로 debounce라는 커링 함수를 반환해 활용의 유연성을 확보하는 선택을 했다.

const goForward = () =>{
	...
};
  
const goBack = () =>{
	...
};
  
const debounce = useDebounce();

const goForwardWithDebounce = debounce(goForward,500);
const goBackWithDebounce = debounce(goBack,1000);

같은 논리로 만약 여러 함수에 각각 timeout을 적용하고 싶다면 useDebounce를 여러번 호출해주어야한다.

const forwardDebounce = useDebounce();
const backDebounce = useDebounce();

const goForwardWithDebounce = forwardDebounce(goForward,500);
const goBackWithDebounce = backDebounce(goBack,1000);

useThrottle

import {useCallback, useRef} from 'react';

export const useThrottle = () => {
  //현재 함수 실행 후 timeout을 기다리는 상태인지 나타내는 boolean
  const isWaiting = useRef(false);

  return useCallback(
    (callback: (...arg: any) => void, delay: number) =>
      (...arg: any) => {
    	//대기 상태라면 아무것도 하지 않고, 대기상태가 아니라면 아래 코드 실행
        if (!isWaiting.current) {
          //callback 함수 실행
          callback(...arg);
          //대기상태로 변경
          isWaiting.current = true;
          //delay 이후 대기상태 해제
          setTimeout(() => {
            isWaiting.current = false;
          }, delay);
        }
      },
    [],
  );
};
const App = () =>{
    const onClickButton = () =>{
    	...
    }
	const throttle = useThrottle();
    const onClickButtonWithThrottle = throttle(onClickButton,500);
    
    return <div>
      <button onClick={onClickButtonWithThrottle}/>
    </div>
}

전체적인 구조는 useDebounce와 유사하다.

+ type 강화하기

이 글을 쓰면서 debounce나 throttle로 감싼 함수는 원본 함수의 paramter 타입이 소실된 채 any가 되고 있었다는 것을 깨달았다. 그래서 이참에 타입을 보강해봤다.

import {useCallback, useRef} from 'react';

export const useDebounce = () => {
  const schedule = useRef<NodeJS.Timer>();
  
 return useCallback(
    <T extends (...args: any[]) => void>(callback: T, delay: number) =>
      (...args: Parameters<T>) => {
        clearTimeout(schedule.current);
        schedule.current = setTimeout(() => callback(...args), delay);
      },
    [],
  );
};
const sampleConsole = (a:number, b:string)=>{
	console.log(a,b);
};
const debounce = useDebounce();
const sampleConsoleWithDebounce = debounce(sampleConsole,500);
sampleConsoleWithDebounce(1,'a')
sampleConsoleWithDebounce(1) //Expected 2 arguments, but got 1.
sampleConsoleWithDebounce(1,1) //Argument of type 'number' is not assignable to parameter of type 'string'.
sampleConsoleWithDebounce('a',1) //Argument of type 'string' is not assignable to parameter of type 'number'.
profile
React-Native 개발블로그

0개의 댓글