RxSwift - withUnretained
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 RxSwift의 withUnretained에 대해 정리해보겠습니다 🙋🏻
RxSwift를 사용하면서 메모리 누수 혹은 강한 순환 참조 문제를 만날 수 있는 상황이 많습니다.
물론 이를 위해 weak self를 사용하여 해결할 수 있지만, 귀찮잖아요..?
그래서 보다 가독성이 좋고 편리하게 withUnretained를 사용할 수 있습니다.
그럼 한번 withUnretained가 무엇이고 어떻게 사용하는지 알아볼까요?
withUnretained?
withUnretained는 RxSwift에서 weak self 패턴을 보다 명확하고 안전하게 사용할 수 있도록 도와주는 연산자입니다.
즉, 기존 weak self 사용 시 발생시키는 옵셔널을 처리하는것에 대해 동일하게 처리를 해주는 동시에 가독성도 높여줄 수 있어요.
withUnretained는 기본적으로 self를 약한 참조로 캡처한 후, self가 여전히 존재하는 경우에만 클로저 내부에서 안전하게 사용할 수 있도록 해줍니다.
결국 순환 참조를 방지해주는 본질적인 역할을 하면서 코드의 가독성, 안정성과 개발 편리성을 가져갈 수 있죠 😃
withUnretained 사용 방법
코드를 하나 먼저 볼까요?
import RxSwift
import RxCocoa
class ExampleViewModel {
let disposeBag = DisposeBag()
let buttonTap = PublishSubject<Void>()
init() {
buttonTap
.withUnretained(self)
.subscribe(onNext: { owner, _ in
owner.buttonTapped()
})
.disposed(by: disposeBag)
}
func buttonTapped() {
print("버튼이 탭되었습니다.")
}
}
요렇게 AI에게 withUnretained 관련 간단 예시 코드 하나 보여줘~ 해서 나온 코드입니다 😁
코드를 보면, buttonTap Subject가 방출이 될 때 self를 강한 순환 참조가 되지 않도록 withUnretained를 사용할 수 있어요.
withUnretained에 self를 담아주어 연산자로 넣기만 하면 간단히 해결됩니다.
withUnretained(self)는 코드에서 self가 nil이 아닐 때만 안전하게 사용됩니다.
기존의 weak self 패턴과는 달리 self가 옵셔널이 아니게 되기에, if let이나 guard let으로 옵셔널 바인딩 해제할 필요가 없어졌죠!
그럼 기존 방식으로 했던 weak self하고 어떤점이 차이가 있는지 살펴볼께요 🧐
weak self vs withUnretained
먼저 같은 코드에 대해 weak self 사용 예시를 볼까요?
buttonTap
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.buttonTapped()
})
.disposed(by: disposeBag)
요렇게 guard let 옵셔널 바인딩으로 해제해주고 있는 익숙한 형태입니다.
withUnretained를 사용하면 아래처럼 간결하게 표현되겠죠?
buttonTap
.withUnretained(self)
.subscribe(onNext: { owner, _ in
owner.buttonTapped()
})
.disposed(by: disposeBag)
공통점으로는 옵셔널 바인딩 해주는 역할은 같아요.
차이점으로는 이렇게 정리해볼 수 있을것 같아요. (아주 당연하고 단순한 이야기지만..)
항목 | weak self | withUnretained |
self 처리 방식 | Optional로 캡처 | Non-Optional로 안전하게 처리 |
후속 옵셔널 해제 필요 여부 | 필요 | 불필요 |
가독성 | 조금 떨어짐 | 깔끔함 |
그럼 이어서 withUnretained의 내부 동작 원리를 살펴볼께요 🏃🏻
withUnretained 내부 동작 원리
withUnretained는 내부적으로 weak self와 유사한 방식으로 동작하지만, 클로저 내에서 Optional을 직접 해제하지 않아도 되도록 설계되어 있습니다.
설계된 코드를 볼까요?
public func withUnretained<Object: AnyObject, Out>(
_ obj: Object,
resultSelector: @escaping (Object, Element) -> Out
) -> Observable<Out> {
map { [weak obj] element -> Out in
guard let obj = obj else { throw UnretainedError.failedRetaining }
return resultSelector(obj, element)
}
.catch { error -> Observable<Out> in
guard let unretainedError = error as? UnretainedError,
unretainedError == .failedRetaining else {
return .error(error)
}
return .empty()
}
}
코드를 보시면 map 내부에서 obj를 weak로 캡처해 메모리에서 해제될 수 있도록 해줍니다.
그리고 guard let 바인딩을 통해 obj가 해제된 경우 예외를 던져주죠.
예외 시 catch를 통해 명시적으로 에러를 처리하고 있어요.
여기선 empty를 반환하도록 설계되어 있죠.
사실 상 compactMap의 구현체와 비슷하다는 느낌이 들죠?
nil인 경우 자동으로 필터링 되는게 아니라 여기선 nil을 empty 처리한것의 차이 정도죠.
에러가 아니라면 클로저 실행을 통해 Out을 반환해요.
그럼 실제로 이 withUnretained를 어떻게 활용하는지 예시를 살펴볼께요 😃
withUnretained 실제 사용 사례
UI 이벤트 핸들링에 사용될 수 있습니다.
class ViewController: UIViewController {
let disposeBag = DisposeBag()
let button = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
button.rx.tap
.withUnretained(self)
.subscribe(onNext: { owner, _ in
owner.showAlert()
})
.disposed(by: disposeBag)
}
func showAlert() {
print("Alert 표시")
}
}
요런식으로 간단하게 표현이 가능하죠.
또한, 네트워크 요청에서도 사용이 가능하죠.
class APIService {
func fetchData() -> Observable<String> {
return Observable.just("데이터 로드 완료")
}
}
class ViewModel {
let disposeBag = DisposeBag()
let apiService = APIService()
func loadData() {
apiService.fetchData()
.withUnretained(self)
.subscribe(onNext: { owner, data in
owner.handleData(data)
})
.disposed(by: disposeBag)
}
func handleData(_ data: String) {
print("데이터: \(data)")
}
}
결국 사실상 옵셔널 바인딩이 필요할것 같다면 어디든 사용할 수 있는것이죠.
Observable 타입이 사용되는곳에서요 😃
편리한 이 withUnretained도 사용 시 주의할 점이 있어요.
withUnretained 사용 시 주의점
1️⃣ withUnretained는 self가 사라진 경우에 방출 자체가 이뤄지지 않기에, 특정 시점 이후에도 이벤트가 방출되어야 하는 경우라면 적절치 않음
2️⃣ 남용 시 디버깅이 어려워질 수 있기에 상황에 맞게 사용이 필요
3️⃣ Driver에서는 사용하지 못함 (정확히는 share(replay: 1)을 사용하는 환경에서는 같이 사용할 수 없음)
3번에 대해선 아래 PR을 참고해보시면 좋을것 같아요.
순환 참조의 발생이 요지입니다.
Add new subscribe/drive/emit with object, Deprecate withUnretained on Driver by freak4pc · Pull Request #2290 · ReactiveX/RxSw
tl;dr This PR will: Hard deprecate withUnretained for Driver specifically Update documentation for the other implementations of withUnretained so it’s obvious to look out for replaying behavior Ad...
github.com
즉, 결국 옵셔널 바인딩을 내부에서 해결해주기에 디버깅이 어려울 수 있고 nil로 들어올 경우 이후 작업이 진행되지 않기에 그런 상황을 꼭 판단해봐야 합니다!
마무리
RxSwift에선 사실상 메모리 관리가 매우 중요한 포인트이기에 withUnretained를 이용해 조금 더 안전하고 가독성 좋게 해결해보면 좋을것 같아요.
무엇보다 편리합니다 😁
레퍼런스
RxSwift/RxSwift/Observables/WithUnretained.swift at main · ReactiveX/RxSwift
Reactive Programming in Swift. Contribute to ReactiveX/RxSwift development by creating an account on GitHub.
github.com