Throttle과 Debounce
Throttle과 Debounce는 자주 사용되는 이벤트나 함수의 실행 빈도를 줄여서, 성능 상의 유리함을 가져오는 개념이다. 자주 사용되는 간단한 예로는 자동 완성이 있다.
1. Throttle
Throttle은 입력 주기를 방해하지 않고, 일정 시간 동안의 입력을 모아서 한번씩 출력을 제한한다.
여러 번 발생하는 이벤트를 일정 시간 동안 한 번만 실행되도록 만드는 개념이다. throttole을 500ms로 제한하면 500ms에 한 번 씩 실행이 되는 것이다.
2. Debounce
Debounce는 입력 주기가 끝나면, 출력한다.
여러 번 발생하는 이벤트에서, 가장 마지막 이벤트만을 실행되도록 만드는 개념이다. 500ms동안 동일한 이벤트가 발생한다면, 입력이 끝날 때, 가장 마지막 이벤트만을 실행한다.
이 둘의 차이점
이 둘의 차이점은 이벤트를 언제 발생시킬지의 시점 차이이다.
Debounce는 입력이 끝날 때까지 무한정으로 기다리지만, Throttle은 입력이 시작되면, 일정 주기로 계속 실행한다.
대표적인 예시로, 일정 주기로 자동으로 완성되는 리스트를 보여주는 것에는 사용자 측면에서는 throttle이 유리하고, 성능상에서는 debounce가 훨씬 유리할 수 있다.
구현 방법
1. 라이브러리
2. 직접 구현하기
1. debounce
- debounce의 핵심 : 이벤트 발생 → waiting time 동안 동일한 이벤트가 또 발생하면 이전의 타이머를 지우고 새로운 타이머 생성, waiting time 동안 이벤트가 발생하지 않으면 이전의 타이머가 지워지지 않고 콜백 함수가 실행
-
useDebounce 훅
export default function useDebounce(value: string, delay = 300) { const [debouncedValue, setDebouncedValue] = useState(value) useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value) }, delay) return () => { clearTimeout(handler) } }, [value, delay]) return debouncedValue }❓ 왜 useEffect를 사용했을까?
useEffect의 return의 callback함수가 실행되는 순간은 2가지가 있다.
-
useEffect를 실행한 컴포넌트가 언마운트 되었을 때
-
의존성 배열의 값에 변경이 생겼을 때
❓ 그렇다면 useEffect의 callback함수가 실행되는 순간은?
우선, 아래 3가지 경우 모두 컴포넌트가 마운트 되었을 때 실행된다.
- 의존성 배열이 존재할 때 : 의존성 배열의 값이 변경되었을 때(빈 배열이라면 컴포넌트가 마운트되었을 때만)
- 두 번째 인자 의존성 배열이 아예 없을 때 : 컴포넌트가 리렌더링이 일어날 때마다
즉, 의존성 배열의 첫 번째 요소인 value값이 변경되면 setTimeout을 clearTimeout로 제거한다. 그리고 useDebounce훅을 호출한 컴포넌트 또한 리렌더링이 일어났으므로(state값이 변경되며) 새로운 useEffect가 실행이 되며 setTimeout또한 새롭게 생성된다.
useEffect의 return문을 통해 value값이 변경되면, setTimeout이 clearTimeout에 의해 제거된다는 것을 알게 되었다. 그럼 delay가 지난다면, setTimeout의 콜백함수(
setDebouncedValue(value))가 실행된다. 그럼,debouncedValue값이 변경되어 값을 return하는 것이다.-
사용 예시
const debouncedKeyword = useDebounce(search) useEffect(() => { const searchParamsManager = new SearchParamsManager(searchParams) searchParamsManager.addSearchParam('keyword', debouncedKeyword, true) router.push(`?${searchParamsManager.getSearchParams()}`) }, [debouncedKeyword, router, searchParams])debouncedKeyword값이 바뀔 때마다 데이터를 fetch한다
-
-
Vanilla JS
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <input type="text" id="input"> <script> const inputEL = document.getElementById("input"); let debounceTimer; const debounce = (callback, time) => { clearTimeout(debounceTimer) debounceTimer = setTimeout(callback, time) } const inputHandler = (e) => console.log(e.target.value) inputEL.addEventListener('input', (e) => debounce(() => inputHandler(e), 500)) </script> </body> </html>- input에 값이 입력될 때마다 clearTimeout(debounceTimer)를 실행함으로써 debounceTimer를 초기화한다.
- 그리고, debounceTimer는 전역으로 관리하여 값을 전역에서 공유한다.
위 코드의 문제점은 debounceTimer를 전역으로 관리한다는 점이다. 이는 사이드 이펙트를 일으킬 수 있다.
-
Vanilla JS로 구현한 코드를 Closure로 구현하기
💡 Closure
중첩함수에서 발생하는 현상으로, 내부 함수가 외부 함수의 종료 이후에도 외부 함수의 변수를 기억하고 접근하여 사용할 수 있는 현상
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <input type="text" id="input"> <script> const inputEL = document.getElementById("input"); const debounceClosure = () => { let debounceTimer; return (callback, time) => { clearTimeout(debounceTimer) debounceTimer = setTimeout(callback, time) } } const debounce = debounceClosure(); const inputHandler = (e) => console.log(e.target.value) inputEL.addEventListener('input', (e) => debounce(() => inputHandler(e), 500)) </script> </body> </html>
3. throttle
- throttle의 핵심 : 이벤트 발생 → 일정 시간 기다리기 → 기다리는 중에는 이벤트가 발생하여도 무시됨 → 기다림이 끝나면 콜백 함수 실행하고 다시 이벤트를 받음
-
Vanilla JS
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <input type="text" id="input"> <script> const inputEL = document.getElementById("input") let timer; let isWaiting = false; const throttle = (value,delay) => { if(isWaiting) return; isWaiting=true; timer = setTimeout(() => { isWaiting = false; console.log(value) },delay) } inputEL.addEventListener('input', (e) => throttle(e.target.value,1000)) </script> </body> </html> -
Vanilla JS로 callback 함수를 throttle에 넘기기
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <input type="text" id="input"> <script> const inputEL = document.getElementById("input") let timer; let isWaiting = false; const throttle = (callback, delay) => { if (isWaiting) return; isWaiting = true; timer = setTimeout(() => { isWaiting = false; callback() }, delay) } const inputHandler = (e) => throttle(() => console.log(e.target.value), 300) inputEL.addEventListener('input', (e) => inputHandler(e)) </script> </body> </html> -
scroll에 throttle 설정하기
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <style> html { height: 1000vh } </style> <body> <input type="text" id="input"> <script> const inputEL = document.getElementById("input") let timer; let isWaiting = false; const throttle = (callback, delay) => { if (isWaiting) return; isWaiting = true; timer = setTimeout(() => { isWaiting = false; callback() }, delay) } const scrollHandler = (e) => console.log('scroll') window.addEventListener('scroll', () => { throttle(scrollHandler, 500) }) </script> </body> </html> -
closure 사용
- closure의 효과 : 외부로부터 변수 보호, 재사용성이 높아진다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
html {
height: 1000vh
}
</style>
<body>
<input type="text" id="input">
<script>
const inputEL = document.getElementById("input")
const throttleHandler = () => {
let timer;
let isWaiting = false;
return (callback, delay) => {
if (isWaiting) return;
isWaiting = true;
timer = setTimeout(() => {
isWaiting = false;
callback()
}, delay)
}
}
const throttle = throttleHandler();
const scrollHandler = (e) => console.log('scroll')
window.addEventListener('scroll', () => {
throttle(scrollHandler, 500)
})
</script>
</body>
</html>