티스토리 뷰

반응형

개요

MVVM을 학습하며 GitHub에서 여러 Repository를 참고했어요.

그 과정에서, 같은 MVVM이지만 개발자의 설계에 따라 구현 구조가 크게 다른 것을 확인했어요.

MVVM은 정의하기에 따라 다양한 형태를 가질 수 있기 때문에 이런 문제를 해결하기 위해 여러 회사들에서 ReactorKit을 활용한다고 들었어요.

 

저 역시 두 가지 프로젝트를 MVVM으로 설계하며 위의 문제를 경험했었기 때문에 ReactorKit 공식 GitHub의 README를 해석하며 전체적인 구조를 이해해보려고 해요 :)

 

글이 길어져서 우선 이 글에서는 기본적인 내용만 다루고, Advanced 이후 내용은 다른 글에서 추가해볼게요!

 

Basic Concept (기본 콘셉트)

ReactorKit은 Flux와 Reactive Programming의 조합이에요.

저는 Flux가 무엇인지 몰라서 학습하며 추가적으로 Flux에 관한 글을 작성해두었어요.

여기서는 자세히 설명하지 않도록 할게요!

 

Action이나 State에 대한 개념이 이해되지 않는다면 위의 글을 읽거나 추가적인 학습 후 이 글을 읽으면 더 좋을 것 같아요 :)

 

유저의 Action이나 View의 상태(이하 state로 표현)들은 Observable stream을 통해 각 계층에 전달이 돼요.

이때, 이 stream들은 단방향(unidirectional)이기 때문에 한쪽은 전달하기만 하고, 다른 한 쪽은 전달받을 수만 있는 구조가 돼요.

조금 더 정확히 이야기하면 view는 action들을 방출할 수만 있고, reactor는 state만 방출할 수 있어요.

 

Design Goal(디자인 목표)

  1. 테스트 가능한 구조(Testability):
    ReactorKit의 가장 큰 목적은 view에서 비즈니스 로직을 분리하는 것이에요. 이것을 통해 코드가 테스트 가능해져요(testable).
    Reactor는 view에 어떠한 의존도 하지 않아요. 그래서 Reactor와 binding만 테스트하면 돼요.

  2. ReactorKit 도입을 위한 코스트가 크지 않음(Start Small):
    이미 존재하는 프로젝트에 ReactorKit을 적용할 때 전체 구조에 전부 ReactorKit을 적용할 필요는 없어요. 부분적으로 적용하기 시작할 수 있기 때문에 적용에 대한 비용이 크지 않아 쉽게 시작할 수 있어요.

  3. 코드 양을 최소화하기(Less Typing):
    ReactorKit은 복잡한 코드를 피하는 것에 초점을 맞추고 있어요. 
    그래서 ReactorKit은 다른 아키텍처에 비해 적은 코드를 요구해요.

 

View란?

View는 대부분의 아키텍처에서 그러하듯 정보를 보여주는 역할을 해요.

ViewController와 Cell을 View라고 할 수 있어요.

View는 유저의 입력을 action stream에 bind 해요.

🤔 저는 유저의 input이 발생하면 그 이벤트를 action에 전달하도록 설계될 것이라고 이해했어요.

 

또한 View의 state들을 각각의 UI component에 bind 해요.

🤔 Flux를 학습했던 내용을 기반으로 생각해봤을 때 특정 View가 SubView 등 다른 View에게 state를 기반으로 갱신 등의 영향을 주는 경우를 위해 bind 되어 있다고 이해했어요.

 

가장 중요한 것은 View 계층에는 비즈니스 로직이 존재하지 않아요!

View는 단순히 action stream과 state stream을 어떻게 연결할 것인지에 대해서만 정의해요.

 

class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // inject reactor

profileViewController의 reactor 프로퍼티가 변경될 때(위의 경우 UserViewReactor를 주입), bind(reactor:) 메서드가 호출돼요.

이 메서드는 아래와 같이 정의돼요.

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh } // Void한 이벤트를 refresh로 mapping
    .bind(to: reactor.action) // 유저 input에 따라 reactor가 가진 action의 비즈니스 로직 작동
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing } // reactor의 상태에 따라 View가 갱신되는 구조로 bind
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

 

🏂  Storyboard 지원

스토리보드를 사용할 경우 ViewController를 초기화하기 위해 StoryboardView 프로토콜을 사용할 수 있어요.

다른 것은 다 똑같은데 유일하게 다른 점이 StoryboardView는 view가 load가 된 후 binding이 일어난다는 점이에요.

let viewController = MyViewController()
viewController.reactor = MyViewReactor() // `bind(reactor:)`가 즉시 실행되는 것이 아님

class MyViewController: UIViewController, StoryboardView {
  func bind(reactor: MyViewReactor) {
    // ViewDidLoad가 끝난 후 binding이 됨
  }
}

 

Reactor란?

Reactor는 view의 상태를 관리하기 위한 계층으로, UI에서 독립적이에요.

Reactor의 가장 우선되는 역할은 view에서 control flow를 분리하는 것이에요.

모든 View는 대응되는 Reactor를 가지고, 모든 로직을 Reactor에 위임해요. 

Reactor는 View에 의존성이 없기 때문에 테스트에 용이해요.

 

Reactor는 프로토콜로서 실 객체가 이 프로토콜을 채택하게 함으로써 사용 가능해요.

Reactor 프로토콜에서 반드시 구현해주어야 하는 요소에는 3가지가 있어요.

 

class ProfileViewReactor: Reactor {
  // represent user actions
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // represent state changes
  enum Mutation {
    case setFollowing(Bool)
  }

  // represents the current view state
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}

 

 

Action은 user interaction 의미하고, State는 View의 state를 의미해요.

Mutation은 Action과 State 사이의 연결고리예요. 

 

https://github.com/ReactorKit/ReactorKit

 

mutate()

mutate 메서드는 Action을 받아서 그에 맞는 Observable<Mutation>을 반환해요.

비동기 작업이나 API 호출 같은 모든 side effect 작업은 이 메서드 안에서 실행돼요.

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }

  case let .follow(userID):
    return UserAPI.follow()
      .map { _ -> Mutation in
        return Mutation.setFollowing(true)
      }
  }
}

 

reduce()

reduce 메서드는 기존 State와 Mutation을 기반으로 새로운 State를 생성해요.

이 메서드는 순수 함수(pure function)에요.

새로운 State를 동기적으로 즉시 반환하게 해야 해요.

이 메서드에서 가장 중요한 점은 side effect가 발생하면 안 된다는 것이에요.

 

transform()

transform은 각각의 stream을 변경해요.

transform 메서드에는 세 가지 종류가 있어요.

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

다른 observable stream과 합치거나 observable stream을 변경하려고 할 때 이 메서드를 사용하면 돼요.

예를 들어, transform(mutation:)은 global event stream을 mutation stream에 합칠 때 가장 적합한 메서드예요.

 

이 메서드들은 또 디버깅을 위해 사용될 수 있어요.

func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}

 

정리

README에 있는 내용만으로 바로 적용하기는 어렵기 때문에, Repository에 첨부되어있는 Example을 이해하고 실제로 적용해봐야 조금 더 깊이 있는 이해가 가능할 것 같아요 :)

 

Reference: ReactorKit 공식 문서

 

반응형
댓글