본문 바로가기
Web/JS

Debounce와 Throttle을 통한 최적화

by 가나닩 2024. 12. 13.

개발을 하다보면 초당 수십~수백번의 연산을 처리해야 하는 경우가 있다.

대표적인 예로 사용자 입력 처리 작업 등을 들수있는데 스크롤, 리사이즈, 드래그 등과 같은 마우스 커서 또는 터치를 활용한 입력은 실시간으로 좌표 등의 데이터를 받아 처리하기 때문에 매우 많은 연산량을 발생시킨다.

 

Debounce와 Throttle은 빈번한 이벤트 처리로 인한 성능 저하 문제를 해결하기 위해 사용한다. 둘의 간단한 용도를 설명하면 아래와 같다.

  • Debounce : 사용자 입력이 멈추면 특정 작업을 실행한다.
  • Throttle : 특정 작업을 일정 간격을 두고 실행하도록 한다.

자세한 설명과 사용법을 알아보자.

 

 

 

1. Debounce

특정 이벤트가 매우 빈번하게 발생할때 Debounce는 이를 무시하다가 이벤트 발생이 더이상 일어나지 않고 일정 시간이 흐르면 특정 작업을 수행하도록 한다. 아래는 JS로 구현한 간단한 Debounce 예시이다.

 

 

1-1. resize 이벤트에 Debounce 적용 (JS)

function debounce(func, delay) {
  let timeout;
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), delay);
  };
}

const handleResize = debounce(() => {
  console.log('Resized!');
}, 300);

window.addEventListener('resize', handleResize);

 

해당 예시는 사용자가 resize를 할 경우 resize발생시 필요한 연산을 수행하지 않다가 resize가 종료되고 0.3초가 지나면 연산을 수행하도록 한다. resize 이벤트는 사이즈를 조정하는동안 매우 높은 빈도로 갱신하며 발생되므로 debounce를 적용하여 최적화해야한다.

 

resize 이벤트는 사이즈를 조절할때 빠른속도로 여러번 연산을 발생시킨다. debounce 함수의 작동 방식을 설명하면 아래와 같다.

 

  1. resize 이벤트가 발생하면 handleResize 함수를 호출한다.
  2. handleResize함수는 debounce함수를 통해 정의되어있다. debounce 함수는 실행할 내용을 담은 함수와 지연시간을 인자로 받는다.
  3. debounce 함수로 실행할 함수가 전달되고 setTimeout에 의해 타이머가 작동된다.
  4. 이때 지정된 지연시간동안은 함수가 실행되지 않는다.
  5. 지정된 지연시간 내에 함수호출이 없을경우(resize 이벤트가 더이상 발생하지 않을 경우) 실행하고자 했던 연산을 실행시킨다.
  6. 지정된 지연시간 내에 함수호출이 발생할경우(resize 이벤트가 계속 발생할때) debounce 함수가 다시 호출되고 clearTimeout에 의해 이전 타이머가 정리(제거)된다. 이후 setTimeout으로 다시 지연시간을 측정한다.

 

예시에서는 console.log의 간단한 내용이 들어있지만 resize 이벤트에 따라 UI의 재조정이나 복잡한 연산을 진행해야 할 경우 debounce를 사용하여 과도한 연산 진행을 막을 수 있다.

 

React에서는 useEffect를 활용해서 구현할 수 있다.

 

 

1-2. 사용자의 검색어 입력과정에 Debounce 적용 (React)

import { useState } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      onSearch(debouncedQuery);
    }
  }, [debouncedQuery, onSearch]);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}

 

해당 예시는 사용자가 검색어를 입력하는 동안에는 검색기능(API요청 등)을 실행하지 않고 검색어 입력이 종료되고 0.5초가 지나면 검색기능을 실행하도록 한다. debounce를 적용하지 않으면 매 키 입력이 있을때마다 검색기능을 실행해야 하므로 DB 조회나 API 요청을 수십번 반복하게 될 수 있다.

 

React에서는 state와 useEffect의 특성을 활용해 구현할 수 있다. useEffect는 의존성배열의 변화를 감지하고 변화를 감지했을경우 반환문을 통해 함수를 정리한뒤 함수를 재실행하게된다.

useDebounce 훅의 useEffect에는 clearTimeout이 반환값으로 들어있으므로 의존성배열의 값이 변화하면(사용자가 검색어를 계속 입력) 기존 타이머가 제거되고 새로운 타이머가 시작된다.

 

 

 

 

2. Throttle

특정 이벤트가 매우 빈번하게 발생할때 Throttle은 이를 무시하고 정해진 간격에 한번씩만 실행한다. 예를들어 5초간 500번의 연산 요청이 있을경우 Throttle을 1초로 적용하면 매초 한번씩 총 5번만 실행하도록 하는것이다. 아래는 js로 구현한 throttle의 예시이다.

 

 

2-1. 사용자 스크롤 사용에 Throttle 적용 (JS)

function throttle(func, limit) {
  let lastFunc;
  let lastRan;

  return function (...args) {
    const context = this;

    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(() => {
        if (Date.now() - lastRan >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

const handleScroll = throttle(() => {
  console.log('Scrolled!');
}, 200);

window.addEventListener('scroll', handleScroll);

 

해당 예시는 사용자가 스크롤을 사용할때 스크롤에 관련된 이벤트를 실행시키지 않고 무시하다가 지정한 시간에 도달할때마다 한번씩 실행하도록 한다. scroll 이벤트도 resize 이벤트처럼 매우 빈번하게 발생하므로 최적화가 필요하다.

 

기본적인 구조는 위의 debounce와 비슷하다. 하지만 정해진 시간을 측정하는 방식이 조금 다르다.

처음 함수가 호출될때 현재 시간을 기록하고 이후 반복적으로 함수가 호출될때마다 기록했던 과거의 시간과 현재시간을 비교한다. 비교한 시간이 지정한 시간보다 더 클경우(시간초과) 지정한 함수를 한번 실행하도록 한다. 이 과정을 반복하도록 한다.

 

 

 

3. 결론 : Debounce vs Throttle 어떤걸 사용해야하나?

매우 빈번한 연산을 방지하여 성능을 최적화한다는 목적은 같지만 작동방식이 다른 둘은 상황에 맞게 골라서 사용해야한다. 예를들면 아래와 같다.

  1. 스크롤 이벤트가 발생할때
    • Debounce : 스크롤이 모두 종료된 후 API 호출 혹은 UI의 완전한 완성
    Throttle : 사용자가 스크롤할 때 중간중간 UI의 간단한 업데이트, 스크롤 양에 따른 반응형 디자인
  2. 사용자가 검색어를 입력할때
    Debounce : 검색어 입력이 모두 끝난 후 검색기능 수행
    Throttle : 검색어 입력 도중 연관검색어, 추천검색어 등의 기능 제공

쉽게말하면 마지막 한번만 실행하느냐 중간중간 간헐적으로 실행하느냐의 차이인데 이를 고려하여 선택해 사용해야한다. 두 기능이 모두 필요할때는 두 기능을 섞어서 사용하는것도 좋다. Debounce나 Throttle의 사용을 고려해보고 상황에 맞는 기능을 잘 적용하면 최적화에 큰 도움이 될것이다.