4 분 소요


1. throttling & debouncing 개요

짧은 시간 간격으로 연속해서 이벤트가 발생했을 때 과도한 이벤트 핸들러 호출을 방지하는 기법을 말한다.


1.1 throttling 개념

짧은 시간 간격으로 연속해서 발생한 이벤트들을 일정시간 단위(delay)로 그룹화하여 처음 또는 마지막 이벤트 핸들러만 호출되도록 하는 것을 말한다. (예: 무한스크롤)

즉, 입력 주기를 방해하지 않고, 일정 시간 동안의 입력을 모와서, 한 번씩 출력을 제한한다.


1.2 debouncing 개념

짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간(delay)이 경과한 후에 한 번만 호출하도록 하는 것을 말한다. (예: 입력값 실시간 검색, 화면 resize 이벤트)

즉, 입력 주기가 끝나면 출력한다.



2. 실습

2.1 throttle & debouncing 구현

먼저 react-router-dom을 설치해주자

yarn add react-router-dom


timer id(Throttling과 debounce을 제어하는 키)

setTimeout 함수는 타이머 ID를 반환한다.
이 타이머 ID는 나중에 clearTimeout 등의 메서드를 사용하여 해당 타이머를 취소하거나 제어하는 데 사용한다.


// src > pages > Home.jsx
export default function Home() {
  // timer id(Throttling과 debounce을 제어하는 키)
  let timerId = null;

  // Leading Edge Throttling
  const throttle = (delay) => {
    if (timerId) {
      // timerId가 있으면 바로 함수 종료
      return;
    }
    console.log(`API 요청 실행. ${delay}ms 동안 추가 요청은 받지 않습니다.`);
    timerId = setTimeout(() => {
      console.log(`${delay}ms가 지났으므로 추가 요청을 받습니다.`);
      timerId = null;
    }, delay);
  };

  // 반복적인 이벤트 이후, delay가 지나면 function
  const debounce = (delay) => {
    if (timerId) {
      // 할당되어있는 timerId에 해당하는 타이머 제거
      clearTimeout(timerId);
    }
    timerId = setTimeout(() => {
      console.log(`마지막 요청으로부터${delay}ms 지났으므로 API 요청 실행`);
      timerId = null;
    }, delay);
  };

  return (
    <div style={{ paddingLeft: 20, paddingRight: 20 }}>
      <h1>Button 이벤트 예제</h1>
      <button onClick={() => throttle(2000)}>쓰로틀링 버튼</button>
      <button onClick={() => debounce(2000)}>디바운싱 버튼</button>
    </div>
  );
}


하지만, 아래와 같이 state를 설정했을 경우 throttle이 발동할 때 state가 변경되면서 리렌더링이 되므로 다시 함수가 호출되면서 let timerId = null;로 설정되므로 정상적으로 동작하지 않는 문제가 있다.

let timerId = null;
const [state, setState] = useState(true); // state 생성

// Leading Edge Throttling
const throttle = (delay) => {
  if (timerId) {
    return;
    setState(!state); // 리렌더링 → 다시 함수 호출
  }
  console.log(`API 요청 실행. ${delay}ms 동안 추가 요청은 받지 않습니다.`);
  timerId = setTimeout(() => {
    console.log(`${delay}ms가 지났으므로 추가 요청을 받습니다.`);
    timerId = null;
  }, delay);
};


2.2 메모리 누수 문제 해결

메모리 누수(Memory Leak)란? 필요하지 않은 메모리를 계속 점유하고 있는 현상을 말한다.


아래와 같이 Router을 설정해주자.

// App.jsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Company from "./pages/Company";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/company" element={<Company />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
// src > pages > Company.jsx
import React from "react";

export default function Company() {
  return <div>Test Page</div>;
}


그리고 버튼 클릭시 다른 페이지로 이동할 수 있게하자

import { useState } from "react";
import { useNavigate } from "react-router-dom"; // import

export default function Home() {
  let timerId = null;
  const [state, setState] = useState(true);
  const navigate = useNavigate(); // navigate 생성

  const throttle = (delay) => {
    ...
  };

  const debounce = (delay) => {
   ...
  };

  return (
    <div>
      <h1>Button 이벤트 예제</h1>
      <button onClick={() => throttle(2000)}>쓰로틀링 버튼</button>
      <button onClick={() => debounce(2000)}>디바운싱 버튼</button>

      {/* 아래 코드 추가 */}
      <div>
        <button onClick={() => navigate("/company")}>페이지 이동</button>
      </div>
    </div>
  );
}


만약, 쓰로틀링 버튼을 누르고 다른 페이지로 이동했을 때 어떻게될까?

콘솔창을 확인해보면 전 페이지에 요청을 하고 있는 것이 아직도 영향을 끼치는 것을 확인할 수 있다. → 메모리 누수


따라서 useEffect를 활용하여 메모리 누수 문제를 해결할 수 있다.

import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

export default function Home() {
  let timerId = null;
  const [state, setState] = useState(true);
  const navigate = useNavigate();

  // useEffect 활용
  useEffect(() => {
    return () => {
      // unmount 시
      if (timerId) {
        clearTimeout(timerId);
      }
    };
  }, []);

  const throttle = (delay) => {
    ...
  };

  const debounce = (delay) => {
    ...
  };

  return (
    <div>
      <h1>Button 이벤트 예제</h1>
      <button onClick={() => throttle(2000)}>쓰로틀링 버튼</button>
      <button onClick={() => debounce(2000)}>디바운싱 버튼</button>
      <div>
        <button onClick={() => navigate("/company")}>페이지 이동</button>
      </div>
    </div>
  );
}



3. 라이브러리 Lodash

설치하기

yarn add lodash


lodash는 throttle와 debounce 함수를 제공하여 이벤트 핸들러의 호출을 제어할 수 있다.

import _ from "lodash";

const debouncedFunction = _.debounce(
  (arg1, arg2) => {
    console.log("Debounced function with arguments:", arg1, arg2);
  },
  1000 // milliseconds
);

// 디바운스된 함수 호출
debouncedFunction("arg1", "arg2");
  • 첫 번째 인자: 디바운싱할 함수
  • 두 번째 인자: 디바운싱할 대기 시간 (밀리초 단위), 이 시간 동안 새로운 호출이 없을 때 함수가 호출된다.


입력값을 넣고, 디바운싱 테스트를 할 수 있는 예제를 만들어보자.

import React, { useState } from "react";

function App() {
  const [searchText, setSearchText] = useState("");
  const [inputText, setInputText] = useState("");

  // 공통 함수
  const handleChange = (e) => {
    setInputText(e.target.value);
  };

  return (
    <div>
      <input
        placeholder="입력값을 넣고 디바운싱 테스트를 해보세요"
        type="text"
        onChange={handleChange}
      />

      <p>Search Text : {searchText}</p>
      <p>Input Text : {inputText}</p>
    </div>
  );
}

export default App;


위 코드를 베이스로 디바운싱을 적용해보자
Search Text엔 디바운싱을 적용할 것이고, Input Text엔 디바운싱을 적용하지 않을 것이다.

import React, { useState } from "react";
import _ from "lodash"; // import
import { useCallback } from "react";

function App() {
  const [searchText, setSearchText] = useState("");
  const [inputText, setInputText] = useState("");

  const handleSearchText = useCallback(
    _.debounce((text) => {
      setSearchText(text);
    }, 2000),
    []
  );
  const handleChange = (e) => {
    setInputText(e.target.value);
    handleSearchText(e.target.value);
  };

  return (
    <div>
      <input
        placeholder="입력값을 넣고 디바운싱 테스트를 해보세요"
        type="text"
        onChange={handleChange}
      />

      <p>Search Text : {searchText}</p>
      <p>Input Text : {inputText}</p>
    </div>
  );
}

export default App;
  • handleSearchText: lodash의 debounce를 사용하여 디바운스된다. 마지막 호출 이후 2000 밀리초(2초) 동안 setSearchText의 실행을 지연시킨다.
  • handleInputChange: 입력 필드의 변경에 대한 이벤트 핸들러이다. 입력이 변경될 때마다 새 텍스트로 handleSearchText 함수를 호출한다.
  • setSearchText: 디바운싱 기간 후에 가장 최근의 검색 텍스트로 상태를 업데이트한다.

useCallback의 사용

⭐ useCallback을 사용해야 디바운싱이 정상적으로 작동한다!

useCallback을 사용하여 함수를 메모이제이션하고 빈 배열([])을 의존성 배열로 전달함으로써, 해당 함수가 렌더링 중에 변하지 않도록 할 수 있다.

여기서 useCallback이 사용된 handleSearchText 함수는 _.debounce 내부에서 클로저를 형성하게 되는데, 이 클로저 함수는 외부 함수의 변수에 계속해서 참조를 갖고있기 때문에 타이머 아이디 등을 기억할 수 있게 된다.

따라서, 매 렌더링 시에 새로운 함수가 생성되지 않고 기존의 클로저가 유지될 수 있는 것이다.



4. 참조

  • https://pks2974.medium.com/throttle-%EC%99%80-debounce-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0-2335a9c426ff

태그:

카테고리:

업데이트:

댓글남기기