Programming Language/🟨 Javascript

throttle과 debounce를 통해 중복된 요청을 줄여보자

DarrenKwonDev 2021. 3. 16. 22:13

요청을 언제 줄여야 하나?

넵 위의 경우입니다. 이를 전문 용어로 '따닥'이라고 합니다. 버튼을 빨리 눌러서 중복된 요청이 가는 경우에 발생합니다.

 

이런 문제를 해결하기 가장 쉬운건, 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