UX 향상 기법 - Throttling & Debouncing, lodash

짧은 시간 연속으로 이벤트가 발생했을 때 어떻게 처리할 것인가!

 

Throttling

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

 

Leading Edge

- 이벤트가 처음 발생할 때 핸들러가 실행

- 이후 주어진 시간 동안은 이벤트가 무시됨

- 사용자가 스크롤을 시작하는 첫 시점에만 API 호출이 이루어지고, 일정 시간동안 추가 호출이 무시됨

 

Trailing Edge

- 이벤트가 마지막으로 발생한 후 주어진 시간이 지나면 핸들러가 실행

- 사용자 입력 필드에 타이핑을 멈춘 후 일정 시간이 지나야 서버에 검색 요청이 전송됨

 

Leading & Trailing Edge

- 이벤트가 처음 발생할 때와 마지막으로 발생한 후 주어진 시간이 지나면 핸들러가 실행

- 사용자가 버튼을 여러번 클릭할 때 처음 클릭시 바로 API 호출이 이루어지고, 마지막 클릭 후 일정 시간이 지나 다시 호출됨

 

 

Debouncing

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

 

 

 

Throttling과 Debouncing의 메모리 누수

두 기법은 setTimeout을 활용하는데, 타이머 함수가 종료되는 것이 보장된다면 메모리 누수가 없음

하지만 타이머 함수가 동작중인 상태에서 컴포넌트가 언마운트될 때 clearTimeout을 안해주고 페이지를 이동한다면 컴포넌트는 언마운트 되었음에도 불구하고 타이머는 여전히 동작하고 있음 -> 이것이 메모리 누수에 해당!

 

useEffect를 활용해 언마운트될 때 clearTimeout을 활용해 타이머 함수를 종료시킨다!

 

// 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>
  );
}

 

 

Lodash

자바스크립트 유틸리티 라이브러리

배열, 객체, 문자열 등의 데이터 조작을 쉽게 할 수 있는 다양한 함수들을 제공

성능 최적화와 가독성을 높이는 데 유용

 

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

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

  const handleSearchText = _.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 Home;

 

Debouncing + useCallback

const handleSearchText = _.debounce((text) => setSearchText(text), 2000);

Debouncing은 동일한 이벤트가 여러번 발생했을 때 하나로 그룹화해 마지막 이벤트 발생 후 delay가 지나면 마지막 한번만 호출하는 것

그런데 예제와같이 input이 일어나면서 state가 변하면 예상과는 다르게 동작한다

그 이유는 state 변화에 의해 리렌더링이 일어나면서 매번 이벤트 함수가 새로운 함수가 되기 때문!

따라서 useCallback을 사용해 마운트 됐을 때 한번만 함수가 생성되게 처리해줘야 함

 

 

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

'React' 카테고리의 다른 글

[TanStack Query] Query Cancellation, Optimistic Updates  (2) 2024.09.07
[TanStack Query] useQuery() 옵션 - enabled, select  (0) 2024.09.06
[React] Zustand  (0) 2024.09.06
Tanstack Query 캐시데이터의 생명주기  (0) 2024.09.05
TanStack Query  (0) 2024.09.05