ReactorKit 예시 - 1

2 minute read

Intro

앞에서 알아본 ReactorKit으로 정말 간단한 예제를 만들어보려 한다.

카운터 앱은 해당 링크를 참고했다.


Counter App

reactorKit_4

간단하게 카운터 앱을 만들어보자.


명세서

아래는 카운터 앱의 명세서이다.

  • 가운데에 숫자를 표시하는 label이 있다.
  • 숫자를 표시하는 label 왼쪽에는 - button이, 오른쪽에는 + button이 있다.
  • -button을 tap하면 숫자가 1 감소한다.
  • +button을 tap하면 숫자가 1 증가한다.
  • 숫자가 증감하는 동안 label 아래의 indicator 애니메이션이 실행된다.

Step1

화면을 구성했다.

reactorKit_5

Step2

CounterViewReactor 파일을 생성했다.

이름에서 보이듯 해당 앱의 Reactor 역할을 할 파일이므로 Reactor 프로토콜을 채택한다.

final class CounterViewReactor: Reactore { ... }

ReactorAction, State, Mutation, initialState를 선언해줘야한다.

Action

enum Action {
  case increase
  case decrease
}

Action 사용자 인터랙션을 나타낸다.

카운터 앱에서는 값을 ‘증가/감소’ 시키는 인터랙션만 있으므로 increase, decrease를 선언한다.

State

struct State {
  var value: Int
  var isLoading: Bool
}

State는 현재 뷰의 상태를 표현한다.

카운터 앱에서는 숫자를 표시할 value와 indicatorView의 애니메이션 여부를 알려줄 isLoading이 필요하다.

Mutation

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

Mutation은 ‘represent state changes’라고 한다.

즉, state의 변화를 표현한다.

+button을 tap하면 value State가 증가해야 하므로 increaseValue를 정의한다.

-button을 tap하면 value State가 감소해야 하므로 decreaseValue를 정의한다.

indicatorView 애니메이션 여부는 Bool 값을 통해 확인한다.

initialState, init()

let initialState: State

init() {
  self.initialState = State(
    value: 0,
    isLoading: false
  )
}

초기 State를 정의해준다.

CounterViewReactor는 class이기 때문에 init()할 때 모든 프로퍼티가 정의되어야 한다.

따라서 init()에서 initialState를 정의해준다.

처음에는 숫자를 표시하는 label은 0 을 표시해야 하고, indicatorView의 애니메이션은 정지된 상태여야 한다.

Step3

Reactor는 전달받은 Action을 2가지 단계를 거쳐 State로 변환한다.

  • mutate(): Action -> Mutation
  • reduce(): Mutation -> State

mutate()

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

increase/decrease Action이 전달되면 Mutation으로 변환시킨다.

rxSwift는 나중에 다루도록 하고, 코드를 봐보자.

위에서 정의한 enum Mutation을 사용한다.

  • Mutation.setLoading(true)로 indicatorView의 애니메이션을 시작한다.

  • Mutation.increaseValue로 value를 증가시키고, delay(.milliseconds(500))으로 지연을 준다.

  • Mutation.setLoading(false)로 indicatorView의 애니메이션을 멈춘다.

위의 과정을 담은 Mutation을 생성하고 반환한다.

reduce()

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
}

reduce()는 이전의 State와 전달받은 Mutation으로 새로운 State를 생성한다.

case .increaseValue를 보자.

state.value += 1이 실행되는데 이는, 이전의 State.value += 1이라고 볼 수 있다.

return state에서의 state는 새로운 State로 볼 수 있다.

Step4

Reactor 구성을 완료했으니 View를 구성하자.

Reactor에서 View를 구성하기 위해서는 View 프로토콜을 채택해야 한다.

해당 예제는 Storyboard로 화면을 구성했으므로 StoryboardView 프로토콜을 채택한다.

final class ViewController: UIViewController, StoryboardView { ... }


rxSwift를 사용하므로 var disposeBag = DisposeBag()을 선언해준다.

reactor

ViewControllerStoryboardView를 채택함으로써 reactor라는 프로퍼티가 생겼다.

View의 밖에서 reactor 프로퍼티를 지정해준다.

viewController.reactor = CounterViewReactor()

bind()

func bind(reactor: CounterViewReactor) {
  // Action
  increaseButton.rx.tap
  	.map { Reactor.Action.increase }
  	.bind(to: reactor.action)
  	.disposed(by: disopseBag)
  
  decreaseButton.rx.tap
  	.map { Reactor.Action.decrease }
  	.bind(to: reactor.action)
  	.disposed(by: disposeBag)
  
  // 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)
}

ViewAction을 전달하기도 하고 State를 받기도 한다.

따라서 bind()ActionState 둘 다에 대해서 구현해야 한다.


Outro

간단한 예제를 구현해 봤다.

역시 직접 구현해보는게 이해하는데 더 빠른거 같다.


구현하면서 느낀점은

  • View, Reactor로 구분을 하니 역할 분담이 명확하게 잘 되는거 같다.

  • 일정한 패턴이 있다보니 손에 익으면 작성이 좀 더 쉬워질거 같다.

  • 한 화면에서 관리하는 Action, State가 많아지면 bind() 관리가 어려워지지 않을까?

다음에는 좀 더 복잡한 예제를 구현하면서 ReactorKit에 대해 느낀 점을 정리해봐야겠다.

Leave a comment