[RxSwift] Closure에서 Memory Leak 피하기

이남준·2021년 11월 24일
1
post-thumbnail

[RxSwift] Closure에서 Memory Leak 피하기

Swift는 ARC를 통해서 메모리를 관리하고,
Closure에서 self를 참조할 때 순환 참조가 발생해 메모리 누수가 발생할 수 있습니다.

RxSwift를 사용하면, Observable 객체를 subscription할 때 거의 필수적으로 closure를 사용하고, 값을 가지고, UI를 최신화 할 때 self.view와 같이 ViewController의 UI 객체에 접근하는 경우가 많기 때문에 closure안에서 self의 사용이 빈번합니다.

Reference Cycle

class ViewController: UIViewController {
    let disposeBag = DisposeBag()

    button.rx.tap
        .bind { self.label.text = "Hello World" }
        .disposed(by: disposeBag)
}

위 코드는 기본적인 RxSwift 문법으로,
Observable을 Subscription하고, DisposeBag으로 Disposable을 관리하는 코드입니다.

disposeBagViewControllerdisposeBag의 참조를 해제했을 때 가지고 있는 disposable을 모두 dispose합니다.

이 말은 즉, disposeBag의 할당이 해제되지 않으면 모든 disposable의 할당이 해제될 수 없다는 말입니다.

그런데 위의 코드에서는,
ViewController -> disposeBag -> Subscription(bind) -> self(ViewController) 형태의 순환참조가 있기 때문에
disposeBag은 dealloc을 평생 기다리고, 담당하는 disposable들도 평생 dispose하지 못합니다.

해결 방법

1. [weak self] 사용하기

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)

2. Declarative style (선언형 스타일)로 self 사용 안하기

결국 순환 참조 문제는 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를 사용하는 방법을 정리해봤습니다.

여러 가지 방법이 있는 만큼 적용할 코드에 적절한 방법을 선택하면 될 것 같습니다.

profile
iOS 개발자의 기록

0개의 댓글