반응형

특정 기간동안 함수가 너무 많이 호출될 경우, 함수 실행을 건너뛸 수 있는 debounce 나 throttle 기법을 고려할 수 있다.

그 중 debounce 는 이벤트를 그룹화하여 많은 이벤트가 발생해도 모두 무시하고, 하나의 이벤트만 실행시키도록 하는 기법이다. lodash 라이브러리에서 지원한다.

 

input 태그에서 사용자가 입력을 수시로 변할 때, 백엔드에 input 데이터를 보내는 예제를 생각해보자.

 

const Input = () => {
  const [value, setValue] = useState();

  const sendRequest = (value) => {
    // 백엔드에 input 데이터를 보냄
  };

  const debouncedSendRequest = debounce(sendRequest, 500);

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}

 

위와 같이 코드를 생각해볼 수 있다. onChange 함수에서 value 를 세팅하고 debounced 된 함수를 호출하는 형태이다.

하지만 위 코드는  value 값이 바뀔 때마다 리렌더링이 되면서 아래와 같은 문제점이 생긴다.

 

  • sendRequest, debouncedSendRequest 함수가 계속 파괴되고 생성된다.
  • timer 로 인해 바로 파괴되는 것이 아니라 timer 시간 동안 유지되었다가 참조하는 곳이 없어서 가비지 컬렉션에 의해 정리된다.

 

const Input = () => {
  const [value, setValue] = useState("initial");

  const sendRequest = useCallback((value) => {
    // 백엔드에 input 데이터를 보냄
  }, []);

  const debouncedSendRequest = useMemo(() => {
  	return debounce(sendRequest, 500);
  }, [sendRequest]);

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}

 

리렌더링이 되어서 함수가 파괴가 되어 참조를 못 하는 문제를 해결하기 위해 sendRequest 함수 자체에는 useCallback 훅으로 감싸고, debouncedSendRequest 함수는 useMemo 훅으로 감싸서 코드를 수정해보았다. 정상적으로 동작하는 것처럼 보인다.

 

debouncedSendRequest(value) 구문보면 계속해서 value 인자를 주고 있는데 이 부분을 useCallback 의 종속성 인자로 바꾸어 보면, 결국 처음 코드와 같게 된다.

 

리바운싱할 때마다 디바운스 함수를 생성하지 않게 하는 것이 관건인데 보통 useRef 훅으로 함수를 감싸는 것을 추천한다. 아래와 같은 형태가 되는데 만약 useRef 내부 함수에서 state 값을 참조하면 클로저가 되어버리기 때문에 주의해야 한다.

 

const ref = useRef(debounce(() => {
    // value 가 scope 를 벗어난 외부 변수이기 때문에 초기값으로 세팅된다 (클로저)
    console.log(value);
}, 500));

 

value 상태 값을 항상 최신으로 유지하기 위해 함수를 다시 호출하고 ref 에 다시 할당해야 한다.

(즉, 클로저라는 문제 때문에 value 값이 변경될 때마다, ref.current 값을 교체해주어야 된다.)

 

useEffect(() => {
    ref.current = debounce(() => {
    }, 500);
}, [value]);

 

또 처음 코드와 같게 된다. 클로저로 변형된 함수들을 Ref 로 묶고, debouncedCallback 함수만 useMemo  로 결과 값만 재 사용한다면 위 useMemo 와 useCallback 의 이점을 동시에 사용할 수 있게 된다.

 

[최종 코드]

  const [value, setValue] = useState("initial");
  const ref = useRef();

  const onChange = () => {
  };

  useEffect(() => {
    ref.current = onChange;
  }, [onChange]);

  const debouncedCallback = useMemo(() => {
    const func = () => {
      ref.current?.();
    };
    return debounce(func, 1000);
  }, []);

 

[출처]

How to debounce and throttle in React without losing your mind (developerway.com)

반응형

+ Recent posts