f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
202 lines
6.3 KiB
Markdown
202 lines
6.3 KiB
Markdown
---
|
|
id: wiki-2026-0508-throttling-debouncing
|
|
title: Throttling Debouncing
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [Throttle, Debounce, Rate Limiting Frontend]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.9
|
|
verification_status: applied
|
|
tags: [frontend, performance, event-handling, javascript]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: typescript
|
|
framework: vanilla
|
|
---
|
|
|
|
# Throttling Debouncing
|
|
|
|
## 매 한 줄
|
|
> **"매 throttle 의 rate-limit, debounce 의 quiet-period wait"**. 두 기법 모두 high-frequency event (scroll/resize/input/keypress) 의 handler call 빈도를 줄이지만, semantics 가 정반대 — throttle 의 every N ms 1 call, debounce 의 last call after N ms idle.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 Throttle
|
|
- **Semantic**: max 1 call per interval.
|
|
- **Use**: scroll, resize, mousemove — continuous events 의 sampling.
|
|
- **Variants**: leading (immediate first), trailing (last in window), both.
|
|
- **Visualizable**: `XX_XX_XX_XX_X` (regular ticks).
|
|
|
|
### 매 Debounce
|
|
- **Semantic**: 1 call after last invocation + delay (no calls during burst).
|
|
- **Use**: search input, form validate, autocomplete, window-resize-finish.
|
|
- **Variants**: leading (immediate, then ignore), trailing (default), maxWait (force flush).
|
|
- **Visualizable**: `XXX____X` (settles after burst).
|
|
|
|
### 매 응용
|
|
1. Search-as-you-type: debounce 300ms.
|
|
2. Infinite scroll trigger: throttle 100ms.
|
|
3. Auto-save: debounce 1s with maxWait 10s.
|
|
|
|
## 💻 패턴
|
|
|
|
### Basic debounce (trailing)
|
|
```typescript
|
|
function debounce<T extends (...args: any[]) => void>(
|
|
fn: T,
|
|
ms: number
|
|
): (...args: Parameters<T>) => void {
|
|
let t: ReturnType<typeof setTimeout> | null = null;
|
|
return (...args) => {
|
|
if (t) clearTimeout(t);
|
|
t = setTimeout(() => fn(...args), ms);
|
|
};
|
|
}
|
|
|
|
// Usage
|
|
const onSearch = debounce((q: string) => fetchResults(q), 300);
|
|
input.addEventListener('input', e => onSearch((e.target as HTMLInputElement).value));
|
|
```
|
|
|
|
### Basic throttle (leading + trailing)
|
|
```typescript
|
|
function throttle<T extends (...args: any[]) => void>(
|
|
fn: T,
|
|
ms: number
|
|
): (...args: Parameters<T>) => void {
|
|
let last = 0;
|
|
let t: ReturnType<typeof setTimeout> | null = null;
|
|
let lastArgs: Parameters<T> | null = null;
|
|
return (...args) => {
|
|
const now = Date.now();
|
|
const remaining = ms - (now - last);
|
|
lastArgs = args;
|
|
if (remaining <= 0) {
|
|
if (t) { clearTimeout(t); t = null; }
|
|
last = now;
|
|
fn(...args);
|
|
} else if (!t) {
|
|
t = setTimeout(() => {
|
|
last = Date.now();
|
|
t = null;
|
|
if (lastArgs) fn(...lastArgs);
|
|
}, remaining);
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
### Debounce with cancel + flush
|
|
```typescript
|
|
function debounceAdv<T extends (...args: any[]) => void>(fn: T, ms: number) {
|
|
let t: ReturnType<typeof setTimeout> | null = null;
|
|
let lastArgs: Parameters<T> | null = null;
|
|
const debounced = (...args: Parameters<T>) => {
|
|
lastArgs = args;
|
|
if (t) clearTimeout(t);
|
|
t = setTimeout(() => { fn(...lastArgs!); t = null; }, ms);
|
|
};
|
|
debounced.cancel = () => { if (t) { clearTimeout(t); t = null; } };
|
|
debounced.flush = () => {
|
|
if (t && lastArgs) { clearTimeout(t); fn(...lastArgs); t = null; }
|
|
};
|
|
return debounced;
|
|
}
|
|
```
|
|
|
|
### React hook: useDebouncedValue
|
|
```typescript
|
|
import { useEffect, useState } from 'react';
|
|
|
|
export function useDebouncedValue<T>(value: T, ms = 300): T {
|
|
const [v, setV] = useState(value);
|
|
useEffect(() => {
|
|
const t = setTimeout(() => setV(value), ms);
|
|
return () => clearTimeout(t);
|
|
}, [value, ms]);
|
|
return v;
|
|
}
|
|
|
|
// Usage
|
|
function Search() {
|
|
const [q, setQ] = useState('');
|
|
const debounced = useDebouncedValue(q, 300);
|
|
useEffect(() => { if (debounced) fetchResults(debounced); }, [debounced]);
|
|
return <input value={q} onChange={e => setQ(e.target.value)} />;
|
|
}
|
|
```
|
|
|
|
### rAF throttle (60fps cap)
|
|
```typescript
|
|
function rafThrottle<T extends (...args: any[]) => void>(fn: T) {
|
|
let scheduled = false;
|
|
let lastArgs: Parameters<T>;
|
|
return (...args: Parameters<T>) => {
|
|
lastArgs = args;
|
|
if (scheduled) return;
|
|
scheduled = true;
|
|
requestAnimationFrame(() => {
|
|
fn(...lastArgs);
|
|
scheduled = false;
|
|
});
|
|
};
|
|
}
|
|
|
|
// Best for scroll/resize on 60-120hz displays
|
|
window.addEventListener('scroll', rafThrottle(() => updatePosition()));
|
|
```
|
|
|
|
### lodash equivalents
|
|
```typescript
|
|
import { debounce, throttle } from 'lodash-es';
|
|
|
|
const onResize = throttle(handleResize, 200, { leading: true, trailing: true });
|
|
const onSearch = debounce(handleSearch, 300, { maxWait: 1000 });
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => () => { onResize.cancel(); onSearch.cancel(); }, []);
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| Search input | debounce 200-400ms |
|
|
| Scroll handler (positional) | rAF throttle |
|
|
| Resize end detection | debounce 150ms |
|
|
| Auto-save | debounce 1s + maxWait 10s |
|
|
| Rate-limited API call | throttle (leading) |
|
|
| Button double-click guard | debounce leading-only |
|
|
|
|
**기본값**: lodash `debounce`/`throttle` for non-trivial cases; rAF throttle for animation-tied work; useDebouncedValue hook for React inputs.
|
|
|
|
## 🔗 Graph
|
|
- 부모: [[Event Handling]] · [[Frontend Performance]]
|
|
- 변형: [[Rate Limiting]] · [[Backpressure]]
|
|
- 응용: [[Infinite Scroll]]
|
|
- Adjacent: [[requestAnimationFrame]] · [[Web Worker (웹 워커)|Web Workers]] · [[Reactive Streams]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: input handler optimization, scroll/resize perf, rate-limiting UI events.
|
|
**언제 X**: server-side rate limiting (use token bucket / Redis), animation timing (use rAF directly).
|
|
|
|
## ❌ 안티패턴
|
|
- **Debounce on click button**: user feels lag; use throttle leading-only.
|
|
- **No cleanup on unmount**: timer fires on unmounted component → setState warning / leak.
|
|
- **Throttle 16ms manually**: use rAF throttle — sync to display refresh.
|
|
- **Debounce without maxWait on auto-save**: user types continuously → never saves.
|
|
- **Inline `() => debounce(fn, 300)` in render**: new function each render, debouncing breaks.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (lodash docs, MDN event handling, React docs useDeferredValue/useTransition equivalence).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — full canonical (debounce/throttle/rAF/React hook + lodash) |
|