RIBs 정리 (2)

Yang Si Yeon·2022년 1월 22일
0

RIBs

목록 보기
2/2

자식 Life Cycle 관리는 부모가

부모 router가 child를 attach 할 때, child의 router를 또 생성하지 않도록 방어 로직을 작성할 수 있다.

func attachSuperPayDashboard() {
    // 방어 로직
    if superPayRouting != nil {
        return
    }

    let router = superPayDashboardBuildable.build(withListener: interactor)
        
    let dashboard = router.viewControllable
    viewController.addDashboard(dashboard)
        
    self.superPayRouting = router
    attachChild(router)
}

이렇게 superPayDashboard 뷰컨을 띄웠다가 닫고, 다시 attachSuperPayDashboard() 함수를 호출하면 화면이 뜨지 않는다. superPayRouting이 nil이 아니기 때문.

뷰컨을 띄웠던 부모가 무조건 자식을 책임지고 닫아야 한다는 것이 RIBs를 사용할 때 강제되어 좋은 점 중 하나이다. 따라서 present와 dismiss가 쌍으로 같은 class에 있게 되고 해당 Riblet이나 뷰컨은 어디서든 재사용 가능하다.

자신을 dismiss하는 코드를 자신이 부르면 사실상 해당 뷰컨은 재사용하기가 어려워진다. 부모의 입장에서 자식 뷰컨의 라이프사이클을 전적으로 관리할 수 없기 때문.

더 이상 ViewController가 앱을 주도하지 않고, Riblet의 Interactor가 앱의 로직을 주도하기 때문에 화면에 얽매이지 않고, 비즈니스 로직을 마음껏 구현할 수 있음.


Viewless Riblet

: View 없이 로직만 있는 Riblet

Viewless Riblet의 Dependency에는 ViewControllable이 추가되어 있다.
따라서 부모 Riblet이 Viewless Riblet을 위한 ViewController를 하나 지정해줘야 한다. 그리고 이 ViewController는 Router한테 넘겨지고, Router에서 해당 ViewController에 접근할 수 있다. Viewless Riblet에서 화면을 새로 띄우려면 이 ViewController에서 present나 dismiss하면 된다.

Viewless Rib을 붙이거나 떼기 위해서는 present나 dismiss 메서드를 사용하지 않고, Router에서 attachChild나 detachChild만 해주면 된다.

Viewless Riblet Router의 추가적인 역할

  • View가 있는 Riblet
    : 자식이 부모에게 "나 끝났다(didClose)" 를 알려주면 떠있던 화면을 dismiss 시키는 것은 부모의 역할.

  • Viewless Riblet
    : 부모가 직접 present한 View가 없기 때문에 부모가 dismiss를 하지 않는다. 따라서 Viewless Rilbet은 자신의 역할이 모두 끝난 후 본인이 띄웠던 화면을 모두 닫아줘야 한다.
    Router의 cleanupViews 메서드가 해당 용도로 사용

cleanupViews
: Interactor의 willResignActive 메서드 안에서 불린다. 즉, 부모가 Viewless Riblet을 detach할 때 불린다는 뜻. 따라서 cleanupViews는 이때 자기가 띄웠던 모든 뷰들을 닫아줘야 한다.

func cleanupViews() {
    if viewController.uiviewController.presentationController != nil,
       navigationController != nil {
           navigationController?.dismiss(completion: nil)
        }
}

Riblet에 데이터 전달하기

자식 Riblet에 데이터를 전달하는 방법은 2가지가 있다. 나는 개인적으로 1번을 선호하는 편이지만, 1번이 안되고 2번을 이용해야만 하는 경우가 종종 있다.

1번. 부모의 Component에 자식의 Dependency를 채택해서 전달하기

ChildInteractor에서 어떤 repository가 필요할 때, 이를 dependency로 주입받을 것이다. 이때 부모가 해당 repository를 가지고 있을 필요가 없으면 ChildBuilder에서 바로 생성해줄 수 있고, 부모가 repository를 필요로 하면 부모로부터 주입받을 것이다.

코드를 통해 살펴보자.

ChildInteractor.swif

protocol ChildInteractorDependency {
    var repository: Repository // interactor가 필요한 데이터
}

final class ChildInteractor: 편의상 생략 {
    private let dependency: ChildInteractorDependency

    init(dependency: ChildInteractorDependency) {
        self.dependency = dependency
    }
    
    override func didBecomeActive() {
        super.didBecomeActive()
        
        // repository 사용
        dependency.repository.somthing()
    }
}

ChildBuilder.swift

protocol ChildDependecy {
    var repository: Repository
}

// Component가 InteractorDependency 채택함
// repository는 부모 RIB으로부터 받을 것이기 때문에 ChildDependency에 repository 명세
protocol ChildComponent: ChildInteractorDependecy {
    var repository: Repository { dependency.repository }
}

final class ChildBuilder: 편의상 생략 {
    func build(withListener listener: ChildListener) -> ChildRouting {
        let component = ChildComponent(dependency: dependency)
        let interactor = ChildInteractor(dependency: component)
        interactor.listener = listener
        return ChildRouter(interactor: interactor)
    }
}

ParentBuilder.swift

protocol ParentComponent: ChildDependency {
    var repository: Repository
    
    init(
        dependency: ParentDependency,
        repository: Repository
    ) {
        self.repository = repository
        super.init(dependency: dependency)
    }
}

final class ParentBuilder: 편의상 생략 {
    func build(withListener listener: ParentListener) -> ParentRouting {
        let component = ParentComponent(
            dependency: dependency,
            repository: RepositoryImpl()
        )
        // ...
        
        // 여기가 중요 !
        let childBuilder = ChildBuilder(dependency: component)
        
        return ParentRouter(
            // 생략,
            childBuilder: childBuilder
        )
    }
}

ParentBuilder 코드에서 여기가 중요 ! 라고 표시한 부분을 보면 ParentBuilder의 build 함수가 실행될 때, 즉 조부모(부모의 부모)가 부모를 attach할 때 (조부모의 Router에서 부모 builder를 build해서 router를 return 받음) 부모는 자식 Builder를 만들면서 component를 전달한다.

이 방법으로 데이터를 전달하면 코드가 아주 깔끔하다. 프로토콜을 채택을 통해 부모가 가지고 있는 데이터를 전달받을 수 있기 때문에 의존성이 낮아지고, 필요한 데이터가 추가되어도 코드 수정량이 적다.

하지만 이 타이밍에 데이터를 전달한다면, 부모가 자식을 attach 하는 타이밍에 데이터를 전달할 수는 없다. repository나 Observable(스트림)을 통해 전달할 수 있는 데이터만 전달 할 수 있게 된다.

그럼 부모가 자식을 attach 하는 타이밍에 데이터를 전달하고 싶으면 어떻게 해야할까?

2번. 부모가 자식을 attach할 때 자식의 build 함수로 데이터 전달하기

부모의 Interactor에서 이벤트가 발생했을 때 Router를 통해 자식을 attach 한다. 이때 부모가 가지고 있는 데이터를 파라미터로 전달 전달해주면 자식이 build 함수에서 받을 수 있다. 예를 들어 어떤 버튼을 눌렀을 때 자식에게 Todo 배열을 전달할때는 어떻게 할까? 코드로 살펴보자.

ParentInteractor.swift

protocol ParentRouting {
    func attachChild(todoList: [Todo])
}

final class ParentInteractor: 편의상 생략 {
    private let todoList: [TODO] = []
    
    func buttonClicked() {
    	router?.attachChild(todoList: todoList)
    }
}

ParentRouter.swift

final class ParentRouter: 편의상 생략 {
    func attachChild(todoList: [Todo]) {
        let router = childBuilder.build(
            withListener: interactor, 
            todoList: todoList
        )
        
        attachChild(router)
        // 이하 생략
    }
}

ChildBuilder.swift

final class Childbuilder: 편의상 생략 {
    func build(withListener listener: ChildListener, todoList: [Todo]) -> ChildRouting {
        // 생략
        
        let interactor = ChildInteractor(todoList: [TODO])
        return ChildRouter(interactor: interactor)
    }
}

코드를 보면 알 수 있듯이 필요한 데이터를 함수의 파라미터에 추가해 전달해주고 있다.
만약 전달해줘야 하는 데이터가 변경되거나 많아지면 수정해야할 코드의 양도 많을 것으로 보인다. 따라서 Observable을 적절히 사용해서 코드를 작성하는게 좋을 것 같다.

결론

2개의 데이터 전달 방법을 알아봤는데, 뭐가 좋고 뭐가 나쁘다 라는 것보다는 데이터 전달할을 할 때 적절한 방법이 무엇일지 고민하면서 써보는게 좋을 것 같다 ! 그리고 RIBs를 공부한지 오래된 상태에서 정리를 한게 아니고 배우면서 정리를 했기 때문에 .. 포스팅을 읽어주시는 분들은 그냥 이런 방법이 있구나 ~ 정도로만 아시면 좋을 것 같다 ㅎㅎ

profile
가장 젊은 지금, 내가 성장하는 데에 쓰자

0개의 댓글