Flow debounce를 이용한 검색 기능 구현

thsamajiki·2022년 11월 9일
0

Coroutine

목록 보기
5/8

앱 내에 검색 기능을 구현할 때, EditText의 변화가 감지될 때마다 api를 호출할 경우 너무 많은 비용이 소모될 수 있다. 단어가 완성되기 전에 몇번이고, 오타를 낼때마다 의도치 않게 api를 호출하고, 이것을 한명의 유저만 수행하는 것이 아니기 때문에...
ZiggyMovie 앱에서 이렇게 하면 네이버 측에서 서버 과부하를 방지하고자 api 사용을 못하게 할 수도 있다는 생각이 들었다.

따라서 검색을 구현할때는 입력이 발생하는 즉시 가 아닌 어느정도 시간의 텀을 두고 유저의 입력이 종료되었다고 판단될 무렵에 api를 호출하는 것이 바람직하다.

기존의 RxJava를 통해 검색 기능을 구현해보자. 코드는 다음과 같다.

// rx 적용부분
    // Observable 통합 제거를 위한 CompositeDisposable
    private var myCompositeDisposable = CompositeDisposable()
    
            // 에딧텍스트 옵저버블
            val editTextChangeObservable = mySearchViewEditText.textChanges()

            val searchEditTextSubscription : Disposable =
                // Observable에 오퍼레이터들 추가
                editTextChangeObservable
                    // 글자가 입력 되고 나서 0.8 초 후에 onNext 이벤트로 데이터 흘려보내기
                    .debounce(1000, TimeUnit.MILLISECONDS)
                    // IO 쓰레드에서 돌리겠다.
                    // Scheduler instance intended for IO-bound work.
                    // 네트워크 요청, 파일 읽기,쓰기, DB 처리 등
                    .subscribeOn(Schedulers.io())
                    // 구독을 통해 이벤트 응답 받기
                    .subscribeBy(
                        onNext = {
                            Log.d("RX", "onNext : $it")
                            // TODO:: 흘러들어온 이벤트 데이터로 api 호출
                            if (it.isNotEmpty()){
                                searchPhotoApiCall(it.toString())
                            }
                        },
                        onComplete = {
                            Log.d("RX", "onComplete")
                        },
                        onError = {
                            Log.d("RX", "onError : $it")
                        }
                    )
            // compositeDisposable 에 추가
            myCompositeDisposable.add(searchEditTextSubscription)
            
            
            
   	override fun onDestroy() {
        Log.d(TAG, "PhotoCollectionActivity - onDestroy() called")
        // 모두 삭제
        this.myCompositeDisposable.clear()

        super.onDestroy()
    }

Coroutine의 Flow를 통한 구현

fun EditText.textChangesToFlow(): Flow<CharSequence?> {
    return callbackFlow {
        val listener = object : TextWatcher {
            override fun afterTextChanged(s: Editable?) = Unit
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) =
                Unit

            override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) {
                //offer(text)
                // 값 내보내기
                trySend(text)
            }
        }
        addTextChangedListener(listener)
        // 콜백이 사라질때 실행, 리스너 제거
        awaitClose { removeTextChangedListener(listener) }
    }.onStart {
        // event 방출
        emit(text)
    }
}
 private var myCoroutineJob: Job = Job()
    private val myCoroutineContext: CoroutineContext
        get() = Dispatchers.IO + myCoroutineJob
        

			// Rx의 스케줄러와 비슷
            // IO 스레드에서 돌리겠다
            GlobalScope.launch(context = myCoroutineContext) {

                // EditText가 변경되었을때
                val editTextFlow = mySearchViewEditText.textChangesToFlow()

                editTextFlow
                    // 연산자들
                    // 입력되고 나서 1초 뒤에 받는다
                    .debounce(1000)
                    .filter {
                        it?.length!! > 0
                    }
                    .onEach {
                        Log.d(TAG, "flow로 받는다 $it")
                        // 해당 검색어로 api 호출
                        searchPhotoApiCall(it.toString())
                    }
                    .launchIn(this)
            }
            

  override fun onDestroy() {
        Log.d(TAG, "PhotoCollectionActivity - onDestroy() called")
        
        myCoroutineContext.cancel()

        super.onDestroy()
    }

collect가 아닌 launchIn을 통해서 이벤트를 처리한다.
그 이유는 launchIn은 collect와 다르게 새로운 코루틴을 생성하기 때문에
별도의 스레드, 별도의 코루틴에서 이벤트를 감시하고 처리할 수 있다.

새로운 스레드를 생성하지 않는 collect를 통해 event를 처리하면, event 는 버튼을 클릭하거나, 키보드 입력이 발생하는 등 페이지를 나가거나, 심지어 앱이 실행될 동안 계속 들어올 수 있기 때문에 다음에 발생할 동작(UI 갱신, Network 호출)들을 수행할 수 없다.

참고로 launchIn의 파라미터인 this는 코루틴 스코프 (현재 Global scope 내부에 있기 때문에 해당 코루틴을 지칭한다)

이처럼 코루틴의 flow를 이용하여 rxJava에서 수행하였던 debounce를 비슷하게 구현할 수 있다.

profile
안드로이드 개발자

0개의 댓글