문제상황
'이디야' 를 검색하고 싶다고 가정할때, 이 -> 이디 -> 이디야 이런식으로 사용자가 키보드에 검색어를 입력하게 된다.
이때 결과적으론 가장 마지막에 '이디야' 에 대한 검색 결과가 화면에 표시되어야 하는데 '이디' 에 대한 검색 결과만 표시되는 경우가 있다.
특히 이런 문제는 키보드 입력을 매우 빠르게 했을 때 자주 발생한다.
원인
우선 원인을 찾기 위해 여러가지 가정을 해보고 실험을 해보았다.
1) EditText에 문자열이 제대로 입력되는가? => 문자열은 잘 입력되고 Watcher에도 잘 잡힘
2) cancelable 때문에 네트워크 요청이 취소되는가? => cancelable은 동일한 요청 url 에 대해서만 요청을 취소하는 것이기 때문에 이 문제와 관련없다는 결론
현재 나는 Retrofit 통신 라이브러리를 사용하고 있는데 라이브러리에서 자체적으로 엄청 빠른 시간 안에 연속적으로 API 호출이 발생하는 경우 요청을 취소하거나 다 처리하지 못하는 것이다.
해결방법
사실 이 문제를 해결하고 나면 검색 기능에 딜레이를 주어 실제 입력이 모두 끝났을 때만 검색 API 가 요청되는 기능을 구현해 효율성을 높이는 작업을 진행해보려고 했다. 그런데 원인을 분석해보니 이 2가지 문제를 한번에 해결할 수 있겠다는 생각이 들었다.
다시말해, 사용자가 빠르게 '이디야' 를 입력한다면 사실 '이' 나 '이디' 에 대한 검색 결과는 필요가 없다.
'일정시간' 안에 지속적으로 사용자 입력이 들어오지 않는 경우, 검색어 입력을 모두 마쳤다고 생각하고 마지막으로 들어온 검색어에 대해서만 검색을 진행하는 것 이다.
처음에는 이 기능을 구현하기 위해 타이머..? 같은걸 사용해야 하나 싶었다. EditText에 입력이 들어오기 시작하면 타이머를 재기 시작하고 일정 시간 안에 새로운 입력이 들어오면 넘어가고 그렇지 않으면 검색 API 요청을 해당 검색어로 보내는 것이다.
그러다가 코루틴 Flow에서 Debounce 라는 것을 지원한다는 것을 알게 되었다.
Debounce의 개념적 의미는 연속적으로 발생한 이벤트를 하나로 처리하는 방식 이고
코루틴 Flow 내부에서 debounce 라는 함수를 통해 일정 시간 동안 입력값이 없는 경우에만 Flow가 방출되는 방식으로 동작한다.
1. debounce를 사용하기 위해서는 우선 Flow 를 사용해야 하는데, 이전에는 EditText를 통해 입력받은 데이터를 그냥 var 변수에 저장했다면 이제는 MutableStateFlow (변경 가능한 StateFlow) 에 저장하고 관리하고 있다.
2. 그리고 나는 0.3 초를 기준으로 잡고 0.3초 동안 새로운 검색어가 입력되지 않는 경우, 가장 마지막에 입력된 검색어를 사용해 검색 API 요청을 보내기로 했다. (만약, 0.3초 안에 다른 입력어가 입력되는 경우에는 코루틴이 자체적으로 취소된다.)
3. 0.3초 동안 입력이 없는 경우에만 MutableStateFlow 값이 방출되면서 viewmodel.initList() 가 실행된다.
방출되는 값이 가장 마지막값이여야 하기 때문에 collectLastest 를 써야 하는 것 아닌가 생각할 수도 있지만 나는 사실 collect를 통해 방출되는 값을 사용하지 않기 때문에 크게 의미가 없다.
몇 달 전 검색 기능을 구현 초기에, 무한스크롤 기능을 구현하기 위해서 새롭게 입력되는 키워드 값을 그때그때 뷰모델에 선언한 LiveData(현재는 StateFlow) 변수에 저장하고 관리해서 재사용하고자 했었다. 그래서 검색 요청을 하는 함수인 initList() 함수도 매개변수로 키워드 값을 전달하는 것이 아니라 이 LiveData(현재는 StateFlow) 를 통해 키워드값을 받아오고 usecase 함수 호출시 매개변수로 넘겨주는 방식으로 구현했었다.
binding.EditTextSearchBar.addTextChangedListener {
viewmodel._keyword.value = it.toString()
// 새로운 입력값 생길때마다 새로운 코루틴 생성
lifecycleScope.launch {
viewmodel._keyword
.debounce(300) // 0.3초 동안 입력값이 없는 경우만 방출
.collect {
viewmodel.initList()
}
}
}
Debounce를 적용하기 전에는 모든 입력값에 대한 검색 결과를 전달받았고 심지어 실제 검색하고 싶었던 '이디야' 에 대한 검색 결과는 나오지도 않았다.
Debounce를 적용하고 나니 엄청 빠르게 '이디야' 검색어를 입력했을 때, 딱 '이디야' 에 대한 검색어만 잘 나오는 것을 확인할 수 있었다.
이렇게 Debounce 기능 구현 덕분에 사용자가 검색 키워드를 입력할 때마다 API를 호출하는 대신, 사용자가 입력을 멈춘 후 일정 시간 동안 추가 입력이 없을 때에만 API를 호출하도록 할 수 있도록 해 지나치게 빠른 연속 호출을 줄이고 오류 해결과 성능 개선까지 성공할 수 있었다.
** 나처럼 Flow에서 제공해주는 debounce를 함수를 사용하지 않고 직접 코루틴을 사용해 구현하게 된다면 다음과 같이 구현할 수 있다.
https://velog.io/@ho-taek/Android-debounce-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B5%AC%ED%98%84
'Android' 카테고리의 다른 글
[Android/세미나실] 2024 Google I/O 참가 후기 및 정리 (0) | 2024.08.15 |
---|---|
[Android/세미나실] 안드로이드를 더 잘하기 위한 자료 모음집 (0) | 2024.08.12 |
[안드로이드/BottomSheet] Persistent Bottom Sheet 외부 화면 클릭 기능 구현 (0) | 2024.02.28 |
[안드로이드/아키텍쳐] 클린아키텍쳐 총정리 & 적용 중간 점검 (1) | 2024.02.10 |
[안드로이드/RecyclerView] 리사이클러뷰 + 데이터바인딩 + MVVM 적용하기 (0) | 2024.02.03 |