throttle과 debounce를 통해 중복된 요청을 줄여보자
요청을 언제 줄여야 하나?
넵 위의 경우입니다. 이를 전문 용어로 '따닥'이라고 합니다. 버튼을 빨리 눌러서 중복된 요청이 가는 경우에 발생합니다.
이런 문제를 해결하기 가장 쉬운건, lodash의 debounce, throttle을 이용하는 방법이 있겠습니다. _.debounce(), _.throttle() 꼴로 사용하면 끝이고(트리 쉐이킹의 측면에서는 최악의 import 방법이긴 한데 뭐 ㅋㅋ), 뭐 직접 구현하셔도 괜찮습니다.
throttle이랑 debounce는 뭐가 다른거야? => 제한 시간 연장의 유무
사실 둘 다 비슷하게, 특정 코드의 실행 횟수를 줄여주는 역할을 합니다. 그러나 차이를 명확하게 알아는 둬야 합니다.
* 아래 글에 '이벤트'라고 한건, 보통 위 두 함수가 dom에 붙인 이벤트에 따라 발생하는 콜백을 제어하는데 주로 사용되기 때문에 이벤트라고 편의상 표현한 것입니다.
- debounce
debounce는 주어진 ms 이내에 연속으로 이벤트가 발생할 경우, 주어진 시간을 연장하고 이벤트가 발생하지 않을 때까지 기다리다가, 제한 시간이 모두 지나면 이벤트를 한 번만 발생시킵니다.
이게 잘 이해가 안 갈 수 있는데, 관점을 달리하여 예시를 들어보겠습니다. 편하신 걸로 이해해주세요.
관점 1)
2초 시간 제한이 있는 debounce를 걸어뒀습니다.
1초 동안 이벤트로 인한 콜백이 10번 발생했습니다.
또 다른 1초 동안 아무 이벤트도 발생하지 않으면 제한 시간 2초를 전부 다 쓰게 된 것이니 마지막 이벤트가 실행됩니다.
그러나, 제한 시간 2초가 다 지나가기 전 다른 이벤트가 발생한다면, 해당 이벤트가 발생한 시점 + 2초의 제한 시간이 재설정됩니다.
결국, 이벤트가 새로 발생하지 않을 때까지 계속 기다리다 시간 제한 내에 아무 이벤트가 발생하지 않으면, 마지막 이벤트가 실행됩니다.
관점 2)
마지막 이벤트가 발생한 후 제한 시간이 지나기 전까지 이벤트가 발생하지 않아야 비로소 실행합니다.
- throttle
throttle은 주어진 매 ms마다 실행됩니다.
debounce는 주어진 시간 내에 다시 이벤트가 발생한 경우 시간을 연장하지만, throttle을 그렇지 않습니다.
2초 시간 제한이 끝났다? 그러면 그냥 2초 시간 제한 내의 마지막 이벤트가 발생하는 겁니다.
- 실질적인 활용
검색창의 경우 => debounce
검색창에 유저가 입력하면 자동으로 ajax 요청을 보내는데, '안녕' 하나를 검색하려고 ㅇ아안안ㄴ안녀안녕
이렇게 보내면 글자 2개 치려고 벌써 요청을 6번을 보내게 되었네요 끔찍하게....
이 경우에는 당연이 debounce를 써주는 것이 좋을 겁니다. 유저가 전부 검색어를 친 후에 ajax 명령어를 날리는게 합리적이니깐요.
중간중간에 결과물을 보여주고 싶다면 throttle을 써도 되긴 합니다만.
지도의 경우 => throttle
지도의 경우 당연히 드래그를 하면서 이벤트가 발생합니다. 드래그를 할 때마다 이벤트에 달린 콜백이 실행된다면 무지막지하게 느려지겠죠.
이 경우에는 throttle이 적합해보입니다. debounce를 사용하게 되면, 드래그를 했다가, 유저가 드래그를 멈추는 순간까지 아무 것도 실행되지 않기 때문입니다. 이런 방식이 더 좋은 것 같아면 이렇게 해도 됩니다. 제 취향엔 드래그해가면서 정보를 슥슥 뿌려지는게 적당히 실시간이라고 느낄 정도의 적당한 throttle을 적어주는게 좋습니다.
글 올리기, 메세지 보내기 등 에서 '따닥'을 방지 => throttle도 괜찮고, debounce도 괜찮음. 그러나 debounce 권장
"왜 글이 두 개 올라가요" 와 같은 일을 경험해보셨을 겁니다. 버튼을 빠르게 2번 누르면 발생하는 일입니다.
그런데 악의적인 유저가 버튼을 '따다다다다다다다닫닥' 누르는 경우를 생각해봅시다.
"어, 이거 왜 안돼?" 하면서 "따다다다다다다다닥" 누를 수도 있는거구요.
이 경우 throttle을 짧게 걸어 놓은 경우 요청이 여러번 갈수도 있습니다.
최악의 경우를 생각해보면 debounce이되, 제한 시간을 짧게 걸어놓는게 좋을 것 같습니다.
그래서 어케 씀? (vanilla javascript)
timer 전역 변수를 만들어서 사용하는 분들도 계시던데 어딘가 함수가 순수하지 못하다는 느낌.
물론 제어하기야 편하겠지만
우선 debounce 부터.
const debounce = (callback, ms = 1000) => {
let timeoutId = null;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
callback.apply(null, args);
}, ms);
};
};
// example
inputDom.addEventListener(
'keyup',
debounce(e => {
if (!e.target.value) {
return;
}
console.log(e.target.value);
})
);
다음은 throttling. 시간이 되면 timer를 강제로 null로 만들어주면 된다. 사용 예시는 debounce와 같으니 생략
function throttle(callback, ms = 1000) {
let timer = null;
return function (...args) {
if (timer === null) {
timer = setTimeout(() => {
callback.apply(this, args);
timer = null;
}, ms);
}
};
}
귀찮은데 lodash 쓰면 안될까요?
사실 뭐 이 개념 구현한 건 lodash만 있는게 아니라 rxjs도 있고, 웬만한 함수형 라이브러리에는 다 있슴다.
직접 구현도 가능하지만 일반적으론 lodash를 사용합니다. 딱히 설명도 필요 없이 문서만 바로 보면 됩니다.
lodash.com/docs/4.17.15#debounce
_.debounce(func, [wait=0], [options={}])
lodash.com/docs/4.17.15#throttle
_.throttle(func, [wait=0], [options={}])
option에 있어서. { leading: true, trailing: false } 입니다. 이 의미는 최초값은 출력하겠다는 겁니다.
throttle 타임 이내 발생한 이벤트 중 마지막으로 발생한 이벤트를 확인해보고 싶다면 { leading: false, trailing: true }로 바꿔줍니다.
둘 다 false면 출력이 안되고, 둘 다 true면 둘 다 출력됩니다.
** 주의
빌드할 때 lodash 트리 쉐이킹 꼭 해주십쇼. 이 녀석 무겁습니다.
reference)
https://www.joshwcomeau.com/snippets/javascript/debounce/
chaewonkong.github.io/posts/debounce-js.html