JavaScript에서 함수 실행 제한 기법: 쓰로틀링(Throttling)

쓰로틀링(Throttling) 소개

이전 글에서는 '디바운싱(Debouncing)'에 대해 다루었고, 이번 글에서는 디바운싱의 형제인 '쓰로틀링(Throttling)'에 대해 알아보겠습니다. 두 개념의 차이점을 다시 한번 짚어보며 독자분들에게 개념을 다시 한번 정리해 드리겠습니다. > PS: '개방과 절약'이라는 속담에 나오는 '절약'과 기술적 쓰로틀링은 본질적으로 동일하다고 생각합니다. > > - 개방과 절약에서의 절약은 회사의 재정 지출을 줄이는 것을 의미합니다. > - 프론트엔드 기술에서의 쓰로틀링은 함수 호출 빈도를 희석시켜 CPU 자원을 절약하는 것을 의미합니다. **차이점** - **쓰로틀링**: N초 동안 단 한 번만 실행되며, N초 내에 반복적으로 트리거되어도 첫 번째 실행만 유효합니다. - **디바운싱**: N초 후에 이벤트를 실행하며, N초 내에 반복적으로 트리거되면 타이머가 재설정됩니다. 하지만 디바운싱 관련 글에 있던 한 독자의 댓글이 더 생생하게 느껴져 해당 내용을 인용하겠습니다. 해당 독자분께 감사드립니다. - **쓰로틀링**: 공격 간격으로 생각할 수 있으며, 아무리 빠르게 클릭해도 동시에 두 번 공격할 수 없습니다. - **디바운싱**: 귀환으로 이해할 수 있으며, 클릭할 때마다 다시 시작해야 합니다.

쓰로틀링 예시

여기서 두 가지 일반적인 예시를 들어보겠습니다. 더 좋은 예시가 있다면 댓글로 공유해 주세요.

생활 예시

생활 속 예시를 들어보겠습니다. 일반적으로 여성이 팬들의 메시지에 답장하는 확률은 비교적 낮습니다. 하루에 한 번만 답장한다고 가정해 봅시다. 즉, 만약 오늘 이미 답장을 했다면, 팬이 얼마나 많은 메시지를 보내도 더 이상 답장하지 않습니다. 오늘의 기회는 이미 소진되었고, 다음 날이 되어야 새로운 기회가 생깁니다.

인증번호 발송

인증번호 발송은 생활에서 매우 흔하게 사용됩니다. 실제로 SMS 인증 기능은 쓰로틀링 기법이 적용되어 있습니다. SMS 발송은 비용이 발생하므로, 사용자가 반복적으로 클릭하여 여러 개의 SMS가 발송되는 것을 방지하기 위해 쓰로틀링을 사용합니다. 예를 들어, 인증번호를 한 번 받으면 60초 동안 발송 버튼이 비활성화되어 다시 발송할 수 없습니다.

쓰로틀링 구현 방법

다음으로 쓰로틀링을 구현하는 두 가지 방법을 알아보겠습니다. '타임스탬프' 방식과 '타이머' 방식입니다.

타임스탬프 방식 구현

원리

이벤트가 트리거될 때마다 마지막 실행 시간과의 간격을 확인합니다. 지정된 대기 시간(delay)을 초과하면 함수를 실행하고, 마지막 실행 시간을 현재 시간으로 업데이트합니다.

코드

function throttleByTimestamp(targetFunction, waitTime) {
  let lastExecutionTime = 0; // 마지막 실행 시간 저장

  return function() {
    const currentTime = new Date().getTime();
    // 현재 시간과 마지막 실행 시간의 차이가 대기 시간보다 큰지 확인
    if (currentTime - lastExecutionTime > waitTime) {
      // 조건을 만족하면 원본 함수 실행 및 인자 전달
      targetFunction.apply(this, arguments);
      // 마지막 실행 시간을 현재 시간으로 업데이트
      lastExecutionTime = currentTime;
    }
  };
}

// 사용 예시
const throttledFunction = throttleByTimestamp(function() {
  console.log("쓰로틀링 함수 실행됨");
}, 500); // 500ms마다 한 번 실행

// 대기 시간 테스트 헬퍼 함수
const delay = async(time) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
};

const testThrottle = async() => {
    throttledFunction();
    // 500ms 미만이면 한 번만 실행
    await delay(600);
    throttledFunction();
}

testThrottle()

/**
 * 출력:
 * 쓰로틀링 함수 실행됨
 * 쓰로틀링 함수 실행됨
 */

타이머 방식 구현

원리

이벤트가 트리거될 때 타이머를 설정합니다. - 첫 번째 이벤트 트리거 시, 지정된 시간 후에 함수를 실행하는 타이머를 설정합니다. - 타이머가 만료되기 전에 새로운 이벤트가 트리거되면, 기존 타이머가 있는지 확인하고 있다면 건너뜁니다. - 타이머가 만료되면 현재 타이머를清除(clear)하여 다음 이벤트가 다시 트리거될 수 있도록 합니다.

코드

function throttleByTimer(targetFunction, delay) {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    if (!timer) {
      timer = setTimeout(() => {
        targetFunction.apply(context, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  };
}

타임스탬프 + 타이머 결합 방식 구현

두 가지 구현 방식을 살펴보니 차이점이 보이시나요? - **타임스탬프 방식**: 현재 시간에서 마지막 실행 시간을 빼기 때문에 조건이 만족되면 이벤트가 즉시 실행됩니다. - **타이머 방식**: `targetFunction.apply`가 `setTimeout` 내에 있으므로 트리거되어도 `delay` 시간이 지나야 실행되므로, 이벤트 트리거가 중단된 후에도 한 번 더 실행되는 효과가 있습니다. 만약 이벤트 트리거 시 즉시 실행하고, 트리거가 중단된 후에도 한 번 더 실행하는 쓰로틀링 함수를 구현하려면 어떻게 해야 할까요?

원리

- 각 `delay` 시간 동안 반드시 함수가 한 번 실행되도록 하기 위해, 쓰로틀링 함수 내부에서 시작 시간, 현재 시간과 `delay`를 사용하여 `remaining`을 계산합니다. - `remaining <= 0`이면 함수를 실행해야 할 시점이며, 아직 시간이 안 되었다면 `remaining` 시간 후에 다시 트리거되도록 설정합니다. - 물론 `remaining` 시간 동안 새로운 이벤트가 발생하면 현재 타이머를 취소하고 새로운 `remaining`을 계산하여 현재 상태를 판단합니다.

코드

function combinedThrottle(targetFunction, delay) {
  let timer;
  let lastExecutionTime = 0;

  return function() {
    let currentTime = Date.now();
    // 마지막 실행 후 경과 시간 계산
    let remaining = delay - (currentTime - lastExecutionTime);
    const context = this;
    const args = arguments;

    // remaining 시간 동안 새로운 이벤트가 발생하면 현재 타이머 취소
    clearTimeout(timer);
    // remaining <= 0이면 함수 실행
    if (remaining <= 0) {
      targetFunction.apply(context, args);
      lastExecutionTime = Date.now();
    } else {
      // 아직 시간이 안 되면 remaining 시간 후에 실행
      timer = setTimeout(targetFunction, remaining);
    }
  };
}

태그: JavaScript 쓰로틀링 함수 최적화 프로그래밍 기법 이벤트 처리

6월 13일 00:34에 게시됨