# Throttling & Debouncing이란?
짧은 시간 간격으로 연속해서 이벤트가 발생했을 때 과도한 이벤트 핸들러 호출을 방지하는 기법인 쓰로틀링과 디바운싱에 대해 알아보자. Timer Web API 중 setTimeout 메소드를 사용하여 쓰로틀링과 디바운싱을 각각 구현해보고 원리를 이해하고 적용하는 것이 목표이다.
# Throttling 이란?
- 짧은 시간 간격으로 연속해서 발생한 이벤트들을 일정시간 단위(delay)로 그룹화하여 처음 또는 마지막 이벤트 핸들러만 호출되도록 하는 것
주로 사용되는 예: 무한스크롤
# Debouncing 이란?
- 짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간(delay)이 경과한 후에 한 번만 호출하도록 하는 것
주로 사용되는 예: 입력값 실시간 검색, 화면 resize 이벤트
# 메모리 누수(Memory Leak)란?
필요하지 않은 메모리를 계속 점유하고 있는 현상
# setTimeout 이 메모리 누수(Memory Leak)를 유발하는가?
상황에 따라 메모리 누수를 일으킬 수도 있고 아닐 수도 있다.
하나의 페이지에서 페이지 이동 없이 setTimeout을 동작시키고 타이머 함수가 종료될 때까지 기다린다면 메모리 누수는 없다.
리액트로 만든 SPA 웹사이트는 페이지 이동 시 컴포넌트가 언마운트 된다.
그런데 페이지 이동 전에 setTimeout 으로 인해 타이머가 동작중인 상태에서 clearTimeout을 안해주고 페이지 이동 시 컴포넌트는 언마운트 되었음에도 불구하고 타이머는 여전히 메모리를 차지하고 동작하고 있다.
이 경우 메모리 누수(Memory Leak)에 해당한다고 말할 수 있다.
# 실습
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>
);
}
# 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;
입력값을 넣고, 디바운싱 테스트를 할 수 있는 예제를 만들어봤다. 정상적으로 동작하는 것을 볼 수 있다.
만일, useCallback을 제거하면 어떻게 될까? 정상적으로 동작하지 않는다. 이제, 왜 이렇게 동작하는지를 알아봐야 할 것 같다.
동작 원리를 이해하기 위해서 lodash에서 제공하고 있는 debounce API를 우리가 직접 만들어보자
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를 기억해주게 되면, 이 클로저 함수는 외부 함수의 변수에 계속해서 참조를 갖고있기 때문에 타이머 아이디를 기억할 수 있게 되는 것이다.
클로저의 내용을 잘 모르면 이해가 어려울 수도 있는 부분이다.
'코딩 > React' 카테고리의 다른 글
axios request, response (0) | 2023.05.08 |
---|---|
인증/인가(쿠키, 세션, 토큰, JWT) (0) | 2023.05.01 |
React Query (0) | 2023.04.29 |
Custom Hooks (0) | 2023.04.29 |
Thunk 2 (0) | 2023.04.29 |
댓글