๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
IOS๐ŸŽ/์•„ํ‚คํ…์ณ

[์•„ํ‚คํ…์ณ] ReactorKit์ด๋ž€

by Jouureee 2022. 4. 24.

๋ฐฐ์šธ๊ฒŒ ์ •๋ง ๋งŽ๋‹ค ..! 

ํ•˜๋‚˜ ํ•˜๋‚˜ ์•„ํ‚คํ…์ณ๋“ค์„ ์•Œ์•„๊ฐ€๋ฉด์„œ MVx ์™ธ์˜ ์•ฑ ์•„ํ‚คํ…์ณ ๊ฐœ๋…์ด ์ ์  ๊ตฌ์ฒดํ™”๋˜๋Š”๊ฑฐ ๊ฐ™๋‹ค. ์ด๋ฒˆ์—๋Š” reactorKit ๋„ํ๋จผํŠธ๋ฅผ ์ฝ์–ด๋ณด๊ณ  ๋‹ค๋ฅธ ์•„ํ‚คํ…์ณ์™€ ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ์ง€ ์•Œ์•„๋ณด๋ ค๊ตฌ ํ•œ๋‹ค !!

 

์ฐธ๊ณ  : https://github.com/ReactorKit/ReactorKit

 

GitHub - ReactorKit/ReactorKit: A library for reactive and unidirectional Swift applications

A library for reactive and unidirectional Swift applications - GitHub - ReactorKit/ReactorKit: A library for reactive and unidirectional Swift applications

github.com

ReactorKit์ด๋ž€ Flux์™€ Reactive Programming์˜ ์กฐํ•ฉ์ž…๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž์˜ action๊ณผ view์˜ state๋Š” observable ์ŠคํŠธ๋ฆผ์„ ํ†ตํ•ด ๊ฐ layer์—๊ฒŒ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ํ๋ฆ„์€ ๋‹จ๋ฐฉํ–ฅ์ ์ž…๋‹ˆ๋‹ค. ๋ทฐ๋Š” ์˜ค์ง action๋งŒ ๋ฐฉ์ถœ ํ•  ์ˆ˜ ์žˆ๊ณ , reactor๋Š” state๋งŒ ๋ฐฉ์ถœ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋””์ž์ธ ๋ชฉํ‘œ :

Testability : ReactorKit์˜ ์ฒซ๋ฒˆ์งธ ๋ชฉํ‘œ๋Š” ๋ทฐ๋กœ๋ถ€ํ„ฐ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค. reactor๋Š” ๋ทฐ์— ์˜์กด์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹จ์ง€ reactor์™€ view binding์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

Start Small : ReactorKit์€ ์•ฑ ์ „์ฒด๊ฐ€ ๋‹จ์ผ ์•„ํ‚คํ…์ณ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. reactorKit์€ ํ•˜๋‚˜ ๋˜๋Š” ๊ทธ ์ด์ƒ์˜ ๋ทฐ์— ๋Œ€ํ•ด ๋ถ€๋ถ„์ ์œผ๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ํ”„๋กœ์ ํŠธ์— ReactorKit์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ์žฌ ์ž‘์„ฑ ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. (์ข‹๊ตฐ ..)

Less Typing : ReactorKit์€ ๊ฐ„๋‹จํ•œ ๊ฒƒ์„ ์œ„ํ•ด ๋ณต์žกํ•œ ์ฝ”๋“œ๋ฅผ ํ”ผํ•˜๋Š” ๋ฐ ์ค‘์ ์„ ๋‘ก๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์•„ํ‚คํ…์ณ์™€ ๋น„๊ตํ•˜๋ฉด ๋” ์ ์€ ์ฝ”๋“œ๋ฅผ ํ•„์š”๋กœ ํ•ฉ๋‹ˆ๋‹ค. 

 

 

 

View

view๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. viewController์™€ Cell์€ view๋กœ ์ทจ๊ธ‰๋ฉ๋‹ˆ๋‹ค. view๋Š” user input์„ action stream์— ๋ฐ”์ธ๋”ฉํ•˜๊ณ  view state๋ฅผ UI ์ปดํฌ๋„ŒํŠธ์— ๋ฐ”์ธ๋”ฉํ•ฉ๋‹ˆ๋‹ค. view layer์—๋Š” ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์ด ์—†์Šต๋‹ˆ๋‹ค. view๋Š” ์˜ค์ง action stream๊ณผ state stream์„ ๋งคํ•‘ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. 

view๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์กด์žฌํ•˜๋Š” ๊ธฐ์กด class์— View protocol์„ ์ค€์ˆ˜์‹œํ‚ต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๊ทธ ํด๋ž˜์Šค๋Š” ์ž๋™์œผ๋กœ reactor๋ผ๋Š” ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ํ”„๋กœํผํ‹ฐ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ view ๋ฐ–์—์„œ ์„ ์–ธ๋ฉ๋‹ˆ๋‹ค. 

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

profileViewController.reactor = UserViewReactor() // inject reactor

 

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)
}

 

์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ฒฝ์šฐ StoryboardView ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ฒƒ์ด ๋™์ผํ•˜์ง€๋งŒ ์œ ์ผํ•œ ์ฐจ์ด์ ์€ view๊ฐ€ ๋กœ๋“œ๋œ ํ›„(viewDidLoad) StoryboardView๊ฐ€ ๋ฐ”์ธ๋”ฉ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

let viewController = MyViewController()
viewController.reactor = MyViewReactor() // will not executes `bind(reactor:)` immediately

class MyViewController: UIViewController, StoryboardView {
  func bind(reactor: MyViewReactor) {
    // this is called after the view is loaded (viewDidLoad)
  }
}

 

Reactor

reactor๋Š” ๋ทฐ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉฐ ๋ทฐ์— ์˜์กด์ ์ด์ง€ ์•Š์€ ๊ณ„์ธต์ž…๋‹ˆ๋‹ค. reactor์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์—ญํ• ์€ control flow๋ฅผ view์—์„œ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  view๋Š” ์ƒ์‘ํ•˜๋Š” reactor๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  ๋ชจ๋“  ๋กœ์ง์€ reactor์—๊ฒŒ ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค. reactor๋Š” view์— ์˜์กด์„ฑ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. 

 

reactor๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•ด Reactor ํ”„๋กœํ† ์ฝœ์„ ์ค€์ˆ˜ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ”„๋กœํ† ์ฝœ์€ Action, Mutation, State 3๊ฐ€์ง€ ํƒ€์ž…์„ ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ 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์œผ๋กœ 2๋‹จ๊ณ„์— ๊ฑธ์ณ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. mutate(), reduce()

 

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

func mutate(action: Action) -> Observable<Mutation>

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

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()๋Š” ์˜ˆ์ „ state์™€ mutate๋ฅผ ๋ฐ›์•„ ์ƒˆ๋กœ์šด state๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

func reduce(state: State, mutation: Mutation) -> State

์ด ๋ฉ”์†Œ๋“œ๋Š” ์ˆœ์ˆ˜ํ•จ์ˆ˜ ์ž…๋‹ˆ๋‹ค. ๋‹จ์ง€ ์ƒˆ๋กœ์šด state๋ฅผ ์ƒ์„ฑํ•ด ๋™๊ธฐ์ ์œผ๋กœ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜์—์„œ ์–ด๋– ํ•œ ๋ถ€์ž‘์šฉ๋„ ์ˆ˜ํ–‰ํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.

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

transform()์€ ๊ฐ๊ฐ์˜ stream์„ ๋ณ€ํ˜•ํ•ฉ๋‹ˆ๋‹ค. 3๊ฐ€์ง€ transform() ํ•จ์ˆ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

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

์ด ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๋‹ค๋ฅธ Observable stream์„ ๋ณ€ํ™˜ํ•˜๊ณ  ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, transform(mutation:)์€ global event stream์„ mutation stream์— ๊ฒฐํ•ฉํ•˜๋Š” ๋ฐ ๊ฐ€์žฅ ์ข‹์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ Global State๋ฅผ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค.

 

Global States

redux์™€ ๋‹ฌ๋ฆฌ, ReactorKit๋Š” global app state๋ฅผ ์ •์˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ global state๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฌด์—‡์ด๋“  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. BehaviorSubject, PublishSubject ๋˜๋Š” Reactor๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ReactorKit์€ global state๋ฅผ ๊ฐ•์ œํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํŠน์ • ๊ธฐ๋Šฅ์—์„œ ReactorKit์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ Action → Mutation → State flow์—์„œ global state๊ฐ€ ์—†์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. global state๋ฅผ mutation์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋ ค๋ฉด transform(mutation:)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋ฅผ ์ €์žฅํ•˜๋Š” global BehaviorSubject๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. currentUser๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ Mutation.setUser(User?)๋ฅผ ๋‚ด๋ณด๋‚ด๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

var currentUser: BehaviorSubject<User> // global state

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

๊ทธ๋Ÿฌ๋ฉด view๊ฐ€ reactor์— action์„ ๋ณด๋‚ด๊ณ  currentUser๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค mutation์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

View Communication

์—ฌ๋Ÿฌ view์™€ communication ํ•˜๊ธฐ ์œ„ํ•ด์„  callback closures ๋˜๋Š” delegate pattern์— ์ต์ˆ™ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ReactorKit๋Š” ์ด๋ฅผ ์œ„ํ•ด reactive extensions์„ ์‚ฌ์šฉ ํ•˜๊ธธ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ControlEvent์˜ ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ์˜ˆ๋Š” UIButton.rx.tap์ž…๋‹ˆ๋‹ค. ํ•ต์‹ฌ ๊ฐœ๋…์€ custom view๋ฅผ UIButton ๋˜๋Š” UILabel๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ChatViewController๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ChatViewController๋Š” MessageInputView๋ฅผ ์†Œ์œ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ MessageInputView์—์„œ send ๋ฒ„ํŠผ์„ ํƒญํ•˜๋ฉด ํ…์ŠคํŠธ๊ฐ€ ChatViewController๋กœ ์ „์†ก๋˜๊ณ  ChatViewController๋Š” reactor์˜ action์œผ๋กœ ๋ฐ”์ธ๋”ฉ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” MessageInputView์˜ rx extension์ž…๋‹ˆ๋‹ค.

extension Reactive where Base: MessageInputView {
  var sendButtonTap: ControlEvent<String> {
    let source = base.sendButton.rx.tap.withLatestFrom(...)
    return ControlEvent(events: source)
  }
}

ChatViewController์—์„œ ์œ„์˜ extension์„ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

messageInputView.rx.sendButtonTap
  .map(Reactor.Action.send)
  .bind(to: reactor.action)

Testing

ReactorKit์—๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋‚ด์žฅ ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ง€์นจ์— ๋”ฐ๋ผ ๋ณด๊ธฐ์™€ ๋ฆฌ์•กํ„ฐ๋ฅผ ๋ชจ๋‘ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฒซ๋ฒˆ์งธ๋กœ ๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ• ์ง€ ๊ฒฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธํ•  ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค: view, reactor

view :

Action - ์ฃผ์–ด์ง„ ์œ ์ € ์ธํ’‹์— ๋”ฐ๋ฅธ reactor์—๊ฒŒ ์ ์ ˆํ•œ action์„ ๋ณด๋ƒˆ๋Š”๊ฐ€ ?

State - following state์—์„œ view ํ”„๋กœํผํ‹ฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”๊ฐ€ ?

 

Reactor :

State : action์— ๋”ฐ๋ฅธ ์ ์ ˆํ•œ state๋กœ ๋ณ€๊ฒฝ ๋˜์—ˆ๋Š”๊ฐ€ ?

 

View testing

view๋Š” stub reactor๋กœ ํ…Œ์ŠคํŠธ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. reactor๋Š” stub ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ง€๋Š”๋ฐ action์„ ๊ธฐ๋กํ•˜๊ณ  state๋ฅผ ๊ฐ•์ œ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. reactor์˜ stub์ด ํ™œ์„ฑํ™” ๋˜๋ฉด, mutate()์™€ reduce()๊ฐ€ ๋ชจ๋‘ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. stub์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์†์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

var state: StateRelay<Reactor.State> { get }
var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions

์•„๋ž˜๋Š” ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์— ๋Œ€ํ•œ ์˜ˆ์ œ ์ž…๋‹ˆ๋‹ค.

func testAction_refresh() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.isStubEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. send an user interaction programmatically
  view.refreshControl.sendActions(for: .valueChanged)

  // 4. assert actions
  XCTAssertEqual(reactor.stub.actions.last, .refresh)
}

func testState_isLoading() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.isStubEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. set a stub state
  reactor.stub.state.value = MyReactor.State(isLoading: true)

  // 4. assert view properties
  XCTAssertEqual(view.activityIndicator.isAnimating, true)
}

Reactor testing

reactor๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

func testIsBookmarked() {
  let reactor = MyReactor()
  reactor.action.onNext(.toggleBookmarked)
  XCTAssertEqual(reactor.currentState.isBookmarked, true)
  reactor.action.onNext(.toggleBookmarked)
  XCTAssertEqual(reactor.currentState.isBookmarked, false)
}

๋•Œ๋•Œ๋กœ state๋Š” ๋‹จ์ผ action์— ๋Œ€ํ•ด ๋‘ ๋ฒˆ ์ด์ƒ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด .refresh๋Š” ์ฒ˜์Œ์—” state.isLoading์„ true๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ๋‹ค์Œ refreshing ํ›„ false๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ currentSate๋กœ state.isLoading์„ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ์—” ์‰ฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ  RxTest ๋˜๋Š” RxExpect๊ฐ€ ํ•„์š”ํ• ์ง€๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” RxSwift๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

func testIsLoading() {
  // given
  let scheduler = TestScheduler(initialClock: 0)
  let reactor = MyReactor()
  let disposeBag = DisposeBag()

  // when
  scheduler
    .createHotObservable([
      .next(100, .refresh) // send .refresh at 100 scheduler time
    ])
    .subscribe(reactor.action)
    .disposed(by: disposeBag)

  // then
  let response = scheduler.start(created: 0, subscribed: 0, disposed: 1000) {
    reactor.state.map(\.isLoading)
  }
  XCTAssertEqual(response.events.map(\.value.element), [
    false, // initial state
    true,  // just after .refresh
    false  // after refreshing
  ])
}

 

Scheduling

state stream์„ reduceํ•˜๊ณ  observing ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์ง€์ •ํ•˜๋ ค๋ฉด ์Šค์ผ€์ค„๋Ÿฌ ํ”„๋กœํผํ‹ฐ๋ฅผ ์ •์˜ํ•˜์‹ญ์‹œ์˜ค. ์ด ๋Œ€๊ธฐ์—ด์€ ์ง๋ ฌ ๋Œ€๊ธฐ์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์Šค์ผ€์ค„๋Ÿฌ๋Š” CurrentThreadScheduler์ž…๋‹ˆ๋‹ค.

final class MyReactor: Reactor {
  let scheduler: Scheduler = SerialDispatchQueueScheduler(qos: .default)

  func reduce(state: State, mutation: Mutation) -> State {
    // executed in a background thread
    heavyAndImportantCalculation()
    return state
  }
}

 

Pulse

Pulse๋Š” mutation์ด ์žˆ์„ ๋•Œ๋งŒ diff๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ์ฝ”๋“œ๋กœ ์„ค๋ช…ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

var messagePulse: Pulse<String?> = Pulse(wrappedValue: "Hello tokijh")

let oldMessagePulse: Pulse<String?> = messagePulse
messagePulse.value = "Hello tokijh" // add valueUpdatedCount +1

oldMessagePulse.valueUpdatedCount != messagePulse.valueUpdatedCount // true
oldMessagePulse.value == messagePulse.value // true

 

alertMessage์ฒ˜๋Ÿผ ๋™์ผํ•œ ๊ฐ’์ด๋”๋ผ๋„ ์ƒˆ๋กœ์šด ๊ฐ’์ด ํ• ๋‹น๋œ ๊ฒฝ์šฐ์—๋งŒ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

// Reactor
private final class MyReactor: Reactor {
  struct State {
    @Pulse var alertMessage: String?
  }

  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case let .alert(message):
      return Observable.just(Mutation.setAlertMessage(message))
    }
  }

  func reduce(state: State, mutation: Mutation) -> State {
    var newState = state

    switch mutation {
    case let .setAlertMessage(alertMessage):
      newState.alertMessage = alertMessage
    }

    return newState
  }
}

// View
reactor.pulse(\.$alertMessage)
  .compactMap { $0 } // filter nil
  .subscribe(onNext: { [weak self] (message: String) in
    self?.showAlert(message)
  })
  .disposed(by: disposeBag)

// Cases
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("tokijh")) // showAlert() is called with `tokijh`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called

 

๋Œ“๊ธ€