프론트엔드 개발/JavaScript

자바스크립트) 타이머 - 디바운스, 쓰로틀링

Ella Seon 2023. 7. 12. 16:57

0. 호출 스케줄링

- 일정 시간이 지난 후에 원하는 함수를 예약 실행(호출) 할 수 있게 하는 것

 

호출 스케줄링을 구현하는 방법은 1. setTimeout 과 2. setInterval 이 있다.

1) setTimeout 

- setTimeout 함수로 생성한 타이머는 한 번 동작함

 

🔸setTimeout 예제코드

function sayHi() {
  alert('안녕하세요.');
}

setTimeout(sayHi, 1000);

- 위 코드를 실행하면 1초 후에 sayHi()가 호출된다.

 

function sayHi(who, phrase) {
  alert( who + ' 님, ' + phrase );
}

setTimeout(sayHi, 1000, "홍길동", "안녕하세요."); // 홍길동 님, 안녕하세요.

- 위 코드와 같이 함수에 인수를 넘겨줄 수도 있다.

// 1초(1000ms) 후 타이머가 만료되면 콜백 함수가 호출된다.
setTimeout(() => console.log("Hi!"), 1000);

// 세 번째 인수로 문자열 'Lee' 전달
setTimeout((name) => console.log(`Hi! ${name}.`), 1000, "Lee");

// 두 번째 인수(delay)를 생략하면 기본값 0이 지정된다.
setTimeout(() => console.log("Hello!"));

 

+) clearTimeout

- setTimeout() 함수는 일정시간 후에 실행되도록 예약된 타이머를 생성하고, 해당 타이머를 식별하기 위해 타임아웃아이디(Timeout ID) 라고 불리는 숫자를 반환한다. 타임아웃 아이디는 setTimeout() 함수를 호출할 때마다 내부적으로 생성되는 타이머 객체를 가리킨다.

- 이 값을 인자로 clearTimeout() 함수를 호출하면 기다렸다가 실행될 코드를 취소 할 수 있다.

const timeoutId = setTimeout(() => console.log("5초 후에 실행됨"), 5000);
console.log(timeoutId) // 숫자가 반환됨
clearTimeout(timeoutId); // 예약된 작업을 취소하고 실행되지 않도록 함

 

2) setInterval / ClearInterval

- 함수를 주기적으로 실행하게 만든다. 함수 호출을 중단하려면 clearInterval(timerId)를 사용하면 된다.

// 2초 간격으로 메시지를 보여줌
let timerId = setInterval(() => alert('째깍'), 2000);

// 5초 후에 정지
setTimeout(() => { clearInterval(timerId); alert('정지'); }, 5000);
🔸clearTimeout() 과 clearInterval() 를 사용해야하는 이유(메모리 관리 차원)

 setTimeout() 함수와 setInterval() 함수를 사용한 후에는 반드시 clearTimeout() 함수와 clearInterval() 함수를 사용해서 타이머를 청소해주는 습관을 들이는게 좋다. 특히, SPA(Single Page Application)을 개발할 때는 이 부분이 메모리 누수(memory leak)로 이어질 수 있기 때문에 각별한 주의가 필요하다. 
메모리 누수는 프로그램에서 동적으로 할당된 메모리를 정리하지 않아 메모리가 계속해서 쌓이는 상황을 말한다. 예를 들어, setTimeout을 사용하여 작업을 예약하고 나중에 작업을 취소하지 않는다면, 해당 작업은 실행되지 않지만 메모리에 계속 남아있게된다. clearTimeout와 clearInterval을 사용하여 작업을 취소하면 예약된 작업에 대한 참조를 제거하고, JavaScript 엔진은 해당 메모리를 정리할 수 있다. 

2. 이벤트가 과도하게 호출되어 성능에 문제를 일으킬 경우에는 어떻게 할까?

scroll, resize, mousemove 같은 이벤트는 짧은 시간 간격으로 연속해서 발생한다.
이러한 이벤트에 바인딩한 이벤트 핸들러는 과도하게 호출되어 성능에 문제를 일으킬 수 있다.

(ex: 스크롤 이벤트의 경우 스크롤링 할때마다 발생하는데, 그때마다 같은 작업을 하게되면 성능 문제가 발생할 수 있다)

 

1) 디바운스 2)쓰로틀링 을 사용하여 이벤트가 과도하게 호출되는 것을 막거나, 조절할 수 있다.

 

1) 디바운스

- 짧은 시간 간격으로 이벤트가 연속해서 발생하면, 이벤트 핸들러를 호출(call)하지 않다가 일정 시간이 경과된 이후에 이벤트 핸들러가 한 번만 호출되도록 한다.

- 즉 디바운스는 연속으로 호출되는 함수들 중에 마지막에 호출되는 함수(또는 제일 처음 함수)만 실행되도록 하는 것

 

 

🔸input 태그에서 debounce 활용한 예제 코드

- 실시간 검색 창에서 매 클릭 이벤트 (e.target.value) 마다 ajax 요청을 보내는 것보다 디바운스를 통해 일정 기간을 바탕으로 마지막 이벤트에 대한 ajax 요청을 보내는 것이 서버의 부하를 줄이는 데 더욱 효율적일 것이다.

- 자바스크립트에서는 'debounce' 를 구현하기 위해 일반적으로 'setTimeout' 과 클로저를 활용한다. 

- 1000ms 동안 입력 이벤트가 계속 발생하면 타이머가 재설정되어 콜백 함수가 호출되지 않는다. 그러나 입력 이벤트가 1000ms 동안 발생하지 않으면 타이머가 만료되어 콜백 함수가 실행되고, 입력 필드의 값을 $msg.textContent에 할당하여 화면에 표시한다.

<body>
    <input type="text" />
    <div class="msg"></div>
    <script>
    const $input = document.querySelector("input");
      const $msg = document.querySelector(".msg");

      const debounce = (callback, delay) => {
        let timerId;
        // debounce 함수는 timerId를 기억하는 클로저를 반환한다.
        return (event) => {
           //1000ms 가 경과하기 전에 이벤트가 발생하면 이전 타이머를 취소하고 새로운 타이머 설정
          if (timerId) clearTimeout(timerId); //timerId 가 있으면 타이머를 취소함
          timerId = setTimeout(callback, delay, event); 
        };
      };

      // debounce 함수가 반환하는 클로저가 이벤트 핸들러로 등록된다.
      // 1000ms보다 짧은 간격으로 input 이벤트가 발생하면 debounce 함수의 콜백 함수는
      // 호출되지 않다가 1000ms 동안 input 이벤트가 더 이상 발생하지 않으면 한 번만 호출된다.
      
        const handleInput = (event) => {
    	$msg.textContent = event.target.value};
  
      $input.oninput = debounce(handleInput, 1000); //addEventListner 과 다른 이벤트핸들러 등록방법
    </script>
  </body>

 

 

위의 코드에서 timerId는 새로운 입력 이벤트 발생 시마다 갱신되고, clearTimeout를 통해 이전 타이머가 취소된 후에 새로운 타이머가 설정된다. 

 

🔸코드박스로 위 예제코드 살펴보기

https://codesandbox.io/s/debounce-xg37xs?file=/src/index.js 

 

debounce - CodeSandbox

debounce by EllaSEON using parcel-bundler

codesandbox.io

※ oninput 이벤트 핸들러: 입력 필드의 값이 변경될때마다 즉시 발생함. 입력필드의 값이 변경될 때마다 이벤트 핸들러가 호출된다. 실시간으로 값의 변경을 감지할 수 있다.
※onchange 이벤트 핸들러 : 입력필드의 값이 변경되고, 해당 필드가 포커스를 잃은 후에 이벤트 핸들러가 호출된다. 값이 최종적으로 변경되었을 때에만 이벤트가 발생한다. 
 => oninput은 값의 변경이 실시간으로 감지되고, onchange는 값의 최종 변경 시점에 이벤트가 발생.
🔸클로저를 사용한 이유🔸
1) timerId 변수의 유지
: debounce 함수 내부에 선언된 timerId 변수는 클로저로 인해 외부에서 접근할 수 있다. 이는 이후에 발생하는 이벤트에서 timerId 값을 유지할 수 있게 해준다. 만약 클로저를 사용하지 않았다면, debounce 함수 호출이 끝나면, timerId 변수는 debounce 함수의 렉시컬 환경에서 해제된다. 이후에는 timerId 변수에 접근할 수 없게 된다. 이는 이후에 발생하는 이벤트에서 timerId 값을 사용할 수 없음을 의미한다. 

2) 이전 타이머의 취소와 새로운 타이머 설정
: 클로저를 통해 timerId 변수에 접근하므로 이전 타이머를 취소하고 새로운 타이머를 설정할 수 있다. 이는 입력 이벤트가 연속해서 발생할 때마다 타이머를 재설정하여, 일정 시간 동안 추가 입력이 없을 때에만 콜백 함수를 실행할 수 있도록 한다.
만약 클로저를 사용하지 않았다면 ,  외부에서 timerId 에 접근할 수 없기 때문에 이전 타이머의 취소와 새로운 타이머를 설정할 수 없다. 이는 입력 이벤트가 연속해서 발생할 때마다 이전 타이머가 취소되지 않고, 새로운 타이머가 설정되지 않는 상태로 남게되는 문제가 생긴다. 

🔸클로저를 사용하지 않았을 때 코드
<input type="text" />
<div class="msg"></div>

<script>
  const input = document.querySelector("input");
  const msg = document.querySelector(".msg");
  let timerId; // 클로저를 사용하지 않고 timerId를 선언

  const debounce = (callback, delay) => {
    return (event) => {
      if (timerId) clearTimeout(timerId);
      timerId = setTimeout(() => {
        callback(event);
      }, delay);
    };
  };

  const handleInput = (event) => {
    msg.textContent = event.target.value;
  };

  input.oninput = debounce(handleInput, 300);
</script>​

위의 코드에서는 클로저 대신 timerId를 외부 스코프에서 선언하였다. 하지만 클로저를 사용하지 않으므로 timerId 변수는 debounce 함수가 호출된 후에 사라진다. 따라서 이후의 이벤트에서 timerId에 접근할 수 없게 된다.

 

2) 쓰로틀링

- 쓰로틀링 : 일정시간동안 이벤트를 한번만 발생하게 하는 것

- Throttle의 설정시간으로 100ms 를 주게 된다면, 해당 이벤트는 100ms 동안 최대 한번만 발생하게 된다. 즉, 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 한다. 

- 쓰로틀은 scroll 이벤트 처리나 무한 스크롤 UI 구현 등에 유용하게 사용된다

 

- 아래 움짤을 보면 쓰로틀링이 적용되지 않은 왼쪽 스크롤 이벤트는 굉장히 많은 양의 이벤트가 발생한다.

오른쪽은 제한된 양의 이벤트가 발생한다.

 

🔸throttle 간단한 예제코드

var timer;
document.querySelector('#input').addEventListener('input', function (e) {
  if (!timer) {
    timer = setTimeout(function() {
      timer = null;
      console.log('여기에 ajax 요청', e.target.value);
    }, 200);
  }
});
// 최초 timer 는 undefined 상태
// if문의 조건문을 true로 통과해서 timer에 setTimeout 함수를 실행하게 된다.
// input에 입력될 때 마다 동작하게 되는데, 최초에 if문을 실행하였고 delay 시간 동안에는
// if문을 통과 할 수 없기 때문에 타이머가 활성화되지 않음.
// delay 시간이 다지나고 나면 timer는 null로 할당하게 되고 console.log()가 동작해
// input에 입력된 value를 출력해줌.

🔸throttle 예제코드

<!DOCTYPE html>
<html>
  <head>
    <style>
      .container {
        width: 300px;
        height: 300px;
        background-color: rebeccapurple;
        overflow: scroll;
      }

      .content {
        width: 300px;
        height: 1000vh;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="content"></div>
    </div>
    <div>
      일반 이벤트 핸들러가 scroll 이벤트를 처리한 횟수:
      <span class="normal-count">0</span>
    </div>
    <div>
      쓰로틀 이벤트 핸들러가 scroll 이벤트를 처리한 횟수:
      <span class="throttle-count">0</span>
    </div>

    <script>
      const $container = document.querySelector(".container");
      const $normalCount = document.querySelector(".normal-count");
      const $throttleCount = document.querySelector(".throttle-count");

      const throttle = (callback, delay) => {
        let timerId;
        // throttle 함수는 timerId를 기억하는 클로저를 반환한다.
        return (event) => {
          // delay가 경과하기 이전에 이벤트가 발생하면 아무것도 하지 않다가
          // delay가 경과했을 때 이벤트가 발생하면 새로운 타이머를 재설정한다.
          // 따라서 delay 간격으로 callback이 호출된다.
          if (timerId) return;
          timerId = setTimeout(
            () => {
              callback();
              timerId = null; // 콜백함수가 실행된 후 타이머 초기화 함
              //새로운 스크롤 이벤트가 발생했을 때 다시 타이머를 설정하기 위해 이전 타이머의 영향을 받지 않도록 함. 
              //새로운 스크롤 이벤트가 발생하면 쓰로틀링된 콜백 함수가 다시 실행될 수 있다.
            },
            delay
          );
        };
      };

      let normalCount = 0;
      $container.addEventListener("scroll", () => {
        $normalCount.textContent = ++normalCount;
      });

      let throttleCount = 0;
      // throttle 함수가 반환하는 클로저가 이벤트 핸들러로 등록된다.
      $container.addEventListener(
        "scroll",
        throttle(() => {
          $throttleCount.textContent = ++throttleCount;
        }, 1000)
      );
    </script>
  </body>
</html>

🔸위 예제코드 코드해석순서 보기(더보기 클릭)

더보기

1. 스크롤 이벤트 발생

2. 쓰로틀링된 스크롤 이벤트 핸들러 시작

3. 쓰로틀링된 스크롤 이벤트 핸들러는 'throttle' 함수로 감싸진 클로저 이며, 'setTimeout'을 사용하여 일정시간(delay) 후에 내부의 콜백함수를 실행한다.

4. 쓰로틀링된 콜백함수가 실행되면, throttleCount값을 1 증가시키고, 그 값을 $throttleCount 요소의 텍스트로 설정한다.

🔸코드 샌드박스로 코드 보기

https://codesandbox.io/s/sseuroteul-y9pnm9?file=/src/index.js 

 

쓰로틀 - CodeSandbox

쓰로틀 by EllaSEON using parcel-bundler

codesandbox.io

 

3) debounce VS throttle

쓰로틀링은 디바운스와 비슷한 개념이지만, 디바운스는 입력 이벤트가 연속해서 발생하는 동안 일정 시간 동안 마지막 이벤트만을 처리하는 반면, 쓰로틀링은 일정한 주기마다 이벤트를 처리합니다. 따라서 디바운스는 마지막 이벤트에 대한 응답을 지연시키는 반면, 쓰로틀링은 일정한 간격으로 이벤트를 제한적으로 처리합니다.

 

 

🔸코드샌드박스를 통해서 제대로 비교해보기

https://codesandbox.io/s/dibaunseu-sseuroteul-q2f5rf?file=/src/index.js 

 

디바운스,쓰로틀 - CodeSandbox

디바운스,쓰로틀 by EllaSEON using parcel-bundler

codesandbox.io

 

 

 

 

 


참고자료

https://ko.javascript.info/settimeout-setinterval

 

setTimeout과 setInterval을 이용한 호출 스케줄링

 

ko.javascript.info

https://www.youtube.com/watch?v=By49qqkzmzA 

https://velog.io/@yujuck/Javascript-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8A%A4%EC%99%80-%EC%93%B0%EB%A1%9C%ED%8B%80%EB%A7%81

 

[Javascript] 디바운싱과 쓰로틀링

디바운스와 쓰로틀링 모두 웹에서 발생하는 이벤트를 제어하는 방법이다. 예를 들어 스크롤 이벤트의 경우 스크롤링 할 때마다 발생하는데, 그 때마다 같은 작업을 실행하게 되면 성능 문제가

velog.io

https://www.youtube.com/watch?v=KYAF5yVElLY