React|성능 최적화와 보안
useDebounce vs useThrottle: 검색어 자동완성과 스크롤 이벤트 최적화
26

크리스가 회사 내부망의 '사원 검색' 기능을 만들었다.
"타자를 칠 때마다 실시간으로 검색되면 엄청 편하겠지?"
그런데 배포 후 1시간 만에 백엔드 개발자가 달려왔다.
"크리스! DB CPU가 100%를 쳤어요! 누가 디도스(DDoS) 공격이라도 하는 거 아니에요?"
알고 보니 범인은 크리스의 코드였다. 사용자가 Chris라고 입력하는 동안 C, Ch, Chr, Chri, Chris... 총 5번의 API 요청이 0.1초 간격으로 날아간 것이다. 사용자가 100명만 되어도 서버는 비명을 지른다.
이처럼 빈번하게 발생하는 이벤트(키보드 입력, 스크롤, 리사이즈)를 그대로 처리하면 성능 재앙이 닥친다. 이를 제어하는 두 가지 핵심 기술, Debounce와 Throttle을 비교하고 직접 훅으로 구현해 본다.
1. 디바운스(Debounce): "마지막 한 번만 해!"
구현: useDebounce 훅
리액트에서는 값이 변할 때마다 타이머를 reset 하는 방식으로 구현한다.
// useDebounce.ts
import { useState, useEffect } from 'react';
// value: 감시할 값 (예: 검색어)
// delay: 기다릴 시간 (ms)
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// delay 시간이 지나면 값을 업데이트한다.
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 만약 delay가 지나기 전에 value가 또 바뀌면?
// 이전 타이머를 취소(clear)하고 새로 시작한다. (이것이 핵심!)
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}사용법
// SearchBar.tsx
function SearchBar() {
const [keyword, setKeyword] = useState('');
// 사용자가 타자를 멈추고 0.5초가 지나야 값이 바뀐다.
const debouncedKeyword = useDebounce(keyword, 500);
// useEffect는 debouncedKeyword가 바뀔 때만(즉, 500ms 후) 실행된다.
useEffect(() => {
if (debouncedKeyword) {
console.log('API 요청:', debouncedKeyword);
}
}, [debouncedKeyword]);
return <input onChange={(e) => setKeyword(e.target.value)} />;
}2. 쓰로틀(Throttle): "적당히 간격 좀 둬!"
구현: useThrottle 훅
쓰로틀은 useRef를 사용하여 "마지막으로 실행된 시간"을 기억해야 한다.
// useThrottle.ts
import { useRef, useCallback } from 'react';
export function useThrottle<T extends (...args: any[]) => void>(
callback: T,
delay: number
) {
const lastRun = useRef(Date.now());
return useCallback(
(...args: Parameters<T>) => {
const timeElapsed = Date.now() - lastRun.current;
// 지정한 시간(delay)이 지났을 때만 실행 허용
if (timeElapsed >= delay) {
callback(...args);
lastRun.current = Date.now(); // 시간 갱신
}
},
[callback, delay]
);
}사용법
// ScrollComponent.tsx
function ScrollComponent() {
const handleScroll = () => {
console.log('스크롤 위치 계산 중...');
};
// 스크롤을 아무리 빨리 내려도 0.2초에 한 번만 실행된다.
const throttledScroll = useThrottle(handleScroll, 200);
return (
<div onScroll={throttledScroll} style={{ height: '300px', overflow: 'scroll' }}>
{/* 내용 */}
</div>
);
}3. Debounce vs Throttle: 무엇을 써야 할까?
상황에 따라 명확하게 갈린다.
| 상황 | 추천 기술 | 이유 |
| 검색어 입력 | Debounce | 타자를 치는 중간(ing)에 검색하는 건 의미가 없다. 입력이 끝난 후에 요청해야 한다. |
| 무한 스크롤 | Throttle | 스크롤 하는 중간중간에 위치를 확인해야 바닥에 닿았는지 알 수 있다. |
| 윈도우 리사이즈 | Throttle | 창 크기를 조절하는 동안 레이아웃이 실시간으로 변해야 자연스럽다. |
| 버튼 중복 클릭 방지 | Debounce | 마지막 클릭만 인정하거나(Leading Edge), Throttle로 클릭 간격을 벌린다. |
핵심 정리
이벤트 최적화로 서버와 브라우저의 부하를 줄였다. 그런데 컴포넌트 내부에서 무거운 계산을 매번 다시 하거나, 함수를 계속 새로 만드는 것도 성능 낭비다.
이 낭비를 막아주는 리액트의 메모이제이션 3형제(useMemo, useCallback, React.memo)를 제대로 쓸 시간이다.
"useMemo, useCallback, React.memo: 메모이제이션은 공짜가 아니다" 편에서 계속된다.
🔗 참고 링크
성능 최적화와 보안의 다른 글
이 파트의 다른 글이 없습니다.