Skip to content

✅ [기술 결정] Debounce & Paging 사용해서 검색 구현

박봉팔 edited this page Dec 4, 2024 · 2 revisions

⚠️ 문제 상황

실시간 검색 구현 시에, 입력되는 모든 문자의 검색을 서버에 요청하면, 서버의 리소스 낭비와 과부하로 이어질 수 있다. 따라서, 이러한 이벤트를 적절히 필터링하기 위해서 Debounce 기법을 사용하고자 하였다.


🔍 Debounce

Debounce란, 발생하는 이벤트를 그룹화하여, 일정 시간 동안 이벤트가 발생하지 않으면, 마지막 이벤트를 발생하는 기법이다. timer를 둔다고 이해할 수 있다. 즉, 이벤트가 발생하다가 잠시 멈추는 시점이 일정 delay 만큼의 텀을 가지게 되면 마지막 이벤트를 발생한다.

image

순간 검색 기능을 구현할 때, debounce 기법을 사용한다면, 서버에 불필요한 api 요청을 방지할 수 있다. 예를 들어, 포링을 검색한다고 할 때, ㅍ->포->ㄹ->리->링 순서로 검색한다면, 5번의 요청을 1번으로 줄일 수 있다.


🔍Flow의 Debounce

Flow는 debounce 메서드를 직접 제공하고 있다. 특정 시간 동안 아무 이벤트도 발생하지 않으면, 마지막 이벤트를 방출한다. 아래처럼 parameter로 timeout을 명시적으로 지정할 수 있고, 내부적으로도 debounceInternal을 통해 동작한다.

@FlowPreview
public fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T> {
    require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" }
    if (timeoutMillis == 0L) return this
    return debounceInternal { timeoutMillis }
}

@FlowPreview
@OptIn(kotlin.experimental.ExperimentalTypeInference::class)
@OverloadResolutionByLambdaReturnType
public fun <T> Flow<T>.debounce(timeoutMillis: (T) -> Long): Flow<T> =
    debounceInternal(timeoutMillis)

✔️실제 적용

프로젝트에서는 debounce를 포함한 flow 연산자를 사용해서 검색 기능을 구현했다. debounce 연산자를 사용해서 0.3초 동안 이벤트가 발생하지 않으면, flow를 반환하고, distinctUtilChanged를 통해 이전 검색 text와 같다면, flow의 방출을 무시하도록 하였다.

flatMapLatest는 특정 Flow가 방출하는 값을 기반으로 새로운 Flow를 생성하는 연산자이다. 검색어가 빈 값이 아니라면, 서버에서 받아온 검색 결과를 Flow로 반환하도록 하였다.

  private val _searchQuery = MutableStateFlow("")
  val searchQuery = _searchQuery.asStateFlow()
   
   
    val searchResult = _searchQuery
        .debounce(SEARCH_DEBOUNCE_TIME_MILLIS)
        .distinctUntilChanged()
        .flatMapLatest { query ->
            if (query.isBlank()) {
                flowOf(PagingData.empty())
            } else {
                tagRepository.getTagBySearch(query)
            }
        }
        .cachedIn(viewModelScope)
        
	  companion object {
        const val SEARCH_DEBOUNCE_TIME_MILLIS = 300L
    }

🔍CachedIn

위의 코드에서 .cachedIn(viewModelScope)을 사용하는 것을 볼 수 있다. 프로젝트에서 paging3를 사용해서 검색 결과를 가져오고 있는데, cachedIn은 paging3의 기능 중 하나인 데이터 메모리 캐싱을 활용하는 연산자이다. 이에 대해 가이드 문서에 아래와 같이 설명이 되어있다.

cachedIn() 연산자는 데이터 스트림을 공유 가능하게 만들고, 제공된 CoroutineScope를 사용하여 로드 된 데이터를 캐시한다. 이 예제는 Lifecycle lifecycle-viewmodel-ktx 아티팩트에서 제공하는 viewModelScope를 사용한다.

configuration change가 발생하면 캐싱을 하지 않을 때에는 다시 서버에 요청을 보내지만 캐싱한다면 서버에 다시 요청하지 않고 캐싱된 데이터를 가져온다. 실제 테스트를 진행해본 결과, cachedIn을 적용하고 화면 회전을 하면 다시 서버에 요청을 보내지 않은 것을 확인했다.

데모 영상

default.mp4

Debounce 기법을 통해 실시간 검색 기능에서 불필요한 서버 요청을 줄일 수 있었으며, Flow의 다양한 연산자를 활용하여 효율적인 데이터 처리를 할 수 있었다. 또한 Paging 3의 캐싱 기능을 통해 성능을 더욱 향상시킬 수 있었다ㅣ

Clone this wiki locally