[JavaScript] Debounce와 Throttle

aqualung·2024년 7월 8일

디바운스

대표적으로 검색어 자동완성 기능에 쓰인다.

검색창에 타이핑을 할때마다 API와 통신해 검색어를 불러온다면 너무 많은 트래픽이 발생할 수 있다.

사용자가 타이핑을 잠깐 중단할때, 그러니까 300ms 정도 타이핑이 중단 되었을때 API에 요청을 보내 자동완성검색어는 받아오도록 하는 것이다.

const debounce = <T extends (...args: any[]) => any>(
  func: T,
  delay: number,
  leading = false,
) => {
  let timer: ReturnType<typeof setTimeout> g | null;
  return (...args: Parameters<T>) => {
    if (leading && !timer) {
      func(...args);
    }

    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (!leading) func(...args);
    }, delay);
  };
};

클로저 함수로 timer 참조해 계속해서 업데이트하는 방식으로 동작한다.

타이핑을 할 때마다<input> 태그에 onChange 이벤트가 발생한다고 가정해본다면,

첫 글자를 입력하고 300ms 뒤에 func를 실행시키는 것이다. 그런데 중요한 점은 300ms라는 간격에 다시 타이핑(onChange)이벤트가 발생했다면 마지막 타이핑부터 다시 300ms 뒤에 실행되도록 미뤄진다.

이런식으로 계속 func의 실행시점을 지연시키는 원리이다.


쓰로틀

SNS에서 자주 볼 수 있는 무한스크롤은 보통 HTML의 특정 엘리먼트를 타겟을 두고 그 엘리먼트가 화면에 잡히는지를 조건으로 이벤트를 발생시킨다. 이 엘리먼트는 보통 스크롤 가장 끝에 위치시켜 스크롤이 다 내려갔다면 이벤트를 발생시켜 API로부터 다음 페이지를 불러온다.

우리는 다음 페이지를 딱 한 번만 불러오면 된다.
하지만 여기서 발생하는 문제가 스크롤이 끝에 다다른 그 짧은 시간에 수천 수만번씩 요청이 발생할 수 있다는 것이다. 그래서 쓰로틀이라는 개념이 필요하다.(사실 Intersection Observer API를 쓰면 이런 오작동을 고려하지 않아도 된다. 심지어 비동기로 실행되어 메인스레드에도 영향이 없다고 한다.)

쓰로틀은 설정한 시간동안 딱 한번씩만 함수를 실행하게 하는 장치이다.

const throttle = <T extends (...args: any[]) => any>(
  func: T,
  delay: number,
) => {
  let waiting = false;

  return (...args: Parameters<T>) => {
    if (!waiting) {
      waiting = true;
      func(...args);
      setTimeout(() => {
        waiting = false;
      }, delay);
    }
  };
};

func를 실행하게 되면 wait를 300ms 동안 true로 만들고 func를 실행할 수 없게 만든다.
300ms가 지나면 wait를 다시 false로 바꿔 func가 실행될 수 있도록 풀어준다.


React

React에서 컴포넌트 안에 사용할 때는 주의할 점이 있다.

const App = () => {
  const [value, setValue] = useState("");
  
  // useCallback으로 캐싱해야 한다.
  const throttleConsoleLog = useCallback(
    throttle((text: string) => console.log(text), 1000),
    [],
  );
  
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
    throttleConsoleLog("throttle");
  };
  
  return (
    <div className="flex h-full w-full flex-col">
      <input
        className="border"
        placeholder="throttle test"
        value={value}
        onChange={onChange}
      />
    </div>
  );
};

리렌더링이 일어나 함수 객체가 새로 생성되면 waiting의 참조를 잃어버리는 문제가 있는 것 같다.

debouncethrottle 함수를 useCallback 등으로 캐싱하여 해결하였다.

리액트에서 custom hooks의 형태로 바꾸어 사용하는 것도 세련된 방법 같다.

0개의 댓글