main

디바운스와 스로틀 직접 구현해보기

Log
7

디바운스와 스로틀

디바운스와 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화 하여 과도한 이벤트 핸들러 호출을 방지하는 프로그래밍 기법입니다.


debounce

짧은 시간 간격으로 이벤트가 연속해서 발생하면 이벤트 핸들러를 호출하지 않다가 일정 시간이 지난 이후에 이벤트 핸들러가 한 번만 호출되도록 합니다.
231006-214409

만약에 서버에 요청이 가게되는 버튼이 있다고 가정해보겠습니다. 제가 이 버튼을 연속에서 계속 클릭하게 되면 계속 서버에 요청이 가게 될텐데요. 이런 경우 여러번 누른 것들 중 가장 마지막에 누른 요청 한번으로 서버에 요청을 보내게 합니다.
이 방식이 디바운스 방식을 적용한 것이라고 생각하면 됩니다.

즉, 디바운스란 짧은 시간 간격으로 발생하는 이벤트를 그룹화해서 마지막에 한 번만 이벤트 핸들러가 호출되도록 하는 기법입니다.

  • 디바운스를 사용하는 예시
    • resize 이벤트(브라우저 창 크기 변경) 처리
    • input 요소에 입력된 값으로 서버 요청하는 입력 필드 자동완성 UI 구현
    • 버튼 중복 클릭 방지 처리

디바운스를 커스텀 훅으로 직접 구현해보기

const useDebounce = <T extends any[]>(callback: (...params: T) => void, time: number) => {
  // 타이머의 ID를 관리할 ref 객체
  const timerId = useRef<ReturnType<typeof setTimeout> | null>(null);
 
  // callback에 디바운스 기능을 추가한 함수(클로저)
	// 이 함수는 클로저기 때문에 호출할 때마다 동일한 timerId를 참조할 수 있다.
	// 따라서 현재 작동중인 타이머가 있는지 없는지 확인할 수 있음
  return (...params: T) => {
    // 현재 작동 중인 타이머의 ID가 있는지 체크
		// 있으면 타이머 종료시키기 (함수가 한 번 더 호출됐기 때문에 다시 타이머 재야됨)
 
    if (timerId.current) clearTimeout(timer.current);
		
		// 타이머를 만들고 타이머의 ID를 ref 객체에 저장
    timerId.current = setTimeout(() => {
			// 정해진 time 지나면 callback 실행하고 타이머 폐기(ref 초기화)
      callback(...params);
      timerId.current = null;
    }, time);
  }
}

throttle

짧은 시간 간격으로 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트가 최대 한 번만 호출되도록 합니다.

231006-220121

스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화해서 일정 시간 단위로 이벤트 핸들러가 호출되도록 호출 주기를 만드는 기법입니다.

  • 스로틀을 사용하는 예시
    • scroll 이벤트 처리
    • 무한 스크롤 UI 구현

이해를 돕기 위해 쉽게 설명하자면, 먼저 제가 버튼한번을 누르면 타이머가 시작됩니다. 2초 뒤에 이벤트가 실행될텐데, 이 2초 사이에 버튼을 여러번 눌러도 다시 타이머가 시작되지 않고, 정확히 처음 눌렀을 때 이벤트 핸들러만 실행이 되는 것입니다.


스로틀을 커스텀 훅으로 직접 구현해보기

function useThrottle<T extends any[]>(callback: (...params: T) => void, time: number){
  // 타이머 ID를 관리할 ref 객체
  const timerId = useRef<ReturnType<typeof setTimeout> | null>(null);
  
  // callback에 스로틀 기능을 추가한 함수(클로저)
	// 이 함수는 클로저기 때문에 호출할 때마다 동일한 timerId를 참조할 수 있다.
	// 따라서 현재 작동 중인 타이머가 있는지 없는지 확인할 수 있음
	return (...params: T) => {
		// 현재 작동 중인 타이머가 없으면 타이머 만들기 
		if(!timerId.current) {
			// 타이머를 만들고 타이머의 ID를 ref 객체에 저장
			timerId.current = setTimeout(() => {
			// 정해진 time 지나면 callback 실행하고 타이머 폐기(ref 초기화)
				callback(...params);
				timerId.current = null;
			}, time)
		} // else 현재 작동 중인 타이머가 있음 : 아무것도 안해도 됨. 타이머 끝나면 콜백 실행될 거임.
	};
}

lodash와 underscore에서의 debounce와 throttle

사실 실무에서는 직접 구헌하기보다는, 라이브러리를 사용해서 디바운스와 스로틀을 이용한다고 합니다.
lodashunderscore을 사용하면 쉽게 구현할 수 있습니다.

  • Lodash
// debounce 사용하기
import { debounce } from 'lodash';
 
const debounceFunction = debounce(() => {
	console.log("This is debounce Function");
}, 500);
// throttle 사용하기
import { throttle } from 'lodash';
 
const throttleFunction = throttle(() => {
	console.log("This is throttle Function");
}, 500);

  • underscore
// debounce 사용하기
import { debounce } from 'underscore';
// 또는 import _ from ‘underscore’; 하고 _.debounce(() => ...) 도 가능
 
const debounceFunction = debounce(() => {
	console.log("This is debounce Function");
}, 500);
// throttle 사용하기
import { throttle } from 'underscore';
 
const throttleFunction = throttle(() => {
	console.log("This is throttle Function");
}, 500);

김다은 이모지
Daeun Kim
Junior Frontend Engineer