JavaScript

lodash debounce를 활용하여 성능 향상시키기

jjin502 2023. 6. 3. 16:41

개요

현재 진행중인 프로젝트 STAYMATE에서 아주 심각한 문제가 발견되었다.

이전 글에서 카메라를 사용하기 위해 react-qr-reader라이브러리를 썼는데,

호출이 과도하게 많아지는 문제점을 발견하게 되었다.

 

30초가 채 지나지 않았는데 벌써 50번이 넘게 호출된다 ...

 

이러한 문제점을 발견하고 어떻게 해결을 할까 고민하기 시작했다.

무한 호출을 막을 수 있는 방법

우선 폭포처럼 내리는 무한 호출을 막을 수 있는 방법은, 유한 호출 될 수 있도록 만들면 된다.

 

 

연속으로 발생한 호출은 한 번만 인정해주거나 혹은 몇 초에 한 번씩 인식하도록 하거나.

 

이러한 방법을 해결할 수 있는 개념이 debounce throttle이다.

Debounce와 throttle이란?

Debounce

연속적으로 발생한 이벤트를 하나로 처리하는 방식으로,

주로 처음이나 마지막에 실행된 함수만 실행한다.

 

예를 들면 아래의 예시 코드를 보도록하자.

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [value, setValue] = useState("");
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
    console.log(e.target.value);
  };
  return (
    <div className="App">
      <input type="text" value={value} onChange={handleChange} />
    </div>
  );
}

 

단순히 입력을 useState로 관리하는 것처럼 보이지만,

사실 콘솔에 내용을 찍어보면 이렇게 호출이 상당히 많이 일어나는 걸 볼 수 있다.

 

그저 3글자 입력값 "소금빵"이라는 단어를 입력했을 뿐인데, ㅅ,소,소그,소금, ... 이런식으로 호출이 일어났다.

1번만 호출해도 될 것을 9번이나 호출하게 된다.

 

그래서 이때 debounce를 사용할 수 있다.

"소금빵"이라고 3글자가 완전히 입력되었을 때 API를 호출하게끔하는것이다.

 

좋은 예제가 있어 활용해보았다. setTimeout과 clearTimeout을 활용해서 딜레이를 주었다.

function debounce<T extends (...args: any[]) => void>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: ReturnType<typeof setTimeout>;
  return function (...args: Parameters<T>) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

 

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

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

 

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

그리고 해당 함수를 아까 기존 코드에 적용해주었다.

import { useState, useCallback, ChangeEvent } from "react";
import "./styles.css";

export default function App() {
  const [value, setValue] = useState<string>("");

  const debouncedLog = useCallback(
    debounce((val: string) => {
      console.log(val);
    }, 800),
    []
  );

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const newValue = e.target.value;
    setValue(newValue);
    debouncedLog(newValue);
  };

  return (
    <div className="App">
      <input type="text" value={value} onChange={handleChange} />
    </div>
  );
}

 

아래와 같이 한 번만 호출하는 것을 볼 수 있다.

throttle

이벤트를 일정 주기마다 발생하도록 하는 방식으로,

특정 시간에 한 번만 실행된다.

 

예를 들면 100ms가 주어지면 이 시간 동안 한 번만 실행되는 것이다.

 

둘 다 어쨌든 호출 횟수를 줄일 수 있도록해준다.

필자는 이 중에 debounce를 사용할 예정이다.

 

lodash debounce 활용하기

debounce를 그냥 사용해도 좋지만, lodash라이브러리를 활용해 간단하게 사용해보려고한다.

프로젝트를 빨리 배포하는 것이 목적이기도하고, 사용법이 쉬운 라이브러리를 사용하도록 하자!

 

우선 기존 코드를 보여주려고한다.

<QrReader
    constraints={{ facingMode: selected }}
    scanDelay={1000}
    className="qr"
    onResult={(result, error) => {
      if (result) {
        console.log(`[SUCCESS] QR 문자 : `, result.getText());
      }
      if (error) {
        console.log("[ERROR] QR 코드를 정확하게 인식해주세요.");
      }
    }}
/>

 

이제 이를 debounce 적용을 하면 아래와 같이 나타낼 수 있다.

<QrReader
    constraints={{ facingMode: selected }}
    scanDelay={1000}
    className="qr"
    onResult={(useCallback(
    debounce((result, error) => {
      if (result) {
        console.log(`[SUCCESS] QR 문자 : `, result.getText());
      }
      if (error) {
        console.log("[ERROR] QR 코드를 정확하게 인식해주세요.");
      }
    }, 500),
    []
  )}
/>

 

결과 

적용했을 때, 호출이 줄어들었고 카메라 인식 속도가 훨씬 빨라졌다.

게다가 폭포처럼 쏟아졌던 콘솔로그가 깔끔해졌다.

 

맺으며

이번 포스팅에서는 throttle보다 debounce에 더 집중해서 글을 작성했는데,

throttle은 무한 스크롤을 할때 유용하게 쓰인다고 한다.

 

기회가 된다면 사용해보고 싶다. debounce와 throttle을 적절히 잘 활용하면,

사용자의 경험도 개선할 수 있다는 이점을 확연히 깨달았다.