Frontend/JAVASCRIPT

사용자 경험과 비용을 동시에 잡는 기술: 디바운스와 쓰로틀링 Deep Dive

미니임 2026. 3. 3. 23:01

 

웹 애플리케이션이 고도화되면서 프론트엔드 개발자가 마주하는 가장 흔한 숙제 중 하나는 **'몰아치는 이벤트(Event Flood)'**를 어떻게 제어하느냐입니다. 사용자가 검색창에 타이핑을 하거나, 무한 스크롤을 위해 페이지를 내릴 때 브라우저는 밀리초($ms$) 단위로 수많은 이벤트를 발생시킵니다.

이를 방치하면 불필요한 API 호출로 서버 비용이 급증하거나, 메인 스레드가 차단되어 화면이 버벅거리는 '정크(Jank)' 현상이 발생합니다. 오늘은 이 문제를 우아하게 해결하는 두 가지 핵심 전략, **디바운스(Debounce)**와 **쓰로틀링(Throttle)**의 메커니즘을 파헤쳐 보겠습니다.


1. 핵심 개념 설명: 제어의 미학

두 기술 모두 이벤트 발생 횟수를 제한한다는 목적은 같지만, **'어느 시점에 실행할 것인가'**에 대한 철학이 다릅니다.

디바운스 (Debounce): "다 끝났니? 그럼 시작할게"

디바운스는 연이어 호출되는 함수들 중 마지막(또는 처음) 함수만 실행하도록 하는 기법입니다.

  • 비유: 엘리베이터 문을 생각해보세요. 문이 닫히려는 찰나에 사람이 타면 닫히는 동작이 취소되고 대기 시간이 초기화됩니다. 결국 마지막 사람이 타고 나서 일정 시간이 지나야만 엘리베이터가 움직입니다.

쓰로틀링 (Throttle): "바빠도 정해진 시간표대로만 해"

쓰로틀링은 이벤트를 일정한 주기(Interval)마다 한 번씩만 실행되도록 제한하는 기법입니다.

  • 비유: 일정 간격으로 물방울이 떨어지는 수도꼭지와 같습니다. 아무리 수도꼭지를 세게 틀어도(이벤트가 많이 발생해도), 물방울은 설정된 시간 간격에 맞춰 톡, 톡 떨어집니다.

2. 실전 Hands-on: 이커머스 검색과 무한 스크롤

실제 비즈니스 로직에 적용할 수 있는 코드를 통해 차이를 확인해 보겠습니다.

Case A: 검색 자동완성 (Debounce 적용)

사용자가 'MacBook Pro'를 입력할 때, 철자 하나하나마다 서버에 요청을 보내는 것은 매우 비효율적입니다.

JavaScript
 
/**
 * @param {Function} func - 실행할 실제 로직
 * @param {number} delay - 대기 시간 (ms)
 */
function debounce(func, delay) {
  let timerId;

  return function (...args) {
    // 이전 타이머가 있다면 취소하여 실행을 막음
    if (timerId) clearTimeout(timerId);

    // 새로운 타이머 설정
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 적용 예시: 이커머스 검색 API 호출
const searchProduct = debounce((keyword) => {
  console.log(`[API Request] Searching for: ${keyword}`);
  // fetch(`/api/products?q=${keyword}`)...
}, 500);

// 사용자가 'Apple' 입력 시 'e'까지 입력한 후 0.5초 뒤에 단 한 번만 호출됨

Case B: 실시간 재고 대시보드 스크롤 (Throttle 적용)

데이터가 많은 대시보드에서 스크롤 위치에 따라 추가 데이터를 로드해야 할 때, 스크롤 이벤트마다 로직을 수행하면 성능이 급격히 저하됩니다.

JavaScript
 
/**
 * @param {Function} func - 실행할 로직
 * @param {number} limit - 실행 간격 (ms)
 */
function throttle(func, limit) {
  let inThrottle; // 현재 쓰로틀링 상태인지 확인하는 플래그

  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args); // 주기 내 첫 이벤트 실행
      inThrottle = true;

      setTimeout(() => {
        inThrottle = false; // 설정 시간이 지나면 다시 실행 가능 상태로 변경
      }, limit);
    }
  };
}

// 적용 예시: 스크롤 위치에 따른 데이터 로깅
const handleScroll = throttle(() => {
  const scrollY = window.scrollY;
  console.log(`[UI Update] Current scroll position: ${scrollY}px`);
}, 200);

window.addEventListener('scroll', handleScroll);

3. 트러블슈팅: 흔히 겪는 실수들

  1. Context(this) 유실: 화살표 함수와 일반 함수의 this 바인딩 차이를 이해하지 못하면, 클래스 메서드 내에서 디바운스를 사용할 때 undefined 에러를 만날 수 있습니다. 위 예제처럼 .apply(this, args)를 통해 문맥을 전달하는 것이 안전합니다.
  2. 메모리 누수: SPA(Single Page Application)에서 컴포넌트가 언마운트될 때 clearTimeout을 해주지 않으면, 컴포넌트가 사라진 뒤에도 타이머가 돌아가 예기치 못한 동작을 일으킵니다. 반드시 정리(Clean-up) 로직을 포함하세요.

4. 선택의 기준: Trade-offs

어떤 기술을 선택할지는 비즈니스 요구사항에 따라 결정됩니다.

구분 디바운스 (Debounce) 쓰로틀링 (Throttle)
핵심 이점 불필요한 중간 과정 완전 생략 지속적인 피드백과 부하 조절의 균형
주요 사례 검색어 입력, 윈도우 리사이징(최종 크기 기준) 무한 스크롤, 게임 내 공격 버튼, 마우스 이동 추적
단점 사용자가 입력을 멈추지 않으면 결과가 나오지 않음 마지막 이벤트가 주기 사이에 걸리면 실행되지 않을 수 있음

특히 쓰로틀링의 경우, 마지막 이벤트가 무시되는 문제를 해결하기 위해 trailing: true 옵션을 구현하여 마지막 주기가 끝난 후 한 번 더 실행되도록 보완하기도 합니다.


결론: 성능은 '적절한 멈춤'에서 나온다

모든 이벤트를 성실하게 처리하는 것이 반드시 좋은 소프트웨어는 아닙니다. 오히려 **"언제 일을 하지 않을 것인가"**를 결정하는 것이 시니어 개발자의 역량이죠.

검색 기능에는 디바운스를, 스크롤이나 마우스 트래킹에는 쓰로틀링을 먼저 고려해 보세요. 여러분의 프로젝트에서 가장 성능 개선 효과가 컸던 이벤트 제어 사례는 무엇인가요? 적용 과정에서 겪은 예상치 못한 사이드 이펙트가 있다면 공유해 볼 가치가 있을 것입니다.

반응형