React|성능 최적화와 보안

useDebounce vs useThrottle: 검색어 자동완성과 스크롤 이벤트 최적화

26
useDebounce vs useThrottle: 검색어 자동완성과 스크롤 이벤트 최적화

크리스가 회사 내부망의 '사원 검색' 기능을 만들었다.

"타자를 칠 때마다 실시간으로 검색되면 엄청 편하겠지?"

그런데 배포 후 1시간 만에 백엔드 개발자가 달려왔다.

"크리스! DB CPU가 100%를 쳤어요! 누가 디도스(DDoS) 공격이라도 하는 거 아니에요?"

알고 보니 범인은 크리스의 코드였다. 사용자가 Chris라고 입력하는 동안 C, Ch, Chr, Chri, Chris... 총 5번의 API 요청이 0.1초 간격으로 날아간 것이다. 사용자가 100명만 되어도 서버는 비명을 지른다.

이처럼 빈번하게 발생하는 이벤트(키보드 입력, 스크롤, 리사이즈)를 그대로 처리하면 성능 재앙이 닥친다. 이를 제어하는 두 가지 핵심 기술, DebounceThrottle을 비교하고 직접 훅으로 구현해 본다.

1. 디바운스(Debounce): "마지막 한 번만 해!"

  • 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): "적당히 간격 좀 둬!"

  • Throttle은 이벤트를 일정한 주기(예: 1초에 한 번)마다 발생하도록 제한하는 기술이다.
  • 비유: 물방울이 떨어지는 수도꼭지와 같다. 수도꼭지를 아무리 세게 틀어도, 물방울은 일정한 간격으로 뚝, 뚝 떨어진다.
  • 사용 사례: 스크롤 이벤트(무한 스크롤 감지), 윈도우 리사이징(UI 재배치), 드래그 앤 드롭.
  • 구현: 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로 클릭 간격을 벌린다.

    핵심 정리

  • Debounce: "기다렸다가 마지막 한 번만". 연이어 발생하는 이벤트를 그룹화해서 처리 비용을 아낀다. (API 비용 절약)
  • Throttle: "일정한 간격으로 꾸준히". 이벤트 발생 빈도를 낮추되, 중간 과정을 주기적으로 실행해야 할 때 쓴다. (브라우저 연산 비용 절약)
  • 라이브러리: 직접 구현해도 되지만, 실무에서는 Lodashdebouncethrottle 함수를 많이 사용한다. 하지만 리액트 훅 버전(use-debounce 등)을 쓰는 것이 더 모던한 방식이다.
  • 이벤트 최적화로 서버와 브라우저의 부하를 줄였다. 그런데 컴포넌트 내부에서 무거운 계산을 매번 다시 하거나, 함수를 계속 새로 만드는 것도 성능 낭비다.

    이 낭비를 막아주는 리액트의 메모이제이션 3형제(useMemo, useCallback, React.memo)를 제대로 쓸 시간이다.

    "useMemo, useCallback, React.memo: 메모이제이션은 공짜가 아니다" 편에서 계속된다.


    🔗 참고 링크

  • Lodash Debounce
  • Lodash Throttle
  • CSS Tricks - Debouncing and Throttling Explained
  • 성능 최적화와 보안의 다른 글

    이 파트의 다른 글이 없습니다.