Feature
[Android] 텍스트 검색 기능에 Debounce Search 적용하기
Marchbreeze
2025. 5. 14. 19:18
2023년 8월 11일에 작성되었던 글입니다.
구현 영상
키워드 검색 기능에서 사용자가 글자를 입력할 때마다 즉시 서버 요청을 보내면, 네트워크 낭비와 사용자의 의도와 무관한 요청이 과도하게 발생했습니다. debounce 기법을 적용하여, 사용자가 입력을 멈춘 뒤 일정 시간(0.5초)이 지나면 서버 요청을 보내도록 개선했습니다.
1. Debounce 란
Debounce
입력 이벤트가 연속해서 발생하더라도, 마지막 이벤트 이후 일정 시간이 경과할 때까지 대기하다가 한 번만 처리하는 기법입니다.
- 여러 번 발생하는 이벤트 중 마지막 이벤트를 기준으로 동작 시점을 결정합니다.
- 빠르게 여러 번 발생하는 이벤트(ex. 키보드 입력)에서 사용자 입력이 멈춘 뒤에 지정된 시간 이후 한 번만 처리하도록 했습니다.
- 불필요한 중복 호출을 줄여 네트워크 요청이나 무거운 연산의 횟수를 최소화했습니다.
Throttle
이벤트가 연속해서 발생해도, 일정 시간 간격마다 최대 한 번만 동작하도록 제한하는 기법입니다.
- 연속 발생하는 이벤트 중 처음 이벤트를 기준으로, 일정 주기 내에는 추가 처리를 차단하는 방식입니다.
- 첫 이벤트가 발생하면 즉시 처리한 뒤, 그 시점부터 지정된 기간 동안은 추가 호출을 무시합니다.
- 지정된 기간이 지나면 다시 다음 이벤트를 허용해 처리합니다.
Debounce | Throttle | |
동작 시점 | 마지막 이벤트 이후 지정 시간 지연 후 한 번 동작 | 최초 이벤트 발생 시 즉시 동작, 이후 지정 시간 동안 차단 |
사용 목적 | 입력이 끝난 후 결과를 한 번만 가져와야 할 때 | 이벤트가 너무 잦아도 일정 간격으로만 처리해야 할 때 |
예시 | 검색창 자동완성, 폼 유효성 검사 | 스크롤 위치 업데이트, 창 크기 변경 감지 |
2. 화면 진입 시 키보드 포커스 설정
InputMethodManager
안드로이드의 시스템 서비스로, 키보드(소프트 입력장치)를 제어합니다.
showSoftInput로 특정 뷰에 포커스된 상태에서 키보드를 띄우며, hideSoftInputFromWindow로 키보드를 내립니다.
다음과 같은 방식으로, 해당 액티비티가 시작되었을 때 키보드가 올라오고 EditText에 포커스가 생기도록 구현했습니다.
private fun initFocusToEditText() {
binding.etRecommendSearchBox.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(
binding.etRecommendSearchBox,
InputMethodManager.SHOW_IMPLICIT
)
}
- requestFocus() 로 EditText에 포커스를 강제로 부여했습니다.
- InputMethodManager 를 통해 소프트 키보드를 자동으로 보여주었습니다.
3. Debounce 적용한 서버통신 진행
private val debounceTime = 500L
private var searchJob: Job? = null
private fun setDebounceSearch() {
binding.etRecommendSearchBox.doAfterTextChanged { text ->
// 1) 이전에 등록된 Job이 있으면 취소
searchJob?.cancel()
if (text.isNullOrBlank()) {
// 입력이 없으면 빈 화면 표시
showNoFriendScreen()
binding.layoutRecommendNoSearch.visibility = View.GONE
} else {
// 2) 새로운 debounce Job 생성
searchJob = viewModel.viewModelScope.launch {
delay(debounceTime) // 지정된 시간(0.5초) 대기
val query = text.toString()
searchText = query
viewModel.setListFromServer(query) // 서버 통신 호출
}
}
}
}
- doAfterTextChanged : EditText의 텍스트가 변경된 직후에 호출되는 리스너입니다.
- searchJob?.cancel() : 이전에 시작된 코루틴(Job)이 아직 실행 중이면 즉시 취소합니다.
- launch : viewModelScope.launch { … } 로 코루틴을 시작해,
- delay(debounceTime) : 지정된 시간(0.5초)만큼 일시 중단합니다.
- Job 취소 확인 후 서버 호출 : delay 도중 추가 입력이 들어오면 cancel() 되어 서버 호출이 실행되지 않습니다.
4. 입력 감지 시 로딩 화면 전환
private fun setLoadingScreen() {
binding.etRecommendSearchBox.doOnTextChanged { _, _, _, _ ->
// 입력이 시작되면 로딩 UI 표시
showLoadingScreen()
adapter.submitList(listOf()) // 기존 리스트 초기화
viewModel.setNewPage() // 페이징 변수 초기화
}
}
- doOnTextChanged: 텍스트가 변경되는 그 순간 호출됩니다. (vs doAfterTextChanged: 텍스트 변경 후 호출됩니다.)
5. 서버통신 응답 시 화면 전환
private fun observeSearchListState() {
viewModel.postFriendsListState.observe(this) { state ->
when (state) {
is UiState.Loading -> {
// 서버 요청 중 로딩 화면
showLoadingScreen()
}
is UiState.Success -> {
val list = state.data?.friendList.orEmpty()
if (list.isEmpty()) {
showNoFriendScreen() // 검색 결과 없는 경우
} else {
adapter.addList(list) // 결과 리스트 표시
showFriendListScreen()
}
}
is UiState.Failure -> {
toast(getString(R.string.recommend_search_error))
showFriendListScreen() // 에러 시 이전 UI 복원
}
}
}
}
다음과 같은 방법으로, 사용자가 입력 중에는 서버 요청을 지연시키고, 입력이 멈춘 뒤 0.5초 후에 한 번만 서버 호출이 이루어져, 검색 기능의 효율성과 사용자 경험을 모두 개선할 수 있었습니다.