All Posts

Debounce와 Throttle

Debounce and Throttle

Debounce와 Throttle


들어가며

이 글은 런던에서 활동하는 프론트엔드 엔지니어 David Corbacho의
"Debouncing and Throttling Explained Through Examples"를
번역한 것입니다.

DebounceThrottle은 시간이 지남에 따라 함수를 몇 번이나 실행 할지를 제어하는 두 가지의 비슷하지만 다른 기술입니다.

DebounceThrottleDOM 이벤트에 함수를 붙일 때에 특히 유용합니다. 왜냐하면 이 두 가지 기술이 이벤트와 함수 실행 사이에 제어 계층을 제공하기 때문입니다. (DOM 이벤트는 매우 다양할 수 있으며, 그것을 얼마나 자주 내보내게 할 지 직접 제어하기는 어렵습니다)

예컨대, 스크롤 이벤트에 대해 이야기 해 봅시다. 다음 예제를 보시죠.

트랙패드, 마우스 휠 또는 스크롤바를 사용하여 스크롤 할 때는 초당 최대 30 개의 이벤트가 발생합니다. 하지만 모바일에서 천천히 스크롤하면 초당 최대 100 개의 이벤트가 발생하게 됩니다. 스크롤 핸들러가 이러한 실행 속도를 감당할 수 있을까요?

2011 년에 실제로 트위터 웹사이트에서 문제가 나타났습니다. 모바일에서 트위터 피드를 스크롤할 때 속도가 매우 느려지고 응답이 없었습니다. John Resig(역자 주: jQuery 의 창시자)은 스크롤 이벤트에 값 비싼 기능을 직접 붙이는 것이 얼마나 나쁜지에 대해 역설했습니다.

그 당시에 John Resig이 제안한 솔루션은 onScroll 이벤트 외부에서 250ms 마다 루프를 실행하는 방법이었습니다. 그렇게하면 핸들러가 이벤트에 직접적으로 연결되지 않습니다. 이 간단한 기술 덕분에 트위터는 사용자 경험을 향상시킬 수 있었습니다.

요즘에는 이벤트를 처리할 때에 위보다 더 정교한 방법들이 사용됩니다. 구체적으로 Debounce, Throttle, 그리고 requestAnimationFrame에 대해 소개하겠습니다.

Debounce

Debounce 를 사용하면 여러 개의 순차적 호출을 하나로 “그룹화”할 수 있습니다.

Debounce

Debounce


예를 들어, 여러분이 엘리베이터에 있다고 상상해보세요. 문이 닫히려 하는데 갑자기 다른 사람이 타려고 합니다. 그럼 엘리베이터가 움직이지 않고 문이 다시 열립니다. 그 이후에도 다른 사람이 또 뛰어들며 위와 같은 상황이 반복된다고 칩시다. 그러면 엘리베이터는 올라가거나 내려갈 수 없게 되지만, 자원을 최적화할 수는 있습니다.

아래 CodePen에서 직접 실험해보십시오. 마우스 오버를 하면 작동하기 시작하고 클릭할 때마다 이벤트가 발생합니다.

순차적으로 빠르게 발생하는 이벤트들이 어떻게 단일 Debouncing 이벤트로 표현되는지 볼 수 있습니다. 하지만 이벤트가 큰 간격으로 트리거되면 Debouncing은 발생하지 않습니다.

Leading edge (or “immediate”)

특정 이벤트가 아예 끝나기 전까지는 Debouncing이 함수 실행을 트리거하기 전에 대기한다는 것을 알 수 있습니다. 왜 함수가 즉시 실행되도록 트리거하지 않고 원래의 non-debounced 핸들러와 같이 동작할까요? 순차적으로 빠르게 발생하는 이벤트 중에 짧은 간격의 휴식점이 있으면 함수는 실행되지 않습니다.

다음은 Leading flag가 있는 예제입니다.

Leading Debounce

Leading Debounce


underscore.js 에서는 위와 같은 기능을 Leading 대신 Immediate로 부릅니다.

직접 사용해보십시오.

Debounce 구현

처음으로 JavaScript 에서 Debounce를 구현한 것은 John Hann이었습니다. (그의 블로그에서 처음으로 Debounce라는 용어를 사용)

얼마 지나지 않아 Ben AlmanjQuery 플러그인을 만들었고 (하지만 더 이상 유지 보수되지 않음), 1 년 후 Jeremy AshkenasDebounceunderscore.js에 추가했습니다. lodashDebounce가 추가된 것은 그 이후였습니다. (역자 주: lodashunderscore.jssuperset이며, underscore.js의 API 보다 lodash의 API 가 대체로 더 성능이 좋다고 합니다.)

3 가지 구현(Debounce, Throttle, 그리고 requestAnimationFrame)은 내부적으로 약간 다르지만 인터페이스는 거의 동일했습니다.

2013 년 underscore.js_.debounce 함수에서 버그가 발견되어 이를 수정하기 시작한 이후로 lodash와의 구현이 서로 차별화되었습니다.

lodash_.debounce_.throttle에 더 많은 기능을 추가했습니다. 원래의 Immediate 플래그는 LeadingTrailing 옵션으로 대체되었습니다. 하나 또는 둘 다를 선택할 수 있지만, 디폴트로 Trailing edge 만 활성화되어 있습니다.

새로운 maxWait 옵션 (현재는 lodash에만 존재)은 여기서 다루지 않지만 매우 유용하니 따로 공부하면 좋습니다. 실제로, Throttle 기능은 maxWait 옵션과 함께 _.debounce를 사용하여 정의되어있습니다. (소스 코드)

Debounce 예제들

Resize 예제

데스크톱에서 브라우저 창 크기를 조정할 때 크기 조정 핸들을 드래그하면서 많은 Resize 이벤트를 내보낼 수 있습니다.

브라우저 크기를 조절하면서 직접 눈으로 확인해보세요!

위에서 보다시피, 사용자가 브라우저의 크기 조정을 끝내는 최종 값에만 관심이 있으므로 Resize 이벤트에 대해서 디폴트로 Trailing 옵션을 사용하고 있습니다.

Ajax 예제

사용자가 타이핑할 때에 50ms 마다 서버에 Ajax 요청을 보낼 필요가 있을까요? _.debounce를 사용하면 추가 작업을 피할 수 있으며 사용자가 타이핑을 중단할 때만 요청을 보낼 수 있습니다.

여기에 Leading flag 를 붙이는 것은 의미가 없습니다. 우리는 사용자가 마지막으로 입력하는 문자만 기다리면 됩니다.

위와 비슷한 예시는 사용자가 타이핑을 멈출 때까지 validation 작동을 멈추는 것이 있습니다. “암호가 너무 짧습니다”라는 메시지 유형이 그것입니다.

Debounce 와 Throttle 의 사실과 오해

자신만의 Debounce, Throttle 기능을 만들 수도 있습니다. 하지만 일반적으로는 underscore.jslodash를 사용하는 걸 추천합니다. 만약 _.debounce_.throttle 함수만 필요하다면 lodashcustom builder를 활용해 2KB 로 축소된 라이브러리를 쓸 수도 있습니다. 다음과 같은 간단한 command으로 빌드하십시오.

npm i -g lodash-cli
lodash include = debounce, throttle

대부분은 webpack / browserify / rollup 을 사용하여 lodash / throttlelodash / debounce 또는 lodash.throttlelodash.debounce 패키지를 사용합니다.

하지만 공통적인 문제는 _.debounce 함수를 두 번 이상 호출한다는 점입니다.

// WRONG
$(window).on('scroll', function() {
  _.debounce(doSomething, 300)
})

// RIGHT
$(window).on('scroll', _.debounce(doSomething, 200))

Debouncing 함수에 대한 변수를 생성하면, lodashunderscore.js에서 사용할 수 있는 private 메소드인 debounced_version.cancel()을 호출할 수 있습니다.

var debounced_version = _.debounce(doSomething, 200)
$(window).on('scroll', debounced_version)

// If you need it
debounced_version.cancel()

Throttle

_.throttle을 사용하면 함수가 X milliseconds마다 한 번 이상 실행되도록 허용하지 않습니다.

이것과 Debounce 사이의 가장 큰 차이점은 Throttle은 적어도 X milliseconds마다 정기적으로 기능 실행을 보장한다는 것입니다.

debounce 와 같은 방식으로, 스로틀 기술은 Ben 의 플러그인 인 underscore.js 와 lodash 에 적용됩니다.`

Throttle 예제들

무한 스크롤

일반적인 상황으로서, 사용자가 무한 스크롤 페이지를 아래로 스크롤하는 예시를 들 수 있습니다. 사용자가 얼마나 바닥으로부터 떨어져 있는지 확인해야합니다. 사용자가 맨 아래에 닿으면 Ajax 를 통해 더 많은 콘텐츠를 요청하여 페이지에 추가해야합니다.

이러한 상황에서는 _.debounce가 유용하지 않습니다. 사용자가 스크롤을 멈출 때만 트리거되기 때문입니다. 우리는 사용자가 맨 아래에 도달하기 전에 내용을 가져오기 시작해야합니다. _.throttle을 통해서 사용자가 바닥으로부터 얼마나 멀리 있는지 항상 확인할 수 있습니다.

requestAnimationFrame (rAF)

requestAnimationFrame은 함수 실행 속도를 제한하는 또 다른 방법입니다. 이는 _.throttle(dosomething, 16)으로 생각할 수 있습니다. 그러나 훨씬 정확도가 높기 때문에 더 나은 정확도를 목표로 하는 브라우저 기본 API 입니다. 아래와 같은 장단점을 고려하여 Throttle 기능의 대안으로 rAF API 를 사용할 수 있습니다.

장점
  • 60fps (16ms 프레임)를 목표로하지만 내부적으로 렌더링을 예약하는 방법에 대한 최적의 타이밍을 결정합니다.
  • 상당히 단순하고 표준적인 API 로 향후에도 변경되지 않습니다. 유지 보수가 쉽습니다.
단점
  • .debounce 또는 .throttle과 달리 rAF는 시작/취소를 사용자가 직접 관리해야 합니다.
  • 브라우저 탭이 활성화되어 있지 않으면 실행되지 않습니다.
  • 최신의 모든 브라우저가 rAF를 제공하지만 IE9, Opera Mini 및 오래된 Android에서는 여전히 지원되지 않습니다.(polyfill 이 필요함)
  • rAFNode.js에서 지원되지 않으므로 파일 시스템 이벤트를 조절하기 위해 서버에서 사용할 수 없습니다.

경험적으로, JavaScript 함수가 “painting” 되거나 속성을 직접 애니 메이팅하는 경우 그리고 요소 위치를 다시 계산하는 모든 작업에서 rAF를 사용하는 걸 추첩합니다.

Ajax 요청을하거나 (CSS 애니메이션을 트리거 할 수있는) 클래스 추가/제거 여부를 결정하려면 _.debounce 또는 _.throttle을 고려해보십시오. 낮은 실행 속도 (예 : 16ms 대신 200ms)를 설정할 수 있습니다.

rAF 예제들

Paul Lewis의 글을 참고하여 여기서는 scroll 이벤트에 rAF 사용하는 예제만 다룰 것입니다.

Throttle과 비슷한 퍼포먼스를 보이는 것 같아도, 아마도 rAF는 보다 복잡한 시나리오에서 더 나은 결과를 제공할 것입니다.

결론

debounce, throttle, 그리고 requestAnimationFrame을 사용하여 이벤트 핸들러를 최적화하십시오. 각 기술은 약간 다르지만, 이 세 가지 기술은 모두 유용하고 상호 보완적입니다.

정리하자면:

  • debounce : 키 스트로크와 같은 갑작스런 이벤트를 하나의 이벤트로 그룹화합니다.
  • throttle : X milliseconds마다 실행의 흐름을 일정하게 유지합니다. 200ms 마다 스크롤 위치를 확인하여 CSS 애니메이션을 트리거하는 것과 같습니다.
  • requestAnimationFrame : Throttle의 대안으로서, 함수가 화면에서 요소를 다시 계산하고 렌더링할 때 부드러운 변경이나 애니메이션을 보장하고자 할 때 사용합니다. (참고: IE9 는 지원되지 않습니다)
Published 7 Jul 2018

I'm interested in React, GraphQL.