Overview

현재 브랜디 안드로이드 앱에서는 유저들과 수많은 상호작용(타이핑, 클릭, 스크롤 등)을 하고 있습니다. 물론, iOS나 웹에서도 공통적으로 이런 상호작용을 하고 있지만요.

이를테면 브랜디에서는 사용자가 원하는 상품을 검색을 하기 위해 검색 키워드를 입력한다거나, 로그인 버튼을 터치하여 로그인을 하거나, 상품을 더 보기 위해 스크롤을 내리는 것과 같은 것들이 있습니다.

보통 이러한 상호작용을 이벤트라고 하는데, 자주 발생하는 이벤트 전부를 필터링 없이 서버에 요청한다면 큰 부하를 주게됩니다. 스크롤 하는 이벤트 내내 특정 함수를 실행시키게 된다면? 앱 자체의 속도도 매우 느려지겠죠. 그렇다면 이러한 이벤트를 처리하기 위해서 주로 어떠한 방식을 사용하고, 안드로이드에서는 어떻게 처리할까요?

1. Debounce

저는 개발을 하면서 매번 궁금했던 부분이 있었는데 검색 사이트의 추천 검색어가 표시되는 알고리즘입니다.

athena


위의 이미지처럼 글씨가 입력될 때 순차적으로 추천 검색어가 바뀌어 표시되는 것을 볼 수 있습니다. 그렇다면 해당 알고리즘은 어떻게 구현되어 있을까요?

  • 가설1 : 이벤트가 발생할 때마다 이벤트를 캐치하여 서버에 전송한다.

    앞서 이야기한대로, 매번 발생하는 이벤트 전부를 필터링 없이 서버에 요청한다면 서버에 큰 부하를 주게됩니다. 심지어는 유저가 검색한 부분에 오타가 있어서 내용을 삭제하는 과정도 하나의 이벤트로 취급되기 때문에, 더 많은 부하를 만들어낼 수 있습니다.

  • 가설2 : 일정 시간마다 이벤트를 캐치하여 서버에 전송한다.

    매 100ms마다 이벤트를 보내게 된다면, (빈 텍스트를 이벤트로 취급하지 않는다고 하더라도) 유저가 한 글자를 타이핑한 시점 이후엔 지속적으로 이벤트를 전송하게 됩니다. 유저가 타이핑하는 속도가 느리다면 (debounce라는 텍스트가 목표하는 텍스트일 때) d → d → de → deb → .. debounce 라는 최종 텍스트에 다다르기까지 수많은 이벤트가 쌓이게 됩니다.

따라서 해당 두 가설은 추천 검색어 예제에 적합하지 않은 알고리즘이라고 볼 수 있습니다. 결과적으로 정답은 아래와 같습니다.

정답 : 이벤트를 그룹핑하여 일정 시간동안 이벤트가 들어오지 않으면 서버에 전송한다.

이 문장이 바로 Debounce의 기본적인 개념입니다.

Debounce를 브랜디 앱에서는 어떤 부분에서 사용하고 있을까요?

athena


etEmail.textChanges()
    .debounce(700) // 사용자의 입력 이벤트가 0.7초의 텀 동안 입력되지 않을 때 이벤트를 받아온다.
    .filter {
        // 이메일에 들어갈 수 없는 문자 필터링 (EX. 이모티콘, 특수문자 등)
    }
    .onEach {
        // 이메일 유효성 검사
    }

바로 이처럼 이메일 유효성 검사를 할 때 사용자의 타이핑(textChanges) 이벤트가 잠시 멈추는 시점이 일정 시간 (0.7초) 이상의 텀을 갖게 되면, 해당되는 마지막 이벤트를 받아오는 것이 Debounce입니다.

이메일이 유효한 경우에는 체크 표시를,

이메일이 유효하지 않은 경우에는 “이메일이 유효하지 않습니다”라는 문구를,

이메일이 입력되지 않은 경우에는 “이메일을 입력해주세요”라는 문구를 표시하기 위해 이처럼 이메일 유효성 검사를 하게 됩니다.

그림으로 표현하자면 아래와 같은 방식입니다.

athena

임시로 예제를 만들어서 위의 사진과 같은 debounce를 직접 실행해보면 다음과 같은 결과가 출력됩니다.

etDebounce.textChanges()
    .debounce(1000L) // 1초의 이벤트 텀이 있을 때 이벤트를 캐치한다.
    .onEach {
        Log.d("brandi", "Debounce : $it")
    }
athena


2. Throttle

그렇다면 앞서 말한 ‘가설2. 일정 시간마다 이벤트를 캐치하여 서버에 전송한다’ 에 해당하는 개념이 있다면 어느 곳에 사용될까요?

브랜디 안드로이드 앱에서는 찜 하기, 아이디 유효성 검사, 이름 길이 제한, 휴대폰 번호 포맷팅 등과 같은 부분에서 throttle을 사용하고 있습니다.

이벤트를 일정 시간마다 ‘첫번째’에서 캐치할 것인지, ‘마지막’에서 캐치할 것인지에 따라 throttle은 throttleFirst와 throttleLatest(혹은 Sample)으로 분류가 나뉘게 됩니다.

throttleFirst

throttleFirst는 일정 시간마다 첫번째 이벤트를 받아오도록 필터링합니다.

브랜디에서는 검색어 입력을 하기 ‘시작’하면 오른쪽에 검색어 삭제 버튼(x)이 나타나게 되는데요, qwerty를 입력하는 과정에서 q를 입력함과 동시에 삭제 버튼(x)이 나타나는 것을 보실 수 있습니다.

athena

해당 코드는 이런 식으로 작성되어 있습니다.

etSearchKeyword.textChanges()
    .throttleFirst(300) // 0.3초마다 첫번째 이벤트를 받아온다.
    .onEach {
        // 입력된 텍스트가 비어있지 않다면 삭제 버튼(x) 표시
        ibtnDeleteKeyword.isVisible = it.isNotEmpty() 
    }

일정 시간(0.3초) 동안 쌓인 이벤트 중에 가장 처음 이벤트를 가져옵니다. 만약 검색어의 내용이 빈 텍스트인 경우에는 삭제 버튼을 표시할 필요가 없기 때문에, 삭제 버튼을 숨기게 됩니다.

이 밖에도 찜하기 버튼을 유저가 순간적으로 여러 번 클릭하는 경우 가장 처음 이벤트만을 받아와서 처리하도록 만드는 로직에도 사용됩니다. 그림으로 보면 아래와 같이 필터링됩니다.

athena

위의 그림을 실제로 예제로 만들어보면 아래와 같이 코드를 작성할 수 있고, 가장 앞에 있는 이벤트 로그가 출력됩니다.

etThrottleFirst.textChanges()
    .throttleFirst(1000L) // 1초의 이벤트 텀마다 첫 번째 이벤트를 캐치한다.
    .onEach {
        Log.d("brandi", "throttleFirst : $it")
    }
athena


throttleLast (sample)

throttleLast는 언뜻 보기에 Debounce와 동작이 비슷합니다. 하지만 이벤트를 그룹핑하여 일정 시간 이후에 가져오는 것이 아니라 일정 시간마다 쌓인 이벤트 중 가장 마지막 이벤트를 가져오게 되는데, throttleLatest을 사용하는 브랜디의 적용 부분을 살펴보겠습니다.

athena
etComments.textChanges()
    .throttleLatest(100) // 0.1초마다 마지막 이벤트를 받아온다.
    .map {
        it.trim() // 최종 확정된 이벤트에서 문자열 앞/뒤의 공백을 제거한다.
    }
    .onEach { event ->
        if (event.isNotBlank()) { // 입력된 텍스트가 비어있지 않다면
            ibSend.setImageResource(활성화된 이미지) // 보내기 버튼 설정
        } else {
            ibSend.setImageResource(비활성화된 이미지)
        }
    }

덧글을 입력할 때 일정 시간(0.1)초마다 마지막 이벤트를 받아 와서, 해당 텍스트가 비어있지 않다면 오른쪽의 전송 버튼을 활성화 시킵니다. 만약 텍스트가 비어있다면 아이콘이 비활성화되는 것을 볼 수 있습니다.

athena
etThrottleLatest.textChanges()
    .throttleLatest(1000L) // 1초의 이벤트 텀마다 마지막 이벤트를 캐치한다.
    .onEach {
        Log.d("brandi", "throttleLatest : $it")
    }
athena


3. Debounce와 Throttle의 차이

그렇다면 두 방식의 차이가 무엇일까요?

throttle의 경우 유저가 지정해둔 특정 시간마다의 이벤트 실행을 보장하지만,

Debounce는 특정시간이 지나기 이전에 꾸준히 이벤트를 발생시킬 경우 지속적으로 이벤트를 무시하게 된다는 점이 가장 큰 차이점입니다.

이런 두 방식과 같은 이벤트 핸들링을 사용하면 서버 리소스적인 측면의 이득 뿐만 아니라 함수 실행 횟수를 제한하는 것에도 사용할 수 있기 때문에 유저에게도 더 좋은 사용 경험을 가져다주게 됩니다.

Conclusion

직접 실무에 참여하게 되면서 막연히 추측하고 있던 개념들을 어떻게 사용하고, 적용하는 지를 알게되었습니다. (브랜디 안드로이드에서는 위의 코드처럼 코틀린 플로우(Kotlin Flow)를 사용하여 Debounce와 Thottle을 처리하고 있습니다.)

꽤 기초적인 내용이지만 신입 개발자로서 명확히 공부해보고, 직접 예제를 작성해보는 좋은 경험을 얻게된 것 같습니다. 다른 신입 개발자들에게도 소소한 도움이 되기를 바라며 글을 마칩니다. :)

참고자료

https://medium.com/@progjh/throttle-debounce-개념-잡기-19cea2e85a9f

https://github.com/ReactiveX/RxJava/wiki/Backpressure


이승연 | APP실 MA팀
브랜디, 오직 예쁜 옷만