Jiseob Kim

iOS Developer

(RIBs) Uber/Tutorial의 이해 (하위 리블렛 붙이기) (Without Component)

02 Mar 2022 » Architeture


이전편에서 LaunchRouter를 만들고

립스로 최초 화면을 띄우는 것에 대해 포스팅을 하였다.

이번 편에서는 하위 리블렛을 붙이는 것을 주제로 잡았다.

리블렛(Riblet): Ribs 라는 요리를 본적 있다면, 그 뼈대 하나를 리블렛이라고 한다고 한다. 아키텍쳐에서 의미는 이전편에 Root처럼 하나의 단위를 리블렛이라한다고 한다.



분류


쉽게 이해를 돕기 위해 나눠서 설명을 하고자 한다.

  1. 의존성 주입이 필요없는 경우

  2. 의존성 주입이 필요한 경우 (Component가 필요한 경우)


그 이유는 Router의 역할을 먼저 알아보기만해도 처음엔 저세상 이야기 같은데

의존성 주입까지 하려하면 너무 끔찍했다.


그래서 이번 편에서는 Component가 없이 기본적인 화면 붙이기만 다룰 예정



구조 설명


Uber/Ribs Tutorial을 기반으로 설명을 할 예정이다.

그리고 이번편에서 하고자 하는 것의 구조는 다음과 같다.


  • Root: 이전편에 생성

  • LoggedOut: 사용자 이름 2개를 입력 받는 화면 (이번편에서는 빈 화면만)


그리고 LoggedOut 리블렛을 생성해주자.




Attach LoggedOut


Root는 하는게 없다. 단순히 로그아웃된 화면을 띄워줄 뿐이다.

그렇다는 말은 로그아웃된 화면에선 의존성 주입을 받을 필요가 없다.


attach를 통해 화면 전환이 어떻게 이뤄지는지

시작하기 딱 좋은 예제라 생각 된다.


중점은 Attach를 어떻게 하는지이다.



Root Interactor


누구한테 명령을 해야할까?

화면 전환을 담당하는 Router에게 명령을 해야한다.


누가 지시를 해야할까?

두뇌 역할을 하는 Interactor가 명령을 내려야한다.


그러기 위해선 RootRouting에 시그니처를 다음과 같이 적어준다.

// In RootInteractor.swift

protocol RootRouting: ViewableRouting {
    func attachLoggedOut()
}


이제 Router에게 명령을 할 수 있다.


그럼 언제 지시를 내릴까?


시점은 interactor 안에 존재하는 메소드인

didBecomeActive()가 적당해 보인다.

다음과 같이 작성하자.


override func didBecomeActive() {
    super.didBecomeActive()
    
    router?.attachLoggedOut()
}


그러면 이제 빌드를 해보면 에러가 날 것이다.


왜냐하면 attachLoggedOut()을 명시해준 것은 프로토콜이었고,

이를 준수하는 실제 RootRouter는 이 메소드를 구현 해주지 않았기 때문이다.

직접적으로 router에게 하는것이 아닌 protocol 타입의 객체를 통해 명령을 내려줌으로써 서로간의 의존성을 끊을 수 있다는 장점도 존재.


그럼 이제 실제 RootRouter로 가서 빈 메소드를 만들자.



Root Router


// in RootRouter Class

func attachLoggedOut() {
    
}


일단은 에러는 피했고, 이제 화면 전환을 하기 위한 준비물들이 필요하다.


리블렛의 생성을 담당하는건 Builder다.

그래서 이 Builder가 필요하지만,

위에서 본 것과 같이 interactor도 직접 router객체를 들고 있지 않았다.


여기서도 실제 Builder가 아닌 빌더가 따르는 Buildable을 프로퍼티로 들고 있자.

// in RootRouter Class
private let loggedOutBuildable: LoggedOutBuildable
private var loggedOutRouting: Routing?


loggedOutRouting은 화면을 띄우고 끝이 아닌 제거도 필요할때가 있다.

따라서, 화면을 띄우면 객체로써 들고 있자!


loggedOutBuildable은 초기화가 안되어 있어서 오류가 난다.

init시 초기화 해주자 (override는 제거!)

init(
    interactor: RootInteractable,
    viewController: RootViewControllable,
    loggedOutBuildable: LoggedOutBuildable
) {
    self.loggedOutBuildable = loggedOutBuildable
    super.init(interactor: interactor, viewController: viewController)
    interactor.router = self
}


그럼 우선은 RootRouter.swift파일 내의 오류는 사라진다.


하지만, 다른 오류가 나온다.

init을 수정했기 때문에 어디선가 해당 init을 쓰고 있던 부분은 오류가 난다.


Router가 생성되는 곳은 어딜까

역시 리블렛의 생성 담당인 Builder에 있다.

RootBuilder.swift로 이동하자.



Root Builder


아래에 이미지와 같이 RootRouter 생성하는 부분인을 채워줘야한다.


빌더를 하나 생성해준다.

let loggedOutBuilder = LoggedOutBuilder(dependency: ??)

이제 또 위 코드의 ??을 채워야한다.


하 개인적으로 처음에 이정도 왔을때

뭐이리 할게 많아…? 싶었다…


결론은 같은 메소드 내에 있는 component가 들어가면 된다.

Component는 이전편에서 필요한 객체를 담아두는 주머니 정도로 설명을 했다.


즉, LoggedOutBuilder에 필요한 의존성을 주입하라는 얘기다.


그런데 component를 넣어주게 되면 에러가 난다.

LoggedOutBuilder가 뭐가 필요한지도 모르는데,

일단 아무거나 껴넣었기 때문이다.


따라서 RootComponentLoggedOutDependency를 준수하도록 해줘야

비로서 에러는 사라지며 정상적으로 사용이 가능하다.


위의 사진을 보면 3개의 빨간 박스가 있으며 위의 말들을 정리한 것이다.



다시 Root Router


돌고 돌아 다시 라우터로 왔다.

화면 하나 붙이기 위한 준비 과정이 이제서야 끝났다.


처음엔 이게 뭐야 도대체 왜이래 싶었지만,

하면 할수록 빨라지고 명확해서 좋다는 기분이 들어서 좋다.

그럼에도 가끔씩 너무많아… 싶긴하다…


아까 빈 메소드로 남겨두었던 attachLoggedOut()를 채워보자


router가 이제 loggedOutBuildable이 있으니

build메소드를 통해 router를 얻을 수 있다.

let router = loggedOutBuildable.build(withListener: interactor)


interactor를 넣어주는 이유는

하위 리블렛(LoggedOut)과 상위 리블렛(Root)의

interactor끼리 상호작용이 가능하게 하기 위함이다!


예를 들어, 화면을 띄울 리블렛은

띄운 리블렛을 닫을 책임도 있기 때문에

하위가 상위한테 “화면 닫아줘” 같은 요청을 해야할 때도 있다.


그럴때 하위 interactor가 상위 interactor에게 전달할 방법이 필요하므로

listener로써 위 코드와 같이

현재의 interactor가 전달되어야 한다.


하지만, 아직 하위 리블렛이 무엇을 요청할 수 있는 지를 모른다.


따라서, 현재 interactor가 하위 interactor의 요청을 처리할 수 있도록

같은 파일내에 존재하는 RootInteractable 또한 프로토콜을 추가해줘야한다.


다시 attachLoggedOut() 메소드로 돌아가서 아래 코드를 추가 해주자

attachChild(router)
loggedOutRouting = router


attachChild는 이전편에서 본 것과 같이 몇가지 로직이 수행된다.

이때, interactor도 활성화가 된다는 정도만 알면 될 듯 하다.


그리고 나중에 detach할 경우를 대비해,

아까 만들어둔 loggedOutRoutingrouter로 초기화 해주자.


보험처리로 이미 attach 되어 있는데 또 하면 안되니깐

안전 장치 하나 마련해주고 나면 전체 코드는 다음과 같다.

func attachLoggedOut() {
    // 안전 장치
    guard loggedOutRouting == nil else { return }
    
    let router = loggedOutBuildable.build(withListener: interactor)
    
    attachChild(router)
    loggedOutRouting = router
}


사실 여기까지는 하위 리블렛을 붙여서 활성화 하기까지의 과정이었다.

이제부터는 어떻게 화면을 쓸 것인지를 결정하면 된다!

present하던지 navigationpush하던지

navigation이 없다면 navigation으로 감싸서 present 하던지 등등…


하지만, 여기서 Tutorial의 불편한 점이 나온다.

프로토콜 RootViewControllable 안에 present, dismiss를 넣어주고,

이를 RootViewController에서 구현을 하여,

routerviewController에게 명령을 내리는 식으로 작성되어 있다.


각각의 리블렛마다 그것을 구현하기엔 너무나도 불편했다


그래서 다른 사람들은 아래 코드처럼

extension을 이용해서 이를 해소했다.

public extension ViewControllable {
    
    func present(_ viewControllable: ViewControllable, animated: Bool, completion: (() -> Void)?) {
        self.uiviewController.present(viewControllable.uiviewController, animated: animated, completion: completion)
    }

    func dismiss(completion: (() -> Void)?) {
        self.uiviewController.dismiss(animated: true, completion: completion)
    }
}


이제 우리는 이 추가된 메소드를 통해서 호출을 할것이다.

그러면 화면 전환까지 추가된 전체 코드는 다음과 같다.

func attachLoggedOut() {
    guard loggedOutRouting == nil else { return }
    
    let router = loggedOutBuildable.build(withListener: interactor)
    
    attachChild(router)
    loggedOutRouting = router
    
    // 추가된 화면 전환 코드
    viewControllable.present(
        router.viewControllable,
        animated: true,
        completion: nil
    )
}

아 너무 힘들었다.

뷰컨하나 present하기에 대한 설명이 이토록 길다니!!


실행 해보자.


이런 화면이 나와도 놀라지 말자

LoggedOutViewController의 백그라운드 컬러가 없는 것이다.

아래 코드를 뷰컨에 추가 해주자.


// In LoggedOutViewController
init() {
    super.init(nibName: nil, bundle: nil)
    setUI()
}

required init?(coder: NSCoder) {
    super.init(coder: coder)
    setUI()
}

private func setUI() {
    view.backgroundColor = .systemPink
}


이제 나온다. 핑크 화면!!



마무리


뷰컨 하나를 띄우기 위해서 기존 대비 정말 많은 일을 해야한다.

하지만 중간에 썼던 것처럼 각각의 요소가 할 일이 잘 나뉘어져 있는 것이 포인트이다.


다음편에서는 Component가 실제로 사용되는 리블렛을 붙일 예정이다.

LoggedOut에 입력받을 UI도 그때 추가가 맞다 판단되어,

UI도 다음편에 꾸밀 예정!