Signle은 RxSwift의 Traits로 하나의 이벤트만 방출하는 Observable이다. 

 

Single에는 failure, success라는 2가지 이벤트가 존재한다. 

  • success는 일반 Observable의 next + completed와 같은 역할이고 
  • failure error이벤트와 같은 역할이다.

다음과 같이 초기에 한번만 이루어지는 네트워크 요청에 자주 사용된다. 

static func featchAllData(_ url: String) -> Single<Data> {
	return Single<Data>.create { single in
		URLSession.shared.dataTask(with: URL(string: url)!) { data, res, err in
			if let err = err {
				single(.failure(err))
				return
			}
			
			guard let data = data else {
				let httpsResponse = res as! HTTPURLResponse
				single(.failure(NSError(domain: "no data",
																code: httpsResponse.statusCode,
																userInfo: nil)))
				return
			}
			
			single(.success(data))
		}.resume()
		
		return Disposables.create()
	}
}

 

 

Single With Share

Single과 같은 Cold Observable은

Subscriber 수에 따라 스트림이 생성되기 때문에, 

중복적으로 네트워크 요청을 하는 경우가 발생한다. 

 

그렇기 때문에, share Operator를 사용하거나, 

자체적으로 share가 가능한 DriverSignal을 사용한다. 

DriverSignalshare에 대한 자세한 애기는 해당 포스팅에서 볼 수 있다. 

source.share(replay: 1, scope: .whileConnected) // Driver

source.share(scope: .whileConnected)  // Signal

 

다음과 같은 네트워크 요청이 있다고 가정해보자.

struct Repository {
	func fetchUser() -> Single<User> {
		return Single.create { emitter in
			emitter(.success(User(id: 1, name: "jung", age: 26)))
			return Disposables.create()
		}.debug("User")
	}
}

이를 ViewModel로 가져와서 UI업데이트를 하기 위해 Driver로 가공을 한 후

final class ViewModel {
	let dependency = Repository()
	let userName: Driver<String>
	let userAge: Driver<Int>
	
	init() {
		let user = dependency
			.fetchUser()
			.asDriver(onErrorJustReturn: User(id: 0, name: "", age: 0))
		
		userName = user.map { $0.name }
		userAge = user.map { $0.age }
	}
}

ViewController에서 다음과 같이 Binding을 한다고 가정하자. 

class ViewController: UIViewController {
	...
	override func viewDidLoad() {
		super.viewDidLoad()
		bind()
	}
	
	private func bind() {
		viewModel.userName
			.drive(with: self) { owner, name in
				owner.nameLabel.text = name
			}
			.disposed(by: disposeBag)
		
		viewModel.userAge
			.drive(with: self) { owner, age in
				owner.ageLabel.text = "\(age)"
			}
			.disposed(by: disposeBag)
	}
}

해당 코드의 동작을 예상하자면, 

let user = dependency
	.fetchUser()
	.asDriver(onErrorJustReturn: User(id: 0, name: "", age: 0))

userName = user.map { $0.name }
userAge = user.map { $0.age }

다음과 같이 user이라는 DriveruserNameuserAge가 map하는 형태로 구현을 했기 때문에, 

user의 하나의 공유 스트림으로 userNameuserAge로 이벤트가 방출될 것이다.

 

 

하지만 이를 debug해보면,

하나의 공유 스트림으로 userName userAge로 이벤트가 방출될 것이 아닌

다음과 같이  userName userAge각각 스트림이 생성되는 것을 볼 수 있다.

/*
 User -> subscribed
 User -> Event next(User(id: 1, name: "jung", age: 26))
 User -> Event completed
 User -> isDisposed
 
 User -> subscribed
 User -> Event next(User(id: 1, name: "jung", age: 26))
 User -> Event completed
 User -> isDisposed
 */

이러한 이유는 Single의 경우 이벤트를 방출하자마자 complete이벤트 방출하여 stream이 끊기게 된다.

이는 결국 user의 공유 스트림 역시 끊기게 되며,

map으로 연결된 Driver 역시 disposed되는 결과가 발생한다.

 

즉, Singleshare 연산자를 사용하게 되면,

결과적으로 공유되지 않고 여러 스트림이 생기게 된다.

특히나, Driver의 경우는 끊기지 않는 UI작업을 위해 사용한 Driver가 소용없게 된다. 

 

 

해결 방법

Relay 사용해주기

첫번째 방법으로는 중간 스트림을 Relay로 변환해주는 것이다. 

Relayerror뿐만 아닌, completed 이벤트도 무시하기 때문에 

Single에서 completed로 스트림이 끊기더라도 Relay는 이를 무시하게 된다. 

final class ViewModel {
	let dependency = Repository()
	let disposeBag = DisposeBag()
	let userName: Driver<String>
	let userAge: Driver<Int>
	
	private let user = BehaviorRelay<User>(value: User(id: 0, name: "", age: 0))
	
	init() {
		userName = user.asDriver().map { $0.name }
		userAge = user.asDriver().map { $0.age }
		
		dependency
			.fetchUser()
			.subscribe(with: self) { owner, userInfo in
				owner.user.accept(userInfo)
			}
			.disposed(by: disposeBag)
	
	}
}

다음과 같이 공유 스트림이 제대로 동작하는 것을 볼 수 있으며, 

Driver 역시 Disposed되지 않는다. 

/*
 User -> subscribed
 User -> Event next(User(id: 1, name: "jung", age: 26))
 User -> Event completed
 User -> isDisposed
 */

 

하지만 위와 같은 방식은 한가지 문제점이 존재한다. 

해당 데이터를 실제로 구독하게 될 구간은 ViewController이지만,

시퀀스가 유지되지 않고 ViewModel에서 한번 끊기고 

user라는 Relay를 통해 다시 방출해주게 된다. 

 

 

Signle 끊기지 않게 변환해주기

해당 방법은 concat 연산자와 never 연산자를 사용한다. 

  • concat 연산자는 두 Observable Sequence를 연결해준다.
  • never 연산자는 어떠한 항목도 방출하지 않고, 스트림이 종료되지도 않는 Observable을 생성한다.    
public extension Single {
	func ignoreTerminate() -> Observable<Element> {
		return self.asObservable().concat(Observable.never())
	}
}

concat 연산자는 앞의 Observable이 completed되어야만

뒤의 Observable 이벤트 방출을 시작한다. 

따라서, Single이 completed되면,

아무런 이벤트를 발생하기 않고 스트림이 종료되지 않는 Observable로 전환되기 때문에

스트림이 끊기지 않게 된다. 

 

final class ViewModel {
	...
	
	init() {
		let user = dependency
			.fetchUser()
			.ignoreTerminate()
			.asDriver(onErrorJustReturn: User(id: 0, name: "", age: 0))
		
		userName = user.map { $0.name }
		userAge = user.map { $0.age }
	}
}

다음과 같이 공유 스트림이 제대로 동작하는 것을 볼 수 있으며,

Driver 역시 Disposed되지 않는다. 

/*
 User -> subscribed
 User -> Event next(User(id: 1, name: "jung", age: 26))
 User -> Event completed
 User -> isDisposed
 */

또한, Relay를 사용한 것과 다르게 하나의 Stream으로 ViewController까지 데이터가 전달된다. 

 

 

 

Reference

https://ntomios.tistory.com/8

'iOS > RxSwift' 카테고리의 다른 글

[RxSwift] Relay vs. Signal vs. Driver  (0) 2023.07.25
[RxSwift] Operator(4) - Share  (0) 2023.07.24
[RxSwift] Operator(3) - Combining  (0) 2023.01.08
[RxSwift] Traits  (0) 2022.12.28
[RxSwift] Operator(2) - Transforming  (2) 2022.12.27
복사했습니다!