ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RxFlow란
    소프트웨어공학/아키텍처 패턴 2022. 10. 9. 18:14

    이번 글에서는 RxFlow란 무엇인지에 대해 공부한다.

    RxFlow란

    RxFlow is a navigation framework for iOS applications based on a Reactive Flow Coordinator pattern.

    RxFlow 공식 깃허브에서는 이렇게 정의하고 있다.

    Coordinator 패턴(+ flow coordinator)은 이전 포스트에서 공부한 적이 있는데 잘 모르겠다면 참고하길 바란다.

    결국 Coordinator 패턴을 반응형 기반으로 구현하게 만들어주는 네비게이션 프레임워크인 셈이다.

    RxFlow를 쓰는 이유

    네비게이션 로직을 분리해야 하는 이유는 Coordinator 패턴에 적어뒀기 때문에 다시 언급하진 않으려고 한다.

    그럼 왜 굳이 RxFlow를 써야할까?

    깃헙 리드미에 소개된 블로그에선 앱을 만들때마다 Coordinator 매커니즘을 작성해야 하고,

    VC/VM에서 Coordinator와 직접적으로 상호작용하지 않고 delegate 패턴으로 소통하는 탓에,

    보일러플레이트 코드가 많아진다는 점을 지적한다.

     

    RxFlow는 네비게이션을 선언형으로 작성할 수 있게 해주고,

    내장 Coordinator를 제공하고,

    Coordinator와의 커뮤니케이션을 반응형으로 바꿔 위에서의 보일러플레이트 코드 이슈를 해결하게 해준다.

     

    두번째 이유는 직접 Coordinator를 작성하고 화면을 추가할때마다 매번 delegate 프로토콜을 작성해본 사람이라면,

    공감할 수 있는 문제라고 생각한다.

    첫번째 이유는 앱을 새로 만드는 경험이 자주 있는 것도 아니고, RxFlow 자체의 러닝커브가 높은 편이라서 설득력이 크진 않았다.

    RxFlow를 어떻게 사용할까

    Stepper는 화면 이동을 촉발할 수 있는 객체이다.

    기존에는 화면 이동을 촉발하는 객체(VC/VM)가 delegate에게 화면 전환 액션이 발생할때 대응하는 메서드를 호출해주는 식이었다면,

    RxFlow에서는 Stepper가 내부 스트림을 통해 대응하는 step을 전달하는 식으로 이루어진다.

    물론 VC나 VM만 Stepper가 될 수 있는건 아니다. 화면 이동을 유발할 수 있는 모든 객체를 Stepper라고 부르는 것이다.

     

    step은 화면 전환 액션에 대응되는 네비게이션 상태를 말한다. 이때 전환될 화면과는 연관이 없어야 한다.

    step이 검색 화면, 프로필 화면특정 화면을 나타내도록 구현했다면 RxFlow 설계 의도에서 벗어난 구현 방법이다.

    검색 버튼 눌림, 프로필 버튼 눌림과 같이 네비게이션 상태를 나타내도록 구현하는 것이 의도에 맞는 구현 방법이다.

    버튼이 눌렸다액션뿐만 아니라 영화 목록과 같은 단순한 상태를 표현해도 된다.

    이렇게 해야하는 이유는 context에 따라 네비게이션 상태에 대응하는 도착 화면이 달라질 수 있기 때문이다.

     

    이는 똑같이 프로필 버튼을 눌러도 상황에 따라 다른 화면으로 이동할 수 있다는 의미이다.

    그리고 그 상황, context를 의미하는게 Flow이다.

    flow와 step이 모두 존재해야 도착 화면을 결정할 수 있다.

    Flow는 이름 그대로 회원가입 플로우, 사용자 인증 플로우같은 사용자 플로우를 나타낸다.

     

    그럼 stepper가 flow를 알아야 하는가? 싶었는데 그건 아니었다.

    Stepper는 step만 알면 된다.

    step을 방출하는 stepper가 어느 flow에 속하는지를 알려주는건 해당 stepper의 상위 객체의 책임이다.

     

    예를 들면, 아래와 같은 데모 코드가 있다.

    let appFlow = AppFlow(services: self.appServices)
    
    self.coordinator.coordinate(flow: appFlow, with: AppStepper(withServices: self.appServices))

    stepper인 AppStepper는 자신이 어떤 flow에 속하는지 몰라도 된다.

    AppStepper의 context, 즉 flow가 무엇인지는 AppStepper를 인스턴스화하는 상위 객체의 책임이고,

    코드에서도 상위 객체가 AppStepper의 flow는 AppFlow라고 알려주는 것을 볼 수 있다.

     

    AppStepper 코드를 봐도 step은 알지언정 Flow에는 전혀 의존성이 없는 것을 확인할 수 있다.

    class AppStepper: Stepper {
    
        let steps = PublishRelay<Step>()
        private let appServices: AppServices
        private let disposeBag = DisposeBag()
    
        init(withServices services: AppServices) {
            self.appServices = services
        }
    
        var initialStep: Step {
            return DemoStep.dashboardIsRequired
        }
    
        /// callback used to emit steps once the FlowCoordinator is ready to listen to them to contribute to the Flow
        func readyToEmitSteps() {
            self.appServices
                .preferencesService.rx
                .isOnboarded
                .map { $0 ? DemoStep.onboardingIsComplete : DemoStep.onboardingIsRequired }
                .bind(to: self.steps)
                .disposed(by: self.disposeBag)
        }
    }

     

    그렇게 화면이 전환되어서 화면에 보여질 수 있는 객체를 Presentable이라고 한다.

    이름에서 알 수 있듯 추상화된 객체이다. 보여질 수 있는 객체는 대표적으로 UIViewController가 있다.

    또 Flow도 Presentable인데, 이는 한 Flow가 진행중인 상황에서 필요에 따라 다른 Flow가 화면에 보여질 경우를 상상하면 된다.

    예를 들면 글쓰기 Flow를 진행중인데 본인인증이 필요해서 중간에 인증 Flow가 시작되는 경우이다.

     

    정리하면,

    앱의 모든 네비게이션 경우의 수는 우리가 정의한 flow와 step의 조합으로 표현된다.

    flow는 각 step을 처리할 수 있는 네비게이션 로직을 가져야 한다.

    실제로 네비게이션 로직을 실행해주는 역할은 RxFlow에 의해 제공되는 FlowCoordinator가 맡는다.

     

    FlowCoordinator 덕분에 우리가 임의로 Coordinator 객체를 여러개 만들지 않아도 된다.

    Coordinator 추가하고 Delegate 프로토콜 작성하고 하는 과정들을 줄일 수 있게 되는 것이다.

    FlowContributors

    각 flow에서 step이 주어질때 전환하는 로직은 func navigate(to step: Step) -> FlowContributors 함수에 작성하게 된다.

    navigate 함수의 반환형은 FlowContributors인데 구현 코드는 아래와 같다.

     

    /// FlowContributors represent the next things that will trigger
    /// navigation actions inside a Flow
    ///
    /// - multiple: several FlowContributors will contribute to the Flow
    /// - one: only one FlowContributor will contribute to the Flow (see the FlowContributor enum)
    /// - end: represents the dismissal of this Flow, forwarding the given step to the parent Flow
    /// - none: no further navigation will be triggered
    /// - triggerParentFlow: same as .one(flowContributor: .forwardToParentFlow(withStep: Step)). It is deprecated.
    public enum FlowContributors {
        /// a Flow will trigger several FlowContributor at the same time for the same Step
        case multiple (flowContributors: [FlowContributor])
        /// a Flow will trigger only one FlowContributor for a Step
        case one (flowContributor: FlowContributor)
        /// a Flow will trigger a special FlowContributor that represents the dismissal of this Flow
        case end (forwardToParentFlowWithStep: Step)
        /// no further navigation will be triggered for a Step
        case none
        /// same as .one(flowContributor: .forwardToParentFlow(withStep: Step)). Should not be used anymore
        @available(*, deprecated, message: "You should use .one(flowContributor: .forwardToParentFlow(withStep: Step))")
        case triggerParentFlow (withStep: Step)
    }

     

    중간중간 등장하는 FlowContributor는 아래와 같다.

    /// A FlowContributor describes the next thing that will contribute to a Flow.
    ///
    /// - contribute: the given stepper will emit steps
    /// (according to lifecycle of the given presentable and the allowStepWhenNotPresented parameter) that will contribute
    /// to the current Flow
    /// - forwardToCurrentFlow: the given step will be forwarded to the current flow
    /// - forwardToParentFlow: the given step will be forwarded to the parent flow
    public enum FlowContributor {
        /// the given stepper will emit steps, according to lifecycle of the given presentable, that will contribute to the current Flow
        /// `allowStepWhenNotPresented` can be passed to make the coordinator accept the steps from the stepper even id
        /// the presentable is not visible
        /// `allowStepWhenDismissed` can be passed to make the coordinator accept the steps from the stepper even
        /// the presentable  has dismissed (e.g UIPageViewController's child)
        case contribute(withNextPresentable: Presentable,
                        withNextStepper: Stepper,
                        allowStepWhenNotPresented: Bool = false,
                        allowStepWhenDismissed: Bool = false)
        /// the "withStep" step will be forwarded to the current flow
        case forwardToCurrentFlow(withStep: Step)
        /// the "withStep" step will be forwarded to the parent flow
        case forwardToParentFlow(withStep: Step)
    
        /// Shortcut static func that returns a .contribute(withNextPresentable: _, withNextStepper: _)
        /// in case we have a single actor that is a Presentable and also a Stepper
        ///
        /// - Parameter nextPresentableAndStepper
        /// - Returns: .contribute(withNextPresentable: withNext, withNextStepper: withNext)
        public static func contribute(withNext nextPresentableAndStepper: Presentable & Stepper) -> FlowContributor {
            return .contribute(withNextPresentable: nextPresentableAndStepper, withNextStepper: nextPresentableAndStepper)
        }
    }

    FlowContributor의 contribute 케이스는 주석을 읽어보면,

    새로운 stepper가 step을 방출하도록 등록하는 것으로 보인다.

    이때 Presentable을 넣어서 이 presentable의 생명 주기에 따라 stepper의 step 방출을 받아들일지 여부를 함께 결정하는 구조이다.

    presentable이 보이지 않거나 dismiss되었을때도 이 stepper가 방출하는 step을 받아들일 것인지를 파라미터로 함께 받는 것을 볼 수 있다.

    flowCoordinator는 앱에 하나만 존재하고 모든 stepper가 여기에 연결되어서 step을 방출하려고 하는데,

    어떤 step을 받아들일지, 대응하는 presentable을 참고하여 coordinator가 결정을 내리는 방식으로 보인다.

     

    forwardToCurrentFlowforwardToParentFlow를 보면 단순히 현재 혹은 부모의 flow로 step을 포워딩하는 걸로 보인다.

    좀 더 사용하면서 알아가야 할 필요가 있어보인다.

     

    정리

    iOS 앱을 만들때 네비게이션 로직은 스토리보드의 segue를 이용하는 것에서,

    코드로 UIViewController에 구현하기,

    Coordinator같은 별도의 객체로 분리하기 (화면 -> 화면만),

    하나 이상의 화면 전환으로 구성된 flow 전체를 분리하기로 발전했다.

     

    RxFlow를 이용하면 flow 전체를 분리하고 재사용 가능하게 만들수 있다는 점에서 매력적이지만,

    개인적으로 학습하는 시간이 짧지만은 않았다.

    특정 flow가 앱 전반에서 계속해서 재사용되어야 하는 경우가 아니라,

    단순히 화면 -> 화면의 로직만 분리해주는 것으로 충분한 경우라면 RxFlow를 사용하는건 오버엔지니어링이 될 것이다.

    '소프트웨어공학 > 아키텍처 패턴' 카테고리의 다른 글

    Coordinator 패턴이란  (0) 2022.10.08
    아키텍처 패턴이란  (1) 2022.10.03
Designed by Tistory.