경고 - 제 주관적인 생각과 이해가 많이 포함되어 있습니다.
ReactorKit은 전수열님이 만드신 프레임워크로, 사용자 인터랙션과 뷰 상태가 관찰 가능한 스트림을 통해 단방향 데이터 흐름을 가진 아키텍처 구조를 가지고 있다. 뷰와 비즈니스 로직을 분리할 수 있게 되면서 모듈간 결합도가 낮아지고 테스트하기 쉬워졌다. 또한, 자칫 복잡해질 수 있는 비동기 코드를 일관되게 작성할 수 있게 되었다.
MVVM과 함께 RxSwift를 같이 사용했을때 상태값관리가 어렵단 문제점을 발견하고 이를 해결하고자 만들었다고한다.
ReactorKit의 장점은
ReactorKit에는 UI에 해당하는 View와 UI에 반응하여 비즈니스 로직을 처리하는 Reactor로 구성
View는 State만을 표현한다. 뷰 컨트롤러나 셀도 모두 뷰에 해당한다.
View에서는 인터렉터 이벤트들을 Reactor의 Action값으로 넘기고, reactor의 state값을 구독하고 해당 상태에 따라 UI를 업데이트한다.
즉, View는 비즈니스 로직을 수행하지 않는다.
반대로, Reactor는 View의 상태를 관리한다.
Reactor에 View의 Action을 미리 정의해놓고, 해당 action을 처리하여 다시 View에 State값을 넘기는 것
Reactor는 UI 레이어에서 독립적이기 때문에 비교적 테스트하기 용이하다.
UI가 있고, UI들의 Action을 Reactor에 넘기고, Reactor의 State를 구독하고 있는 형태
View 프로토콜을 적용하면 뷰를 정의할 수 있다. DisposeBag 속성과 bind(reactor:) 메서드를 필수로 정의해야 한다.
import ReactorKit
import RxSwift
class UserViewController: UIViewController, View {
var disposeBag = DisposeBag()
func bind(reactor: UserViewReactor) {
// Action
bindAction()
// State
bindState()
}
private func bindAction(_ reactor: UserViewReactior) {
self.followButton.rx.tap
.map { Reactor.Action.follow }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
}
private func bindState(_ reactor: UserViewReactior) {
reactor.state.map { $0.isFollowing }
.distinctUntilChanged()
.bind(to: self.followButton.rx.isSelected)
.disposed(by: self.disposeBag)
}
}
리액터를 정의하기 위해서는 Reactor 프로토콜을 사용한다.
사용자 인터랙션을 표현하는 Action과 뷰의 상태를 표현하는 State, 그리고 상태를 변경하는 가장 작은 단위인 Mutation을 클래스 내부에 필수로 정의해야 한다.
또한 가장 첫 상태를 나타내는 initialState가 필요하다.
Action
State
mutate(action:) -> Observable
reduce(state:mutation:) -> State
import Foundation
import RxSwift
import RxCocoa
import ReactorKit
class UserViewReactor: Reactor {
let initialState = State()
enum Action {
case increase
case decrease
}
// 처리 단위
enum Mutation {
case increaseValue
case decreaseValue
case setLoading(Bool)
}
// 현재 상태를 기록
struct State {
var value = 0
var isLoading = false
}
// Action이 들어온 경우, 어떤 처리를 할건지 분기
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.increaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
case .decrease:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.decreaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
}
}
// 현재 상태와 처리 단위를 받아서 다음 상태를 반환하는 함수
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .increaseValue:
newState.value += 1
case .decreaseValue:
newState.value -= 1
case .setLoading(let isLoading):
newState.isLoading = isLoading
}
return newState
}
}
무작정 따라하려다보니 막히는 부분도 많았고 어려웠는데 이번 기회에 방향을 잡을 수 있었다. 이번엔 진전이 꼭 있었으면 좋겠다.
이글은 전수열, 김종권의 iOS 앱 개발 알아가기님의 글을 참고하였습니다.