throttling & debouncing

개발자지망생·2023년 12월 1일
0

1. Throttling & Debouncing이란?

짧은 시간 간격으로 연속해서 이벤트가 발생했을 때 과도한 이벤트 핸들러 호출을 방지하는 기법인 쓰로틀링과 디바운싱에 대해 학습해 봅시다.
Timer Web API 중 setTimeout 메소드를 사용하여 쓰로틀링과 디바운싱을 각각 구현해보고 원리를 이해하고 적용하실 줄 알아야 합니다.

  • Throttling 이란?
    - 예시 이미지를 열어봅시다.

    Type 1: Leading Edge

    Type 2: Trailing Edge

    Type 3: Leading & Trailing edge

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

    주로 사용되는 예: 무한스크롤

  • Debouncing 이란?

    • 예시 이미지


출처: 자바스크립트 딥다이브

짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간(delay)이 경과한 후에 한 번만 호출하도록 하는 것

주로 사용되는 예: 입력값 실시간 검색, 화면 resize 이벤트

  • 메모리 누수(Memory Leak)란? 필요하지 않은 메모리를 계속 점유하고 있는 현상
  • (Q&A) setTimeout 이 메모리 누수(Memory Leak)를 유발하는 지? 상황에 따라 메모리 누수를 일으킬 수도 있고 아닐 수도 있습니다. 하나의 페이지에서 페이지 이동 없이 setTimeout을 동작시키고 타이머 함수가 종료될 때까지 기다린다면 메모리 누수는 없습니다. 리액트로 만든 SPA 웹사이트는 페이지 이동 시 컴포넌트가 언마운트 됩니다. 그런데 페이지 이동 전에 setTimeout 으로 인해 타이머가 동작중인 상태에서 clearTimeout을 안해주고 페이지 이동 시 컴포넌트는 언마운트 되었음에도 불구하고 타이머는 여전히 메모리를 차지하고 동작하고 있습니다. 이 경우 메모리 누수(Memory Leak)에 해당한다고 말할 수 있습니다.

2. 실습

react-router-dom 설치

yarn add react-router-dom

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 > Home.jsx

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

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

  // Leading Edge Throttling
  const throttle = (delay) => {
    if (timerId) {
      // timerId가 있으면 바로 함수 종료
      return;
    }
    // setState(!state);
    console.log(`API요청 실행! ${delay}ms 동안 추가요청 안받음`);
    timerId = setTimeout(() => {
      console.log(`${delay}ms 지남 추가요청 받음`);
      // alert("Home / 쓰로틀링 쪽 API호출!");
      timerId = null;
    }, delay);
  };

  // Trailing Edge Debouncing
  const debounce = (delay) => {
    if (timerId) {
      // 할당되어 있는 timerId에 해당하는 타이머 제거
      clearTimeout(timerId);
    }
    timerId = setTimeout(() => {
      // timerId에 새로운 타이머 할당
      console.log(`마지막 요청으로부터 ${delay}ms지났으므로 API요청 실행!`);
      timerId = null;
    }, delay);
  };

  useEffect(() => {
    return () => {
      // 페이지 이동 시 실행
      if (timerId) {
        // 메모리 누수 방지
        clearTimeout(timerId);
      }
    };
  }, [timerId]);

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

src > pages > Company.jsx

import React from 'react;

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

3. lodash 적용 및 useCallback을 써야하는 이유 알아보기

  • (1) lodash 적용해보기 새로운 프로젝트를 만들고, 다음 코드를 작성하겠습니다.

App.jsx

import "./App.css";
import { useState, useCallback } from "react";
import _ from "lodash";

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
      style={{
        paddingLeft: 20,
        paddingRight: 20,
      }}
    >
      <h1>디바운싱 예제</h1>
      <br />
      <input
        placeholder="입력값을 넣고 디바운싱 테스트를 해보세요."
        style={{ width: "300px" }}
        onChange={handleChange}
        type="text"
      />
      <p>Search Text: {searchText}</p>
      <p>Input Text: {inputText}</p>
    </div>
  );
}

export default App;

입력값을 넣고, 디바운싱 테스트를 할 수 있는 예제를 만들어봤어요. 정상적으로 동작하는 것을 볼 수 있습니다.

2초 후에 잘 반영되고 있네요!

만일, useCallback을 제거하면 어떻게 될까요? 정상적으로 동작하지 않네요. 이제, 왜 이렇게 동작하는지를 알아봐야 할 것 같아요. 동작 원리를 이해하기 위해서 lodash에서 제공하고 있는 debounce API를 우리가 직접 만들어봅시다.

App.jsx(수정)

import "./App.css";
import { useState, useCallback } from "react";
import _ from "lodash";

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

  // custom debounce
  const debounce = (callback, delay) => {
    let timerId = null;
    return (...args) => {
      if (timerId) clearTimeout(timerId);
      timerId = setTimeout(() => {
        callback(...args);
      }, delay);
    };
  };

  const handleSearchText = useCallback(
    debounce((text) => setSearchText(text), 2000),
    []
  );

  const handleChange = (e) => {
    setInputText(e.target.value);
    handleSearchText(e.target.value);
  };

  return (
    <div
      style={{
        paddingLeft: 20,
        paddingRight: 20,
      }}
    >
      <h1>디바운싱 예제</h1>
      <br />
      <input
        placeholder="입력값을 넣고 디바운싱 테스트를 해보세요."
        style={{ width: "300px" }}
        onChange={handleChange}
        type="text"
      />
      <p>Search Text: {searchText}</p>
      <p>Input Text: {inputText}</p>
    </div>
  );
}

export default App;

우리가 직접 만든 debounce 함수는 또 값이 아닌 함수를 리턴해주고 있어요.

  // custom debounce
  const debounce = (callback, delay) => {
    let timerId = null;
    return (...args) => {
      if (timerId) clearTimeout(timerId);
      timerId = setTimeout(() => {
        callback(...args);
      }, delay);
    };
  };

그냥 함수가 아닌, 내부 함수에서 외부 함수의 변수에 접근하는 클로저 함수를 리턴하고 있어요. 따라서 useCallback hook을 통해 마운트 시에 debounce를 기억해주게 되면, 이 클로저 함수는 외부 함수의 변수에 계속해서 참조를 갖고있기 때문에 타이머 아이디를 기억할 수 있게 되는거죠!

클로저의 내용을 잘 모르면 이해가 어려울 수도 있는 부분이에요. 자바스크립트 문법이 이렇게나 중요하답니다 😎

출처 : 스파르타코딩클럽 2023 강의자료

profile
프론트엔드개발자를 목표로 공부중입니다.

0개의 댓글