API를 활용해 검색기능을 구현하던 중 통신이 2번 되는 현상이 있었다. 셀을 누르면 네트워크 통신을 하도록 구현해놓았는데, 왜인지 통신이 2번 된다. 왜그럴까?
위 사진에서 post는 input.itemSelected에 이벤트가 들어오면 fetchSearchResult로 유즈케이스에서 검색 결과를 가져오는 평범한 코드이다.
근데 이 옵저블로 emptyPost라는 옵저버블을 하나 더 만들었고, 문제는 여기서 발생한다.
이렇게 옵저버블을 공유하면 input.itemSelected에 대한 구독이 여러개 생기게 되고, subscribe가 여러번 일어나서 통신도 여러번 되는것이다.
그니까 하나의 옵저버블을 두 곳에서 사용했고, 구독이 두 번 생성됐고, itemSelected가 방출하는 이벤트가 두 구독에 각각 전달되면서 통신도 두번씩 하게되었다.
결론은 하나의 옵저버블을 두 군데에서 사용하는게 문제였다.
이 문제를 해결하기 위해서 옵저버블을 한 번만 구독하는 방법에 대해 검색해보았고 share()
라는 키워드를 알게되었다.
share()는 하나의 옵저버블을 여러 구독자에게 공유하게 해준다. 즉, 구독을 한 번만 생성하고 이후에 동일한 구독을 여러 구독자에게 재사용하게 해준다.
코드로 예를 들자면:
let post = input.itemSelected
.flatMap {
self.useCase.fetchSearchResult(with: $0)
}.share()
let emptyPost = post
.map { $0.isEmpty ? "EmptySearch" : nil }
.asDriver(onErrorJustReturn: nil)
post를 share()로 선언하면 emptyPost에서 post를 사용할 때 새롭게 구독을 만드는게 아니라, 동일한 구독을 재사용하게 된다. 이렇게 하면 리소스도 효율적으로 사용할 수 있게 된다.
이렇게 공유하고자 하는 옵저버블에 share 오퍼레이터를 추가해주었다. 그결과 통신이 한 번만 되는것을 확인할 수 있었다. 불필요한 통신을 제거하고 리소스를 절약하게 되었다.
이렇게 행복하게 마무리되는듯 싶었으나, 그럴리 없지.
이번엔 다른 문제다. 두 번째 입력부터 셀이 업데이트 된다...
textField로 들어오는 값을 받아서 테이블뷰 셀을 띄우는 기능인데, 원래라면 첫번째 ㅇ
을 입력했을 때 셀이 업데이트 되어야 한다.
문제는 텍스트필드가 분명 이벤트는 방출하는데, 첫번째 글자를 입력할때는 업데이트가 안되고, 두번째 글자 이벤트를 수신하고 셀이 업데이트 되는 것이다.
처음에는 textField에서 첫번째 이벤트를 안보내는줄 알았다. 근데 .debug()로 확인해보니 이벤트는 정상적으로 보낸다. 그러면 뭐가문제지 생각하다가.. 구독을 늦게하나? 이런 생각이 들었다.
구독 타이밍이 첫번째 글자 이후에 되기 때문인가 하는 의심이 들기 시작했고, 그게 맞았다. 구독하는 시점이 첫 번째 값 방출 이후다 보니까, 첫번째 이벤트를 받을 수 없던 것이었다.
이 문제는 share의 replay를 1로 설정하면서 해결할 수 있었다.
share()는 하나의 구독을 여러 구독자와 공유하게 해주는 역할을 한다.
다만, share() 연산자가 반환하는 Observable을 구독하는 시점이 중요하다.
share는 hotObservable을 반환하기 때문에 구독자가 있던 없던 걍 이벤트를 방출해 버린다. 따라서 share()를 한 후, 해당 Observable을 구독하기 전에 이미 이벤트가 방출되었다면 그 이벤트를 놓칠 수 있게 되는것이다.
그래서 share()를 사용할때는 Observable을 구독하는 시점을 잘 고려해야 한다!
share(replay: 1)는 Observable이 방출하는 가장 최근의 1개의 이벤트를 저장해 둔다는 뜻이다. 만약 이후에 새로운 구독자가 생겼을 때, 이미 방출된 가장 최근의 이벤트를 즉시 받을 수 있도록 해준다.
예를 들어, searchTerm이 "검색어1", "검색어2", "검색어3" 세 개의 이벤트를 순서대로 방출했다고 가정해 보자. share()만 사용했을 때는, searchTerm을 구독하는 시점에 따라 이벤트를 놓칠 수 있다.
하지만 share(replay: 1)를 사용하면, searchTerm을 구독하는 시점에 상관없이 가장 마지막에 방출된 "검색어3" 이벤트를 받을 수 있다. 이후에 searchTerm이 새로운 이벤트를 방출하면, 그 때부터는 새로운 이벤트를 계속 받게 된다.
share(replay: 1) 코드를 추가해줬더니 searchTerm Observable이 이벤트를 방출한 후에 구독해도 이벤트를 받을 수 있게 되었고, 가장 최근 이벤트를 놓치지 않고 셀의 띄울 수 있었다.
이제 첫번째 입력부터 셀이 업데이트 된다!
하나의 네트워크 요청을 여러 곳에서 동시에 사용하고 싶을 때 share() 연산자를 사용할 수 있다. 이 경우, 실제 네트워크 요청은 한 번만 이루어지지만 그 결과를 여러 곳에서 동시에 사용할 수 있게 된다.
그리고 새로운 구독자가 생겼을 때, 이미 방출된 가장 최근의 이벤트를 즉시 받을 수 있도록 하려면 share(replay: n)을 사용하고, 이는 가장 최근의 n개의 이벤트를 저장해 둔다는 의미이다.