RxSwift에서 Swift 6 Concurrency 대응하기
우리 프로젝트에서는 이벤트 흐름을 RxSwift로 관리하고 있고,
버튼 클릭 -> 새로운 ViewController Push 의 흐름을 아래처럼 관리하고 있다
- 버튼 클릭 이벤트 발생
- ViewModel의 input으로 전달
- input이 들어오면 ViewController를 만들어서 output에 전달
- ViewController에서 output을 구독하고 있다가 전달된 ViewController push
ViewController는 어떤 ViewController를 만들어서 push 할지 알 필요 없이 전달되는 ViewController를 열기만 하면 돼서 코드가 간결해지는 장점이 있다
@MainActor
private func makeViewController() -> UIViewController {
return UIViewController()
}
// MARK: - ViewModel
input.openButtonDidTap
.map { [weak self] in
return self?.makeViewController()
}
.bind(to: self.output.pushViewController)
.disposed(by: self.disposedBag)
// MARK: - ViewController
output.pushViewController
.observe(on: MainScheduler.asyncInstance)
.bind { [weak self] in
guard let self else { return }
self.navigationController?.pushViewController($0, animated: true)
}
.disposed(by: self.disposedBag)
이 흐름을 코드로 나타내면 위와 같은데, 기존에는 input.openButtonDidTap.map { } 안에서 MainActor 함수를 호출해도 문제 없었으나 프로젝트의 Swift 버전을 6.0으로 올리면서 컴파일 오류가 발생한다.
MainActor 함수인
makeViewController()가 메인 액터 컨텍스트가 아닌 곳에서 호출되는 것이 문제라는 내용이다.
이 에러를 해결 하기 위해서 ViewModel의 코드를 아래처럼 수정해보려고 했다
input.openButtonDidTap
.bind { [weak self] in
Task { @MainActor in
self?.output.pushViewController.accept(
self?.makeViewController()
)
}
}
.disposed(by: self.disposedBag)
이렇게 수정하면 makeViewController()가 메인 액터 컨텍스트에서 호출되어서 에러가 해결되지만 bind 안에서 새로운 Task 가 생성되는 것도 그렇고, 그냥 보기에도 코드가 깔끔해 보이지 않는다…
이런 문제들을 대응하기 위해서 RxSwift 6.5.0 에서 Swift Concurrency 대응이 추가되었다
let someEvents = PublishRelay<Void>()
// 1. 기존
someEvents
.bind { [weak self] in
self?.doSomething()
}
.disposed(by: self.disposeBag)
// 2. 신규
Task { [weak self] in
for try await event in someEvents.values {
self?.doSomething()
}
}
이런 식으로 RxSwift 스럽게 스트림을 구독하는 코드를 AsyncStream인 .values에 접근하여 await을 사용하여 처리할 수 있도록 되었다
Task { [weak self] in
for try await _ in input.imageSearchButtonDidTap.values {
self?.output.pushViewController.accept(self?.makeViewController())
}
}
이걸 이용해서 위의 에러를 수정하면 위처럼 Swift Concurrency 스럽게 처리할 수 있게 되었다
.subscribe 가 아니라 Task { } 을 사용하면서 귀찮은 [weak self]를 안 붙여도 될까 기대했는데 실제로 AsyncStream을 구독하는 경우에는 [weak self] 를 명시하지 않으면 메모리가 해제되지 않는 걸 알 수 있다 이에 대한 자세한 내용은 # Swift — Task에서 weak self는 언제 쓸까요? 참조
이 방식 덕분에 Swift Concurrency 환경에서도 기존 RxSwift 흐름을 깔끔하게 유지할 수 있었다 앞으로도 Concurrency와 RxSwift의 공존 방식에 대해 좀 더 고민해보면서, 점점 이식해 나갈 예정이다
댓글 남기기