타이머는 함수를 호출할 시간및 반복 호출 간격을 정할수 있게 홰주는 유용한 기능을 제공합니다. 이번 포스팅에서는 타이머를 사용하는 방법과 동작원리, 그리고 타이머를 응용하여 구현할 수 있는 디바운스와 스로틀링에 대해서 알아보겠습니다.
일반적으로 자바스크립트 함수는 호출하면 즉시 실행됩니다. 하지만 일정 시간이 경과된 후에 호출하도록 할 수도 있습니다. 이렇게 즉시 호출하지 않고 호출을 예약하는 것을 호출 스케줄링이라고 합니다. 브라우저는 이렇게 호출 스케줄링을 할 수 있도록 setTimeout과 setInterval이라는 web api(타이머 함수)를 제공합니다.
setTimeout
은 첫 번째 인자로 콜백함수와 두 번째 인자로 시간을 밀리초 단위로 받습니다.setTimeout(callback, milliseconds)
milliseconds
뒤에 callback
함수를 한 번 실행하도록 호출 스케줄링하는 예시입니다. setTimeout
의 콜백함수에 넘겨줄 인자가 필요한 경우 setTimeout의 세 번째 인자부터 넘겨줄 수 있습니다.const timerId = setTimeout(func|code[,delay, param1, param2, ...]);
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;
});
function debounce(callback, delay = 1000) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}
const updateDebounceText = debounce((text) => {
debounceText.textContent = text;
});
input.addEventListener("input", (e) => {
defaultText.textContent = e.target.value;
updateDebounceText(e.target.value);
});
이렇게 다음과 같이 디바운싱을 구현하였습니다.
다음 그림은 디바운싱에 대한 이해를 돕기 위한 그림입니다.
스로틀링은 짧은 시간 간격으로 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 하는 기법입니다.
즉, 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화아여 일정 시간 단위로 이벤트 핸들러가 호출되도록 호출 주기를 만듭니다.
위에서 본 input예제에 스로틀링을 적용해도록 합시다.
스로틀 함수 작성
스로틀 함수는 스로틀 기법에 맞게 어떤 이벤트를 일정 주기에 맞춰 한 번만 취급하도록 만들면 됩니다. 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);
};
}
스로틀 함수에 넘겨줄 콜백함수 작성
const updateThrottleText = throttle((text) => {
throttleText.textContent = text;
});
input의 이벤트 핸들러에 updateThrottleText 등록하기
input.addEventListener("input", (e) => {
defaultText.textContent = e.target.value;
updateDebounceText(e.target.value);
updateThrottleText(e.target.value);
});
같은 input 이벤트에 대해 디폴트, 디바운스, 스로틀을 적용한 각 span태그의 행동은 다음과 같습니다.
다음은 스로틀링 동작에 대한 이해를 돕기 위한 그림입니다.
디바운스는 어떤 이벤트가 연속적으로 발생할 때 해당 이벤트가 다 끝나고 핸들러를 호출하는 기법이며, 이벤트가 끝났다고 판단하는 기준이 debounce함수에 넘겨준 delay(밀리초)입니다.
반면에 스로틀링은 이벤트가 끝났다는 판단을 하는것이 아닌, 연속적으로 발생하는 이벤트에 대해 매번 핸들러를 호출하지 않고, 일정 시간간격으로 한 번만 핸들러를 호출하게 하는 기법입니다.
참고자료:
https://guiyomi.tistory.com/122
https://www.youtube.com/watch?v=cjIswDCKgu0