타이머, 디바운스, 스로틀

0

Javascript

목록 보기
13/13
post-thumbnail

타이머는 함수를 호출할 시간및 반복 호출 간격을 정할수 있게 홰주는 유용한 기능을 제공합니다. 이번 포스팅에서는 타이머를 사용하는 방법과 동작원리, 그리고 타이머를 응용하여 구현할 수 있는 디바운스와 스로틀링에 대해서 알아보겠습니다.

호출 스케줄링

일반적으로 자바스크립트 함수는 호출하면 즉시 실행됩니다. 하지만 일정 시간이 경과된 후에 호출하도록 할 수도 있습니다. 이렇게 즉시 호출하지 않고 호출을 예약하는 것을 호출 스케줄링이라고 합니다. 브라우저는 이렇게 호출 스케줄링을 할 수 있도록 setTimeout과 setInterval이라는 web api(타이머 함수)를 제공합니다.

타이머 함수

setTimeout/clearTimeout

  • setTimeout은 첫 번째 인자로 콜백함수와 두 번째 인자로 시간을 밀리초 단위로 받습니다.
  • setTimeout(callback, milliseconds)
  • 위의 예시는 setTimeout을 이용하여 milliseconds 뒤에 callback함수를 한 번 실행하도록 호출 스케줄링하는 예시입니다.
  • setTimeout의 콜백함수에 넘겨줄 인자가 필요한 경우 setTimeout의 세 번째 인자부터 넘겨줄 수 있습니다.
  • setTimeout은 타이머 아이디를 반환합니다. 이 아이디를 가지고 clearTimeout함수로 스케줄링된 호출을 취소할 수 있습니다.
const timerId = setTimeout(func|code[,delay, param1, param2, ...]);

setInterval/clearInterval

  • setInterval함수는 두 번째 인수로 전달받은 시간마다 반복 동작하는 타이머를 생성합니다.
  • 타이머가 만료될 때마다 첫 번째 인수로 전달받은 콜백함수가 반복 호출됩니다.
const timerId = setInterval(func|code[,delay, param1, param2, ...]);

디바운스와 스로틀

scroll, resize, input, mousemove, mouseover같은 이벤트는 짧은 시간 안에 여러번 일어날 수 있는 이벤트입니다. 따라서 위와 같은 이벤트들에 핸들러 함수를 등록해놓으면 짧은 시간안에 너무 많은 핸들러함수의 호출이 일어나게 되어 성능 저하의 원인이 될 수 있습니다.

예를 들어 type ahead search를 구현할 때 검색바에 사용자의 input이 일어날 때마다 입력값을 포함하는 단어를 query string에 넣어 api호출을 한다고 하면, 너무 많은 api호출이 일어날 수 있습니다. 따라서 이런 경우 input이 일어날 때마다 호출하는 것이 아닌 특정 시간내에 일어난 이벤트들을 한 번만 일어난 것으로 간주하여 핸들러 함수의 호출을 줄일 필요가 있습니다.

이렇게 짧은 시간안에 연속으로 발생하는 이벤트들을 그룹화해서 과도한 핸들러 호출을 막는 프로그래밍 기법이 디바운싱과 스로틀링입니다.

디바운싱

디바운스는 짧은 시간 간격으로 이벤트가 연속으로 발생하면 이벤트 핸들러를 호출하지 않다가 일정 시간이 경과한 이후에 이벤트 핸들러가 한 번만 호출되도록 합니다.

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

예시를 들어 학습해보죠. 예시에는 input태그 하나와 사용자의 입력값을 그대로 출력하게 한 span태그 3개가 있습니다. 첫 번째 span은 디폴트로 두고, 두 번째 span은 input이벤트에 대해 디바운스를 적용했을 때 행동이 어떻게 달라지는지 확인해보도록 하겠습니다.

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="script.js" defer></script>
  </head>
  <body>
    <input type="text" />
    <div>
      <b>Default:</b>
      <span id="default"></span>
    </div>
    <div>
      <b>Debounce:</b>
      <span id="debounce"></span>
    </div>
    <div>
      <b>Throttle:</b>
      <span id="throttle"></span>
    </div>
  </body>
</html>
// script.js
const input = document.querySelector("input");
const defaultText = document.getElementById("default");
const debounceText = document.getElementById("debounce");
const throttleText = document.getElementById("throttle");

// 아무것도 적용하지 않음
input.addEventListener("input", (e) => {
  defaultText.textContent = e.target.value;
}); 

디바운스 구현

  1. 디바운스 함수 작성
    디바운스 함수는 콜백함수와 딜레이를 받아서 콜백함수를 딜레이 이후 실행하는 함수를 리턴하게 작성합니다. 이 때 디바운스 함수가 호출될 때마다 기존의 호출 스케줄을 clear하고 새로운 콜백함수 호출을 스케줄링합니다.
    function debounce(callback, delay = 1000) {
      let timeout;
      return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          callback(...args);
        }, delay);
      };
    }
  2. 디바운스 함수에 넘겨줄 콜백함수 작성
    위의 input 디바운싱 예시에서 디바운스 함수에 넘겨줄 콜백함수는 무엇일까요? 바로 Debounce: 에 표시될 span태그의 innerText를 사용자 입력값으로 바꿔주는 함수입니다.
    const updateDebounceText = debounce((text) => {
      debounceText.textContent = text;
    });
  3. input의 이벤트 핸들러에 updateDebounceText 등록하기
    input.addEventListener("input", (e) => {
      defaultText.textContent = e.target.value;
      updateDebounceText(e.target.value);
    });

이렇게 다음과 같이 디바운싱을 구현하였습니다.

다음 그림은 디바운싱에 대한 이해를 돕기 위한 그림입니다.

스로틀링

스로틀링은 짧은 시간 간격으로 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 하는 기법입니다.

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

위에서 본 input예제에 스로틀링을 적용해도록 합시다.

스로틀링 구현

  1. 스로틀 함수 작성
    스로틀 함수는 스로틀 기법에 맞게 어떤 이벤트를 일정 주기에 맞춰 한 번만 취급하도록 만들면 됩니다. debounce보다 조금 복잡한 로직이 필요합니다.

    스로틀 함수는 이벤트가 발생했을 때 이벤트 핸들러를 호출할지 말지 결정해야 합니다.

  • 대기시간일 때
    아무것도 하지 않고 이벤트를 무시합니다. 하지만 우리는 대기시간중에 발생한 이벤트를 아예 우시할 순 없습니다. 완전히 무시해버린다면 맨 처음 타이핑한 입력값만 취급하고 대기시간동안 입력한 입력값은 아예 잊혀저버리기 때문입니다. 따라서 대기시간동안 핸들러 호출은 하지 않지만, 대기시간안에 발생한 이벤트에 대해서 기억을 해야합니다. 그 동안 발생한 마지막 이벤트에 대해서 다음 인터벌때 한 번은 이벤트 핸들러를 호출해야 하기 때문입니다.

  • 대기시간이 아닐 때
    대기시간이 아니라면 바로 이벤트 핸들러를 호출합니다.

    아래의 스로틀 함수에서 timeoutFunc라는 함수를 만들어 대기시간과 대기시간중에 발생한 이벤트에 대한 정보를 기억하게 하고 있습니다. 대기시간 정보는 shouldWait라는 boolean값, 이벤트 정보는 waitingArgs라는 변수를 만들어 대기시간 중 발생한 마지막 이벤트에 대해 인자를 기억하게 해놨습니다.

    function throttle(callback, delay = 1000) {
      let shouldWait = false;
      let waitingArgs;
    
      const timeoutFunc = () => {
        if (waitingArgs === null) {
          shouldWait = false;
        } else {
          callback(...waitingArgs);
          waitingArgs = null;
          setTimeout(timeoutFunc, delay);
        }
      };
      return (...args) => {
        if (shouldWait) {
          waitingArgs = args; // delay가 경과하기 전에 들어온 인자들을 save해놓는다.
          return; // delay가 아직 경과하기 전이면 아무것도 하지 않는다
        }
        callback(...args);
        shouldWait = true;
        setTimeout(timeoutFunc, delay);
      };
    }
    
  1. 스로틀 함수에 넘겨줄 콜백함수 작성

    const updateThrottleText = throttle((text) => {
      throttleText.textContent = text;
    });
  2. input의 이벤트 핸들러에 updateThrottleText 등록하기

    input.addEventListener("input", (e) => {
      defaultText.textContent = e.target.value;
      updateDebounceText(e.target.value);
      updateThrottleText(e.target.value);
    });

같은 input 이벤트에 대해 디폴트, 디바운스, 스로틀을 적용한 각 span태그의 행동은 다음과 같습니다.

다음은 스로틀링 동작에 대한 이해를 돕기 위한 그림입니다.

디바운스 vs 스로틀

차이점

디바운스는 어떤 이벤트가 연속적으로 발생할 때 해당 이벤트가 다 끝나고 핸들러를 호출하는 기법이며, 이벤트가 끝났다고 판단하는 기준이 debounce함수에 넘겨준 delay(밀리초)입니다.

반면에 스로틀링은 이벤트가 끝났다는 판단을 하는것이 아닌, 연속적으로 발생하는 이벤트에 대해 매번 핸들러를 호출하지 않고, 일정 시간간격으로 한 번만 핸들러를 호출하게 하는 기법입니다.

사용처

  • 디바운싱
    • type ahead search에 많이 사용됩니다. 사용자 입력과 같은 이벤트는 한 글자 한 글자가 이벤트이지만, 크게 봤을 때는 타이핑의 시작부터 끝까지 하나의 이벤트로 볼 수 있어서 디바운스를 사용하여 최적화 합니다.
  • 스로틀링
    • mousemove, scroll, resize와 같은 아주 짧은 단위로 많이 발생하는 이벤트에 대해 처리할 때 사용합니다. 위에서 언급한 이벤트들은 처음과 끝이 있다기 보다 연속적으로 발생하는 이벤트인데, 그렇다고 매 발생마다 핸들러를 호출하면 오버헤드가 심해질 수 있어 스로틀링 기법을 사용하여 최적화 합니다.

참고자료:
https://guiyomi.tistory.com/122
https://www.youtube.com/watch?v=cjIswDCKgu0

0개의 댓글