์๋ ํ์ธ์.
์ค๋์ ReactorKit์ ๋ํด์ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
ReactorKit?
ReactorKit์ ๊นํ๋ธ์ ๋ค์ด๊ฐ ๋ณด๋ฉด ์๊ฐ๊ธ์ ์ฒซ ๋ฌธ์ฅ์ ์๋์ ๊ฐ์ต๋๋ค.
ReactorKit is a framework for a reactive and unidirectional Swift application architecture.
- ReactorKit์ ๋ฐ์ํ ๋ฐ ๋จ๋ฐฉํฅ ์ ํ๋ฆฌ์ผ์ด์ ์ํคํ ์ฒ๋ฅผ ์ํ ํ๋ ์์ํฌ์ ๋๋ค.
๋ฐ์ํ์ด๋ผ๋ ๋จ์ด๋ง ๋ด๋ Reactive Programming์ ์ํ ํ๋ ์์ํฌ๋ผ๋ ๊ฑธ ์ ์ ์์ต๋๋ค.
๋ฐ๋ฉด์ ๋จ๋ฐฉํฅ์ด๋ผ๋ ๋ง์ ์กฐ๊ธ ์ด์ํ๊ธฐ๋ ํ๊ณ ์ฝ๊ฒ ๋ญ๊ฐ๋ฅผ ๋ ์ฌ๋ฆฌ๊ธฐ ์ด๋ ค์ ๋ ๊ฒ ๊ฐ์ต๋๋ค.
ํด๋น ๊นํ๋ธ๋ฅผ ์กฐ๊ธ ๋ ๋ค์ฌ๋ค๋ณด๋ฉด
ReactorKit์ Flux(๋จ๋ฐฉํฅ ํ๋ฆ์ ์ํคํ ์ฒ) + Reactivex(๋ฐ์ํ ํ๋ก๊ทธ๋๋ฐ)์ ์ฝ์ ํธ๋ก ํ๋ค๋ ๋ถ๋ถ์ด ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๋จ๋ฐฉํฅ ํ๋ฆ์ด ๋ญ์ง ์ดํดํ ํ์๊ฐ ์์ผ๋ Flux๋ฅผ ์กฐ๊ธ ์์๋ณด๊ฒ ์ต๋๋ค.
Flux ?
์์ ์ด๋ฏธ์ง์์ ํ์ดํ ๋ฐฉํฅ์ด ํ ๋ฐฉํฅ์ผ๋ก๋ง ๊ฐ๋ฆฌํค๊ณ ์๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
์ค๋ช ํ์๋ฉด
์ฌ์ฉ์์ ์ ๋ ฅ์ ๊ธฐ๋ฐ์ผ๋ก Action์ ๋ง๋ค๊ณ (createAction) ์ด Action์ Dispatcher์๊ฒ ์ ๋ฌํ์ฌ Store(Model)์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ๋ค์ View์ ๋ฐ์ํ๊ฒ ๋๋ ๋จ๋ฐฉํฅ์ ํ๋ฆ์ ์ด๋ฅผ ๊ฐ์ง๊ณ ์๋ ์ํคํ ์ฒ์ ๋๋ค.
๋จ๋ฐฉํฅ์ ํ๋ฆ์ ์ด๋ฅผ ๊ฐ์ง๊ณ ์๊ณ ์ด๋ ํ ๋ฐฉ์์ผ๋ก ๋์๋๋์ง ์ ๋๋ง ์๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
๋ ๊ถ๊ธํ์ค ์ ์์ผ์ค ๊ฒ ๊ฐ์ ๊ณต์๋ฌธ์ ๋งํฌ ๋จ๊ฒจ๋๊ฒ ์ต๋๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ์ด๋ฌํ ๋จ๋ฐฉํฅ์ ํ๋ฆ์ ์ด์ ๋ฐ์ํ ํ๋ก๊ทธ๋๋ฐ์ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ๊ณ ์๊ฐํ์๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
ReactorKit์์ ๋จ๋ฐฉํฅ ํ๋ฆ์ ์๋์ ์ด๋ฏธ์ง์ ๊ฐ์ต๋๋ค.
View๋ฅผ ํตํด ๋ค์ด์จ ์ ๋ณด๋ฅผ ํ ๋๋ก Action์ ๋ฐฉ์ถํ๊ณ ์ด๋ ๋ค์ Reactor์ mutate๋ฅผ ํตํด ์ํ ๋ณ๊ฒฝ์ ์ํ ์์ ์ ์ํํ๊ณ reduce๋ฅผ ํตํด ์ํ(State)๊ฐ ๋ณ๊ฒฝ๋๊ฒ ๋ฉ๋๋ค.
์๋ฅผ ๋ค์ด ์นด์ดํฐ ์ฑ์ด ์๋ค๊ณ ํด๋ด ์๋ค.
View๋ฅผ ํตํด ๋ฒํผ์ ํฐ์น ์ด๋ฒคํธ๊ฐ ๋ค์ด์๊ณ ๋ฏธ๋ฆฌ ์ ์ํด ๋ Action์์ add๋ฅผ ๋ฐฉ์ถํ๊ฒ ๋๋ฉด Reactor์์ ํด๋น Action์ ๋ํ mutate, reduce๋ฅผ ์์ฐจ์ ์ผ๋ก ์งํํด State๋ฅผ ๋ณ๊ฒฝํ๊ฒ ๋ฉ๋๋ค. ๋ณ๊ฒฝ๋ State๋ View์ ๋ค์ ๋ฐ์๋๊ฒ ๋๊ณ ์.
๊ธ๋ก๋ง ๋ณด๋ฉด ์ด๋ ต๋๋ผ๊ณ ์.
๊ทธ๋์ ๊ฐ๋จํ ์์ ๋ฅผ ์ค๋นํด ๋ดค์ต๋๋ค.
์์
๊ฐ๋จํ๊ฒ TodoList๋ฅผ ๋ถ๋ฌ์ค๊ณ ํด๋น TodoList์์ ํด๋ฆญ ์ check ํ์๊ฐ ๋๋ ์ฑ์ ์์ ๋ก ๋ค์ด๋ณด๊ฒ ์ต๋๋ค.
์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
1. ๋ชจ๋ธ
struct TodoItem {
let id: Int
let title: String
var isDone: Bool
}
- ํ ์ด๋ธ ๋ทฐ์ ํ์ํ ์์ดํ ์ ์ ์ํฉ๋๋ค.
2. Reactor
import RxSwift
import ReactorKit
class TodoListReactor: Reactor {
// View๋ฅผ ํตํด ๋ค์ด์ค๋ Action
enum Action {
case load
case toggleItem(Int)
}
// Action์ ํตํด ์ค์ง์ ์ผ๋ก ํ ๋์๋ค
enum Mutation {
case setLoading(Bool)
case setTodoList([TodoItem])
case setError(Error)
case toggleItem(Int)
}
// Mutation(์ค์ง์ ์ผ๋ก ํ ๋์)์ ๋ฐ๋ผ ์ํ๋ฅผ ๋ณํ
struct State {
var todoList: [TodoItem] = []
var isLoading: Bool = false
var error: Error?
}
// ๋ฐ๋์ ์ด๊ธฐ State๊ฐ ์์ด์ผํจ
// ์์ฑ์์์ ๋ฐ์๋ ์๊ด์์
let initialState = State()
private let todoService: TodoService
init(todoService: TodoService) {
self.todoService = todoService
}
// ์ก์
์ด ๋ค์ด์ค๋ฉด ํธ์ถ
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .load:
return todoService.loadTodoList()
.map { .setTodoList($0) }
.startWith(.setLoading(true))
.catch { .just(.setError($0)) }
case .toggleItem(let id):
return .just(.toggleItem(id))
}
}
// mutate๊ฐ ์คํ๋ ์ดํ ์ํ๋ฅผ ๋ณ๊ฒฝ
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .setLoading(let isLoading):
newState.isLoading = isLoading
case .setTodoList(let todoList):
newState.todoList = todoList
newState.isLoading = false
case .setError(let error):
newState.error = error
newState.isLoading = false
case .toggleItem(let id):
if let index = newState.todoList.firstIndex(where: { $0.id == id }) {
newState.todoList[index].isDone.toggle()
}
}
return newState
}
}
- Reactor ํ๋กํ ์ฝ์ ์ ์๋์ด ์๋ Action, Mutation, State๋ฅผ ์ ์ํฉ๋๋ค.
์ฐธ๊ณ ๋ก ์ ์ ๊ฒฝ์ฐ
Action -> Reacotr(mutate) -> Reactor(reduce) -> State์ ํ๋ฆ์ผ๋ก ์ดํดํ๊ณ ํด๋น ์์ ๋ฅผ ์์ฑํ์ต๋๋ค.
๋ง์ฝ ์๋ชป๋ ๊ฒ์ด๋ผ๋ฉด ๋๊ธ ๋ถํ๋๋ฆฌ๊ฒ ์ต๋๋ค ๐ฅฒ
3. TodoService
protocol TodoServiceType {
func loadTodoList() -> Observable<[TodoItem]>
}
class TodoService: TodoServiceType {
func loadTodoList() -> Observable<[TodoItem]> {
let todoItems = [
TodoItem(id: 1, title: "์์นจ ๋จน๊ธฐ", isDone: false),
TodoItem(id: 2, title: "์ ์ฌ ๋จน๊ธฐ", isDone: false),
TodoItem(id: 3, title: "์ ๋
๋จน๊ธฐ", isDone: false)
]
return Observable.just(todoItems).delay(.seconds(1), scheduler: MainScheduler.instance)
}
}
์ค์ ๋ก๋ ์ถ๊ฐํ๊ฑฐ๋ ์ญ์ ํ๋ ๋ฑ์ ์์ ๋ ์๊ฒ ์ง๋ง ๊ฐ๋จํ ์์๋ฅผ ๋ง๋ค๊ธฐ ์ํด ๋ง๋ค์์ต๋๋ค.
delay๋ฅผ ์ฌ์ฉํ ๊ฒ์ State์ isLoading์ด true์ธ ๊ฒฝ์ฐ, ์ธ๋์ผ์ดํฐ๋ฅผ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋ ๋ก์ง์ด ์๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ์ต๋๋ค.
4. TodoViewController
viewDidAppear์์ reactor?.action.onNext(.load)๋ฅผ ํตํด action์ด load๋ฅผ ๋ฐฉ์ถํ๊ฒ ๋๋ฉด
reactor์ mutate๊ฐ ํธ์ถ๋๊ณ ๋ค์ reduce๊ฐ ํธ์ถ๋๋ฉด์ ํ๋ฉด์ ๋ฆฌ์คํธ๊ฐ ๋ณด์ด๊ฒ ๋ฉ๋๋ค.
import UIKit
import ReactorKit
import RxSwift
import RxCocoa
class TodoViewController: UIViewController, View, UIScrollViewDelegate {
private let tableView = UITableView()
private let activityIndicator = UIActivityIndicatorView(style: .large)
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
...
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// load ์ก์
๋ฐฉ์ถ
self.reactor?.action.onNext(.load)
}
func bind(reactor: TodoListReactor) {
// state๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค todoList๋ฅผ ๋ฐ์
reactor.state
.map { $0.todoList }
.bind(to: tableView.rx.items (cellIdentifier: "TodoCell", cellType: TodoCell.self)) { row, item, cell in
cell.configure(item: item)
}
.disposed(by: disposeBag)
// state๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค state์ isLoading์ ํ์ธํ๊ณ indicator์ ๋ฐ์
reactor.state.map { $0.isLoading }
.distinctUntilChanged()
.subscribe(onNext: { [weak self] isLoading in
if isLoading {
self?.activityIndicator.startAnimating()
} else {
self?.activityIndicator.stopAnimating()
}
})
.disposed(by: disposeBag)
// ์
ํด๋ฆญ์ toggleItem ์ก์
์ reactor ์ก์
์ ๋ฐ์ธ๋ฉ
tableView.rx.modelSelected(TodoItem.self)
.map { .toggleItem($0.id) }
.bind(to: reactor.action)
.disposed(by: disposeBag)
reactor.state.map { $0.error }
.compactMap { $0 }
.subscribe({ [weak self] error in
let alertViewController = UIAlertController(title: "๊ฒฝ๊ณ ", message: "์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", preferredStyle: UIAlertController.Style.alert)
let alertAction = UIAlertAction(title: "์คํค", style: UIAlertAction.Style.default) { action in
alertViewController.dismiss(animated: true)
}
alertViewController.addAction(alertAction)
self?.present(alertViewController, animated: true)
})
.disposed(by: disposeBag)
tableView.rx.setDelegate(self)
.disposed(by: disposeBag)
}
...
}
๋น๋ก ๊ฐ๋จํ ์ฑ์ด์ง๋ง
์ฌ์ฉํ๋ค ๋ณด๋ ์กฐ๊ธ ๋ ๋ณต์กํ ์ฑ์ด๋ผ๋ ํฌ๊ฒ ์ด๋ ต์ง ์์ ๊ฒ ๊ฐ๋ค๋ ๋๋์ ๋ฐ์์ต๋๋ค.
ReactorKit์ผ๋ก ์กฐ๊ทธ๋งํ ํ๋ก์ ํธ๋ฅผ ํ ๋ฒ ํด๋ด์ผ๊ฒ ๋ค์.
์๋ฌด์ชผ๋ก ๋์์ด ๋์๋ฉด ์ข๊ฒ ์ต๋๋ค.
๊ทธ๋ผ ์ด๋ง ๐๐ป ๐๐ป ๐๐ป
'iOS > ์ํคํ ์ฒ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[์ํคํ ์ฒ] Clean Architecture(ํด๋ฆฐ ์ํคํ ์ฒ)๋ฅผ ์ฌ์ฉํ๋ฉด์(feat.DIP) (0) | 2023.09.10 |
---|