Swift는 ARC를 통해서 메모리를 관리하고,
Closure에서 self를 참조할 때 순환 참조가 발생해 메모리 누수가 발생할 수 있습니다.
RxSwift를 사용하면, Observable 객체를 subscription할 때 거의 필수적으로 closure를 사용하고, 값을 가지고, UI를 최신화 할 때 self.view
와 같이 ViewController의 UI 객체에 접근하는 경우가 많기 때문에 closure안에서 self의 사용이 빈번합니다.
class ViewController: UIViewController {
let disposeBag = DisposeBag()
button.rx.tap
.bind { self.label.text = "Hello World" }
.disposed(by: disposeBag)
}
위 코드는 기본적인 RxSwift 문법으로,
Observable을 Subscription하고, DisposeBag으로 Disposable을 관리하는 코드입니다.
disposeBag
은 ViewController
가 disposeBag
의 참조를 해제했을 때 가지고 있는 disposable을 모두 dispose합니다.
이 말은 즉, disposeBag
의 할당이 해제되지 않으면 모든 disposable의 할당이 해제될 수 없다는 말입니다.
그런데 위의 코드에서는,
ViewController -> disposeBag -> Subscription(bind) -> self(ViewController) 형태의 순환참조가 있기 때문에
disposeBag
은 dealloc을 평생 기다리고, 담당하는 disposable들도 평생 dispose하지 못합니다.
class ViewController: UIViewController {
let disposeBag = DisposeBag()
button.rx.tap
.bind { [weak self] in
self?.label.text = "Hello World"
}
.disposed(by: disposeBag)
}
bind closure에 [weak self]를 명시해서 self에 대한 참조를 약한 참조로 해주면, Reference count를 증가 시키지 않기 때문에 순환 참조를 끊을 수 있습니다.
button.rx.tap
.bind { [weak self] in
self?.label.text = "Hello World"
self?.label2.text = "Hello World2"
self?.view.backgroundColor = .red
}
.disposed(by: disposeBag)
그리고 위처럼 self?.
가 너무 많아서 보기 좋지 않다면,
button.rx.tap
.bind { [weak self] in
guard let self = self else { return }
self.label.text = "Hello World"
self.label2.text = "Hello World2"
self.view.backgroundColor = .red
}
.disposed(by: disposeBag)
guard let
을 통해 self를 unwrapping하여 사용할 수 있습니다.
또는 RxSwift 6.0에서 추가된 withUnretained()
operator를 이용할 수 있습니다.
viewModel.someInfo // Observable<String>
.withUnretained(self) // (self, String) 튜플로 변환
.bind { (owner, string) in
owner.label.text = string
}
.disposed(by: disposeBag)
결국 순환 참조 문제는 closure 안에서 self를 참조하기 때문에 생기는 문제기 때문에,
self 사용 자체를 안하면 순환 참조의 문제를 방어할 수 있습니다.
class ViewController: UIViewController {
let disposeBag = DisposeBag()
button.rx.tap
.map { "Hello World" }
.bind(to: label.rx.text)
.disposed(by: disposeBag)
}
위처럼 map
, bind(to:)
를 사용하면 self를 사용하지 않고 원하는 동작을 할 수 있기 때문에 순환 참조가 발생하지 않습니다.
button.rx.tap
.bind {
self.view.layer.shadowColor = UIColor.red.cgColor
self.view.layer.shadowOpacity = 1
self.view.layer.shadowOffset = .zero
self.view.layer.shadowRadius = 10
}
.disposed(by: disposeBag)
하지만 위처럼 여러개의 property들을 한 번에 변경하고 싶을 때는 선언형 스타일을 적용하기 어렵습니다.
이를 위해서 RxCocoa
에 포함된 Binder<>
를 사용하면 여러 명령을 하나의 bindable property로 만들 수 있습니다.
lazy var enableShadow = Binder<Void>(self.view) { view, _ in
view.layer.shadowColor = UIColor.red.cgColor
view.layer.shadowOpacity = 1
view.layer.shadowOffset = .zero
view.layer.shadowRadius = 10
}
button.rx.tap
.bind(to: enableShadow)
.disposed(by: disposeBag)
이번 포스트에서는 RxSwift에서 순환 참조로 인한 메모리 누수를 해결하기 위해서,
[weak self]
와 Binder
를 사용하는 방법을 정리해봤습니다.
여러 가지 방법이 있는 만큼 적용할 코드에 적절한 방법을 선택하면 될 것 같습니다.