치춘짱베리굿나이스

Throttle & Debounce 본문

Javascript + Typescript/이론과 문법

Throttle & Debounce

치춘 2022. 5. 18. 03:24

Throttle & Debounce

예제에 useEffect가 들어가서 React 관련 게시글로 뺄까 고민해봤는데 그냥 자바스크립트 / 타입스크립트로 지정하였다

Throttle

지정된 시간 동안 함수를 최대 한 번만 호출하도록 한다 (일정 시간이 지나기 전까지 재호출을 방지한다)

특정 함수를 한번 호출했을 경우 지정한 시간 (예를 들면 1초) 이 흐르기 전까지 재호출이 되지 않도록 막는다

구현하기

...
let timeoutValue;
if (!timeoutValue) {
  timeoutValue = setTimeout(() => {
    console.log(stateValue);
    // 여기에 throttling으로 실행시킬 함수 및 코드 배치
    timeoutValue = null; // timeoutValue 초기화
  }, 1000);
}

어떻게 된게 예제가 죄다 lodash 라이브러리에서 throttle 구현된거 불러와서 사용하거나, let const 안 쓰고 var 쓰던 시절 + jquery 밖에 없다..

라이브러리 가져다 사용하기

import { throttle } from 'lodash';

const throttled = throttle(func, cycle);

lodash 라이브러리를 사용하면 위와 같이 쉽게 사용가능하다

작동 방식

  1. timeoutValuenull으로 초기화한다
  2. 최초로 함수를 실행하면, timeoutValuenull이기 때문에 조건문 안의 코드 (setTimeout()) 가 실행된다
  3. setTimeout의 반환값은 timeoutValue로 들어가므로, timeoutValue가 더이상 null이 아니기 때문에 함수를 재실행해도 setTimeout이 실행되지 않는다
  4. 지정한 시간이 흐르고 나면 setTimeout으로 예약한 코드가 실행되고, timeoutValue가 다시 null이 된다
  5. 따라서 지정한 시간이 흐르고 나서 코드를 재실행할 수 있게 된다

결론

쓰로틀링을 이용해서 무한스크롤 시에 데이터 fetching 횟수 제한을 거는 등 api 호출을 아낄 수 있다

Debounce

이벤트를 그룹화하고, 특정 시간이 지난 후 그중 하나의 이벤트만 발생하도록 한다

inputonChange 같은 짧은 시간에 여러 번 일어나는 이벤트를 다룰 때, 지정한 시간동안 일어난 다수의 이벤트 중 마지막 이벤트 하나만 실행되도록 한다

구현하기

...
useEffect(() => {
    const timeoutValue = setTimeout(() => {
        console.log(stateValue);
        // 여기에 debounce로 실행시킬 함수 및 코드 배치
    }, 1000); // 두번째 인자로 딜레이 (ms) 지정

    return () => {
        clearTimeout(timeoutValue);
    }; // useEffect unmount될 때 실행되는 clean-up
}, [stateValue]);
ㅇ
아
안
안ㄴ
안녀
안녕 // debounce 안 썼을 때

안녕 // debounce 사용해서 딜레이 줬을 때

디바운싱 함수 내에서 상태값을 사용하는 경우가 대부분이므로, 해당 상태값을 dependency로 갖는 useEffect로 구현한다

stateValue가 업데이트될 때마다 useEffect도 같이 업데이트되며, 그와 함께 clean-up 함수가 실행되면서 이전 입력값에 대한 clearTimeout() 이 실행된다 (이부분은 컴포넌트 생명주기 및 clean-up 함수를 공부하자)

따라서 stateValue가 짧은 시간 내에 여러번 바뀌면, setTimeout을 통해 함수 실행을 대기하던 이전 stateValue들은 전부 clearTimeout() 에 의해 사라졌으므로 설정한 딜레이가 지난 뒤의 마지막 stateValue에 대해서만 코드가 실행되는 것이다

저 clean-up 함수를 stateValue를 dependency로 갖는 함수가 아니라, dependency가 없는 useEffect에서 반환하면 stateValue가 바뀔 때가 아니라 컴포넌트 언마운트 시에만 clearTimeout() 이 되므로, 디바운싱이 제대로 적용되지 않게 된다

결과값으로 작동 방식 알아보기

ㅇ // 1000ms 이전에 stateValue가 '아' 로 바뀜과 동시에, ㅇ은 clearTimeout() 에 의해 대기가 사라진다
아
안
안ㄴ
안녀
안녕

안녕 // debounce 사용해서 딜레이 줬을 때

stateValueinput 태그의 onChange 핸들러에 의해 매 입력 시마다 바뀐다고 할 때, 출력값을 통해 디바운싱이 어떻게 일어나는지 알아보자

  1. 사용자는 ’안녕' 을 입력하기 위해, 제일 먼저 ‘ㅇ’를 입력한다
  2. setStateValue() 에 의해 stateValue가 ‘’ 에서 ‘ㅇ'으로 업데이트된다, 이 단계에서는 이전 stateValue가 빈 문자열이므로 디바운싱은 신경쓰지 않도록 하자
  3. stateValue가 ‘ㅇ' 으로 변경되면서 stateValue를 dependency로 갖는 useEffect가 함께 실행되고, setTimeout()에 의해 ‘ㅇ' 을 출력하는 함수의 실행 (첫 번째 인자) 이 1000ms만큼 (두 번째 인자) 미뤄진다. 이때 setTimeout() 을 통해 예약한 함수 실행은 1000ms가 흐르지 않은 상태에서 clearTimeout() 가 실행될 때 취소됨을 기억하자
  4. 사용자가 다음 글자인 ‘ㅏ' 를 입력하자마자, setStateValue가 실행되면서 stateValue가 ‘아' 로 업데이트된다
  5. stateValue가 업데이트되었으므로 useEffect가 실행되며, 같은 dependency를 갖던 이전 useEffectclean-up 함수가 실행되고 사라진다
  6. 이때 지정한 clean-up 함수는 clearTimeout() 이었으므로, stateValue가 ‘ㅇ' 이었던 시절의 setTimeout() 예약은 취소된다, 따라서 ‘ㅇ' 은 1000ms가 지나도 출력되지 않는다
  7. ‘아' 를 출력하는 함수의 실행이 setTimeout() 에 의해 예약되며, 다음 글자인 ‘ㄴ' 을 입력하면 4 ~ 6번 과정이 반복되면서 ‘아' 의 출력 예약도 사라진다
  8. 위의 과정을 반복하면 결국 처음 입력을 시작한 시점부터 1000ms 뒤에 입력되어있는 값인 ‘안녕' 만이 출력되는 것이다

결론

setTimeout() 함수의 첫 번째 인자로 지정한 함수 (예시에서는 () ⇒ { console.log(stateValue) } ) 는 두 번째 인자로 지정한 시간 (1000ms) 이 흐른 뒤에만 호출되며, 따라서 1000ms 가 흐르고 함수를 호출하는 시점의 stateValue의 값을 기준으로만 동작하는 것이다

디바운싱을 이용해서 검색창에서 사용자가 입력을 할 때, 특정 시간마다만 API를 호출하거나, 버튼을 빠르게 연타했을 때 상태값이 빠르게 변경되지 않도록 할 수 있다

커스텀 훅으로 만들기

export const useDebounce = (func, delay, dependency) => {
    useEffect(() => {
        const timeoutValue = setTimeout(() => {
            func();
        }, delay);

        return () => {
            clearTimeout(timeoutValue);
        };
    }, [func, dependency, delay]);
};

...
// 컴포넌트 내부
useDebounce(() => { console.log(stateValue) }, 1000, [stateValue]);

위의 useEffect 부분을 커스텀 훅으로 분리하면 간단하게 한 줄로 처리 가능하다

디바운스는 다른 컴포넌트에서도 종종 쓸 일이 있기 때문에, 훅으로 분리하여 코드를 간결하게 만들 수 있다

func() 가 연산량이 많은 함수라면, 커스텀 훅 내에서 useCallback() 으로 감싸서 사용하는 것을 추천한다

참고자료

ZeroCho Blog

[React]React Hook에서 Throttle, Debounce 사용

throttle & debounce 폴리필

 

'Javascript + Typescript > 이론과 문법' 카테고리의 다른 글

자바스크립트의 대부분 요소는 객체인가요?  (0) 2022.07.26
require, import, export  (0) 2022.07.25
비동기 처리와 Promise  (0) 2022.05.13
[Typescript] Type vs Interface  (0) 2022.05.09
spread, rest  (0) 2022.04.12
Comments