React : UX 향상 기법 - Throttling, Debouncing
UX(사용자 경험)을 향상시키는
Throttling & Debouncing
Throttling
짧은 시간 간격으로 연속해서 발생한 이벤트들을 일정시간 단위(delay)로 그룹화하여 처음 또는 마지막 이벤트 핸들러만 호출, 주로 무한스크롤에서 사용
이벤트의 반복 발생 시 처리 방법
타입 | 설명 | 예시 |
Leading Edge | 이벤트가 처음 발생할 때 핸들러가 실행됨. 이후 주어진 시간 (delay) 동안은 이벤트가 무시됨. |
사용자가 스크롤을 시작할 때 처음에만 API 호출이 이루어지고, 일정 시간 동안 추가 호출 무시 |
Trailing Edge | 이벤트가 반복적으로 실행될 때, 주어진 시간(delay)이 지나면 마지막 이벤트를 처리 |
Leading Edge와 비슷하지만 주어진 시간의 마지막 이벤트에 API 호출이 이루어짐 |
Leading & Trailing Edge | 주어진 시간에 대해 이벤트가 처음 발생할 때 핸들러가 실행되고, 주어진 시간이 지나면 마지막 이벤트도 처리 |
사용자가 버튼을 여러 번 클릭할 때 처음 클릭 시 바로 API 호출이 이루어지고, 주어진 시간의 마지막 이벤트에도 API 호출이 이루어짐 |
Debouncing
짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간(delay)이 경과한 후에 한 번만 호출, 주로 입력값 실시간 검색, 화면 resize 이벤트 등에서 사용
서버에 대한 불필요한 API 호출을 줄이고 UI가 급격한 수의 이벤트를 처리하는 것을 방지하기 위해 사용
메모리 누수(Memory Leak)
필요하지 않은 메모리를 계속 점유하고 있는 현상
setTimeout 이 메모리 누수(Memory Leak) 유발 ?
하나의 페이지에서 페이지 이동 없이 setTimeout을 동작시키고
타이머 함수가 종료될 때까지 기다린다면 메모리 누수는 없음
리액트로 만든 SPA 웹사이트는 페이지 이동 시 컴포넌트가 언마운트
페이지 이동 전에 setTimeout 으로 인해 타이머가 동작중인 상태에서 clearTimeout을 안해주고 페이지를 이동 => 컴포넌트는 언마운트 되었음에도 불구하고 타이머는 여전히 메모리를 차지하고 동작
=> 이 경우 메모리 누수(Memory Leak)에 해당
setTimeout을 멈추지 않고 계속 실행되는 상태에서 다른 페이지로 이동하면 메모리누수
예제 (직접 코드로)
셋업
import React from 'react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Home from './pages/Home'
import Company from './pages/Company'
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} /> // 실제 throttling, debouncing 사용할 페이지
<Route path='/company' element={<Company />} />
</Routes>
</BrowserRouter>
)
}
export default App
Home.jsx
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
const Home = () => {
const navigate = useNavigate();
let timerId = null;
// setTimeout의 반환 값
// 함수 밖에서 관리해야 쓰로틀링이 처음 실행됐을 때,
// setTimeout이 동작 중인지 아닌지 판단 가능
// 쓰로틀링 ( Leading Edge )
const throttling = (delay) => {
if (timerId) {
// 이미 setTimeout이 진행 중인 상태! => delay가 경과하지 않은 상태
return;
}
console.log(`API 요청 실행! ${delay}ms 동안 추가요청 안받음!`);
timerId = setTimeout(() => { // setTimeout에 id를 부여
console.log(`${delay}ms 지남. 추가요청 받음!`);
timerId = null;
}, delay);
};
// 디바운싱
const debounce = (delay) => {
if (timerId) clearTimeout(timerId);
// timerId가 존재한다면(setTimeout이 실행 중이라면) 제거(setTimeout 종료)
timerId = setTimeout(() => {
console.log(`마지막 요청으로부터 ${delay}ms 지났으므로 API 요청 실행!`);
timerId = null;
}, delay);
};
// 페이지가 unmount 될 때, 그 페이지의 이벤트는 없어져야함 => 메모리 누수 막음
useEffect(() => {
return () => { // useEffect에서 return문 : 클린업 함수 => unmount시 발생
// 페이지를 벗어날 때, 즉 unmount 될 때
if (timerId) clearTimeout(timerId); // timerId가 존재하면 timerId제거
}
}, []);
return (
<div>
<h2>Button 이벤트 예제</h2>
<button onClick={() => throttling(2000)}>쓰로틀링 버튼</button>
<button onClick={() => debounce(2000)}>디바운싱 버튼</button>
<div>
<button onClick={() => navigate('/company')}>페이지 이동</button>
</div>
</div>
)
}
export default Home
결과
Throttling ( Leading Edge )
쓰로틀링 버튼을 계속 클릭해도 콘솔은 2초 간격으로 출력, 2초가 지나야만 콘솔에 출력
=> 클릭이벤트가 실행되고 delay시간(2초)동안은 동일한 이벤트 발생하지 않음
Debouncing
디바운싱 버튼을 마지막으로 클릭한 시점에서 delay시간(2초) 뒤에 콘솔에 출력
useEffect를 이용한 메모리 누수 방지 결과
useEffect 처리 전
디바운싱 버튼을 클릭하고 페이지를 이동했을 때, 이전 페이지의 이벤트가 계속 남아있음
=> 메모리 누수
useEffect 처리 후
디바운싱 버튼을 클릭 후 페이지를 이동하고 delay시간이 지나도 콘솔에 이벤트가 출력되지 않음
=> unmount 되었을 때, 이벤트 종료 => 메모리 누수 X