Combine에서 withUnretained 사용하기

지눙·2025년 5월 25일

Combine의 클로저

viewModel.$nickname
    .sink { value in
        self.updateUI(for: value)
    }
    .store(in: &cancellables)

Combine을 사용하다 보면 클로저 내부에서 self를 캡처하는 경우가 많습니다. 이때 무심코 strong reference로 self를 참조하면 메모리 누수(Retain Cycle)가 발생할 수 있는데요.

viewModel.$nickname
    .sink { [weak self] value in
        guard let self = self else { return }
        self.updateUI(for: value)
    }
    .store(in: &cancellables)

그렇기에 위와 같이 작성하곤 합니다.

하지만, 매번 반복해야 한다는 점이 번거롭고, 실수할 여지도 많습니다. 특히 여러 스트림을 다루다 보면 코드가 점점 지저분해지고 가독성이 떨어집니다.

RxSwift의 withUnretained

RxSwift는 이런 상황을 깔끔하게 처리할 수 있는 withUnretained 연산자를 제공합니다:

viewModel.nickname
    .withUnretained(self)
    .subscribe(onNext: { owner, value in
        owner.updateUI(for: value)
    })

이 방식은 클로저에서 self를 약한 참조로 안전하게 사용할 수 있도록 해줍니다. null 체크까지 내부에서 처리되기 때문에 guard 구문도 필요 없고 훨씬 간결하죠.

Combine에서 withUnretained 사용하기

extension Publisher {
    func withUnretained<T: AnyObject>(_ object: T) -> Publishers.CompactMap<Self, (T, Self.Output)> {
        compactMap { [weak object] output in
            guard let object = object else {
                return nil
            }
            return (object, output)
        }
    }
}

약한 참조로 순환 참조를 방지하고, guard-let을 이용하여 객체가 살아있는지 확인합니다.

compactMap을 이용하여 객체가 존재하지 않는다면 방출하지 않고, 존재하면 튜플을 반환합니다.

이제 뷰컨트롤러에서 Combine 스트림을 다룰 때 다음처럼 사용할 수 있습니다:

viewModel.$nickname
    .withUnretained(self)
    .sink { owner, value in
        owner.updateUI(for: value)
    }
    .store(in: &cancellables)

0개의 댓글