1. Throttling & Debouncing이란?
Throttling 이란?
짧은 시간 간격으로 연속해서 발생한 이벤트들을 일정시간 단위(delay)로 그룹화하여 처음 또는 마지막 이벤트 핸들러만 호출되도록 하는 것
주로 사용되는 예: 무한스크롤
Debouncing 이란?
짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간(delay)이 경과한 후에 한 번만 호출하도록 하는 것
주로 사용되는 예: 입력값 실시간 검색, 화면 resize 이벤트
메모리 누수(Memory Leak)란?
필요하지 않은 메모리를 계속 점유하고 있는 현상
setTimeout 이 메모리 누수(Memory Leak)를 유발하는 지?
상황에 따라 메모리 누수를 일으킬 수도 있고 아닐 수도 있습니다.
하나의 페이지에서 페이지 이동 없이 setTimeout을 동작시키고 타이머 함수가 종료될 때까지 기다린다면 메모리 누수는 없습니다.
리액트로 만든 SPA 웹사이트는 페이지 이동 시 컴포넌트가 언마운트 됩니다.
그런데 페이지 이동 전에 setTimeout 으로 인해 타이머가 동작중인 상태에서 clearTimeout을 안해주고 페이지 이동 시 컴포넌트는 언마운트 되었음에도 불구하고 타이머는 여전히 메모리를 차지하고 동작하고 있습니다. 이 경우 메모리 누수(Memory Leak)에 해당한다고 말할 수 있습니다.
2. Throttling & Debouncing 적용하기
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를 기억해주게 되면, 이 클로저 함수는 외부 함수의 변수에 계속해서 참조를 갖고있기 때문에 타이머 아이디를 기억할 수 있게 되는거죠!
지금까지 throttling & debouncing 에 대해 알아보았습니다. 필요한 기능에 적절히 이용해 보셨으면 좋겠네요!
'React' 카테고리의 다른 글
Next.js 와 React.js 의 차이 (1) | 2024.03.04 |
---|---|
React - React Query (0) | 2023.12.09 |
React - Custom Hooks (0) | 2023.12.09 |
React - Redux Thunk 자세히 다뤄보기 (1) | 2023.12.03 |
React - Thunk(feat. Redux)란? (1) | 2023.11.30 |