React
throttling & debouncing 처리하기
차돌박이츄베릅
2023. 7. 6. 13:26
Throttling
짧은 시간 간격으로 연속해서 발생한 이벤트들을
일정시간 단위(delay)로 그룹화하여
처음 또는 마지막 이벤트 핸들러만 호출되도록 하는 것
=> 주로 사용되는 예: 무한스크롤
Debouncing
짧은 시간 간격으로 연속해서 이벤트가 발생하면
이벤트 핸들러를 호출하지 않다가
마지막 이벤트로부터 일정 시간(delay)이 경과한 후에 한 번만 호출하도록 하는 것
=> 주로 사용되는 예: 입력값 실시간 검색, 화면 resize 이벤트
메모리 누수(Memory Leak)
필요하지 않은 메모리를 계속 점유하고 있는 현상.
리액트로 만든 SPA 웹사이트는 페이지 이동 전에 setTimeout 으로 인해 타이머가 동작중인 상태에서 clearTimeout을 안해주고 페이지 이동 시 컴포넌트는 언마운트 되었음에도 불구하고 타이머는 여전히 메모리를 차지하고 동작하여 메모리 누수(Memory Leak)에 해당함
throttling & debouncing 예제
(src/pages/Home.jsx)
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
const Home = () => {
// const [state, setState] = useState(true);
const navigate = useNavigate();
// 타이머 아이디
let timerId = null;
// Leading Edge Throttling
const throttle = (delay) => {
if (timerId) {
// timerId가 있으면 바로 함수 종료
return;
}
// setState(!state); // 리렌더링 -> 다시 Home함수가 호출되면서 timerId가 무조건 null이 된다는 얘기 -> 정상동작하지 않음
console.log(`API요청 실행! ${delay}ms 동안 추가 요청은 안받습니다.`);
timerId = setTimeout(() => {
console.log(`${delay}ms 지남 추가요청 받습니다!`);
timerId = null;
}, delay);
};
// Trailing Edge Debouncing
// 반복적인 이벤트 이후, delay가 지나면 function
const debounce = (delay) => {
// 누를때마다 초기화되고 다시 타이머연결되서 마지막으로 누른 뒤 2초를 다시 기다려야되는거
if (timerId) {
// 할당되어 있는 timerId에 해당하는 타이머 제거
clearTimeout(timerId);
}
timerId = setTimeout(() => {
// timerId에 새로운 타이머 할당
// delay시간 후에 여기 로직이 실행됨
console.log(`마지막 요청으로부터 ${delay}ms 지났으므로 API 요청 실행!`);
timerId = null;
}, delay);
};
useEffect(() => {
return () => {
// 페이지 이동 시 실행(언마운트 시)
// 기존에 존재하고 있는 타이머를 없애달라. 메모리에서 없애달라. 메모리 누수 방지
// 다른 페이지로 이동 시 이전에 요청한 타이머가 남아서 메모리 누수를 유발시킬 수 있기 때문에 필요함
if (timerId) {
clearTimeout(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>
);
};
export default Home;
lodash
디바운스 예제
설치
yarn add lodash
(App.jsx)
우리가 직접 만든 커스텀 debounce 함수는 또 값이 아닌 함수를 리턴해주고 있어요.
그냥 함수가 아닌, 내부 함수에서 외부 함수의 변수에 접근하는 클로저 함수를 리턴하고 있어요.
따라서 useCallback hook을 통해 마운트 시에 debounce를 기억해주게 되면,
이 클로저 함수는 외부 함수의 변수에 계속해서 참조를 갖고있기 때문에 타이머 아이디를 기억할 수 있게 되는거죠!
import logo from './logo.svg';
import './App.css';
import { useState } from 'react';
import _ from 'lodash';
function App() {
const [searchText, setSearchText] = useState('');
const [inputText, setInputText] = useState('');
// 1. 커스텀 디바운스 함수 만들어보기
const debounce = (callback, delay) => {
let timerId = null;
// 메모이제이션을 해놨기 때문에 새로 함수가 리턴되는 것이 아니라 이걸 기억하고 있는거.
// 그래서 여전히 똑같은 값을 참조하고 있는거
// 클로저?
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
callback(...args);
}, [delay]);
};
};
// 인자 1. 함수 2. 초ms
// useCallback을 써서 debounce함수를 메모이제이션을 해야 정상 동작이 됨
const handleSearchText = useCallback(
// _.debounce((text) => {
debounce((text) => { // 2. 커스텀 디바운스 함수 사용하기
setSearchText(text);
}, 2000),
[]
);
const handleChange = (e) => {
setInputText(e.target.value);
handleSearchText(e.target.value);
};
return (
<div
style={{
paddingLeft: '20px',
paddingRight: '20px',
}}
>
<h1>디바운싱 예제</h1>
<input placeholder="입력값을 넣고 디바운싱 테스트를 해보세요" style={{ width: '300px' }} type="text" onChange={handleChange} />
<p>디바운싱Search Text : {searchText}</p>
<p>Input Text : {inputText}</p>
</div>
);
}
export default App;