Debounce, Throttle과 rAF

January 29, 2023·7 min read
Debounce, Throttle과 rAF

이벤트가 발생하면 해당 이벤트에 해당하는 Callback 함수가 Task Queue(Callback Queue)에 쌓이게 됩니다. JavaScript Call Stack이 비어있다면 Callback 함수를 실행합니다. 그러나 Scroll Event나 Input Event와 같이 짧은 시간에 많은 이벤트가 발생하면 지속적으로 Task Queue에 쌓이고, 불필요한 Call Stack이 지나치게 많아져서 성능 저하를 초래할 수 있습니다. 이러한 문제를 해결하기 위한 방법에 대해 알아봅시다.

Debounce와 Throttle

Debounce란?

요청이 들어오면 일정 시간을 기다린 후 요청을 수행합니다. 그러나 일정 시간 안에 추가로 동일한 요청이 들어오면 이전 요청은 취소됩니다. 다시 말해, 가장 마지막 요청만을 수행합니다.

debounce는 위에 설명한 것처럼, 일정시간 내에 발생한 많은 이벤트를 통한 요청 중 가장 마지막 요청만을 수행합니다. 물론, 마지막 요청은 일정시간 기다린 후에 수행합니다.

위와 같은 방식은, 동일한 이벤트가 여러 번 발생하는 경우, 모든 Callback 함수가 Task Queue에 쌓이지 않고, setTimeout을 사용하여 일정 시간(wait) 후에 쌓이도록 하여, 쌓이기 전에 이미 Callback 함수가 있다면 취소(clearTimeout)하고, 새로운 setTimeout 호출을 통해 다시 일정 시간 후에 쌓이도록 구현합니다.

이를 간단하게 구현하면 아래와 같습니다.

function debounce(callback, wait) {
  let timeoutId;
  return () => {
    // 다시 한 번 호출된다면, 기존의 setTimeout을 초기화 합니다.
    clearTimeout(timeoutId);
    // 콜백 함수를 setTimeout을 통해 호출합니다.
    timeoutId = setTimeout(() => {
      callback();
    }, wait);
  };
}
  • 이벤트가 발생합니다.
  • setTimeout을 통해서 wait 후에 callback가 호출됩니다.
  • wait시간 이전에 이벤트가 발생하면, clearTimeout을 통해 이전에 호출된 setTimeout을 취소합니다.
  • 다시 한번, setTimeout을 통해서 wait 후에 callback가 호출됩니다.

Throttle란?

일정시간 동안 요청이 한 번만 수행되도록한다.

throttledebounce와 다르게 일정시간 동안 하나의 이벤트만 실행합니다.

이를 간단하게 구현하면 아래와 같습니다.

function throttle(callback, wait) {
  let throttled = false;
  return () => {
    // throttled가 true라면 callback을 호출하지 않습니다.
    if (throttled) {
      return;
    }
    // callback이 호출되면 throttled는 true가 됩니다.
    callback();
    throttled = true;
    // 일정 시간(wait)이 지난 후에 다시 throttled는 false가 됩니다.
    setTimeout(() => {
      throttled = false;
    }, wait);
  };
}
  • 이벤트가 발생합니다.
  • callback 함수가 호출되고, throttledtrue가 됩니다.
  • setTimeout을 통해서 wait 후에 throttledfalse가 됩니다.
  • throttledfalsewait 시간동안 실행되는 이벤트의 callback은 호출되지 않습니다.

requestAnimationFrame

requestAnimationFrame이란?

window.requestAnimationFrame() 메서드는 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트 바로 전에 브라우저가 애니메이션을 업데이트할 지정된 함수를 호출하도록 요청합니다. 이 메서드는 리페인트 이전에 호출할 인수로 콜백을 받습니다. (MDN 공식 문서)

requestAnimationFrame은 브라우저가 렌더링 할 수 있는 능력에 맞춰 Callback 함수를 호출할 수 있습니다. 따라서, throttle 처럼 굳이 400ms씩 호출 하려고 하지 않아도 되는 것입니다. 일반적인 Callback의 수는 보통 1초에 60회지만, 대부분의 브라우저에서는 W3C 권장사항에 따라 그 수가 디스플레이 주사율과 일치하게됩니다.

또한, setTimeout과 마찬가지로 callback으로 넘겨지는 function을 비동기 task로 분류하여 처리합니다. 다만 requestAnimationFrame은 Task Queue가 아니라 Animation Frame에서 처리됩니다.

이벤트 큐의 우선순위는 다음과 같습니다.

  1. Call Stack 작업을 처리한다.
  2. Call Stack이 비어있다면 Microtask Queue를 확인하고 작업이 있다면 Microtask Queue의 task를 작업을 Call Stack으로 넣고 처리한다.
  3. 만약 microtask가 비어있다면 Animation Frame를 확인하고 브라우저 렌더링이 발생한다.
  4. 1, 2, 3번 작업이 완료되었다면 Task Queue를 확인하고 작업이 있다면 Task Queue의 작업을 Call Stack으로 넣고 처리한다.

따라서, setTimeoutsetInterval과 다르게 렌더링 이전에 우선적으로 처리 됩니다.

이벤트 루프
이벤트 루프

setInterval보다 requestAnimationFrame의 장점?

이전에는 setInterval을 사용해서, 연속된 애니메이션을 구현했다면, 최근에는 아래와 같은 이유로 requestAnimationFrame을 사용합니다.

  • 우리가 직접 최적화하는 것이 아닌 브라우저에서 최적화합니다.
  • 활성화 되지 않은 탭에서는 멈추게 된다.
  • 배터리를 절약할 수 있다. (특히, 모바일 환경)
  • 프레임 유실이 발생할 수 있다.