Published on

๐ŸŽ Swift - ReactorKit

Authors
  • avatar
    Name
    ์ด์ฐฝ์ค€
    Twitter

ReactorKit

์•ฑ์Šคํ† ์–ด์— ๋“ฑ๋ก๋œ ์•ฑ๋“ค์˜ ์˜คํ”ˆ์†Œ์Šค๋ฅผ ํ›”์ณ๋ณด๋ฉด ๊ฝค๋‚˜ ์ž์ฃผ ๋ณด์˜€๋˜ ReactorKit์ž…๋‹ˆ๋‹ค.

์‚ฌ์‹ค ๋ง‰์—ฐํ•˜๊ฒŒ React Native๋ž‘ ๊ด€๋ จ๋œ๊ฑด๊ฐ€๋ณด๋‹ค~ ํ•˜๊ณ  ์ง€๋‚˜์น˜๊ณค ํ–ˆ์—ˆ๋Š”๋ฐ.. ์ „ํ˜€ ์•„๋‹ˆ์˜€์ฃ ? ๐Ÿ˜œ

๊ฒจ์šธ ํ”„๋กœ์ ํŠธ ๋™์•ˆ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„์„œ ๊ณต๋ถ€ํ•ด๋ณด๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

ReactorKit

ReactorKit์€ RxSwift๋ฅผ ์ œ๋Œ€๋กœ, ๋˜ ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋‚˜์˜จ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.

๊ณต์‹ github์— ์„ค๋ช…๋œ ๊ธ€์„ ์ฝ์–ด๋ณด๋ฉด, ReactorKit์€ ๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜๊ณ  ์ผ๋ฐฉ์„ฑ์„ ๊ฐ–๋Š” Swift ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์กŒ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ์•กํ‹ฐ๋ธŒํ•œ๊ฑฐ์•ผ RxSwift ๊ธฐ๋ฐ˜์ด๋‹ˆ ๊ทธ๋ ‡๋‹ค๊ณ  ์น˜๊ณ , ์ผ๋ฐฉ์„ฑ์— ์ฃผ๋ชฉํ•˜๊ณ  ๊ณต๋ถ€๋ฅผ ํ•ด๋ณด๋ฉด ๋˜๊ฒ ๋„ค์š”!

๊ธฐ๋ณธ ์ปจ์…‰

ReactorKit์€ View์™€ Reactor๋ผ๋Š” ๊ฒƒ ์‚ฌ์ด๋ฅผ ์œ ์ €์˜ Action๊ณผ ๋ทฐ์˜ State๋ฅผ Observable stream์„ ๋งŒ๋“ค์–ด ์ „๋‹ฌํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋‹น์ตœ ๋ฌด์Šจ ๋ง์ธ์ง€ ์•Œ ์ˆ˜๊ฐ€ ์—†๋„ค์š” ๐Ÿคฏ

ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ด…์‹œ๋‹ค.

View

View๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ๊ทธ View ์ž์ฒด์ž…๋‹ˆ๋‹ค.

ํ™”๋ฉด์„ ํ‘œ์‹œํ•˜๋Š” ๋ชจ๋“  ์š”์†Œ๋“ค์„ View๋ผ๊ณ  ํ•˜๊ณ  ์ด View๋“ค์€ ์œ ์ €์˜ ์ž…๋ ฅ์„ action stream์œผ๋กœ bindํ•˜๊ฑฐ๋‚˜ ๊ฐ๊ฐ์˜ UI Component๋“ค์—๊ฒŒ view states๋ฅผ bindํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ 1. ์œ ์ €์˜ ์ธํ„ฐ๋ž™์…˜์„ ๋ฐ›๊ฑฐ๋‚˜ 2. ํ•˜์œ„ UI Component๋“ค์—๊ฒŒ Reactor์˜ ์ƒํƒœ๋ฅผ ์•Œ๋ ค์ค€๋‹ค๋Š” ๋œป์ธ ๊ฒƒ ๊ฐ™๋„ค์š”.

์ด View์—๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์—†๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ง€ ์ „๋‹ฌ์ž์˜ ์—ญํ• ๋งŒ ํ•œ๋‹ค๊ณ  ๋ณด๋ฉด ๋  ๊ฒƒ ๊ฐ™์•„์š”!

View Controller๊ฐ€ View๋ผ๋Š” ๊ฒƒ์„ ๋ช…์‹œํ•˜๊ธฐ ์œ„ํ•ด์„œ View ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

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

View ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜๋Š” ๊ฒƒ ๋งŒ์œผ๋กœ ์ด ๋ทฐ์ปจ์€ reactor ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

profileViewController.reactor = UserViewReactor()

์œ„์™€ ๊ฐ™์€ ์ž‘์—…์ด ์ž๋™์œผ๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ์ฃผ์–ด์ง„ reactor๋ผ๋Š” ํ”„๋กœํผํ‹ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ฒŒ ๋˜๋ฉด, bind(reactor:) ๋ฉ”์„œ๋“œ๊ฐ€ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. action stream๊ณผ state stream์„ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•ด ๋ทฐ์ปจ ์•ˆ์— ์ด ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์ค์‹œ๋‹ค.

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

์Šคํ† ๋ฆฌ๋ณด๋“œ๋„ ์ง€์›ํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค!

์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ทฐ์ปจ์€ View๊ฐ€ ์•„๋‹ˆ๋ผ StoryboardView๋ฅผ ์ฑ„ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!

class MyViewController: UIViewController, StoryboardView {
  func bind(reactor: MyReactor) {
    ...
  }
}

Reactor

Reactor๋Š” UI์™€๋Š” ์ „ํ˜€ ๊ด€๋ จ์—†๋Š” ๋…๋ฆฝ๋œ ๋ ˆ์ด์–ด ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ View์˜ ์ƒํƒœ๋ฅผ ๊ฒฐ์ •ํ•ด์ฃผ๋Š” ์•„์ฃผ ์ค‘๋Œ€ํ•œ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  View๋Š” ๊ฐ์ž์˜ Reactor๋ฅผ ๊ฐ€์ ธ์•ผํ•˜๊ณ , ๋ชจ๋“  ๋กœ์ง์„ ๊ทธ Reactor์— ์œ„์ž„(delegate)ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด์„œ๋„ Reactor๋Š” View์— ๋Œ€ํ•œ ์˜์กด์„ฑ์ด 1๋„ ์—†๊ธฐ ๋•Œ๋ฌธ์— Unit Testํ•˜๊ธฐ์—๋„ ์ˆ˜์›”ํ•˜๋‹ค๊ณ  ํ•˜๋„ค์š”!

Reactor๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” View์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ Reactor ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•œ ๊ฐ์ฒด๋Š” Action, Mutation, State ์„ธ๊ฐ€์ง€ ํƒ€์ž…์„ ๊ฐ€์ ธ์•ผํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ initialState๋ผ๋Š” ํ”„๋กœํผํ‹ฐ๋„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

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 ์œ ์ € ์ธํ„ฐ๋ž™์…˜์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

  • State View์˜ ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

  • Mutation Action๊ณผ State๋ฅผ ์ด์–ด์ฃผ๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

Reactor๋Š” ๋‘ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์ณ action stream์„ state stream์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ”๋กœ mutate()์™€ reduce()๋ฅผ ํ†ตํ•ด์„œ์ฃ .

mutate()๋Š” Action์„ ๋ฐ›์•„ Observable<Mutation>์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  ๋น„๋™๊ธฐ ์ž‘์—…๋“ค์ด๋‚˜ API ํ˜ธ์ถœ ๋“ฑ์ด ์—ฌ๊ธฐ์— ๋“ค์–ด๊ฐ€๋Š” ์ž‘์—…์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive action
    return UserAPI.isFollowing(userID) // create 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()๋Š” ์ด์ „์˜ State์™€ Mutation์œผ๋กœ๋ถ€ํ„ฐ ์ƒˆ๋กœ์šด State๋ฅผ ๋งŒ๋“œ๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด State๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ ์™ธ์˜ ๊ธฐ๋Šฅ์€ ๋„ฃ์ง€ ๋ง๋ผ๊ณ  ํ•˜๋„ค์š”.

func reduce(state: State, mutation: Mutation) -> State {
  var state = state // create a copy of old state
  switch mutation {
  case let .setFollowing(isFollowing):
    state.isFollowing = isFollowing // manipulate the state, creating a new state
    return state // return new state
  }
}

transform()์€ ๊ฐ stream์„ ๋‹ค๋ฅธ stream์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

์‹ฌํ™”

Global States

Redux์™€ ๋‹ค๋ฅด๊ฒŒ ReactorKit์€ global app state๋ฅผ ์ •์˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ €๋Š” Redux๊ฐ€ ๋ญ”์ง€ ๋ชจ๋ฅด๋‹ˆ ์ผ๋‹จ ๋„˜์–ด๊ฐ€๋ณผ๊ฒŒ์š”..

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— global state๋ฅผ ์•„๋ฌด๊ฑธ๋กœ๋‚˜ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. BehaviorSubject๋‚˜ PublishSubject, ํ˜น์€ reactor๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•˜๋„ค์š”.

๋™์‹œ์— global state๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๊ฐ•์ œํ•˜์ง€๋„ ์•Š๋Š”๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

ReactorKit์—๋Š” Action โžก๏ธ Mutation โžก๏ธ State Flow๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ global state๋ฅผ Mutation์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋ ค๋ฉด transform(mutation:)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

var currentUser: BehaviorSubject<User> // global state

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
  return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}

์œ„ ์˜ˆ์‹œ๋Š” currentUser๊ฐ€ ๋ฐ”๋€” ๋•Œ Mutation.setUser๋ฅผ ์‹คํ–‰ํ•ด๋‹ฌ๋ผ๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์ธ ์ŠคํŠธ๋ฆผ์ธ mutation๊ณผ Global State์˜ currentUser๋ฅผ ๋ณ‘ํ•ฉ(merge)์‹œํ‚ค๋Š” ์ฝ”๋“œ์ธ๋ฐ์š”,

currentUser๋Š” Mutation.setUser๋กœ ์ •์˜๋œ Mutation์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ๋“ค์–ด๊ฐ€๊ณ  ์žˆ๋„ค์š”.

View Communication

View๊ฐ„์˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์ด ์ด๋ฃจ์–ด์งˆ ๋•Œ ๋ณดํ†ต delegate ํŒจํ„ด์ด๋‚˜ closure๋ฅผ ์‚ฌ์šฉํ•˜์ฃ ?

ReactorKit์€ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์— ์œ„ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜์ง€ ๋ง๊ณ  reactive extension์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

// Read as "Reactive Extension where Base is a SomeType"
extension Reactive where Base: SomeType {
  // Any specific reactive extension for SomeType
}

View A๋ฅผ ChatViewController, View B๋ฅผ MessageInputView๋ผ๊ณ  ํ•ฉ์‹œ๋‹ค.

View B๋Š” View A์˜ subview์ž…๋‹ˆ๋‹ค.

View B๊ฐ€ View A์— ControlEvent๋ฅผ ๋ณด๋‚ด๋ฉด View A์—์„œ ๋ณธ์ธ์˜ reactor์ธ Reactor A์— Action์„ ์ „๋‹ฌํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค์ž…๋‹ˆ๋‹ค.

์›๋ž˜์˜ ๋ฐฉ์‹๋Œ€๋กœ View B์—์„œ View A๋กœ ์ด๋ฒคํŠธ๋‚˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋ ค๋ฉด delegate๋‚˜ closure๋ฅผ ์‚ฌ์šฉํ–ˆ์—ˆ์ฃ .

๋Œ€์‹  ReactorKit์ด ์ถ”์ฒœํ•˜๋Š” ๋ฐฉ์‹๋Œ€๋กœ Reactive extension์„ ์‚ฌ์šฉํ•ด์„œ ๊ตฌํ˜„์„ ํ•ด๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// MessageInputView.swift
extension Reactive where Base: MessageInputView {
  var sendButtonTap: ControlEvent<String> {
    let source = base.sendButton.rx.tap.withLatestFrom(...)
    return ControlEvent(events: source)
  }
}
// ChatViewController.swift
messageInputView.rx.sendButtonTap
  .map(Reactor.Action.send)
  .bind(to: reactor.action)