[TIL] lodash throttle, debounce 분석

이진호·2024년 1월 31일
0

TIL

목록 보기
61/66

최종 프로젝트를 진행하면서 throttle과 debounc를 사용할 일이 생겼다.

lodash에서 throttle과 debounce 메서드를 제공하고 있어서 쉽게 사용할 수 있는데 이 외에는 사실 쓸 일이 없어서 lodash를 그것만을 위해서 넣는 것은 불필요한 종속성을 추가하는 것 같아서 따로 lodash에서 어떻게 구현을 했는지 알아볼 생각이다.

먼저 throttle과 debounce가 뭔지 부터 알아보자

둘의 공통점은 이벤트가 발생했을 경우에 과도한 이벤트 핸들러 호출을 방지하는 기법을 의미한다.

throttle

throttle은 짧은 시간 간격으로 연속해서 발생한 이벤트들을 일정시간 단위로 그룹화하여 처음 혹은 마지막 이벤트 핸들러만 호출하도록 하는 것이다.

해당 그림을 보면 조금 더 직관적으로 의미를 알 수 있다. 이를 통해서 scroll 이벤트와 같이 조금만 움직여도 발생하는 많은 이벤트들에 대해서 특정 시간단위로 구분하여 해당 이벤트를 실행시킬 수 있다.

debounce

debounce는 짧은 시간 간격으로 연속해서 발생한 이벤트들을 이벤트 핸들러를 호출하지 않고 있다가 마지막 이벤트로부터 일정 시간이 경과된 후에 한 번만 호출하도록 하는 기법을 의미한다.

주로, 입력값에 대한 실시간 검색, 화면 resize 이벤트 등에 이용된다.

lodash throttle, debounce

간단히 개념에 대해서 짚었으니 lodash에서 throttle과 debounce를 어떻게 구현했는지 한번 살펴 보자면 다음과 같이 구현돼있다.

function throttle(func, wait, options) {
  var leading = true,
      trailing = true;
  
  if(typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  
  if(isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  
  return debounce(func, wait, {
    'leading' : leading,
    'maxWait' : wait,
    'trailing' : trailing
  });
}

위 코드를 한번 쓱 보자면,

leading은 경주에서 선두의 라는 뜻을 가지고 있고, trailing은 leading의 반대의 의미로 쓰인 듯 하다.
반환하는 값이 debounce인 만큼 debounce에서 타이밍을 조절하여 throttle을 만드는 듯 하다.

debounce의 함수는 아래처럼 구현돼있다.

function debounce(func, wait, options) {
	var lastArgs,
        lastThis,
        maxWait,
        result,
        timerId,
        lastCallTime,
        lastInvokeTime = 0,
        leading = false,
        maxing = false,
        trailing = true;
  
  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  
  wait = toNumber(wait) || 0;
  
  if(isObject(options)) {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    maxWait = maxing ? nativeMax(toNumber(options.maxWait)) || 0,wait) : maxWait;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  //...//
  function debounced() {
    var time = now(),
        isInvoking = shouldInvoke(time);
    
    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;
    
    if(isInvoking) {
      if(timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if(maxing) {
        clearTimeout(timerId);
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if(timerId === undefined) {
      timerId = setTimeout(timerExpired,wait);
    }
    return result;
  }
  
  debounced.cancel = cancel;
  debounced.flush = flush;
  
  return debounced;

}

throttle은 생각보다 짧았는데 반대로 debounce는 생각보다 길어서 어느정도 생략을 하고 밑에 어떤 것이 반환되는지만 가져와 봤다.
invoke는 뭔가 알리다의 느낌이 강한 단어인것 같다. 아직 어떤 것을 하는지는 정확하게 모르겠지만 우선, 언급하거나 알리는 정도의 느낌인 것 같다.

우선 하나하나 살펴보도록 한다면

debounced 함수 내에서는 timerId가 처음 undefined라면 timerExpired라는 함수를 wait시간 뒤에 실행을 한다.

그렇다면 timerExpired 함수는 어떻게 생겼느냐..

function timerExpired() {
  var time = now();
  if(shouldInvoke(time)) {
    return trailingEdge(time);
  }
  timerId = setTimeout(timerExpired, remainingWait(time));
}

현재 호출된 시간이 invoke해야만 하는 시간이 아니라면 다시 한번 timerExpired를 실행하고 invoke해야만 한다면 trailingEdge에 time을 넘겨 버린다.

위에서도 그렇고 지금도 그렇고 trailingEdge(time)과 leadingEdge(time)에 대해서 제대로 어떤 작업을 실행하는지 감이 안오기 때문에 그 부분만 체크하고 넘어가보자

function trailingEdge(time) {
  timerId = undefined;
  
  if(trailing && lastArgs) {
    return invokeFunc(time);
  }
  lastArgs = lastThis = undefined;
  return result;
}

function leadingEdge(time) {
  lastInvokeTime = time;
  timerId = setTimeout(timerExpired, wait);
  return leading ? invokeFunc(time) : result;
  
}

trailinEdge는 이제 마지막으로 이벤트 핸들러를 처리를 하고 끝내는 분위기이고 leading Edge는 다시 한번 timerExpired를 실행한다.

그럼 공통적으로 사용하는 invokeFunc(time)은 또 무엇인가...

function invokeFunc(time) {
  var args = lastArgs,
      thisArg = lastThis;
  
  lastArgs = lastThis = undefined;
  lastInvokeTime = time;
  result = func.apply(thisArg,args);
  return result;
}

아! 이 함수가 드디어 func을 실행하는 함수 부분이다. 즉 invokeFunc는 func을 실행을 하는 함수이고, invokeTime은 가장 최근에 실행한 이벤트 중에서 delay를 정하기 위한 기준이 되는 값인 것 같다.

음... 그렇지만 여러 경우에 대해서 처리를 하고 있기 때문에 debounce를 option까지 생각하지 않고 간단하게 구현하기 위해서는 어떤 식으로 구현할 수 있을까를 좀 고민해보는 편이 좋을 것 같다.

중요한 것은 여러 번 반복적으로 실행되는데 언제 실행을 막는지가 중요하기 때문에 다음과 같이 간단하게 구현할 수 있을 것 같다.

let timerId = null;

function debounce(func,delay) {
  if(timerId) {
	clearTimeout(timerId);
  }
  timerId = setTimeout(() => {
    func();
	timerId = null;
  },delay);
}
profile
dygmm4288

0개의 댓글