(Android) Debounce와 Throttle 이해하기

Flow Operator— 1. debounce vs sample

Jaesung Lee
jaesung dev
9 min readFeb 23, 2023

--

Photo by Jakob Owens on Unsplash

비동기 처리를 위해 Coroutine Flow를 주로 사용하고 있습니다. 개발을 하다보니 아는 부분들만 기계적으로 쓰고있다는 생각이 들었고, 사용할 수 있는 연산자들을 한 번 정리해봐야 겠다는 생각이 들었습니다. 그렇게 개발자 문서를 확인해보니 중간, 종단 연산자들을 포함해 약 60개 정도의 연산자들이 있는 것을 확인했습니다. 물론, 모든 연산자들을 완벽하게 학습해서 개발할 때 모두 사용하겠다는 무모한 확신은 하지 않지만, 주로 사용되는 연산자들에 대해서는 완벽하게 정리해야겠다는 생각이 들었습니다.

Flow 주요 연산자 정리 시리즈의 첫 번째 글인 Debounce와 Throttle에 대한 정리입니다.

Debounce와 Throttle의 사전적 의미

오랜만에 회로도를 보니 아찔..

debounce를 사전에 검색해보면 전자 공학에서 사용되는 기계식 스위치에서 짧은 접촉을 만들 때 형성되는 전류의 bounce를 제거해주는 것으로 설명합니다. 버튼을 눌렀다가 때는 과정에서 발생할 수 있는 비정상적인 전류(bounce)를 제거한다고 이해할 수 있습니다. 회로 설계에 debounce를 적용하면 더 이상 불규칙한 전류가 발생하지 않는다고 합니다.

throttle은 사전에 유체 흐름이 압축이나 차단에 의해 통제되는 구조라고 설명하고 있습니다. 보통 비행기에 부착되어있는 연료 조절 장치로 사용되는 레버를 의미한다고 합니다.

프로그래밍에서의 Debounce와 Throttle

안드로이드 뿐만 아니라 프론트엔드 개발을 할 때는 사용자 상호작용에 따른 이벤트 처리를 중요하게 생각해야 합니다. 특히, 이러한 이벤트로 API 서버 호출을 해야하는 경우에 모든 이벤트를 그대로 전달하면 서버 리소스 낭비와 과부하로 이어질 수 있습니다. 따라서, 자주 발생하는 이벤트들을 적절히 Filtering 해야할 필요가 있습니다. Debouncing과 Throttling 기법은 이러한 이벤트들을 Flitering하는 기법의 종류입니다.

Debouncing

프로그래밍에서 Debounce는 발생하는 이벤트를 그룹화하여, 일정 시간동안 이벤트가 발생하지 않으면 마지막 이벤트를 전달하는 기법입니다. 이 시간을 위해 timer를 둔다고 이해할 수 있습니다. 즉, 이벤트가 발생하다가 잠시 멈추는 시점이 일정 delay 만큼의 텀을 갖게 되면 마지막 이벤트를 전달합니다. 또한, 새로운 이벤트가 발생할 때마다 timer를 초기화하고 다시 시작하고 delay가 지났을 시점의 마지막 이벤트를 전달합니다.

이러한 특징들은 아래 그림을 통해 확인할 수 있습니다.

출처 : 브랜디 랩스

1부터 5까지의 이벤트가 불규칙적으로 발생하고, delay를 250ms만큼 설정하는 예시입니다. 1에 해당하는 이벤트가 발생하고 timer를 돌리고 250ms 안에 발생하는 이벤트가 없을 경우 해당 이벤트를 전달합니다. 이 후, 2 3 4에 해당하는 이벤트가 연속적으로 들어오게 되며 각 이벤트가 들어올 때마다 timer를 돌리지만 250ms 안에 연속적으로 들어오기 때문에 가장 마지막 이벤트인 4를 전달합니다. 최종적으로 마지막 5에 해당하는 이벤트도 동일하게 동작하여 전달됩니다.

Debouncing 예시

Debounce를 사용하는 대표적인 예시는 바로 순간 검색 기능과 회원가입 필드 유효성 검사입니다.

순간 검색기능을 구현한다면 모든 입력 이벤트를 전달해 하나 하나 확인하게 된다면 앞서 설명한 서버의 과부하로 이어질 것입니다. 따라서, 적절한 delay를 두어 마지막 텍스트 이벤트만 전달해 과부하를 줄일 수 있습니다. 예를 들어, 안드로이드를 검색한다면 ㅇ → 아 → 안 → 안ㄷ → … 식이 아닌 안드로이드만 전달하는 것입니다.

회원가입 필드에서 이메일 유효성 검사를 한다고 해도 동일한 방식일 것입니다. 중복 계정을 확인하기 위해 API 통신이 이루어질텐데, 적절한 delay를 두어 API를 호출하는 것입니다. 추가로, 텍스트가 비어있거나, 부적절한 문구가 들어있는 경우에 filter하는 식으로도 구현할 수 있을 것입니다.

debounce 순간 검색(Instant Search) 예시

Throttling

Throttle은 일정 주기마다 이벤트를 캐치해 전달하는 기법입니다. Debounce는 마지막 이벤트를 기점으로 delay에 해당하는 timer를 돌리지만, Throttle은 이벤트와 관계 없이 timer를 돌린 상태에서 그 주기 안에서 발생하는 이벤트를 처리합니다. 만약, 주기를 1000ms로 설정한 timer를 돌린다면 해당 주기동안 여러 이벤트가 발생하더라도 단 하나의 이벤트만 전달하게 됩니다.

구현 방식에 따라 한 주기동안 발생하는 이벤트 중 첫 번째 이벤트를 전달할 지(throttleFirst), 마지막 이벤트를 전달할 지(throttleLast) 나누기도 합니다. 마찬가지로 아래 그림에서 확인할 수 있습니다.

출처 : 브랜디 랩스

위 그림은 발생하는 이벤트 중 첫 번째 이벤트를 전달하는 예시입니다. 발생하는 이벤트들과는 독립적으로 timer가 돌고 있고, 해당 주기 동안 발생하는 이벤트들 중 첫 번째 이벤트를 전달합니다. 그림 상 두 번째 주기에 해당하는 쪽을 살펴보면 1~7까지 연속적으로 이벤트가 발생하지만 timer가 끝날 때의 마지막 이벤트는 3이기 때문에 해당 주기에서 발생했던 첫 번째 이벤트인 1(노란색)이 전달됩니다.

출처 : 브랜디 랩스

동일한 상황에서 throttle 종류만 바꿨을 때의 결과입니다. sample은 이후에 자세하게 살펴보겠지만 throttleLast에 해당합니다. 즉, 발생하는 이벤트 중 마지막 이벤트를 전달하는 예시에 해당합니다. 위에서 살펴본 상황과 동일하게 발생하는 이벤트와는 독립적으로 timer가 돌고 있고, 해당 주기동안 발생한 이벤트들 중 마지막 이벤트를 전달합니다. 두 번째 주기에서의 마지막 이벤트는 3이기 때문에 throttleFirst와는 달리 3(녹색)을 전달합니다.

Throttling 예시

Throttling을 사용하는 대표적인 예시는 버튼 중복 클릭 방지 및 활성화 처리입니다.

좋아요나 스크랩 버튼에 아무런 처리가 되어있지 않은 채 사용자가 사용하게 된다면 반복적으로 버튼을 클릭하는 상황이 발생할 수 있습니다. 이 경우, 수많은 API 호출이 발생하고 서버의 과부하가 일어나게 됩니다. ThrottleFirst를 통해 첫 번째 이벤트만 전달하면 이러한 문제를 해결할 수 있습니다. 좋아요 버튼이 비활성화된 상태에서 아무리 많은 클릭 이벤트가 발생하더라도 서버에는 좋아요 활성화에 대한 이벤트 한 번만 전달되기 때문에 부하를 줄일 수 있게 됩니다.

예를 들어, 메시지 전송 기능에서 전송 버튼이 항상 활성화되어 있다면 비어있는 텍스트가 전송될 수도 있습니다. 즉, 불필요한 API 호출로 인해 리소스를 낭비할 수도 있게됩니다. 이 경우, throttleLast를 이용해 텍스트 입력의 마지막 이벤트를 받아서 해당 텍스트가 비어있다면 전송 버튼 비활성화, 텍스트가 비어있지 않다면 전송 버튼을 활성화하여 불필요한 API 호출을 방지하고, 사용자에게 더 좋은 UX를 제공할 수 있습니다.

좋아요 활성화 및 비활성화

Debounce, Throttle 구현

Flow는 이미 debounce 메서드를 자체적으로 제공하고 있습니다. 앞에서 살펴본 내용들을 바탕으로 쉽게 적용할 수 있습니다. 그러나 throttle은 지원하지 않습니다. 대신, sample이라는 메서드를 사용하여 throttleLast를 대체할 수 있습니다. throttleFirst는 개발자가 직접 확장 함수 형태로 구현해야 합니다.

Debounce

debounce는 위와 같이 구현되어 있습니다. 파라미터로 timeout을 명시적으로 지정할 수 있으며, 방출되는 데이터에 따라 timeout을 다르게 지정할 수도 있습니다. 내부적으로는 debounceInternal을 통해 동작합니다.

아래처럼 사용할 수 있습니다.

예시에서는 400ms 이내에 발생하는 이벤트들은 debounce 시키고 있기 때문에 로그와 같이 출력될 것입니다.

Debounce를 순간 검색에 사용한다면 아래처럼 사용할 수 있습니다.

Throttle

RxJava에는 throttle 관련 메서드가 있어서 쉽게 사용할 수 있지만, Flow에는 해당 기능을 별도로 제공하지 않습니다. 기본적으로는 개발자가 직접 구현해주어야 하지만, throttleLast의 경우 Flow의 sample 메서드를 이용하여 구현할 수 있습니다.

sample도 debounce와 유사하게 시간을 파라미터로 넘깁니다. 하지만, debounce와는 달리 해당 시간 파라미터를 기반으로 ticker(timer)를 만들어 동작시킵니다. 즉, 지정된 샘플링 주기에 해당하는 데이터를 수집한다고 이해할 수 있습니다.

sample은 아래처럼 사용할 수 있습니다.

예시에서는 2000ms 간격의 timer를 동작시키고 해당 timer가 만료되면 마지막으로 emit한 데이터를 수집하게 됩니다.

throttleFirst는?

throttleFirst를 구현하는 방법은 여러 가지가 있습니다. 가장 직관적인 방법 중 하나는 아래와 같이 구현하는 것입니다.

위와 같이 구현하게 되면 마지막 발행 시간과 현재 시간을 비교하여 데이터를 발행하고 나머지 데이터는 무시하게 됩니다.

Coroutine Flow를 사용하여 중복 클릭 방지를 구현하는 것에 대해 의문을 제기하는 글을 봤습니다. 위에 작성한 코드와 유사하게 충분히 간단한 코드로 중복 클릭을 방지할 수 있습니다. 배보다 배꼽이 더 큰 느낌이기는 하지만, 만약 throttleFirst와 같은 특정 기능이 필요하다면 충분히 확장 함수로 구현하여 사용할 가치가 있다고 생각합니다.

--

--