[iOS] ReactorKit 알아보기

ohtt-iOS·2022년 1월 2일
2

iOS

목록 보기
24/24
post-thumbnail
post-custom-banner
오늘은 ReactorKit에 대해서
아주아주 겉핥기 수준으로 알아보겠습니다 :) 

🧐 리액터 킷 ?

ReactorKit은 반응형 단방향 앱을 위한 프레임워크 입니다 :)

이렇게나 많은 회사에서 사용하고 있는 프레임워크랍니다. ( 물론 더 있겠지만 ! )
오늘은 이 리액터 킷의 개념을 익혀보고자 합니다 !



ReactorKit의 구조

rx 공부를 하다보면 느끼겠지만 사람마다 MVVM 을 구현하는 방식이 가지각색입니다.

ReactorKit은 그런 부분에 있어서 어느정도 제약을 두어서
좀 더 정형화된 방식으로 구현할 수 있게 만들어줍니다.

ReactorKit은 크게 ViewReactor로 이루어져있으며
View 는 Reactor 에게 Action을 전달하고 Reactor는 View에게 State를 전달하는
단방향 구조입니다.



Example

리액터킷 깃허브 에 가면 리액터킷으로 만든 여러가지 앱들이 있습니다.
Counter 와 GitHub Search가 가장 기본적인 예제이니 처음 접하시는 분들은 이걸 참고하시면 좋을 것 탄아요 :)

아래에서 소개하는 예제는 Counter 입니다!

일단 어떤식으로 만드는지 한번 보고갑시다 !

  • -/+ 버튼이 있고 버튼을 누르면 0.5 초 후에 숫자가 바뀌게 됩니다.

class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let viewController = self.window?.rootViewController as! CounterViewController
    viewController.reactor = CounterViewReactor()
    return true
  }
}

우선 AppDelegate에서 viewController에게 reactor를 주입해줍니다.

위에서 설명했듯이 View는 Reactor에게 Action을 전달합니다.
여기서의 Action 는 ?

  • 마이너스 버튼
  • 플러스 버튼

Reactor는 State를 View에게 전달합니다.
여기서의 State 는 ?

  • loading 중 ?
  • 숫자 값

우리는 이걸 토대로 View와 Reactor를 구현해주면 됩니다 !



Reactor

우선 Reactor의 구조를 간단하게 살펴보겠습니다.

final class CounterViewReactor: Reactor {
  
  enum Action {
    case increase
    case decrease
  }

  enum Mutation {
    case increaseValue
    case decreaseValue
    case setLoading(Bool)
  }

  struct State {
    var value: Int
    var isLoading: Bool
  }
  
  let initialState: State
  
  init() {
    self.initialState = State(
      value: 0, // start from 0
      isLoading: false
    )
}

여기에서 리액터는 Reactor라는 프로토콜을 채택해주고 있습니다.
Reactor를 채택하면 Action, Mutation, State에 대해서 정의해주어야하고
initialState가 필요합니다.




reactorkit은 action이 들어오면 mutate라는 메소드를 통해 Observable<Mutation> 타입을 return하고
이 Mutation은 reduce 메소드를 통해 현재의 state 를 변경시킵니다.



// Action -> Mutation
  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case .increase:
      return Observable.concat([
        Observable.just(Mutation.setLoading(true)),
        Observable.just(Mutation.increaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
        Observable.just(Mutation.setLoading(false)),
      ])

    case .decrease:
      return Observable.concat([
        Observable.just(Mutation.setLoading(true)),
        Observable.just(Mutation.decreaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
        Observable.just(Mutation.setLoading(false)),
      ])
    }
  }

  // Mutation -> State
  func reduce(state: State, mutation: Mutation) -> State {
    var state = state
    switch mutation {
    case .increaseValue:
      state.value += 1

    case .decreaseValue:
      state.value -= 1

    case let .setLoading(isLoading):
      state.isLoading = isLoading
    }
    return state
  }

여담 : 여기서 concat은 두개 이상의 observable을 직렬로 연결합니다


여기까지가 Reactor의 구현이었고, View로 넘어가봅니다.


View

이 예제에서는 storyboard를 사용해주었기 때문에 StoryboardView를 채택해주었습니다.
코드로 구현되었을 경우에는 View 를 채택해주면 됩니다.
( 둘의 차이는 StoryboardView를 채택했을 경우에는 뷰가 로드가 된 후 바인딩을 해준다는 것 !
다른 점은 모두 동일하다고 합니다.)

final class CounterViewController: UIViewController, StoryboardView {
  @IBOutlet var decreaseButton: UIButton!
  @IBOutlet var increaseButton: UIButton!
  @IBOutlet var valueLabel: UILabel!
  @IBOutlet var activityIndicatorView: UIActivityIndicatorView!
  var disposeBag = DisposeBag()

  // 리액터가 주입되면 bind가 바로 실행 !
  func bind(reactor: CounterViewReactor) {
    // Action 
    // 플러스 버튼
    increaseButton.rx.tap               
      .map { Reactor.Action.increase }  
      .bind(to: reactor.action)         
      .disposed(by: disposeBag)
	
    // 마이너스 버튼
    decreaseButton.rx.tap
      .map { Reactor.Action.decrease }
      .bind(to: reactor.action)
      .disposed(by: disposeBag)

    // State
    // Reactor의 state와 연결
    // 숫자
    reactor.state.map { $0.value }   
      .distinctUntilChanged()
      .map { "\($0)" }              
      .bind(to: valueLabel.rx.text)  
      .disposed(by: disposeBag)

    // 로딩
    reactor.state.map { $0.isLoading }
      .distinctUntilChanged()
      .bind(to: activityIndicatorView.rx.isAnimating)
      .disposed(by: disposeBag)
  }
}

마무리

모든 기술이 처음에는 어렵고 이게 무슨소리지 ... ? 싶지만
모든 도구가 그렇듯이 계속 쓰다보면 손에 익는 날이 오겠죠 ... ?
올 해 목표는 Reactorkit으로 개인 앱 내보기 ~ ✨

profile
오뜨 삽질 🔨 블로그
post-custom-banner

0개의 댓글