학습 목표
- thorttling 과 debouncing 의 개념에 대해 이해합니다.
- lodash 를 이용해 thorttling 과 debouncing 을 적용할 수 있습니다.
- 리액트에서 thorttling 과 debouncing 시 useCallback 을 적용하는 이유를 설명할 수 있습니다.
Thorttling & Debouncing 이란?
짧은 시간 간격으로 연속해서 이벤트가 발생했을 때 과도한 이벤트 핸들러 호출을 방지하는 기법인 쓰로틀링과 디바운싱에 대해 학습했습니다. Timer Web API 중 setTimeout 메서드를 사용해 쓰로틀링과 디바운싱을 각각 구현해보고, 원리를 이해하고 적용해보았습니다.
Thorttling 이란?
Type 1: Leading Edge
Type 2 : Trailing Edge
Type 3 : Leading & Trailing Edge
짧은 시간 간격으로 연속해서 발생한 이벤트들을 일정시간 단위(delay) 로 그룹화해 처음 또는 마지막 이벤트 핸들러만 호출되도록 하는 것입니다.
Debouncing 이란?
짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간(delay)이 경과한 후에 한 번만 호출하도록 하는 것입니다.
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 써야하는 이유
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 를 직접 만들어 보았습니다.
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 함수는 또 값이 아닌 함수를 return 해주고 있습니다.
// custom debounce
const debounce = (callback, delay) => {
let timerId = null;
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
callback(...args);
}, delay);
};
};
그냥 함수가 아닌, 내부 함수에서 외부 함수의 변수에 접근하는 클로저 함수를 리턴하고 있습니다. 따라서 useCallback hook 을 통해 마운트 시에 debounce 를 기억하면, 이 클로저 함수는 외부 함수의 변수에 계속해서 참조를 갖고 있기 때문에 타이머 아이디를 기어할 수 있게 되는 거죠.
'항해 16기 > Today I Learned' 카테고리의 다른 글
Lexical Editor 사용법 및 Dribble (0) | 2023.09.26 |
---|---|
[항해 37일차] TIL_React 심화주차: 인증/인가(쿠키, 세션, 토큰, JWT) (0) | 2023.09.13 |
[항해 34일차] TIL_React 심화주차: React Query (0) | 2023.09.12 |
[항해 33일차] TIL_React 심화주차: Custom Hooks (0) | 2023.09.12 |
[항해 32일차] TIL_React 심화주차: Thunk 2 (0) | 2023.09.11 |