🧑🏻💻오늘의 할 일
앞으로 프로젝트를 진행하면서 사용한 기술들과 다양한 이슈들에 대해서 글을 적어볼 예정입니다. 그 시작으로 오늘은 버튼의 중복 클릭을 막는 방법에 대해 소개해보겠습니다. 그전에 저희 다온길 프로젝트에 대해서 소개하겠습니다. 다온길은 다 함께 오는 길이라는 뜻으로, 한국 관광 정보 공사 무장애 여행 API를 사용해 장애인, 노약자, 영유가 가족들이 이용할 수 있는 여행지, 관광지를 소개하는 어플리케이션으로 무장애 여행(Barrier-Free Travel)은 장애인, 고령자, 임산부 등 이동에 제약이 있는 사람들이 불편함 없이 여행을 즐길 수 있도록 배려하는 여행 형태입니다.
👨🏻🏫 Throttle
사실 이전에 스로틀과 디바운싱에 대한 글을 쓴 적이 있었습니다. 디바운싱은 바로 개인 프로젝트에서 사용했지만 스로틀은 마땅한 사용처가 없어 도입하지 못했는데 이번 프로젝트에서 필요한 상황이 생겨 사용할 수 있었습니다. 간략하게 디바운싱과 스로틀에 대해 설명하자면 디바운스는 검색어 자동 완성 API 호출 같이 API 호출이 잦은 곳에서 매번 검색어를 입력 할 때 마다 API를 호출할 경우 서버엔 트래픽 과부하가 생길 수 있습니다. 이에 대한 대처 방안으로 디바운싱을 사용하면 특정 이벤트가 트리거 된 후 일정 시간 동안 이벤트가 발생하지 않을 경우 다음 동작을 수행할 수 있습니다. 예를 들어 검색어를 입력하고 잠시동안 검색을 하지 않을 경우 API를 호출해 연관 검색어를 보여주는 등의 동작이 가능합니다.
이렇게 디바운싱은 마지막 이벤트를 기점으로 동작하는 반면, 스로틀은 일정 주기동안 일어나는 이벤트들을 하나의 그룹으로 묶어 단 하나의 이벤트만 일어나게 합니다. 이 때, 이벤트를 캐치하는 시점에 따라서 Throttle First, Throttle Last로 나눌 수 있습니다.
📌 Issuses
문제 상황은 다음과 같습니다. 버튼을 클릭 이벤트가 발생하면 바텀 시트를 띄우고 있습니다. 이 때 버튼을 빠르게 클릭할 경우 바텀 시트가 여러 번 호출되는 문제가 발생했습니다.
📌 코드 구성
const val THROTTLE_DURATION = 2000L
fun View.clicks(): Flow<Unit> = callbackFlow {
setOnClickListener {
this.trySend(Unit)
}
awaitClose{ setOnClickListener(null) }
}
Trotthle은 flow로 구현되기 때문에 먼저 클릭 이벤트로 발생한 콜백을 flow 형태로 만들어줘야 합니다.
calllbackFlow는 클릭 이벤트로 발생하는 callback을 flow builder를 사용해 flow로 만들어 줍니다. flow builder 내부에선 trysend를 사용해 클릭 리스너 내부에서 클릭 이벤트가 발생했다는 사실만을 전달하기 위해 Unit을 반환합니다.
awaiteClose는 flow가 cancel, close 되었을 때 호출 되므로 사용한 리소스를 해제하는 구문이 들어가야 하며 이를 명시하지 않을 경우 메모리 누수가 일어날 수 있기 때문에 반드시 block의 맨 마지막에 명시되어야 합니다.
fun <T> Flow<T>.throttleFirst(periodMillis: Long): Flow<T>{
require(periodMillis > 0) { "period should be positive" }
return flow{
var lastTime = 0L
collect{ value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastTime >= periodMillis){
lastTime = currentTime
emit(value)
}
}
}
}
이 확장 함수는 일정 시간 동안 이벤트를 수집하고 마지막 발행 시간과 현재 시간을 비교해서 첫 번째 이벤트를 발행합니다.
lastTime은 마지막 이벤트가 방출된 시간을 저장해 새로운 이벤트가 주어진 시간 간격 (periodMillis) 내에 발생했는지 여부를 판단하는 데 사용됩니다.
예시
예를 들어, periodMillis가 1000ms(1초)라고 가정합니다.
- 첫 번째 이벤트 발생: lastTime이 초기값(0)이므로, 이벤트가 방출되고 lastTime이 현재 시간으로 설정됩니다.
- 두 번째 이벤트가 500ms 후에 발생: currentTime - lastTime이 500ms이므로, 이벤트가 방출되지 않습니다.
- 세 번째 이벤트가 1000ms 후에 발생: currentTime - lastTime이 1000ms 이상이므로, 이벤트가 방출되고 lastTime이 현재 시간으로 업데이트됩니다.
활용
// ViewExt.kt
const val THROTTLE_DURATION = 2000L
fun View.clicks(): Flow<Unit> = callbackFlow {
setOnClickListener {
this.trySend(Unit)
}
awaitClose{ setOnClickListener(null) }
}
fun View.setClickEvent(
uiScope: CoroutineScope,
windowDuration: Long = THROTTLE_DURATION,
onClick: () -> Unit
){
clicks()
.throttleFirst(windowDuration)
.onEach { onClick.invoke() }
.launchIn(uiScope)
}
// Fragment
this@SearchMainFragment.repeatOnViewStarted {
viewModel.physicalDisabilityOptions.collect { options ->
btnPhysicalDisability.setClickEvent(this) {
showBottomSheet(options, DisabilityType.PhysicalDisability)
}
}
}
- clicks( ): View의 확장 함수로 정의한 함수로, View의 클릭 이벤트를 Flow <Unit>으로 변환합니다.
- onEach{ }: Flow의 각 이벤트에 대해 주어진 onClick 콜백 함수를 호출합니다. 이벤트가 방출될 때마다 onClick 함수가 실행됩니다.
- launchIn( ) : Flow를 uiScope 내에서 실행합니다.
실제 코드는 아래 링크에서 확인할 수 있습니다.
https://github.com/APP-Android2/FinalProject-DaOnGil/pull/76
🙇🏻 참조
존경하는 민지멘토님 블로그
https://m1nzi.tistory.com/2
'Android' 카테고리의 다른 글
안드로이드 클린아키텍처 에러 핸들링 (부제: 상속을 지양하자) (2) | 2024.07.28 |
---|---|
[Android]프로젝트를 클린 아키텍처로 마이그레이션해보자 (0) | 2024.07.12 |
ViewLifeCycleOwner 제대로 알고 사용해보자 (1) | 2024.04.28 |
Room Like + StateFlow debouce와 Throttle (0) | 2024.03.31 |
[Android] 프로퍼티의 초기화 시점이 중요한 이유! (0) | 2024.01.28 |