과거
과거 안드로이드 앱을 개발할 때 검색과 관련된 기능이 있었는데, 당시에 값이 변하면 API를 호출해서 결과를 갱신해줘야 한다는 생각에, Text 값이 변할 때마다 API를 호출하도록 했었다.
그러나 Debounce를 적용하여 이를 개선할 수 있다는 사실을 알게 되었고, 특히 Flow를 통해 Android에서 간단하게 구현하여 많은 개선을 이뤄낼 수 있기에 이를 정리해보고자 한다.
Debouce
디바운스란 연속적으로 이벤트가 발생할 때, 마지막 이벤트가 발생한 후 일정 시간이 지날 때까지 추가 이벤트가 없으면 딱 한 번만 실행하는 기술이다
즉 검색 기능을 예시로 들자면 일정 시간을 5초라고 두고, 이벤트를 텍스트 값 변경이라고 했을 때, 사용자가 검색을 하기 위해서 타이핑을 하는데, 키 입력 이벤트가 발생한 지 5초가 지났는데도 다음 키 입력 이벤트가 들어오지 않는다면, 한번 실행되는 기술이다
기존과 차이를 비교하기 위해 아래의 Hello를 입력할 때 시간별 텍스트 변화를 가정해보았다
1초 : H
2초 : E
3초 :
4초 :
5초 : L
6초 : (L을 지움)
7초 : L
8초 : L
9초 : O
10초 :
11초 :
12초 :
13초 :
14초 :
위 코드를 기준으로 아무것도 적용하지 않았을 때와 Debounce를 5초로 적용해보았을 때를 가정해보자
Debounce의 기준은 예시를 위해 잡은 것으로 5초로 해야 하는 것이 아니다.
아무것도 적용하지 않았을 때에는 각 텍스트 변화가 일어났을 때마다 API 호출이 일어나게 될 것이므로 1, 2, 5, 6, 7, 8, 9초에 각각 API 호출이 일어나 총 7번이 호출될 것이다
하지만 Debounce를 5초로 적용했다면, O를 입력하는 순간까지 모두 5초 이내로 계속해서 입력이 이루어지기 때문에 O를 입력하고 5초 뒤 즉 14초가 되어서야 입력 이벤트가 발생하지 않아 1번만이 호출되게 될 것이다
구현
Kotlin에는 Flow에 이미 debounce라는 함수를 통해 간단하게 Debounce를 적용할 수가 있다
아래의 ViewModel코드에서처럼 관찰 대상을 collect를 할 때 debounce 함수를 적용해서 구독하고, 들어왔을 때 실행할 함수를 작성함으로써 text 값이 단순히 바뀔 때마다 호출하는 것이 아니라 debounce에 넣은 시간 이내로 이벤트가 발생하지 않았을 경우 호출되도록 한다
// file: "DebounceViewModel.kt"
class DebounceViewModel : ViewModel() {
val textInputState: MutableStateFlow<String> = MutableStateFlow("")
init {
viewModelScope.launch {
textInputState
// timeoutMillis 동안 새로운 입력이 없으면 마지막 값을 방출
.debounce(800L)
.collect { finalInput ->
// Debounce를 거쳐서 들어온 값을 로그로 찍기
// 이렇게 Debounce를 통과했을 때 API를 실행해서 호출 횟수 최적화
logDebouncedQuery(finalInput)
}
}
}
fun onTextChange(value: String) {
viewModelScope.launch {
textInputState.emit(value)
}
}
private fun logDebouncedQuery(query: String) {
if (query.isNotBlank()) {
Log.d("DebounceTest", "Debounced! -> 최종 입력어: '$query'")
}
}
}
// file: "Debounce.kt"
@Composable
fun Debounce(modifier: Modifier = Modifier, viewModel: DebounceViewModel = viewModel()) {
val state by viewModel.textInputState.collectAsStateWithLifecycle()
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
OutlinedTextField(
value = state, // TextField의 값을 ViewModel의 state와 동기화
onValueChange = { newQuery ->
// 텍스트가 변경될 때마다 ViewModel의 onQueryChanged 함수를 호출
viewModel.onTextChange(newQuery)
},
label = { Text("여기에 입력하세요...") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(20.dp))
Text(text = "아래 Logcat(태그: DebounceTest)을 확인하세요.")
}
}
결과
입력이 일정 시간 없을 때만 로그가 찍히는 것을 볼 수 있다