RxFlow 파트 3 : 팁과 요령
2018.12.03
#RxSwift • #RxFlow • #Architecture
by Thibault Wittemberg, translated by pilgwon
길었던 RxFlow 시리즈의 마지막입니다. 프레임워크의 핵심 기능이나 규칙들은 이미 아래 두 글에서 소개했으니 읽어보는 것을 추천드립니다.
지금부터 개발하면서 얻게 된 팁과 요령에 대해 얘기해보겠습니다.
UIViewController 반응형으로 만들기
2편에서처럼 우리는 어떤 지점에선 Presentable 이 보여질지 말지를 반응형의 방식으로 알아야 했었습니다. Presentable은 3가지 옵저버블을 가지고 있습니다.
/// 현재 Presentable이 보여지고 있으면
/// 불리언 값으로 트리거하는 옵저버블입니다
var rxVisible: Observable<Bool> { get }
/// 이 presentable이 화면에 처음으로 그려지면
/// 딱 한 번 트리거됩니다
var rxFirstTimeVisible: Single<Void> { get }
/// presentable이 사라지면 딱 한 번 트리거됩니다
var rxDismissed: Single<Void> { get }
RxFlow 에선 UIViewController가 이 프로토콜을 따릅니다. 그래서 우리는 이것들을 반응형으로 만들어줄 방법을 찾아야합니다.
이 작업을 할 때 저에게 도움을 많이 준 훌륭한 프로젝트가 있습니다. 바로 RxViewController입니다.
이 프로젝트는 제가 Verstatile name space in Swift라는 글에서 설명했던 패턴처럼 UIViewController에게 반응형 익스텐션을 제공합니다. 더 나아가서 셀렉터 호출을 관찰할 수 있게 해주는 RxCocoa의 내장된 함수를 사용합니다.
extension Reactive where Base: UIViewController {
/// 뷰가 처음으로 나타나면 트리거됩니다
public var firstTimeViewDidAppear: Single<Void> {
return sentMessage(#selector(Base.viewDidAppear)).map { _ in
return Void()
}.take(1).asSingle()
}
/// 뷰가 사라지면 트리거됩니다
public var dismissed: ControlEvent<Bool> {
let source = sentMessage(#selector(Base.dismiss))
.map { $0.first as? Bool ?? false }
return ControlEvent(events: source)
}
/// 뷰의 상태가 변경되면 트리거됩니다
public var displayed: Observable<Bool> {
let viewDidAppearObs = sentMessage(#selector(Base.viewDidAppear))
.map { _ in true }
let viewWillDisappearObs = sentMessage(#selector(Base.viewWillDisappear))
.map { _ in false }
return Observable<Bool>.merge(viewDidAppearObs, viewWillDisappearObs)
}
}
다음은 Flow 의 “navigate(to:)” 함수가 만들어낸 “nextPresentable”이라는 Presentable 이 있을 때 Coordinator 를 사용하는 방법의 예시입니다. 관련된 Presentable 이 처음으로 보여지고 난 직후에 다음 Stepper 의 행동에 대해서만 듣도록 하겠습니다.
nextPresentable.rxFirstTimeVisible.subscribe(onSuccess: { [unowned self,
unowned nextPresentable,
unowned nextStepper] (_) in
// presentable의 Stepper를 듣습니다
// 새로운 Step 값마다 우리는 새로운 네비게이션 프로세스를 트리거합니다
// 이것은 RxFlow 전체 매커니즘의 핵심 규칙입니다
// 이 처리 과정은 presentable이 화면에서 사라질때마다 멈춥니다
// 예를 들어 다른 presentable이 그것의 위에 있을 경우의 ViewController의 계층처럼요
nextStepper.steps
.pausable(nextPresentable.rxVisible.startWith(true))
.asDriver(onErrorJustReturn: NoStep())
.drive(onNext: { [unowned self] (step) in
// the nextPresentable's Stepper fires a new Step
self.steps.onNext(step)
}).disposed(by: nextPresentable.disposeBag)
}).disposed(by: self.disposeBag)
잠시 쉬어가겠습니다
또 다른 RxFlow 의 핵심 규칙 중 하나는 Flow 에 무슨 일이 있더라도 Flow 에 있어라 입니다. 그래서 저는 Flow가 뷰 계층의 제일 위에 있지 않을 때에는 Step 의 구독을 “멈추는” 방법을 찾아야 했습니다.
RxSwift에 구독을 멈출 수 있는 즉시 사용 가능한 방법을 제공하지 않습니다. 하지만 RxSwiftExt는 제공하죠. 이것은 RxSwiftCommunity의 프로젝트입니다. 이 프로젝트는 RxSwift에 “pausable“같은 수많은 새로운 연산자를 추가해줍니다.
이는 두 번째 옵저버블 시퀀스의 마지막 요소가 참이 아닐때까지 원본 옵저버블의 요소를 멈춥니다.
다음 구현을 보시죠.
extension ObservableType {
/// 두 번째 옵저버블 시퀀스의 마지막 요소를 기반으로
/// 원본 옵저버블 시퀀스의 요소를 멈춥니다.
/// 두 번째 시퀀스의 가장 최근 발생한 값이
/// `true` 가아닐때까지 요소는 무시됩니다.
/// - Parameter Pauser: 원본 옵저버블 시퀀스를 멈추는데에 쓰이는 옵저버블 시퀀스입니다.
/// - Returns: pauser 옵저버블 시퀀스에 기반해서 멈춘 옵저버블 시퀀스입니다.
public func pausable<P: ObservableType> ( _ pauser: P) -> Observable<E>
where P.E == Bool {
return withLatestFrom(pauser) { element, paused in
(element, paused)
}.filter { _, paused in
paused
}.map { element, _ in
element
}
}
}
실제론 이것은 그저 RxSwift 내장 연산자 셋의 조합입니다.
- withLatestFrom: 메인 옵저버블에 의해 트리거된 값과 관련돼있습니다. 또 다른 옵저버블의 마지막 값은 “pauser”라고 불립니다.
- filter: “pauser” 옵저버블에서 나오는 값 중에 true인 값만 수용합니다.
- map: pauser 옵저버블의 값은 무시하기 때문에 반환되는 값은 오직 메인 옵저버블의 값 중 하나입니다.
다음은 Coordinator 에서 쓰이는 방식입니다.
nextStepper
.steps
.pausable(nextPresentable.rxVisible.startWith(true))
.asDriver(onErrorJustReturn: NoStep())
.drive(onNext: { [unowned self] (step) in
// nextPresentable의 Stepper가 새로운 Step을 생성합니다
self.steps.onNext(step)
}).disposed(by: nextPresentable.disposeBag)
코드 내용은 매우 직관적입니다. “rxVisible” 옵저버블의 값이 거짓이면 nextStepper의 Step은 멈춥니다.
프로토콜과 저장 프로퍼티(stored property)?
프로토콜 지향 프레임워크로서 RxFlow 는 개발자들이 여러 프로토콜을 만들었으면 합니다. 여러분이 그런 프레임워크를 만드는 사람이더라도 사용자가 프로토콜을 만들기 위해 수많은 함수와 프로퍼티들을 구현하는 모습을 보는 건 원하지 않을 것입니다.
프로토콜의 익스텐션으로 기본적인 구현을 제공할 수 있는 함수는 큰 문제가 아닙니다. 진짜 문제는 프로퍼티인데, Swift는 익스텐션에 무언갈 저장하는 것을 허락하고 있지 않기 때문입니다.
예를 들어 여러분이 Stepper 프로토콜을 구현하려고 할 때, 새로운 Step 값을 트리거할 수 있게 해주는 “step” 프로퍼티를 제공할 것입니다. 어떻게 해야할까요?
이 부분에서 RxSwiftCommunity가 또 한 번 큰 도움을 주었습니다. 저는 NSObject-Rx에서 영감을 얻었습니다. 이 프로젝트는 RxSwift DisposeBag을 저장하는 NSObject 익스텐션을 제공하고 있고, 목표는 NSObject를 받아서 쓰는 모든 클래스(특히 UIViewController)에 기본으로 DisposeBag을 제공하는 것입니다. 저는 정확히 이러한 기능을 프로토콜 익스텐션에서 제공하고 싶었습니다. 다음은 그게 적용된 Stepper의 코드입니다.
private var subjectContext: UInt8 = 0
/// Stepper에겐 오직 하나의 목적만이 있습니다.
/// 특정한 네비게이션 상태에 대응하는 Step을 발생시키는 것입니다.
/// 특정 Flow의 컨텍스트에선 상태 변경이 네비게이션 액션으로 이어지기도 합니다.
public protocol Stepper: Synchronizable {
/// Rx Observable이 새로운 Step을 트리거할 것입니다.
var steps: Observable<Step> { get }
}
public extension Stepper {
/// 새로운 Step을 발행할 step
public var step: BehaviorSubject<Step> {
return self.synchronized {
if let subject = objc_getAssociatedObject(self, &subjectContext)
as? BehaviorSubject<Step> {
return subject
}
let newSubject = BehaviorSubject<Step>(value: NoStep())
objc_setAssociatedObject(self,
&subjectContext,
newSubject,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newSubject
}
}
/// 새로운 Step을 트리거할 Rx Observable
public var steps: Observable<Step> {
return self.step.asObservable()
}
}
마법같은 일은 연산 프로퍼티(computed property)인 “step“에서 일어납니다. 우리는 BehaviorSuject에 대한 참조를 저장하기 위해 “objc_setAssociatedObject” 함수를 사용할 것입니다. (NSHipster의 글을 참고하세요) 이 프로퍼티에 접근할 때마다 우리는 이 저장된 레퍼런스를 되찾아옵니다. (처음 호출에선 BehaviorSuject가 생성되고 subjectContext 레퍼런스에 연관됩니다)
이 요령에는 결점이 있습니다. 프로토콜은 구조체와 같은 밸류 타입으로 적용될 수 있습니다. 이 말은 메모리가 힙이 아닌 스택에서 관리될 수 있다는 뜻입니다 (레퍼런스 타입처럼요). 그래서 구조체 인스턴스의 라이프 사이클과 재사용성이 Swift 런타임의 손에 달렸다는 뜻입니다. 런타임이 인스턴스를 다시 사용하려고 하면 “objc_getAssociatedObject”에 어떤 일이 일어날지 확신이 없다는 것입니다. 이를 더 안전하게 만들기 위해서 이러한 종류의 프로토콜은 클래스에서만 쓰일 수 있게 제한하는 구현이 필요합니다. 그렇게 한다면 모든 일이 힙에서 일어나는 것이 보증되기 때문이죠.
커뮤니티에 다시 돌려주기
앞에 글에서도 적었듯이 RxFlow의 몇몇 핵심 기능은 개발자 커뮤니티가 이미 만들어둔 작업을 기본으로 하고 있습니다. 저는 커뮤니티에게 받은 도움을 다시 돌려주는 것이 중요하다고 생각합니다.
RxFlow의 경우엔 두 번의 PR 기회가 있었고 모두 머지되었습니다.
제 코드가 다른 개발자들에게 도움이 돼서 정말 좋았습니다.
결론
첫 번째 오픈 소스 프로젝트를 만들면서 꽤 많은 도전 과제들이 있었고 절대 쉽지만은 않았습니다. 다음과 같은 이유들 때문입니다.
- 프로젝트에 넣고자 하는 모든 아이디어(이전 프로젝트에서 오거나, 직면했던 문제들을 해결했던 경험에서 오거나, 등)를 모아서 합성해야 합니다. 그러니 코딩하기 전에 생각하는 시간을 가지셔야 합니다.
- 여러분의 프로젝트의 복잡성에 적절한 패턴을 시도해야 합니다. 오버 엔지니어링을 하지마세요.
- 사용자의 마음으로 생각해야 합니다. 최대한 간단하게 유지하세요. (이게 가장 어려운 일입니다)
- 코드만으로는 프로젝트를 매력적이게 보이게 만들 수 없습니다. 좋은 README를 작성하셔야 합니다.
- 소스 관리를 전문적이게 하셔야 합니다. 못생긴 프로젝트엔 아무도 기여하려고 하지 않을 것입니다. (git CLI는 아주 좋은 친구입니다)
- 작업한 내용을 공유하는 도구로 블로그를 사용해보세요. 똑똑한 사람들에게 피드백이 올 것입니다.
- 믿음을 가지셔야 합니다. 매 시간 자신의 결정이 맞나 의심하게 될 것입니다. 그러니 지쳤을 땐 머리를 비우고 쉬셔야 아이디어들이 다시 생각날 것입니다.
RxFlow 1.0.1 버전이 CocoaPods와 Carthage에서 사용 가능합니다. 저는 앞으로 제 사이드 프로젝트에 계속 써볼 생각입니다. 그리고 그에 관한 내용도 블로그 글로 작성하겠습니다!
RxFlow GitHub 저장소: https://github.com/RxSwiftCommunity/RxFlow
다음에 또 뵙겠습니다.