[React] 함수의 중복 호출을 막기 위한 throttling 기능 적용기

profile image pIutos 2024. 1. 4. 00:06

문제사항

const handleSubmit = async () => {
  // API 호출...
};

API를 호출하는 위와 같은 함수가 있습니다. 이러한 함수는 보통 button, input의 이벤트 함수에서 실행되는데, 예를 들어 유저가 단시간 내에 버튼을 여러번 클릭시 API 통신 오류가 발생하는 경우가 있었습니다.

이에 해당 함수의 클릭 시간을 조절하는 throttle기능이 필요했고, 이를 구현한 방법을 소개하겠습니다.

중복 호출을 막기 위한 useThrottle 훅 구현하기

throttle기능을 구현하는 방법 중에는 여러 방법이 있지만, 저는 slash 라이브러리처럼 함수 전체를 감싸서 throttle기능을 적용하는 방식으로 구현하고 싶었습니다.

이에 throttle의 callback의 시간을 관리하는 방법으로 Date 객체를 사용하는 방식을 생각했으며, 아래와 같이 throttle기능을 구현했습니다.

import { useRef } from 'react';

const THROTTLE_DEFAULT_TIME = 1 * 1000;

const useThrottle = <T extends Function>(
  callback: T,
  throttleTime: number | undefined = THROTTLE_DEFAULT_TIME,
): Function => {
  const time = useRef<ReturnType<Date['valueOf']>>(0);

  return () => {
    const callbackExecutionTime = new Date().valueOf();

    if (callbackExecutionTime - time.current < throttleTime) return;

    time.current = callbackExecutionTime;
    callback();
  };
};

export default useThrottle;

현재 시간의 ms시간을 반환하는 Date의 valueOf 메서드를 이용해 callbackExecutionTime을 선언합니다.

이 값과 useRef에 저장된 time값을 비교하여 throttleTime보다 작다면(throttle 중)실행 callback함수를 실행하지 않습니다.

그렇지 않은 경우 ref에 해당 시간을 저장한 다음 callback함수를 실행합니다.

다른 방식으로 구현하기

하지만 해당 방식은 아래와 같은 문제가 있습니다.

  1. throttleTime이 정확하게 동작하지 않을 수 있습니다. 미미한 시간이지만 훅에서 Date를 생성하고, if문을 체크하며 ref에 저장하는 동작을 실행하는 동안의 시간이 있기 때문입니다.
  2. callback이 실행될 때마다 callbackExecutionTime을 할당하기 때문에, useRef에 할당된 값이 메모리에서 해제되지 않아 메모리 누수 현상이 발생할 수 있습니다.

따라서 lodash에서 사용하는 방식인 setTimeout 함수를 이용하는 방식으로 useThrottle 훅을 구현해보았습니다.

import { useRef } from 'react';

const THROTTLE_DEFAULT_TIME = 1 * 1000;

const useThrottle = <T extends Function>(
  callback: T,
  throttleTime: number | undefined = THROTTLE_DEFAULT_TIME,
): Function => {
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

  return () => {
    if (timer.current) return;

    callback();
    timer.current = setTimeout(() => {
      timer.current = null;
    }, throttleTime);
  };
};

export default useThrottle;

timeout 함수를 ref에 저장한 다음, timeout이 완료되면 현재 ref를 null로 초기화합니다. 만약 timer가 존재한다면 callback 함수를 실행하지 않습니다.

useRef에 할당된 값이 저장되는 heap 공간에 현재 시간의 string값(Date.valueof) 대신 setTimeout함수를 저장한다면 약간의 메모리 비용이 증가하겠지만, 성능상에 큰 차이점은 없을것입니다.

오히려 메모리 누수 현상이 발생하는 이전 방법과 다르게 ref를 해제하기 때문에 더 나은 방법이라 할 수 있을 것입니다.

결과적으로

const handleSubmit = useThrottle(async () => {
  // API 호출...
});

const handleClick = useThrottle(async () => {
  // API 호출...
}, 2 * 1000);

어느 방식으로든 useThrottle을 원하는 동작으로 사용할 수 있었습니다.

참고문서

https://minoo.medium.com/useref-%EA%B0%80-%EC%88%9C%EC%88%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%9C%EB%8B%A4%EB%8A%94-%EC%9D%98%EB%AF%B8%EB%A5%BC-%EA%B3%B1%EC%94%B9%EC%96%B4%EB%B3%B4%EA%B8%B0-8a0857fc5ebb