mason
mason-log.

Debounce와 Throttle.

mason

masonfe-hyunsu

🧵 1. 개요.

Debounce Throttle 배너

프론트에서 이벤트는 생각보다 자주 온다. 스크롤, 입력, 리사이즈만 해도 한 동작에 핸들러가 수십 번 붙을 수 있다.

그럴 때마다 바로 일을 시키면 UI도 버벅이고, 검색처럼 API까지 물리면 서버만 바빠진다. 그래서 호출 빈도를 의도적으로 줄이는 패턴이 필요하다.

이 포스팅에서는 Debounce(입력이 멈춘 뒤에야 한 번 실행)랑 Throttle(일정 간격마다 최대 한 번 실행)을 구분하고, 어디에 쓰면 좋은지 짧게 정리해 본다.

개념만 짧게 보고 싶으면 Debounce와 Throttle (Docs)도 같이 보면 된다.


⚖️ 2. 둘의 차이.

둘 다 “너무 자주 오는 호출을 줄인다”는 목적은 같다. 줄이는 방식이 다르다.

  • Debounce: 연속 호출이 이어지는 동안은 계속 미룬다. 마지막 호출 뒤 delay가 지나야 실행한다.

  • Throttle: 연속 호출 중에도 delay마다 한 번씩은 실행한다.

디바운스와 스로틀 타이밍 비교

검색은 타이핑이 끝난 뒤에 한 번이면 충분하다. 스크롤 중 헤더 투명도는 “가끔씩”이라도 따라가야 한다.

입력이 멈춘 뒤 한 번이 Debounce, 계속 오는 동안 간격마다 최대 한 번이 Throttle이다.

leading / trailing 옵션이 붙으면 첫 호출을 바로 쏠지, 마지막만 남길지가 갈린다. UX에 맞게 고르면 되고, 기본은 “검색은 trailing, 클릭 연타는 leading” 정도로 생각하면 편하다.


🛠️ 3. 어떨때 쓰면 좋을까?

어떨 때 쓰면 좋을까 고민

검색, 태그 필터 입력. → Debounce

Posts 쪽 태그 검색처럼, 입력할 때마다 목록을 거르거나 API를 치면 타이핑 중간값이 다 나간다.

300~500ms Debounce를 걸면 "re"를 치는 동안은 조용히 두고, 손을 뗀 뒤 한 번만 돌린다. 자동완성, 주소 검색, admin 필터도 같은 패턴이다.

스크롤에 묶인 UI. → Throttle

이 블로그 Header, Profile는 scroll로 진행도를 읽는다. 지금은 매 스크롤마다 바로 읽는데, setState가 잦아지면 렌더가 과해진다.

100~200ms Throttle이면 스크롤 중에도 따라가되, 프레임을 다 잡아먹지는 않는다. passive: true도 같이 두는 편이 좋다. 핸들러에서 preventDefault를 안 쓰겠다는 신호라, 브라우저 기본 스크롤을 방해하지 않는다.

상세 글 목차(현재 섹션 하이라이트)도 비슷하다. 스크롤 위치를 보고 활성 heading을 바꾸는 일은 중간 업데이트가 필요해서 Debounce는 잘 안 맞는다. 이 블로그 목차는 requestAnimationFrame으로 프레임당 한 번만 돌리게 묶어 뒀다. timer Throttle도 같은 계열이다.

리사이즈, 윈도우 폭 계산. → Throttle (또는 Debounce)

resize도 연속으로 온다. 레이아웃을 실시간으로 맞출 거면 Throttle, “손 놓고 난 뒤 한 번만 재계산”이면 Debounce다.

차트 리사이즈, 사이드바 접힘 기준폭 같은 게 여기 해당한다.

버튼, 저장 연타. → Debounce / 짧은 Throttle

저장, 좋아요, 댓글 전송처럼 같은 요청이 연속으로 나가면 답이 꼬인다.

첫 클릭만 받고 나머지는 막는 leading Throttle, 또는 제출 후 잠깐 비활성도 같은 계열이다. “입력이 끝난 뒤”가 아니라 “연속 트리거를 한 번으로 접는다”에 가깝다.

스크롤 로딩, 무한 스크롤. → Throttle + 가드

하단 근처인지 매번 계산할 때도 Throttle이 기본이다. 다만 이미 fetch 중이면 플래그로 한 번 더 막는 게 안전하다. 호출 빈도만 줄이고, 중복 요청까지는 별도로 막는다.


📐 4. 고를 때 기준.

정리하면 이렇다.

  • 최종 결과만 필요하면 Debounce (검색, 저장 확정, resize 후 재측정).

  • 중간 상태도 보여야 하면 Throttle (스크롤 진행, 드래그 좌표, 진행률).

  • 애니메이션 프레임에 맞출 거면 requestAnimationFrame도 후보. 스크롤에 묶인 페인트는 rAF가 Throttle 대용으로 자주 쓰인다.

  • 요소가 보이는지만 알면 되면 스크롤 핸들러 대신 IntersectionObserver(뷰포트에 들어왔는지 알려 주는 API)를 먼저 본다. 이 블로그 카드 fade-in이 그 쪽이다.

최적화 방향은 “함수를 싸맨다”만으로 끝나지 않는다.

  • 리스너는 passive가 가능한지 본다. (preventDefault로 스크롤을 막지 않겠다고 브라우저에 알려 주는 옵션이다).

  • React면 debounced/throttled 함수를 매 렌더마다 새로 만들지 않는다. lodash를 쓰면 cleanup에서 cancel()도 챙긴다.

  • 서버로 나가는 호출이면 클라이언트 제한 + 요청 취소(AbortController)를 같이 둔다. 타이머만 줄여도 in-flight 요청은 남을 수 있다.

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

function throttle(fn, delay) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= delay) {
      last = now;
      fn(...args);
    }
  };
}

구현은 Docs에 둔 최소형이다. 위 throttle은 간격 안에서 앞쪽 호출만 통과시키고, 마지막 호출을 꼭 보장하진 않는다. trailing이 필요하면 구현을 늘리거나 lodash를 쓰면 된다. lodash면 unmount 때 cancel()도 챙긴다. 핵심은 어느 쪽에 걸지를 먼저 정하는 일이다.


🧭 5. 정리.

Debounce는 입력이 멈춘 뒤 한 번, Throttle은 계속 오는 동안 간격마다 최대 한 번.

검색 입력은 Debounce, 스크롤에 묶인 UI나 목차 갱신은 Throttle/rAF 쪽이 자연스럽다. 스크롤마다 무거운 일을 넣고 있다면, 일단 빈도를 줄이고, 보이기만 하면 되는 일은 Observer로 빼는 순서가 부담이 적었다.


🔗 6. 참고 자료.

이전 글
SEO와 AEO.

댓글

불러오는 중...
목록으로