[JS] 디바운스, 쓰로틀 (Debounce, Throttle) with lodash

디바운스(debounce)

연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

왜쓸까?

검색어 자동완성기능을 구현하는 경우를 생각해보자.
ㄷ 디 딥 디바 디방 디바우 디바운 디바운ㅅ 디바운스
'디바운스' 검색하는데 쓸데없이 api 호출 9번하면 자원낭비 넘 심하고요?
이 때 호출되는 이벤트 콜백 다 실행하지말고 마지막에 호출한거만 실행하는게 디바운스임.

사용예시

ajax요청, 리사이즈 등...

구현

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

스로틀(throttel)

마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것

왜쓸까?

무한스크롤링 구현을 위해 스크롤 이벤트를 받는 경우를 생각해보자.
이거 스크롤 1px마다 스크롤 이벤트 콜백 다 수행하면 브라우저 과로사함..
브라우저는 다른 할일도 많으니까 이런 이벤트 콜백 수행은 일정시간마다 한번씩만 하게 해주는게 쓰로틀임.

사용예시

무한 스크롤링, 지도 이동 등...

구현

function throttle(callback, delay) {
  let timer
  return event => {
    if (timer) return;
    timer = setTimeout(() => {
        callback(event);
        timer = null;
      }, delay, event)
	}
}

lodash의 debounce, throttle

JavaScript deep dive책에서 위의 코드는 허접이니 더 심화기능 쓰고싶으면 lodash 라이브러리꺼 쓰라고 함
그래서 찾아보니 option을 줘서 기능을 디테일하게 조절하여 사용할 수 있음

_.debounce

_.debounce(func, [wait=0], [options={}])

  • options
    [options.leading=false] : 이게 true면 첫 발생 이벤트는 일단 실행함
    [options.maxWait] : 함수 호출이 딜레이 될 수 있는 maximum 시간.
    [options.trailing=true] : 이게 true면 마지막 이벤트 발생 후 wait만큼 지난 후 함수 실행함

options.maxWait 설정하면 debounce + throttle 합친 것처럼 동작함. wait 만큼 debounce 딜레이 적용되고, maxWait만큼 이벤트 최대 호출주기 적용되는거임

그래서 throttle 코드보면 wait == maxWait인 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 invokeFunc(time) {
    var args = lastArgs,
        thisArg = lastThis;

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

  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time;
    // Start the timer for the trailing edge.
    timerId = setTimeout(timerExpired, wait);
    // Invoke the leading edge.
    return leading ? invokeFunc(time) : result;
  }

  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
        timeSinceLastInvoke = time - lastInvokeTime,
        timeWaiting = wait - timeSinceLastCall;

    return maxing
      ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
        timeSinceLastInvoke = time - lastInvokeTime;

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
  }

  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // Restart the timer.
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  function trailingEdge(time) {
    timerId = undefined;

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }

  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }

  function debounced() {
    var time = now(),
        isInvoking = shouldInvoke(time);

    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        // Handle invocations in a tight loop.
        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

_.throttle(func, [wait=0], [options={}])

  • options
    [options.leading=false] : 이게 true면 첫 발생 이벤트는 일단 실행함
    [options.trailing=true] : 이게 true면 마지막 이벤트 발생 후 wait만큼 지난 후 함수 실행함

  • 코드 원문

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
  });
}

요약

  1. 연속적으로 일어날 가능성이 있는 이벤트의 실행 횟수를 줄여 성능적으로 개선하기 위해서 디바운스, 쓰로틀 기법을 사용한다.
  2. 디바운스는 연속된 이벤트 호출중에 마지막 호출만 실행한다.
  3. 쓰로틀은 연속된 이벤트를 일정시간마다 한번씩 실행한다.

참고링크
https://lodash.com/docs/4.17.15

좋은 웹페이지 즐겨찾기