Lodash debounce 탐색

2025. 1. 29. 00:57·⭐FE

배경

lodash에서 제공하는 debounce를 각잡고 알아보고 내부 구현코드도 하나씩 살펴보았다. 

lodash debounce 옵션 

https://lodash.com/docs/4.17.15#debounce

 

Lodash Documentation

_(value) source Creates a lodash object which wraps value to enable implicit method chain sequences. Methods that operate on and return arrays, collections, and functions can be chained together. Methods that retrieve a single value or may return a primiti

lodash.com

 

공식문서 인데 들어가서 보면 아래와 같이 옵션이 기재되어 있다.

func은 debounce되는 함수, wait은 지연되는 시간이다. 보통 이 2가지 옵션만 보통 쓸일이 많을 것이다. 이 2가지 옵션말고 object형태의 option을 줄 수 있는데 options.leading이 true일 경우 맨처음에만 debounce된 함수가 delay없이 호출된다. ( 물론 설정한 wait이 지난 후에도 해당 함수는 호출된다. )  그리고 option.maxWait은 func 가 호출되기전 최대로 지연될 수 있는 횟수라 생각하면 된다. 그리고 마지막 options.trailing은 default가 true이고 이 값이 false일 경우 우리가 설정한 wait 이후에 func가 호출되지 않는다. 사실상 false로 설정하면 debounce를 쓸 필요가 없다.

부연설명

이해하기 쉽게 주석을 달아놨다. 여기에는 일부러 모든 옵션을 파악할 수 있도록 { leading, maxWait, trailing } 명시적으로 설정했는데 실제로는 설정없이 많이 사용할 것 같다. 

   let debouncedFn = _.debounce(fn, 1000, {
        // * leading에 대한 설명
        // default: false, 만약 true로 설정 시 
        // 처음 debouncedFn 호출시 delay없이 debouncedFn를 한번 호출되고 시작된다.
        leading: true, 
        
        // maxWait에 대한 설명
        // 설정 시 최대 3번까지 delayed 될 수 있다.  
        // maxWait을 설정하지 않을 시 wait(1000ms) 내에 계속 debouncedFn이 호출 시 계속 delay 될 수 있다.
        maxWait: 3,
        
        // * trailing에 대한 설명
        // default : true, 만약 false로 설정 시 delay 후 fn가 호출되지 않음 ( 사실상 쓸 일이 없다. )
        trailing: true,
    });

lodash debounce 간단하게 구현테스트.

아래와 같이 간단하게 사용해볼 수 있다. 따라해보면 대충 감이 온다. 인풋에 마지막 keydown 이벤트 발생 시점 이후 1초내에 다시 keydown 이벤트가 발생할 시 마지막 이벤트에 대해서만 console.log("debounced!") 가 호출된다.

// index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- lodash cdn -->
    <script src=https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js></script>
    <!-- script -->
    <script defer>

        window.addEventListener("DOMContentLoaded", () => {
            const fn = () => {
                console.log("debounced!")
            }
            const debouncedFn = _.debounce(fn, 1000)
            const inputElement = document.getElementById("inputElement");
            inputElement.addEventListener('keydown', () => {
                debouncedFn()
            })
        })
    </script>
</head>

<body>
    <input id='inputElement'></input>
</body>

</html>

debounce 핵심원리

lodash lib내부에 구현된 debounce.js를 확인해보면 핵심은 setTimeout을 이용한 함수지연호출과 clearTimeout을 이용한 설정된 setTimeout 삭제 이다. 

setTimeout

자바스크립트 내장함수 이며 return 값으로 timer id가 반환된다. 반한된 timer id를 clearTimeout(id) 와 같이 호출하면 해당 setTimeout은 삭제되어 해당 setTimeout으로 설정한 함수는 호출되지 않는다. 

https://developer.mozilla.org/ko/docs/Web/API/Window/setTimeout

 

setTimeout() 전역 함수 - Web API | MDN

전역 setTimeout() 메서드는 만료된 후 함수나 지정한 코드 조각을 한 번 실행하는 타이머를 설정합니다.

developer.mozilla.org

clearTimeout

자바스크립트 내장함수 이며 setTimeout으로 생성된 타이머를 삭제할 수 있다. 사용은 clearTimeout(id) 와 같이 사용할 수 있다. id는 예상할 수 있듯이 setTimeout(fn,wait) 의 return 값으로 예를들어 const id = setTimeout(fn,1000); clearTimeout(id) 와 같이 사용할 수 있다.  

https://developer.mozilla.org/ko/docs/Web/API/Window/clearTimeout

 

clearTimeout() 전역 함수 - Web API | MDN

전역 clearTimeout() 메서드는 setTimeout()으로 생성한 타임아웃을 취소합니다.

developer.mozilla.org

(참고)lodash lib 내부 code

실제로 lodash에서 정의한 debounce.js이다. 해당 코드의 핵심만 보고 싶다면. debounce, shouldInvoke, timerExpired 정도만 보면 될 것 같다. 처음에 debounced가 호출되 때 setTimeout( timerExpired, wait ) 이 설정되고 wait이 지난 후 timerExpired가 호출되며 timerExpired 내부에서 shouldInvoke 값이 true일 경우 debounced된 함수를 호출하고 timerId를 undefine로 설정한며 false 일 시 setTimeout( timerExpired, 남은시간) 으로 다시 debounced된 함수 호출을 지연시킨다.  

var isObject = require('./isObject'),
    now = require('./now'),
    toNumber = require('./toNumber');

/** Error message constants. */
var FUNC_ERROR_TEXT = 'Expected a function';

/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax = Math.max,
    nativeMin = Math.min;

/**
 * Creates a debounced function that delays invoking `func` until after `wait`
 * milliseconds have elapsed since the last time the debounced function was
 * invoked. The debounced function comes with a `cancel` method to cancel
 * delayed `func` invocations and a `flush` method to immediately invoke them.
 * Provide `options` to indicate whether `func` should be invoked on the
 * leading and/or trailing edge of the `wait` timeout. The `func` is invoked
 * with the last arguments provided to the debounced function. Subsequent
 * calls to the debounced function return the result of the last `func`
 * invocation.
 *
 * **Note:** If `leading` and `trailing` options are `true`, `func` is
 * invoked on the trailing edge of the timeout only if the debounced function
 * is invoked more than once during the `wait` timeout.
 *
 * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
 * until to the next tick, similar to `setTimeout` with a timeout of `0`.
 *
 * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 * for details over the differences between `_.debounce` and `_.throttle`.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Function
 * @param {Function} func The function to debounce.
 * @param {number} [wait=0] The number of milliseconds to delay.
 * @param {Object} [options={}] The options object.
 * @param {boolean} [options.leading=false]
 *  Specify invoking on the leading edge of the timeout.
 * @param {number} [options.maxWait]
 *  The maximum time `func` is allowed to be delayed before it's invoked.
 * @param {boolean} [options.trailing=true]
 *  Specify invoking on the trailing edge of the timeout.
 * @returns {Function} Returns the new debounced function.
 * @example
 *
 * // Avoid costly calculations while the window size is in flux.
 * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 *
 * // Invoke `sendMail` when clicked, debouncing subsequent calls.
 * jQuery(element).on('click', _.debounce(sendMail, 300, {
 *   'leading': true,
 *   'trailing': false
 * }));
 *
 * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
 * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
 * var source = new EventSource('/stream');
 * jQuery(source).on('message', debounced);
 *
 * // Cancel the trailing debounced invocation.
 * jQuery(window).on('popstate', debounced.cancel);
 */
function debounce(func, wait, options) {
  var lastArgs,
      lastThis,
      maxWait,
      result, // func 함수 return 값 
      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;  // lastArgs 와 lastThis를 모두 undefined로 설정
    lastInvokeTime = time;  // 마지막 호출 시간 갱신
    result = func.apply(thisArg, args); // thisArg를 this로, args 배열을 인수로 사용하여 func 함수 호출 
    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.
    // 처음 호출시 lastCallTime 은 undefined 이므로 
    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;
}

module.exports = debounce;

 

'⭐FE' 카테고리의 다른 글

React19 useActionState Hook 활용 예시  (0) 2025.02.23
React Server Component  (0) 2025.02.17
서버단에서 쿠키 설정 후 브라우저에서 확인해보기  (0) 2025.01.05
Next.js 프로젝트 메모리 사용량 확인  (1) 2024.12.20
Nextjs 터보팩?  (0) 2024.11.18
'⭐FE' 카테고리의 다른 글
  • React19 useActionState Hook 활용 예시
  • React Server Component
  • 서버단에서 쿠키 설정 후 브라우저에서 확인해보기
  • Next.js 프로젝트 메모리 사용량 확인
devWarrior
devWarrior
  • devWarrior
    devWarrior
    devWarrior
  • 전체
    오늘
    어제
    • 🧩Dev (263)
      • ⭐FE (34)
      • 🔒Algorithm (155)
      • ➕Etc. (11)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    코테
    티스토리챌린지
    실버2
    프론트엔드
    node.js
    실버3
    Lv2
    leetcode
    코딩테스트
    구현
    nodejs
    dp
    그리디
    react
    Algorithm
    FE
    js
    백준
    오블완
    프로그래머스
    실버1
    BFS
    자바스크립트
    Easy
    골드5
    실버4
    javascript
    알고리즘
    자스
    DFS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
devWarrior
Lodash debounce 탐색
상단으로

티스토리툴바