안녕하세요.
이번 글에선 반응형 애플리케이션의 데이터 흐름을 단반향으로 만들 수 있게 도와주는 프레임워크 ReactorKit에 대해 알아보겠습니다.
ReactorKit의 정의
ReactorKit의 정의는 ReactorKit Github에 설명되어 있었습니다.
ReactorKit은 Flux와 Reactive Programming의 조합입니다. 사용자 작업 및 보기 상태는 관찰 가능한 스트림을 통해 각 계층에 전달됩니다. 이러한 스트림은 단방향입니다. 뷰는 작업만 내보낼 수 있고 리액터는 상태만 내보낼 수 있습니다.
위와 같이 ReactorKit은 Flux 아키텍처를 채택하고 있으며, 데이터 흐름이 단 반향으로 흐르는 아키텍처를 Flux라고 칭합니다.
위 이미지와 같이 View에서 Action이 발생하면, Reator는 Action에 따른 State를 변경하고, 변경된 State는 그대로 View에 표시되게 됩니다.
정의는 비교적 어렵지 않은 것 같습니다.
Reactor 타입 알아보기
ReactorKit은 기존 MVVM 패턴에 사용되는 ViewModel의 역할을 이 Reactor가 대신하게 됩니다.
Reactor에 대해 알아보겠습니다.
Reactor 타입을 만드는 방법은 아주 간단합니다.
Reactor
라는 프로토콜을 채택하면 끝입니다.
위 사진과 같이 타입을 만들고, Reactor
프로토콜을 채택하면, Reactor의 역할을 수행하는 타입을 만들 수 있습니다.
Reactor
프로토콜을 채택하면 Action
과 State
, initialState
를 필수적으로 구현해야 합니다.
initialState
는 말 그대로 초기 State 값입니다.
- Action: Action은 View를 통해 받는 Action을
enum
타입으로 케이스로 나열한 것입니다. - State: State는 현재 상태를 기록하고, View는 State의 값을 가지고 View에 원하는 데이터를 표시합니다.
- Mutation: Mutation은 Action이 발생하면, 처리해야 하는 작업을
enum
타입으로 케이스로 나열한 타입입니다.
⚠️ Mutation은 Action과 State와 다르게 필수 구현 요소가 아닙니다.
하지만, 구현하지 않을 경우 Action의 타입을 그대로 따라가게 됩니다.
이제 마지막으로 두 메서드를 구현해야 합니다.
func mutate(action:) -> Observable<Mutation>
: 해당 메서드는 파라미터로 받은action
을switch
문을 통해 분기 처리하여, Action에 따른 Mutation을Observable
타입으로 반환합니다.func reduce(state:, mutation:) -> State
: 해당 메서드는 파라미터로 받은mutation
을switch
문을 통해 분기 처리하여, Mutation에 맞게 State를 변경 후 리턴합니다. State는 구조체이기 때문에 새로운 변수에 파라미터로 받은 State를 저장하고, 변수를 반환해야 합니다.
여기까지 Reactor 타입에 대해 알아보았습니다.
이제는 View에서 Reactor로 Action을 보내는 방법과, Action에 따른 변경된 State를 어떻게 View에게 전달하는지 알아보겠습니다.
Reactor 사용법
사진과 같이 Button과 같은 Control View의 Control Event를 RxSwift의 map
오퍼레이터를 통해 원하는 Reactor Action으로 변환시키고, Reactor에 bind 하면, 위에서 언급한 mutate
와 reduce
메서드가 순차적으로 실행되게 됩니다.
위 사진과 같이 원하는 State의 값을 구독해두면, State값이 변경될 때마다 값이 방출됩니다.
⚠️ .distinctUntilChanged() 오퍼레이터를 사용하는 이유는 불필요한 스트림이 발생하는 것을 막기 위함입니다. 예를 들어 State의 title만 변경되어도 State가 변경된 걸로 간주되어 모든 State 값의 스트림이 방출됩니다.
여기까지 Reactor 사용법의 사용법까지 알아보았습니다.
ReactorKit을 사용하며 느낀 점과 전체 코드를 마지막으로 마무리하겠습니다.
느낀 점
ReactorKit을 공부하게 된 계기는 기존 RxSwift만 사용하면서, MVVM 패턴으로 앱을 개발할 때 스트림의 흐름을 일관성 있게 디자인하는 것이 또 하나의 일이었습니다. 때문에 스트림의 흐름이 ViewModel에서 Dispose 되기도 하고, View에서 되기도 하는 일이 발생했었습니다.
하지만, ReactorKit 같은 경우 패턴이 정해져 있다 보니 사용하면서 편하고, 팀원들과 협업 시에 큰 강점이 있을 것 같습니다.
또한, State 타입을 통해 값을 저장해두고 있기 때문에, View에 필요한 상태 값 관리가 용이했습니다.
코드
해당 코드는 ReactorKit을 공부하며 토이 프로젝트로 진행했던 SearchBooks 프로젝트의 일부분입니다.
DetailReactor
import Foundation
import ReactorKit
final class DetailReactor: Reactor {
let initialState: State
let useCase: SearchBookUseCaseable
init(useCase: SearchBookUseCaseable, book: Book) {
self.useCase = useCase
self.initialState = State(
title: book.title,
isFavorites: book.isFavorites,
item: book
)
}
enum Action {
case favoritesButtonDidTap
}
enum Mutation {
case favoritesValue(Bool)
}
struct State {
var title: String
var isFavorites: Bool
var item: Book
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .favoritesButtonDidTap:
var newFavoriteValue = false
if currentState.isFavorites {
useCase.removeFavoritesBook(isbn: currentState.item.isbn)
} else {
useCase.addFavoritesBook(isbn: currentState.item.isbn)
newFavoriteValue = true
}
return .just(.favoritesValue(newFavoriteValue))
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .favoritesValue(let isFavorites):
newState.isFavorites = isFavorites
return newState
}
}
}
DetailViewController
import UIKit
import RxCocoa
import RxSwift
import SnapKit
final class DetailViewController: UIViewController {
private let mainView = DetailView()
private let favoritesButton = UIBarButtonItem()
private let disposeBag = DisposeBag()
private let reactor: DetailReactor
weak var coordinator: DetailCoordinator?
init(reactor: DetailReactor) {
self.reactor = reactor
super.init(nibName: nil, bundle: nil)
}
deinit {
coordinator?.removeCoordinator()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
attribute()
layout()
bind(reactor)
}
private func attribute() {
view.backgroundColor = .systemBackground
navigationItem.rightBarButtonItem = favoritesButton
favoritesButton.tintColor = .systemYellow
}
private func layout() {
view.addSubview(mainView)
mainView.snp.makeConstraints {
$0.edges.equalTo(view.safeAreaLayoutGuide)
}
}
private func bind(_ reactor: DetailReactor) {
bindAction(reactor)
bindState(reactor)
}
private func bindAction(_ reactor: DetailReactor) {
favoritesButton.rx.tap
.map { DetailReactor.Action.favoritesButtonDidTap }
.bind(to: reactor.action)
.disposed(by: disposeBag)
}
private func bindState(_ reactor: DetailReactor) {
reactor.state.map { $0.item }
.distinctUntilChanged()
.bind(to: mainView.setContent)
.disposed(by: disposeBag)
reactor.state.map { $0.title }
.distinctUntilChanged()
.bind(to: navigationItem.rx.title)
.disposed(by: disposeBag)
reactor.state.map { $0.isFavorites }
.distinctUntilChanged()
.bind(to: setFavoritesButtonImage)
.disposed(by: disposeBag)
}
}
extension DetailViewController {
private var setFavoritesButtonImage: Binder<Bool> {
return Binder(self) { owner, isFavorites in
let image = isFavorites ? UIImage(systemName: "star.fill") : UIImage(systemName: "star")
owner.favoritesButton.image = image
}
}
}
'iOS' 카테고리의 다른 글
Appearance사용해 NavigationBar 커스텀하기(iOS 13.0+) (0) | 2023.07.19 |
---|---|
[iOS] Clean Architecture 파헤치기 (0) | 2023.01.16 |
[iOS] Coordinator Pattern (0) | 2023.01.09 |
[iOS]PhotoKit 알아보기(최종) (1) | 2022.11.22 |
[iOS]PhotoKit 알아보기(2) (0) | 2022.11.20 |