RxSwift는 사용하다 보면 참 편리한 아이인 것 같다. 반응형으로 사용자의 이벤트를 구독하고 방출된 이벤트를 관찰하고 있다가 처리해주다 보니 MVVM 패턴과도 찰떡인 게 사용하면서 체감된다.
하지만 우리가 이벤트를 방출할 때 사용해주는 Observable이 편리하긴 하지만 여러 곳에서 구독을 할 경우 유의해주어야 할 점이 있는데 이것에 대해 알아보자!
ReactiveX를 공부할 때 반드시 알아야할 multicast, unicast 개념과도 연결되니 꼭 알고 넘어가기로!
📍 Observable Stream
우리는 이미 Observable를 여러번 구독할 수 있다는 것을 알고 있는데... 근데 이렇게 여러번 구독하게 되면 각각 구독하는 시점에 Observable stream이 새롭게 생긴다. 즉, 동일한 Observable stream이 그대로 전달되는 게 아니라는 것이다.
근데 이게 무슨 문제가 있어서 유의해주어야하는 걸까? Observable stream은 우리 생각보다 중요하다! 왜냐!
- Observable stream으로인해 코드의 흐름에 따라 방출된 이벤트가 변화될 가능성이 있고,
- 무거운 작업(예를들어 네트워킹!)이 이 Observable steam에서 실행될 경우 구독 시점마다 새로 실행될 가능성이 있기 때문에 의도치 않게 api를 중복 호출 하거나 불필요하게 앱의 리소스를 사용하게 될 수도 있다!!
사실 나도 이게 왜 중요한 개념인지 몰랐는데 내 코드에서 발생한 문제점 때문에 Observable stream이 공유되는지 혹은 안되는지의 여부가 얼마나 중요한지 알게 되었다 ㅎㅎ( 나도 알고 싶지 않았써…)
그래도 🌱새싹 🌱 세션 중에 Observable stream에 이러한 특성이 있고 Observable stream이 공유되게 하려면 어떻게 해야 하는지에 대한 세션을 들은 바로 그날, 문제가 될 수 있는 상황을 맞닥뜨려버려서 럭키!! 이런 키워드 몰랐으면 왜 이런 문제가 발생하는지 한참 헤맸을 것 같다.
📍문제가 될 수 있는 상황
1️⃣
아래 코드를 보면 이벤트 발생하는곳은 test 한 곳인데 구독(bind)을 세 군데에서 하고 있다.
그냥 뇌피셜을 돌려보면 test 한 군데에 이벤트발생에 대한 Observable stream을 정의해 두었기 때문에 한번 정의된 Observable stream이 모든 구독에서 사용되는거 아니야?라고 생각할 수 있다. ( 당연히 나도 그렇게 생각했고 ㅎㅎ)
but, 위에서 말한 대로 Observable의 특성상 Observable stream이 공유되지 않아, 구독(subscribe, bind)를 할 때마다 새로운 스트림이 생기기 때문에 하나의 이벤트 발생을 상수에 할당하여 정의해두었다고 하더라도 구독(bind)할 때마다 새로운 스트림이 생겨서 프린트가 이렇게 나오는 것이다.
let test = nextButton.rx.tap
.map{"안녕하세요 \(Int.random(in: 1...100))"}
//이벤트 발생하는곳은 한 곳인데 구독을 세 군데에서 하고 있음
test
.bind(onNext: { text in
print("1 --> ", text)
})
.disposed(by: disposeBag)
test
.bind(onNext: { text in
print("2 --> ", text)
})
.disposed(by: disposeBag)
test
.bind(onNext: { text in
print("3 --> ", text)
})
.disposed(by: disposeBag)
2️⃣ 네트워킹
네트워킹 통신의 결과가 unicast(이벤트 스트림이 공유되지 않음)로 반환될 경우에 여러 관찰자를 통해 구독할 경우 api를 중복으로 호출하는 문제점이 발생할 수 있다
func getBoxOfficeData(dateString : String) -> Observable<BoxOffice> {
let url = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=\(koficKey)&targetDt=\(dateString)"
let boxofficeResult = Observable<BoxOffice>.create { observer in
guard let url = URL(string: url) else {
observer.onError(APIError.invalidUrl)
return Disposables.create()
}
URLSession.shared.dataTask(with: url) { data, response, error in
//error control code ...
if let data = data, let appdata = try? JSONDecoder().decode(BoxOffice.self, from: data) {
observer.onNext(appdata)
observer.onCompleted()
}
}
.resume()
return Disposables.create()
}
.debug("💜boxofficeResult💜")
return boxofficeResult
}
let boxOfficeObservable : Observable<BoxOffice> = searchBar.rx.searchButtonClicked
.debug("-- debug --")
.map{ "20240707" }
.flatMap{ query in
NetworkManager.shared.getBoxOfficeData(dateString: query)
}
boxOfficeObservable
.subscribe(with: self){ owner, data in
print("subscribe 1️⃣")
}
.disposed(by: disposeBag)
boxOfficeObservable
.subscribe(with: self){ owner, data in
print("subscribe 2️⃣")
}
.disposed(by: disposeBag)
boxOfficeObservable
.subscribe(with: self){ owner, data in
print("subscribe 3️⃣")
}
.disposed(by: disposeBag)
📍multiCast, uniCast
위에 살짝 uniCast라는 것을 언급하기도 했는데
multiCast와 uniCast는 이름에서도 유추할 수 있듯이 상반되는 개념이다.
- multiCast : 이벤트 스트림을 공유
- uniCast : 이벤트 스트림을 공유하지 않음
위에서 스트림에 대해서 어느정도 살펴봤으니 감이 오죠?
ReactiveX를 공부할 때 반드시 알아야 하는 개념인 Subject와 Observable도 이 지점에서 차이가 있는데, Subject는 multiCast특성을 가지고 , Observable은 uniCast특성을 가진다. 그래서 지금까지 이벤트 스트림이 공유되지 않는 uniCast에 대한 예시를 들 때 모두 Observable이었던 것! (이건 다음에 쓸 Hot Observable, Cold Observable 관련한 글과도 연관되는 내용이니 잘 인지하고 넘어가자!)
위에서 살펴본 문제 상황에서 모든 구독 상황에 새로운 스트림이 만들어지도록 의도가 된 것 이면 이러한 상황이 문제가 없겠지만 같은 Observable stream을 공유할 수 있도록 하고 싶은데 이런 코드의 실행이라면 문제가 있을 것이다.
그렇다면, unicast 인 Observable을 multi cast로 바꿀 수 있을까??
📍uniCast -> multiCast
1️⃣ PublishSubject 활용
첫 번째 방법은 PublishSubject에 바인딩시킨 다음 그 PublishSubject를 사용해 주는 방법이다.
PublishSubject는 Observable과 달리 이벤트 스트림을 공유하는 multicast 특성을 가졌기 때문에 PublishSubject를 여러 번 구독하더라도 하나의 stream에서 방출된 이벤트가 저장되었다가 Observer가 동일한 이벤트를 처리할 수 있게 된다.
(다음 글에 쓸 예정인 Hot Observable과도 연결되는 내용이니 여기서 Subject은 Hot Observable이기 때문에 multicast 특성을 가졌다고 이해하고 넘어가 보자! )
let boxOfficeSubject = PublishSubject<BoxOffice>() //⭐️
searchBar.rx.searchButtonClicked
.debug("-- debug --")
.map{ "20240707" }
.flatMap{ query in
NetworkManager.shared.getBoxOfficeData(dateString: query)
}
.subscribe(onNext : { boxOffice in
boxOfficeSubject.onNext(boxOffice) //⭐️
})
.disposed(by: disposeBag)
boxOfficeSubject
.subscribe(with: self){ owner, data in
print("subscribe 1️⃣")
}
.disposed(by: disposeBag)
boxOfficeSubject
.subscribe(with: self){ owner, data in
print("subscribe 2️⃣")
}
.disposed(by: disposeBag)
boxOfficeSubject
.subscribe(with: self){ owner, data in
print("subscribe 3️⃣")
}
.disposed(by: disposeBag)
2️⃣ .share
그리고 가장 간단한 방법으로는 share 연산자를 사용할 수 있다.
이렇게 하면 boxOfficeObservable을 여러 번 구독하더라도 하나의 이벤트 스트림을 공유한다!
let boxOfficeObservable : Observable<BoxOffice> = searchBar.rx.searchButtonClicked
.debug("-- debug --")
.map{ "20240707" }
.flatMap{ query in
NetworkManager.shared.getBoxOfficeData(dateString: query)
}
.share()
3️⃣ .asDriver
asDriver 연산자를 사용해서 Driver로서 처리해 주는 방법도 있다. Driver는 ReactiveX에서 제공하는 traits 중에 하나인데 모른다면 문서 읽어보는 것을 추천!
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md#driver
간단히 요약하면
1) 이벤트의 처리가 mainThread에서 일어나서 UI를 변경/업데이트할 경우 유용하고,
2) 에러를 방출하지 않으며,
3) 그리고 이벤트 스트림을 공유한다.
문서에서 asDriver(onErrorJustReturn: []) 이 코드 한 줄에 밑에 코드랑 동일하다고 설명한다.
let safeSequence = xs
.observeOn(MainScheduler.instance) // observe events on main scheduler
.catchErrorJustReturn(onErrorJustReturn) // can't error out
.share(replay: 1, scope: .whileConnected) // side effects sharing
return Driver(raw: safeSequence) // wrap it up
다시 본론으로 돌아와서 asDriver 연산자를 사용하여 Driver로 만들어주면 스트림을 공유하게 된다.
let boxOfficeObservable : Observable<BoxOffice> = searchBar.rx.searchButtonClicked
.debug("-- debug --")
.map{ "20240707" }
.flatMap{ query in
NetworkManager.shared.getBoxOfficeData(dateString: query)
}
.asDriver(onErrorJustReturn: /*에러발생시 방출할 반환값*/ )
이렇게 multiCast/uniCast와 Observable stream의 특, 그리고 unicast를 multicast로 바꾸어 처리해 줄 수 있는 방법들을 알아보았다.
개념 자체는 어렵지 않은데, 실제 사용할 때 multiCast, uniCast를 염두하고 코드를 짜야 의도치 않은 사이트이펙트를 생성하지 않는다는 걸 경험을 통해 체득한 기회였다 ㅎㅎ
'RxSwift' 카테고리의 다른 글
[RxSwift] 메모리 누수 일어나기 딱 좋은(?) .bind(with:onNext:) & 중첩 클로저의 객체 참조 (4) | 2024.09.09 |
---|---|
[RxSwift] Hot&Cold Observable (3) | 2024.08.29 |