디바운싱 vs 스로틀링

이민선(Jasmine)·2024년 7월 9일
0

최근에 검색어 입력에는 디바운싱을, 검색어 자동완성에는 쓰로틀링을 사용했다. 프로젝트를 하면서 이해도를 높인 만큼 포스팅을 하면서 개념을 다시 한 번 정리해보려고 한다. 프로젝트에서는 라이브러리를 사용했지만, 이번 포스팅에서는 자바스크립트와 리액트 코드로도 만들어서 원리를 파헤쳐보려고 한다.

공통점

사용자에 의해 연속적인 이벤트가 trigger되었을 때 여러 이벤트 중 부분적인 이벤트만 실행되도록 하는 것이다. 왜 여러 이벤트 중 하나만 실행해야 하는 경우가 생길까?
검색 네트워크 요청을 생각해보자. 이 검색창은 검색 버튼을 따로 클릭하지 않아도 사용자가 검색창에 키워드를 타이핑하면 자동으로 네트워크 요청을 보낸다. 이 때 다음과 같이 입력한다.

ㄷ 디 딥 디바 디방 디바우 디바운 디바운ㅅ 디바운시 디바운싱

ㄷ도 검색하고 디바도 검색하고 디바우도 검색하기에는 api 호출이 지나치게 빈번하다. 네트워크 요청 보내기, DB에서 검색하기, 최종 검색 결과 출력하기.. 이 모든 일련의 과정을 타이핑 한 글자 한 글자 할 때마다 매번 거치기에는 리소스 낭비가 크다. 만약 검색 횟수가 제한된 유료 API를 사용한다면? 한 글자 한 글자 검색 결과 보여주는 것이 모두 비용이다. 성능 측면에서 지나치게 많은 이벤트를 다 처리하려고 하면 부담이 될 수 밖에 없다.

따라서 디바운싱과 쓰로틀링과 같은 최적화 방법을 사용하면 성능을 개선하고 불필요한 비용을 절약할 수 있다.

차이점

그림으로 설명하는 개념

디바운싱

연속적인 이벤트 중 마지막 이벤트만 처리한다.
예를 들어 사용자가 마지막 글자를 타이핑하는 순간까지 기다렸다가, 마지막 한번 네트워크 요청을 보내려고 할 때 유용하다.

쓰로틀링

이벤트 실행 주기를 만든다.
예를 들어 나는 검색어 자동완성에 쓰로틀링을 사용하였는데, 사용자가 글자를 타이핑하는 0.3초마다 네트워크 요청을 보내려고 할 때 사용할 수 있다.
또는 스크롤할 때 특정 이벤트를 걸어놓았다면, 연속적으로 모든 이벤트를 다 실행하는 것이 아니라 일정 주기로 이벤트를 처리하여 성능을 유지하고자 할 때 유용하다.

Javascript 코드 예제 (클로저 사용)

디바운싱

function debounce(callback, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer); // 연속적으로 타이핑하면 한글자 한글자 입력할 때마다 이전 timer를 제거하는 것이다.
    timer = setTimeout(() => {
      callback(...args); // 한 글자 타이핑 후 delay 시간이 지난 후에야 callback 실행
    }, delay);
  };
}

// 사용 예시
const searchInput = document.getElementById('search');
searchInput.addEventListener(
  'input',
  // outer 함수를 호출하여 inner 함수 반환하여 전달. 이 때 외부함수의 변수인 timer를 기억한다.
  debounce((event) => {
    console.log('검색어:', event.target.value); // 마지막 입력 후 실행
  }, 300)
);

쓰로틀링

function throttle(callback, delay) {
  let lastTime = 0; // 마지막 실행 시간을 저장
  return function (...args) {
    const now = Date.now(); // 이벤트가 발생할 때마다 갱신된다.
    if (now - lastTime >= delay) {
      callback(...args); // delay 시간이 지나면 실행
      lastTime = now; // 마지막 실행 시간 갱신
    }
  };
}

// outer 함수를 호출하여 inner 함수 반환하여 전달. 이 때 외부함수의 변수인 lastTime을 기억한다.
const scrollHandler = throttle(() => {
  console.log('스크롤 이벤트');
}, 500);

 
window.addEventListener('scroll', scrollHandler);

React 코드 예제 (React custom hook)

디바운싱

import { useEffect, useState } from 'react';

export const useDebounce = (value: string, limit: number): string => {
  	// 초기값을 컴포넌트가 마운트된 시점의 value로 지정했다가, handler가 호출될 때마다 새로 지정됨
	const [debouncedValue, setDebouncedValue] = useState(value);

	useEffect(() => {
      	// limit 시간이 지나면 debouncedValue를 업데이트
		const handler = setTimeout(() => {
			setDebouncedValue(value);
		}, limit);
		// useEffect가 재호출되거나 컴포넌트가 언마운트될 때 타이머를 정리
		return () => {
			clearTimeout(handler);
		};
	}, [value, limit]); // limit도 의존성 배열에 포함하여 변경 시 타이머가 재설정되도록 함

	return debouncedValue;
};

쓰로틀링

import { useEffect, useState } from 'react';

export const useThrottle = (value: string, limit: number): string => {
    // 초기값을 컴포넌트가 마운트된 시점의 value로 지정했다가, handler가 호출될 때마다 새로 지정됨
    const [throttledValue, setThrottledValue] = useState(value);
    // 마지막으로 실행된 시간을 상태로 저장. default 값은 컴포넌트가 마운트되는 시점.
    const [lastExecuted, setLastExecuted] = useState(Date.now());

    useEffect(() => {
        const handler = setTimeout(() => {
            const now = Date.now();
          	// limit 시간이 지났다면 값 업데이트 및 마지막 실행 시간 갱신
            if (now - lastExecuted >= limit) {
                setThrottledValue(value);
                setLastExecuted(now);
            }
        // 남은 시간 후에 핸들러 실행
        }, limit - (Date.now() - lastExecuted));
        // useEffect가 재호출되거나 컴포넌트가 언마운트될 때 타이머를 정리
        return () => {
            clearTimeout(handler);
        };
    }, [value, limit, lastExecuted]); // 타이핑할 때마다 useEffect 재실행. lastExecuted도 의존성 배열에 포함하여 변경 시 타이머가 재설정되도록 함

    return throttledValue;
};

이번 글에서는 디바운싱과 쓰로틀링의 공통점과 차이점을 알아보았다. 이벤트의 연속적인 호출이 필요할 때 적절히 사용해서 성능과 리소스의 사용을 최적화할 수 있겠다.

profile
기록에 진심인 개발자 🌿

0개의 댓글