Rxswift button tap stream error handling
이번 포스팅의 주제는 로그인이나 회원가입 로직 등을 구현할 때 버튼을 클릭하고 네트워크 통신을 RxSwift로 구현하였을 때 어떻게 에러 핸들링을 할 것인가? 에 대한 주제입니다.
문제 상황
해당 문제는 회원가입 기능을 구현하던 중 발생한 문제였습니다. 회원가입 시 필요한 정보(이메일, 비밀번호, 닉네임..) 등을 받고 마지막으로 로그인 버튼을 tap 한다면 해당 CombineLatest로 묶어놓았던 데이터들과 함께 Stream을 시작하여 네트워크 통신하는 Rx 로직을 구성하였습니다. 그 과정에서 flatMap을 통해 미리 싱글톤 패턴으로 구현해놓았던 NetWork 클래스 안의 로그인 API 통신 메서드를 호출하였습니다.
- Network.swift
func requestObservableConvertible<T: Decodable> (
type: T.Type,
router: Router
) -> Observable<T> {
return Observable.create { emitter -> Disposable in
let request = AF.request(
router,
interceptor: AuthManager()
)
.validate()
.responseDecodable(of: T.self) { response in
switch response.result {
case .success(let success):
emitter.onNext(success)
emitter.onCompleted()
case .failure(let failure):
emitter.onError(failure)
}
}
return Disposables.create() {
request.cancel()
}
}
}
- loginViewModel.swift
input.loginButtonTap
.withLatestFrom(loginModelObservable)
.flatMap {
Network.shared.fetch(
type: LoginResponse.self,
router: .login(model: $0)
)
}
.subscribe(with: self) { owner, data in
print("data: \(data)")
} onError: { owner, error in
print("Rx login onError")
} onCompleted: { owner in
print("Rx login onCompleted")
} onDisposed: { owner in
print("Rx login onDisposed")
}
.disposed(by: disposeBag)
로그인 API 통신을 진행했고 로그인API 명세서의 요구사항에 맞게 데이터를 전달하지 않아 에러가 나는 상황이 생겼습니다. 여기서 로그인 button의 rx tap stream은 네트워크 통신의 에러와 함께 onError 이벤트를 방출하면서 dispose되었습니다.
Rx login onError
Rx login onDisposed
이렇게 되면 여기서부터 문제가 발생합니다. 유저는 로그인에 실패했더라도 로그인 정보를 수정한다면 다시 버튼을 누를수 있어야하지만 이미 로그인 버튼의 stream은 dispose된 상태이기 때문에 아무리 정보를 올바르게 수정한다 하더라도 버튼은 더 이상 동작하지 않게 됩니다.
문제 해결
1. 첫번째로 시도했던 방법은 catchAndReturn으로 에러 발생시 기본값을 return함으로써 onError 이벤트를 방출하지 못하게 하는 방법을 사용했습니다.
.flatMap {
Network.shared.fetch(
type: LoginResponse.self,
router: .login(model: $0)
)
.catchAndReturn(LoginResponse(_id: "", token: "", refreshToken: ""))
}
하지만 이 방법은 에러 발생 시 미리 세팅해둔 기본 값만 방출하기 때문에 서버에서 오는 다양한 error handling하기 부적합하다는 것을 깨달았습니다.
지금 해결해야하는 부분은 stream이 끊기지 않으면서 서버의 에러까지 핸들링 할 수 있어야하는 것입니다.
2. 두번째 방법은 Single traits를 사용하여 fetch 메서드에서 방출하는 단일 Observable 이벤트를 래핑하여 error를 방출하지 않고 해당 버튼 tap stream에서 에러를 처리하는 방법입니다.
- Network.swift
func fetchSingle<T: Decodable> (
type: T.Type,
router: Router
) -> Single<Result<T, AFError>> {
return Single.create { emitter -> Disposable in
let request = AF.request(
router,
interceptor: AuthManager()
)
.validate()
.responseDecodable(of: T.self) { response in
switch response.result {
case .success(let success):
emitter(.success(.success(success)))
case .failure(let failure):
emitter(.success(.failure(failure)))
}
}
return Disposables.create() {
request.cancel()
}
}
}
- loginViewModel.swift
input.loginButtonTap
.withLatestFrom(loginModelObservable)
.flatMap {
Network.shared.fetchSingle(
type: LoginResponse.self,
router: .login(model: $0)\
)
}
.subscribe(with: self) { owner, result in
switch result {
case .success(let data):
print(data)
case .failure(let error):
// Server Error Handling
print(error)
}
} onError: { owner, error in
print("Rx login onError")
} onCompleted: { owner in
print("Rx login onCompleted")
} onDisposed: { owner in
print("Rx login onDisposed")
}
.disposed(by: disposeBag)
flatMap 에서 네트워크 통신하는 메서드에서 Single Traite이 Observable을 방출할 때 에러시에도 Success case에 한번 래핑한 뒤 방출하게 되면 flatMap Operator를 통해 새로운 Observable을 방출하게 될 때 Result Type과 Single Traits이 래핑된 상태로 방출되기 때문에 subscribe에서 switch 문을 통해 래핑을 해제하고 서버에서 받아온 에러를 처리하면 됩니다. 이렇게 되면 로그인 button의 stream은 살아있게 되며 에러 핸들링까지 가능하게 되었습니다.
button tap stream에서 한번 더 래핑을 해제하기 때문에 switch case를 한번 더 사용하게 되는 단점이 있긴 하지만 원하는 방향으로 기능할 수 있게 되어 문제해결을 할 수 있었습니다.
Single Traits
그럼 Single이 도대체 뭘까? Single은 Traits의 종류 중 하나입니다. 그렇다면 Traits는 뭘까요?
RxSwift Github Document를 기반으로 알아보겠습니다.
RxSwift- Traits Documents
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md
Traits는 모든 경계에서 사용할수 있는 원시 Observable과 비교할때 인터페이스 경계에서 observable 프로퍼티를 전달하고 보장하며, 문법적으로도 더 쉽고 구체적인 사용 사례를 타켓팅하는데 도움이 됩니다.
Document에서 Traits를 다음과 같이 설명하고 있습니다. 말이 어렵네요.. 풀이 하자면 RxSwift 문법을 조금 더 간단하고 쉽게 활용할 수 있도록 도와주는 객체들이라고 생각하면 편할 것 같습니다. Traits이 전혀 다른 기능을 하는 Rx Operator나 객체가 아니기 때문에 Traits 없이 모든 코드를 작성할 수 있지만 잘 활용한다면 편리하게 사용할 수 있을 것 같습니다.
Traits는 여러 종류가 있지만 자세한 것은 다른 포스팅에서 다루고 이번 주제에서는 Single을 다뤘으니 Single Traits에 대해서 살펴보고 넘어가겠습니다.
- Single은 일련의 요소를 방출하는 대신 항상 단일 요소 또는 오류를 방출하도록 보장되는 Observable의 변형입니다.
설명은 어려운데 예시를 살펴보면 이해가 쉬울 것 같습니다. 앞서 네트워크 통신을 하면서 Observable을 Single로 변형시켰던 적이 있습니다. Single은 단일 요소 또는 오류를 방출하도록 하기 때문에 Success, failure 2가지 형식으로만 방출하도록 설계되어 있습니다.
그렇다면 Single이 어떤 방식으로 구현되어 있는지 내부 코드를 한번 살펴보죠.
public enum SingleTrait { }
/// Represents a push style sequence containing 1 element.
public typealias Single<Element> = PrimitiveSequence<SingleTrait, Element>
public typealias SingleEvent<Element> = Result<Element, Swift.Error>
extension PrimitiveSequenceType where Trait == SingleTrait {
public typealias SingleObserver = (SingleEvent<Element>) -> Void
/**
Creates an observable sequence from a specified subscribe method implementation.
- seealso: [create operator on reactivex.io](http://reactivex.io/documentation/operators/create.html)
- parameter subscribe: Implementation of the resulting observable sequence's `subscribe` method.
- returns: The observable sequence with the specified implementation for the `subscribe` method.
*/
public static func create(subscribe: @escaping (@escaping SingleObserver) -> Disposable) -> Single<Element> {
let source = Observable<Element>.create { observer in
return subscribe { event in
switch event {
case .success(let element):
observer.on(.next(element))
observer.on(.completed)
case .failure(let error):
observer.on(.error(error))
}
}
}
return PrimitiveSequence(raw: source)
}
아 이미 Observable로 처음 성공 이벤트가 발생하면 onNext로 데이터를 방출하고 onCompleted로 Stream을 종료시키도록 코드를 구현했던 적이 있습니다. 이것이 그냥 하나로 합쳐져있다고 보면 될 것 같습니다. 그래서 위에서 설명했듯 기존 Observable로도 구현할 수 있지만 조금 더 편리하게 RxSwift를 사용할 수 있도록하는 Traits(특성)이라고 보면 되겠습니다.
이번 포스팅에서는 회원가입, 로그인 로직 뿐만 아니라 RxSwift로 구현하는 모든 로직에서 Stream을 종료시키지 않고 Error Handling하는 방법과 Single Traits에 대해서 알아보았습니다.
'iOS > RxSwift' 카테고리의 다른 글
RxSwift - Unicast, Multicast (1) | 2023.11.06 |
---|---|
RxSwift - Dispose, Disposable, DisposeBag (2) | 2023.11.03 |
RxSwift - Subject (1) | 2023.11.02 |
RxSwift 알아보기 (1) | 2023.11.01 |