18. Debounce로 검색 기능을 효율적으로 만들기!

과거

과거 안드로이드 앱개발할 때 검색과 관련된 기능이 있었는데, 당시에 값이 변하면 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)을 확인하세요.")
    }
}

결과

입력이 일정 시간 없을 때만 로그가 찍히는 것을 볼 수 있다


© 2025. Na2te All rights reserved.