오늘은 ReactorKit에 대해서
아주아주 겉핥기 수준으로 알아보겠습니다 :)
ReactorKit은 반응형 단방향 앱
을 위한 프레임워크 입니다 :)
이렇게나 많은 회사에서 사용하고 있는 프레임워크랍니다. ( 물론 더 있겠지만 ! )
오늘은 이 리액터 킷의 개념을 익혀보고자 합니다 !
rx 공부를 하다보면 느끼겠지만 사람마다 MVVM 을 구현하는 방식이 가지각색입니다.
ReactorKit은 그런 부분에 있어서 어느정도 제약을 두어서
좀 더 정형화
된 방식으로 구현할 수 있게 만들어줍니다.
ReactorKit은 크게 View와 Reactor로 이루어져있으며
View 는 Reactor 에게 Action을 전달하고 Reactor는 View에게 State를 전달하는
단방향 구조입니다.
리액터킷 깃허브 에 가면 리액터킷으로 만든 여러가지 앱들이 있습니다.
Counter 와 GitHub Search가 가장 기본적인 예제이니 처음 접하시는 분들은 이걸 참고하시면 좋을 것 탄아요 :)
아래에서 소개하는 예제는 Counter 입니다!
일단 어떤식으로 만드는지 한번 보고갑시다 !
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 는 ?
우리는 이걸 토대로 View와 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로 넘어가봅니다.
이 예제에서는 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으로 개인 앱 내보기 ~ ✨