Debounce와 Throttle

KyungminLee·2024년 6월 17일
post-thumbnail

1. Debounce와 Throttle

디바운스(Debounce)쓰로틀(Throttle)은 모두 함수의 연속 실행을 제한하기 위해 설계된 기법이다. Debounce는 특정 시간 동안 함수의 실행을 모두 무시하고, 마지막에 호출된 함수만 실행하도록 한다. 반면, Throttle은 함수 실행 후 특정 시간 동안 추가적인 호출을 모두 무시하도록 한다. 이 두 기법은 비슷해 보이지만, 실제로는 함수 실행을 허용하는 시간 간격을 조절하는 방법이 서로 다르다.

Debounce와 Throttle은 특히 Future와 Stream 관련 함수에서 자주 사용된다.

API 요청에서 Debounce와 Throttle이 유용하게 사용될 수 있다. 만약 API 요청이 1초에 30번씩 발생한다면, 또는 검색할 때마다 한 글자씩 타이핑할 때마다 요청이 전송된다면, 서버에 큰 부담을 줄 수 있다.

서버뿐만 아니라 사용자 경험에도 좋지 않은 영향을 미칠 수 있다. 예를 들어, 스크롤할 때마다 무거운 이벤트가 발생한다면, 이는 사용자에게 불편을 초래할 수 있다. 2011년 John Resig은 이러한 문제를 제기했으며, 그는 스크롤 이벤트를 250ms마다 발생시키는 방법을 제안했다. 현재는 Debounce와 Throttle과 같은 더 정교한 기법이 사용되고 있다.

2. Debounce

간단하게 정의하면 debounce는 일정 시간 이후에 함수를 호출한다.

Debounce는 함수가 마지막으로 호출된 후 일정 시간이 지나야만 함수가 실행되도록 하는 방식이다. 주로 사용자가 입력이나 스크롤과 같은 이벤트를 멈출 때까지 함수의 실행을 지연시키고자 할 때 사용된다.

예를 들어, 사용자가 입력할 때마다 검색 기능이 작동하는 검색창이 있다고 가정해 보자. 이 검색 기능에 300밀리초의 디바운스를 적용하면, 사용자가 최소 300밀리초 동안 입력을 멈추지 않는 한 기능이 실행되지 않는다. 사용자가 입력을 계속하면 타이머가 리셋되고, 300밀리초가 경과해야만 기능이 실행된다.

// useDebounce.ts
import { useEffect, useState } from 'react'

const useDebounce = (value: string, time: number) => {
  const [debouncedValue, setDebouncedValue] = useState(value)
  useEffect(() => {
    const timerId = setTimeout(() => {
      // 일정 시간 이후에 변경한다
      // 변경 사항이 생긴다면, 그로부터 time을 다시 센다
      return setDebouncedValue(value)
    }, time)
    // 기능 수행을 완료하면 구독을 해제한다
    return () => clearTimeout(timerId)
  }, [value, time])

  // 변경된 값을 return
  return debouncedValue
}

export default useDebounce
// 실행
const debouncedText = useDebounce(text, 1000)

위 예시 이미지 처럼, input에 입력한 text바로 바뀌지만, debounceText입력한 후에 일정 시간 이후에 바뀌는 것을 볼 수 있다.

3. Throttle

간단하게 정의하면 throttle 일정 시간마다 함수를 호출한다.

Throttle은 Debounce와 유사하지만, 개념상으로는 다르다. Throttle은 연속적으로 발생하는 이벤트를 특정 시간 간격으로 나누어 처리하는 방법이다. 즉, 지정된 시간 간격(Time Interval) 내에는 최대 한 번만 이벤트를 처리한다는 개념이다. Throttle은 주어진 시간 구간 동안의 호출 빈도를 조절하려는 경우에 유용하다. (스로틀링은 출력을 조절한다는 의미로 이벤트를 일정주기마다 발생하도록 하는 기술)

예를 들어, API 호출을 트리거하는 버튼이 있다고 가정해 보자. 이 버튼의 클릭 이벤트에 1초 간격으로 Throttle을 적용하면, 버튼을 클릭하더라도 1초 내에는 API 호출이 한 번만 발생한다. 1초 간격 내의 추가 클릭은 무시되고, 다음 1초 간격이 만료될 때까지 대기한다.

//useThrottle
import { useEffect, useState } from 'react'

const useThrottle = (callbackFunc: () => void, time: number): any => {
  const [isWaiting, setIsWaiting] = useState(false)

  useEffect(() => {
    if (!isWaiting) {
      callbackFunc()
      setIsWaiting(true) // 함수가 호출되자마자 true로 바꾸어 호출 중단

      setTimeout(() => {
        // 특정 시간 이후에 false로 바꾸어 재호출
        setIsWaiting(false)
      }, time)
    }
  }, [callbackFunc, isWaiting, time])
}

export default useThrottle
useEffect(() => {
  const interval = setInterval(() => setCount(count => count + 1), 100)
  return () => clearInterval(interval)
}, [])

useThrottle(() => {
  setThrottledCount(throttledCount + 1)
}, 1000)

debounce와 달리 연속적으로 api를 호출(ex. 추천검색어, 무한스크롤)이 필요할 때 일정 시간마다 호출하여 실행한다. (무한 스크롤 일 때 스크롤을 움직일 때 마다 함수가 실행되는 문제. 스크롤이 마지막이 아니면 데이터가 추가되지 않지만 그래도 계속해서 스크롤이 마지막인지 확인하는 비교를 수행하다보니 성능문제가 있다.)

4. Debounce, Throttle 사용 예시

사내 코드 어시스턴트 개발을 진행 하면서 발생했던 문제와, 어떻게 해결했는지 작성해보려고 한다. 아래 이미지는 스크롤을 일정 높이만큼 내렸을때 scroll handler smooth 옵션을 줘서 천천히 내려가게 구현한 이미지이다.

사진으로는 정확하게 안 보이지만, 일정 높이 체크 후 스크롤을 확 내렸을 때 이벤트가 여러번 발생하여 스크롤이 한번 위로 튕겼다가 아래로 내려오는 현상이다.

throttle을 적용한 경우 : 채팅내역이 아래로 계속 내려올 때마다 맨 아래 위치를 계산해서 위치이동 함수를 호출한다. 계속 스크롤이 자동으로 내려와야 하는 상황에서 너무 빠르게 호출 되다보면 성능에 영향을 줄 수 있다.

사용자가 직접 스크롤을 빠르게 내렸을 경우 위치가 재 계산되어서, throttle에서의 이전 초(시간)에 불렀던 함수가 호출 돼 위 높이를 계산하고 아래로 내려오는 문제였다.

그래서 사용자가 직접 스크롤을 내렸을 경우에(scrollY 계산) 맨 아래인지 체크하는 함수를 추가하고, debounce를 적용하여 throttle 함수 재 호출하는걸 막았다.

6. 개선해야할 내용

스크롤 이벤트에서는 현재의 높이 값을 알기 위해offsetTop 을 사용하는데 정확한 값을 가져오기 위해 매번 layout을 새로 그리게 된다.

layout을 새로 그린다는 것은 렌더 트리를 재생성한다는 뜻인데, reflow라고도 불리우는 이 일련의 과정이 반복되면 당연히 브라우저의 성능이 저하되고 화면의 버벅거림이 생길 수 밖에 없다.

7. 참고

1. Debounce와 Throttle은 뭘까?

2. Debounce, Throttle

profile
끊임없이 발전해가는 개발자.

0개의 댓글