본문 바로가기
iOS/RxSwift

[RxSwift] withUnretained 를 대체하는 subscribe(with:onNext:)

by 최지철 2024. 10. 28.
반응형

 

 

withUnretained란

RxSwift 6부터 나온 기능으로, Observable 시퀀스에서 방출되는 요소들과 함께 특정 객체를 약한 참조로 묶어(unretained), 메모리 누수(Memory Leak)를 방지하는 데 사용합니다

* 이렇게 적으니 먼가 애매하죠? RxSwift공식 깃헙에 적힌 내용입니다만, 코드로 보는게 더 이해가 빠를 것 같네요

 

       	 info
            .subscribe(onNext: { [weak self] in
      		/// ~내용~~
      			
            })
            .disposed(by: disposeBag)

클로저 내부의 self는 강한 참조로 캡처되며, disposeBag도 self에 의해 유지되므로 강한 참조 순환이 발생합니다. 이로 인해 ViewController가 더 이상 필요 없더라도 메모리에서 해제되지 않아 메모리 누수(Memory Leak)가 발생할 수 있습니다. 그렇기 때문에 저희는 다들 아시다시피 약한참조[weak self]로 메모리 누수를 강한 참조 순환을 방지합니다. 

withUnretained은 이러한 [weak self]를 대채하여, 코드의 가독성을 높이고자 등장하였습니다.

withUnretained를 사용하면 코드가 더 간결하고 명확할 뿐만 아니라, 'guard let self = self else { return }'을 사용해서 self의 존재를 확인할 필요도 없습니다.

 

withUnretained 사용 예제

import RxSwift

class ViewController: UIViewController {
    let disposeBag = DisposeBag()
    let timerObservable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        timerObservable
            .withUnretained(self)
            .subscribe(onNext: { (owner, value) in
                owner.updateUI(value)
            })
            .disposed(by: disposeBag)
    }
    
    func updateUI(_ value: Int) {
        print("Timer value: \(value)")
    }
}

 

  1. 최상위 클로저에서 한 번 'withUnretaind(self)'를 사용해서 'self'를 안전하게 처리합니다.
  2. owner로 접근해서 코드를 간결하게 유지할 수 있습니다.
  3. 중첩 클로저 내부에서는 'weak owner'를 사용하여 약한 참조를 유지할 수 있습니다
    self가 더 이상 존재하지 않을 때 구독을 자동으로 해제하여 메모리 관리가 더 안전합니다

 

불안정한 withUnretained

withUnretained의 관한 PR

Driver에 대해 특별히 Unretained를 사용하여 더 이상 사용하지 마십시오.
withUnretained의 다른 구현에 대한 문서를 업데이트하여 재생 동작을 살펴보는 것이 명확해지도록 했습니다.
모든 항목(Observable, Driver, Signal, Infallible, Completable, Single, Maybe)에 대해 새로운 subscribe(with:onNext:etc:) 연산자를 추가합니다.

 

withUnretained은 사이드이펙트가 있습니다.

dirve에서 함께 사용할 경우, 작업 중인 스트림에 share(replay: 1) 작업이 있는 경우, replay buffer 는 값과 함께 self 를 버퍼링하므로 일부 시나리오에서 retain cycle 을 만들어낼 수 있습니다.

그 이유는 Driver 내부적으로 share(replay:1)을 사용하기 때문입니다.

Driver 는 특별하게 기본적으로 마지막 값을 반복하기 때문에 Driver 에 대해서 withUnretained 을 deprecated 해야 했습니다.

 

 

그렇게 등장한 subscribe(with:onNext:)

        timerObservable
            .subscribe(with: self, onNext: { owner, value in
                owner.updateUI(value)
            })
            .disposed(by: disposeBag)
    
    	//MARK: Bind
                
            textField.rx.text.orEmpty
            .bind(with: self, onNext: { owner, text in
                owner.label.text = text
            })
            .disposed(by: disposeBag)
            
       //MARK: Driver
            
         button.rx.tap
            .asDriver()
            .drive(with: self, onNext: { owner, _ in
                owner.label.text = "Button Tapped"
            })
            .disposed(by: disposeBag)
  •  onNext 뿐만 아니라 onError, onCompleted, onDisposed 에도 약한 참조를 한 캡처리스트를 사용할 수 있습니다.(이전에는 onError 경우에는 withUnretained 를 사용하게 되도 약한 참조를 가지는 객체를 전달하지 못함)

 

결론

subscribe(with:onNext:)와 그와 유사한 bind, drive, emit의 확장 메서드를 사용하면 메모리 누수를 방지하면서도 코드의 가독성을 높일 수 있습니다. 특히, 클로저 내부에서 [weak self]와 옵셔널 바인딩을 반복하는 번거로움을 줄여줍니다.

반응형