Post

많은 팀에서 도입하고 있는 RIBs 아키텍처에 대해 스터디 해보겠습니다. RIBs 레포의 설명도 좋지만, 먼저 안정민님이 정리해주신 자료들로 필기해보며 공부를 시작해보겠습니다.

MVC, MVVM, ReactorKit, Viper를 거쳐 RIB 정착기 (1)

https://www.youtube.com/watch?v=3XS6xLzKRjc 강의 필기 입니다.

기존 아키텍처에 왜 만족 못했는가?

  • 화면 단위가 아닌 프로세스 단위로 유연한 개발 필요
  • 자체 제작 아키텍처의 유지 보수 어려움
  • 더 확실한 안정화 필요
    • 테스트 코드 템플릿 또는 가이드가 있는 아키텍처가 거의 없음
    • 체계화된 테스트 코드 작성이 필요

아키텍처 여정

MV(C)

  • 장점: 기존에 익숙한 구조. 단순환 화면에만 적용
  • 단점: 기능이 확장되면 깔끔하지 않아짐

MVVM

  • 장점: MVC보다는 코드가 정리되는 느낌
  • 단점: 기능이 복잡해질수록 ViewModel이 빠르게 비대해짐. 표준화된 틀이 존재하지 않음. 이해가 다름. 테스트 코드 작성이 가능하지만 어려움. Rx 허들 및 디버깅…

ReactorKit

  • 장점: 단방향이라 View와 관계된 로직이 깔끔
  • 단점: 프로세스 단위의 아키텍처라 아니라서 아쉬움.

VIPER

  • 장점: VIPER 역할이 명확하게 구분됨
  • 단점: 명확한 가이드, 테스트 코드 템플릿이 정형화되어 있지 않음

RIBs 선택한 이유

  • RIBs에 대한 설명은 https://www.youtube.com/watch?v=BvPW-cd8mpw 이것을 보는게 좀 더 좋다고 해서 좀 더 아래쪽에 따로 정리해뒀습니다.
  • 템플릿화된 코드 및 테스트 작성 https://github.com/uber/RIBs/tree/master/ios/tooling
    • 템플릿을 통해 RIB / RIB Unit Tests / Component extension 을 만들 수 있습니다. (Component extension 이건 뭔지 모르곘네요… 나중에 찾아보겠습니다) RIB template
    • Owns corresponding View: View가 있는 RIB을 만들건지 선택 Adds Storyboard file: View를 Storyboard로 만들지, 아니면 xib로 만들지 선택가능 RIB template 2
    • 체크한 항목에 따라 아래와 같이 파일을 만들어 줌 RIB template 3
    • 템플릿으로 테스트 코드도 구성 가능.
    • 테스트 코드에 Mock 구성하는 방법도 있음. Tutorial2/TicTacToeTests/TicTacToeMocks.swift present가 호출되었는지, 클로저가 호출되었는지 확인하는 가이드를 제공해주고 있음. 이 코드는 템플릿으로 제공해주지는 않고 있음. SourceKitten을 이용해서 Mock 코드 생성하는 스크립트 이용중

프로토콜 지향 프로그래밍

RIBs Protocol

  • 프로토콜이 굉장히 많이 사용되는데… 아직 익숙하지 않아서 그런지 헷갈리네요;;

Interactor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import RIBs
import RxSwift // Rx는 필수일까?

// 이 코드가 Interactor 쪽에 있어야 함
protocol LoggedInRouting: ViewableRouting {
    // 여기에 원하는 명령을 추가
    // -> Router는 컴파일 에러
    // -> Router에 가서 컴파일 에러 수정
}

protocol LoggedInPresentable: Presentable {
    var listener: LoggedInPresentableListner { get set }
}

// 상위 RIB이 어떤 Interaction에 대해 호출을 해야 할 때
// "나는 내 작업이 끝났어" 하고 알려줘야 할 때
protocol LoggedInListener: class {

}

final class LoggedInInteractor: PresentableInteractor<LoggedInPresentable>, LoggedInInteractable, LoggedInPresentableListner {

    // Interactor는 Routing 과 View를 알고 있어야하기 때문에 weak 변수로 가지고 있음
    weak var router: LoggedInRouting?
    weak var listener: LoggedInListener?

    ovrride init(presenter: LoggedInPresentable) {
        super.init(presenter: presenter)
        presenter.listner = self
    }

    ovrride func didBecomeActive() {
        super.didBecomeActive()
        // TODO: Implement business logic here.
    }

    ovrride func willResignActive() {
        super.willResignActive()
        // TODO: Pause any business logic.
        // 여기에 Routing 작업을 할 수 있음
    }
}

Router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import RIBs

protocol LoggedInInteractable: Interactable {
    var router: LoggedInRouting? { get set }
    var listener: LoggedInListener? { get set }
}

// View에 어떤 Present를 할지, Push를 할지 방향을 정하는 역할
protocol LoggedInViewControllable: ViewControllable {

}

final class LoggedInRouter: ViewableRouter<LoggedInInteractable, LoggedInViewControllable>, LoggedInRouting {
    override init(interactor: LoggedInInteractable, viewController: LoggedInViewControllable) {
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
}

View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import RIBs
import RxSwift
import UIKit

// View에서 Action이 일어났을 때 -> Interactor 쪽에다 알려줘야 하는 것을 정의
// Interactor는 이 프로토콜을 구현해야 함
// 한 군데라도 프로토콜을 따르지 않았다면 컴파일 에러가 발생해서 -> 에러가 나는 곳을 구현해주면 됨
protocol LoggedInPresentableListener: class {

}

final class LoggedInViewController: UIViewController, LoggedInPresentable, LoggedInViewControllable {
    weak var listener: LoggedInPresentableListner?
}

부모 RIB <-> 자식 RIB 간의 통신

Interactor를 통해서만 통신을 하게 되어 있음

RIBcommunication

복잡한 로직도 Interactor간의 통신으로만 구현 가능함

RIBcommunication2

의존성 주입 DI

해당 RIB에서 필요한 속성을 Dependency에 정의

1
2
3
4
5
6
7
8
9
10
import RIBs

// 해당 RIB은 여기에 있는 정보를 사용할 수 있음
protocol LoggedInDependency: Dependency {

}

final class LoggedInComponent: Component<LoggedInDependency> {

}

이런식으로 만들 수 있음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import RIBs

protocol LoggedInDependency: Dependency {
    var name: String { get }
    var nickName: String { get }
}

final class LoggedInComponent: Component<LoggedInDependency> {
    fileprivate var name: String {
        return dependency.name
    }

    fileprivate var nickName: String {
        return dependency.nickName
    }
}
  • dependency가 확장되면 컴파일 에러 발생
  • 해당 RIB을 사용하는 곳은 다 작성을 해야 함

RxRIBs, Multiplatform architecture with Rx

https://www.youtube.com/watch?v=BvPW-cd8mpw 회사 세미나에도 발표하러 오셨었는데 들었던 이후로 시간이 꽤 지나서 다시 들으며 정리했습니다.

새로운 아키텍처 필요했던 이유

  • 서비스를 빨리 출시해서 운영 노하우를 얻고자 했음
  • 기존 방식으로 업무를 진행하면 안되겠다고 생각함
  • 두 플랫폼에서 동일한 아키텍처를 사용하고 싶었음

고민 요소

  • MVC는 최대한 피하자
  • Single Activity Application
    • 안드로이드의 Activity 생성, 관리가 까다로움
    • 타다 같은 경우는 map-based 로 만들려고 했음
  • 프레임워크를 처음부터 끝까지 만들 시간이 없었음

RIBs

Interactor

  • RIB에서 비즈니스 로직을 담당함
  • API를 어떻게 호출할지, 데이터를 어떻게 저장할지, 어떤 State로 바꿔줄지 Interactor에서 이루어짐

Router

  • 여러 RIB이 존재할텐데 A RIB -> B RIB으로 바꾸는 과정을 라우터가 신경써야 함
  • 트리상에서 어떻게 립이 존재할지, 트랜지션이 어떻게 일어날지 결정
  • 다른 Interactor를 호출하는것을 봉쇄시킴

Builder

  • 팩토리 패턴으로 RIB의 컴포넌트들을 생성해주는 역할을 함
  • 클래스들의 Mockability가 향상됨 <- 요건 무슨말이지…?
  • Builder는 실제 DI 시스템이 어떻게 구현되어 있는지 알고있는 유일한 클래스

View

  • UI 컴포넌트들을 Layout 하고 애니메이션만 존재하도록

Presenter

  • View 로직이 필요할 떄만 추가함

State 관리

Convoluted state machines

stateMachine

  • State 관리에 장점이 있음
  • 앱에는 많은 State가 있음…

State tree

stateTree

  • 어떠한 형태든 Tree 형태로 상태를 그릴 수 있게 되었음
    • 이게 모든 앱이 다 가능할까???
    • 타다는 우버와 비슷한 목적의 앱이라서 잘 사용할 수 있었던건 아닐까?
    • 나중에 예제 따라해보면서 고민해보기
  • 비즈니스 로직에 따라 상태를 변경함. 경우에 따라 여러 RIB이 attach된 경우도 있음
  • 뷰 로직에 따라 상태를 변경하지 않았음!!

Scope

  • State tree를 사용했을 때 장점은 Scope를 한정 시키는 것

RIBScopes

협업하기 좋아짐

  • 관심사 분리
  • 레고 블럭 쌓듯이 구현해서 조립함

어려움

Flow of Data

우버에서 사용하는 방식은 양방향 데이터 플로우 UberRIBs

VCNC에서는 단방향 데이터 플로우로 변경해서 사용함 VCNCRIBs

Animations

  • UITransitionDelegate를 이용함
  • HeroTransition 이라는 라이브러리를 사용하고 싶었음
  • RIB 간의 transition에서는 위의 두 방식을 사용하게 헀음
  • 그래서 RIB lifecycle을 변경해서 사용함

요약

RIBsWrapUp

배운점

  • 지난번에 한번 시도했다가 프로토콜들이 너무 많아서 힘들었던 기억이 있습니다. 하지만 각 역할을 좀 더 명확하게 하고 프로토콜이 변경되었을 때 컴파일 에러를 발생시키는게 변경을 더 명확하게 알아차리게 하는 장점이 될 수 있음을 알게되었습니다.
  • 안정민님이 사용한 예시가 RIBs tutorial에 있는 것들인데 이것부터 따라해보면 좋겠다는 생각을 했습니다.
  • 테스트 코드에 대한 템플릿도 있는게 굉장히 좋다고 생각합니다. 테스트 작성에 손이 잘 안가는 문제가 있는데, 이렇게 하면 테스트 코드로 가는 허들을 조금 더 낮출 수 있을 것 같습니다.
  • iOS & 안드로이드 플랫폼 둘 다 RIB를 썼을 때 장점이 과연 어떨지 직접 경험해보고 싶다는 생각도 들었습니다. 어차피 Swift / Kotlin으로 각각 나눠서 작업 하는거면 내부의 아키텍처가 동일하더라도 각각 작업하는건 똑같을 것 같은데… 과연 이게 그렇게 큰 장점일지 궁금해졌습니다. 아니면 한 사람이 두 코드 베이스를 유지보수 할 수 있는 뜻인가…? 버그를 수정할 때 아키텍처 이슈보다 UI 오류였던 적이 더 많은 것 같아서 드는 고민입니다.