์๋ ํ์ธ์ ์ ์ธ์ ๋๋ค!
์ค๋์ ์ต๊ทผ ์งํ ์ค์ธ ํ๋ก์ ํธ๋ฅผ MVVM ๊ตฌ์กฐ๋ก ๋ฆฌํฉํ ๋ง์ ํ๋ฉฐ ๋์ ํ๊ฒ๋ ReactorKit(๋ฆฌ์กํฐํท)์ ๋ํด ์ ๋ฆฌํด๋ณด๋ ค ํฉ๋๋ค.
๋ฐ๋ก ์์ํ ๊ฒ์!!
ReactorKit์ด๋?
ReactorKit์ ๋ฐ์ ๋ฐ ๋จ๋ฐฉํฅ Swift ์ ํ๋ฆฌ์ผ์ด์ ์ํคํ ์ฒ๋ฅผ ์ํ ํ๋ ์์ํฌ์ ๋๋ค.
MVVM๊ณผ ๊ฐ์ ์ํคํ ์ฒ์ ๋ํด ๊ณต๋ถํด๋ณด์ ๋ถ๋ค์ด๋ผ๋ฉด ๋๋ผ์ จ๊ฒ ์ง๋ง ์ํคํ ์ฒ๋ ๊ท๊ฒฉํ๋ ํ์์ด ์๊ธฐ ๋๋ฌธ์
๊ฐ๋ฐ์, ํ์ฌ๋ง๋ค ์ฐ๋ ๋ฐฉ์์ด ์ ๋ง ๋ค๋ฅด๊ณ ๋ค์ํฉ๋๋ค.
ํ์ง๋ง, ReactorKit์ ํ์์ด ์กด์ฌํ๊ธฐ ๋๋ฌธ์ MVVM ์ํคํ ์ฒ๋ฅผ ํ๋ก์ ํธ ๋ด์์ ์ ํํ๋ ๋ฐฉ์(๊ฐ์ ํ ํ๋ฆฟ)์ผ๋ก ์ ์ฉํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
์ค์ ๋ก ์์ ๊ฐ์ด ๋ง์ ํ์ฌ์์ ReactorKit์ ์ฌ์ฉํ๊ณ ์๋ค๊ณ ํฉ๋๋ค!
ReactorKit์ด ์ด๋ค ๊ฒ์ธ์ง, ์ด๋ค ์ฉ๋๋ก ์ฌ์ฉํ๋ ๊ฒ์ธ์ง ํ์ ํ์ผ๋
์ด๋ค ๊ตฌ์กฐ์ธ์ง, ์ด๋ป๊ฒ ์ฌ์ฉํ๋ ๊ฒ์ธ์ง ์ข ๋ ์์ธํ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!!
ReactorKit์ ๊ตฌ์กฐ
ReactorKit์ ํฌ๊ฒ View์ Reactor๋ก ์ด๋ฃจ์ด์ ธ์์ผ๋ฉฐ
View ๋ Reactor ์๊ฒ Action์ ์ ๋ฌํ๊ณ Reactor๋ View์๊ฒ State๋ฅผ ์ ๋ฌํ๋ ๋จ๋ฐฉํฅ ๊ตฌ์กฐ์
๋๋ค.
- View: ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ง๋ ๋ฐ์ดํฐ ๋ฐ UI. View๋ ์ฌ์ฉ์์ ์ ๋ ฅ์ Action Stream์ผ๋ก ๋ฐ์ธ๋ฉํ๊ณ View์ State๋ฅผ ๊ฐ๊ฐ์ UI ์ปดํฌ๋ํธ๋ค์ ๋ฐ์ธ๋ฉํ๋ค. View ๋ ์ด์ด๋ ๋น์ฆ๋์ค ๋ก์ง์ ํฌํจํ์ง ์์ผ๋ฉฐ ํ๋์ ๋ทฐ๋ Action Stream๊ณผ State Stream์ ๋งคํํ๋ ๋ฐฉ๋ฒ์ ์ ์ํ๊ธฐ๋ง ํ๋ค.
- Reactor: View์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ UI ๋ ๋ฆฝ ๊ณ์ธต์ผ๋ก, ViewModel๊ณผ ๊ฐ์ ์ญํ ์ ํ๋ค. Reactor์ ๊ฐ์ฅ ์ค์ํ ์ญํ ์ ํ๋ฆ ์ ์ด๋ฅผ ๋ทฐ์์ ๋ถ๋ฆฌํ๋ ๊ฒ์ด๋ค. ๋ชจ๋ ๋ทฐ๋ 1:1๋ก ๋์๋๋ Reactor๋ฅผ ๊ฐ์ง๊ณ ๋ชจ๋ ๋ก์ง์ Reactor์ ์์ํ๋ค. Reactor๋ ๋ทฐ์ ๋ํ ์์กด์ฑ์ด ์์ผ๋ฏ๋ก ์ฝ๊ฒ ํ ์คํธ๊ฐ ๊ฐ๋ฅํ๋ค.
ReactorKit์ ์ฌ์ฉํด๋ณด์
Counter ์์ ๋ฅผ ํตํด์ ReactorKit์ ์ด์ฉํ MVVM ์ํคํ ์ฒ ์ ์ฉ์ ์ฐ์ตํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
์์ ๋์๊ณผ ๊ฐ์ด + ๋ฒํผ ํด๋ฆญ์ ๋ก๋ฉ ์ธ๋์ผ์ดํฐ๊ฐ 1์ด ๋์ ๋์๊ฐ๊ณ , ์ดํ์ ํ์ฌ์ ์ซ์์์ 1์ด ๋ํด์ง ๊ฐ์ผ๋ก ํ๋ฉด์ ํ์๋ฉ๋๋ค. ๋ฐ๋๋ก - ๋ฒํผ ํด๋ฆญ์ 1 ๋ง์ด๋์ค ๋ ๊ฐ์ด ์ ๋ฐ์ดํธ ๋์ด ํ๋ฉด์ ํ์๋ ์ ์๋๋ก ํ๋ ๊ฐ๋จํ ํ๋ฉด์ ๊ตฌ์ฑํด๋ณด๊ฒ ์ต๋๋ค.
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let vc = CounterVC()
vc.view.backgroundColor = .white
window.rootViewController = vc
// vc์ reactor ์ฃผ์
vc.reactor = CounterViewReactor()
self.window = window
window.makeKeyAndVisible()
}
...
}
์ฐ์ , ์คํ ๋ฆฌ๋ณด๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ์ฝ๋๋ก UI ๋ฅผ ๊ตฌ์ฑํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ SceneDelegate์์ window ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ํ๋ฉด์ ์ด๊ธฐํ ํด์ฃผ๊ณ , ์ฑ์ ์ฒซ ํ๋ฉด์ด ๋ window์ rootViewController๋ฅผ ์ง์ ํด์ค๋๋ค.
๊ทธ๋ฐ ๋ค์, ViewController์๊ฒ reactor๋ฅผ ์ฃผ์ ํด์ค๋๋ค.
ViewModel์ ์ฌ์ฉํด MVVM ๊ตฌ์กฐ๋ฅผ ๋ง๋๋ ๊ฒ๊ณผ ๊ฐ์ฅ ๋ค๋ฅธ ๊ณผ์ ์ด ์ด๋ ๊ฒ reactor๋ฅผ ์ฃผ์ ํด์ฃผ๋ ๊ณผ์ ์ธ ๊ฒ ๊ฐ์๋ฐ์, ReactorKit์์๋ ์ฃผ์ ๊ณผ์ ์ด ์๋ค๋ฉด ํ๋ฉด์ด ๋์ ํ์ง ์์ต๋๋ค.
์ฐธ๊ณ ๋ก, ํด๋น ์์ ๋ ๊ฐ๋จํ ์์ ๋ผ ํ๋์ ๋ทฐ๋ง ์กด์ฌํ์ง๋ง, ์ฌ๋ฌ ๋ทฐ๋ก ์ด๋ฃจ์ด์ง ํ๋ก์ ํธ์์๋ ์์์ ์ค๋ช ํ ๊ฒ๊ณผ ๊ฐ์ด View์ Reactor๊ฐ 1:1๋ก ๋์๋๊ธฐ ๋๋ฌธ์ reactor๋ฅผ ์ฃผ์ ํด์ฃผ๋ ๊ณผ์ ์ ํ๋ฉด ์ ํ ์์ ํด์ฃผ๋ฉด ๋ฉ๋๋ค!
์ด์ Reactor์ View๋ฅผ ๊ตฌ์ฑํด๋ด ์๋ค.
์์์ ์ค๋ช ํ๋ฏ์ด View๋ Reactor์๊ฒ Action์ ์ ๋ฌํ๊ณ , Reactor๋ ๋ก์ง์ ์ฒ๋ฆฌํด์ View์๊ฒ State๋ฅผ ์ ๋ฌํฉ๋๋ค.
๊ทธ๋ ๋ค๋ฉด ์ด ์์ ์์ View๊ฐ ๋ฐฉ์ถํ๋ Action์?
- + ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ
- - ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ
์ด ๋๊ฐ์ง ์ ๋๋ค!
Reactor๊ฐ View์ ์ ๋ฌํ๋ State๋?
- ์ซ์ ๊ฐ
- ๋ก๋ฉ ์ธ๋์ผ์ดํฐ์ ์ํ (true or false)
์ด๋ ๊ฒ ๋๊ฐ์ง์์ ๊ธฐ์ตํด๋๊ณ View์ Reactor๋ฅผ ๊ตฌํํด๋ด ์๋ค!
Reactor
๋จผ์ , Reactor๋ถํฐ ๊ตฌํํด๋ด ์๋ค.
import Foundation
import RxSwift
import RxCocoa
import ReactorKit
// ViewModel์ ์ญํ ์ ํ๋ Reactor
// VC์์ Action์ ๋ณด๋ด๋ฉด Reactor์ ๋ด๋ถ์์ mutate์ reduce์ ๊ณผ์ ์ ๊ฑฐ์ณ์ State๋ฅผ ๋ฐฉ์ถํด์ VC๋ก ๋ค์ ๋ณด๋
// VC๊ฐ ๋ฆฌ์กํฐ์ state๋ฅผ ๊ตฌ๋
ํ๋ ํํ
class CounterViewReactor: Reactor {
let initialState = State()
enum Action {
case plus
case minus
}
enum Mutation {
case plusValue
case minusValue
case setLoading(Bool)
}
struct State {
var value = 0
var isLoading = false
}
// mutate()๋ Action ์คํธ๋ฆผ์ Mutation ๋จ์๋ก ๋ฐฉ์ถํด์ฃผ๋ ์ญํ ์ ํ๋ Reactor์ ๋ด๋ถ ํจ์
// ์ต์ ๋ฒ๋ธ: ๊ด์ฐฐ๊ฐ๋ฅํ ๋ฐ์ดํฐ์ ํ๋ฆ. Mutationํ์
์ element๊ฐ ๋ด๊ธด ์ต์ ๋ฒ๋ธ์ด ๋ฆฌํดํ์
์ธ ๊ฒ
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .plus:
return Observable.concat([ // concat์ ๋๊ฐ ์ด์์ ์ต์ ๋ฒ๋ธ ์ง๋ ฌ๋ก ์ฐ๊ฒฐํ๋ operator
Observable.just(.setLoading(true)),
Observable.just(.plusValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
case .minus:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.minusValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
}
}
// reduce()๋ ๋ฐฉ์ถ๋ Mutation ์ต์ ๋ฒ๋ธ ์คํธ๋ฆผ์ ๋ฐ์์ ์ํ๊ฐ(State)์ ๋ทฐ๋ก ๋ฐฉ์ถํ๋ Reactor์ ๋ด๋ถ ํจ์
// ์ด์ ์ํ ๊ฐ๊ณผ ์ฒ๋ฆฌ ๋จ์๋ฅผ ๋ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํจ
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .plusValue:
newState.value += 1
case .minusValue:
newState.value -= 1
case .setLoading(let isLoading):
newState.isLoading = isLoading
}
return newState
}
}
CounterViewReactor๋ Reactor๋ผ๋ ํ๋กํ ์ฝ์ ์ฑํํด์ฃผ๊ณ ์์ต๋๋ค.
Reactor๋ฅผ ์ฑํํ๋ฉด Action, Mutation, State๋ฅผ ํ์์ ์ผ๋ก ์ ์(์ ํํ๋ ๊ตฌ์กฐ๋ก ๊ตฌํ) ํด์ฃผ์ด์ผํ๋ฉฐ initialState๊ฐ ํ์ํฉ๋๋ค.
- Action: View๋ก๋ถํฐ ๋ฐ์ Action์ enum์ผ๋ก ์ ์
- Mutation: View๋ก๋ถํฐ action์ ๋ฐ์ ๊ฒฝ์ฐ, ์ฒ๋ฆฌํด์ผ ํ ๋ฐ์ดํฐ๋ค์ enum์ผ๋ก ์ ์
- State: ํ์ฌ ์ํ๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ, View์์ ํด๋น ์ ๋ณด๋ฅผ ์ฌ์ฉํ์ฌ UI ์ ๋ฐ์ดํธ
Reactor๋ ๋ทฐ์์ action์ด ๋ค์ด์ค๋ฉด mutate()๋ผ๋ ๋ฆฌ์กํฐ ๋ด๋ถ ํจ์๋ฅผ ํตํด Observable<Mutation> ํ์ ์ ๋ฐํํ๊ณ , reduce()๋ผ๋ ๋ด๋ถ ํจ์๊ฐ mutation๊ณผ ํ์ฌ ์ํ๋ฅผ ๋ฐ์ ๋ฐ๋ state ๋ฅผ ๋ฐํํฉ๋๋ค.
- mutate(action:) -> Observable<Mutation>
: Action์ด ๋ค์ด์จ ๊ฒฝ์ฐ, ์ด๋ค ์ฒ๋ฆฌ๋ฅผ ํ ๊ฒ์ธ์ง Mutation์์ ์ ์ํ ๋ฐ์ดํฐ ๋จ์๋ค์ ์ฌ์ฉํ์ฌ Observable๋ก ๋ฐฉ์ถ - reduce(state:mutation:) -> State
: ํ์ฌ ์ํ(state)์ ์์ ๋จ์(mutation)์ ๋ฐ์์ ์ต์ข state๋ฅผ ๋ฐํ. mutate(action:) -> Observable<Mutation>์ด ์คํ๋ ํ ๋ฐ๋ก ํด๋น ๋ฉ์๋ ์คํ
View
Reactor์์ ํ์ํ ๋ฐ์ดํฐ ๋ก์ง์ ๋ชจ๋ ์ฒ๋ฆฌํ๋ค๋ฉด, ์ด๋ฅผ ์ด์ฉํด ๋ทฐ์์ UI๋ฅผ ์ ๋ฐ์ดํธํด๋ด ์๋ค!
VC์์๋ View๋ผ๋ ํ๋กํ ์ฝ์ ์ฑํํด ๊ตฌํํด์ฃผ์ด์ผ ํ๋๋ฐ์, ์ ๋ ์ฝ๋๋ก ๊ตฌํํ๊ธฐ ๋๋ฌธ์ View๋ฅผ ์ฑํํด์ฃผ์์ต๋๋ค.
storyboard๋ฅผ ์ด์ฉํด UI๋ฅผ ๊ตฌ์ฑํ๋ค๋ฉด, StoryboardView๋ฅผ ์ฑํํด์ฃผ๋ฉด ๋ฉ๋๋ค.
(StoryboardView๋ฅผ ์ฑํํ์ ๊ฒฝ์ฐ์๋ ๋ทฐ๊ฐ ๋ก๋๊ฐ ๋ ํ ๋ฐ์ธ๋ฉ์ ํด์ค๋๋ค.)
import UIKit
import SnapKit
import Then
import RxSwift
import RxCocoa
import ReactorKit
class CounterVC: UIViewController {
// UI ๊ตฌ์ฑ
}
// MARK: - bind
// ์ค๋ณด๋ฅผ ์ฌ์ฉํ๋ค๋ฉด StoryboardView๋ฅผ ์ฑํ
// ๋์ ์ฐจ์ด๋ StoryboardView๋ฅผ ์ฑํํ์ ๊ฒฝ์ฐ์๋ ๋ทฐ๊ฐ ๋ก๋๊ฐ ๋ ํ ๋ฐ์ธ๋ฉ์ ํด์ค๋ค๋ ๊ฒ!
extension CounterVC: View {
// ๋ฆฌ์กํฐ๊ฐ ์ฃผ์
๋๋ฉด bind() ๋ฐ๋ก ์คํ
func bind(reactor: CounterViewReactor) {
bindAction(reactor)
bindState(reactor)
}
private func bindAction(_ reactor: CounterViewReactor) {
// ํ๋ฌ์ค ๋ฒํผ
plusBtn.rx.tap
.map { Reactor.Action.plus }
.bind(to: reactor.action)
.disposed(by: disposeBag)
// ๋ง์ด๋์ค ๋ฒํผ
minusBtn.rx.tap
.map { Reactor.Action.minus }
.bind(to: reactor.action)
.disposed(by: disposeBag)
}
// Reactor์ state์ ์ฐ๊ฒฐ
// reactor์ state๋ฅผ bindํด์ state๊ฐ ๋ณํ ๋๋ง๋ค ๋ฐ๋ UI๋ฅผ ์ง์ ํด์ฃผ๊ณ UI์์ ๋ณํ๊ฒ๋ ๋ถ๋ถ๋ค์ ์ฌ๊ธฐ์ ์ง์ ํด์ค
private func bindState(_ reactor: CounterViewReactor) {
reactor.state.map { $0.value }
.distinctUntilChanged()
.map { "\($0)" }
.bind(to: countLabel.rx.text)
.disposed(by: disposeBag)
reactor.state.map { $0.isLoading }
.distinctUntilChanged()
.bind(to: loadingView.rx.isAnimating)
.disposed(by: disposeBag)
}
}
์ฌ์ฉ์๊ฐ + ๋ - ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์ด๋ฒคํธ๊ฐ ๋ฐ์๋๊ฒ ์ฃ !
View๋ ์ด action์ reactor์ ๋๊ธฐ๊ณ , reactor์ state๋ฅผ ๊ตฌ๋ ํ๊ณ ์๋ ํํ์ ๋๋ค.
์ด๋ ๊ฒ action์ ๋ฐฉ์ถํ๊ณ reactor๊ฐ ์ฒ๋ฆฌํ ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํด UI๋ฅผ ์ ๋ฐ์ดํธํ๋ ๊ณผ์ ์ bind(reactor: ) ๋ฉ์๋์์ ๋ชจ๋ ์ํ๋ฉ๋๋ค.
View์์๋ ReactorKit ๋ด๋ถ์ ์ผ๋ก ํธ์ถ๋๋ bind(reactor: ) ๋ฉ์๋์์ ๋ฐ์ธ๋ฉ์ ์ํํฉ๋๋ค.
- bindAction(_:): View์์ Reactor๋ก ์ด๋ฒคํธ ๋ฐฉ์ถ
- bindState(_:): Reactor์์ ๋ฐ๋ state๋ค์ ๊ตฌ๋ ํด์ UI ์ ๋ฐ์ดํธ
์ ์ฒด ์ฝ๋: https://github.com/jane1choi/ReactorKit-Practice
๋๋์
ReactorKit์์ ํ์์ ์ผ๋ก ๊ตฌํํด์ผ ํ๋ ๊ฒ์ด ์๋ค๋ณด๋ ์ ํด์ค ํ ํ๋ฆฟ์ ๋ฐ๋ผ ์ฝ๋๋ฅผ ์ง๋ ๋๋์ด ๋ค์์ต๋๋ค.
MVVM ๊ตฌ์กฐ๋ฅผ ์ฒ์ ๊ตฌ์ฑํ๋ค๋ณด๋ฉด ViewModel์ ์ด๋ป๊ฒ ๊ตฌ์ฑํด์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ถ๋ฆฌํ ์ง ๋ง๋งํ ์ ์๋๋ฐ, ReactorKit์ ๊ฐ์ด๋๋ผ์ธ์ ๋ฐ๋ผ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ทฐ๋ฅผ ๋ถ๋ฆฌํ ์ ์์ด MVVM ์ด์ฌ์๊ฐ ์์ํด๋ณด๊ธฐ์ ์ข์ ์ ์์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋๋ค์!
๊ทธ๋ฆฌ๊ณ ํ ํ๋ก์ ํธ์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ฝ๋๋ฅผ ์งค ์ ์๋ค๋ณด๋ ์ฝ๋ ๋ฆฌ๋ทฐ ํ๊ธฐ์๋ ํธํ ๊ฒ ๊ฐ์ต๋๋ค!
๋ฆฌ์กํฐํท์ผ๋ก MVVM ์ํคํ ์ฒ ์ ์ฉ ์ฐ์ต์ ์ข ํด๋ณด๊ณ ๋น์ฆ๋์ค ๋ก์ง ๋ถ๋ฆฌ์ ์ต์ํด์ง๋ฉด ๋ฆฌ์กํฐํท์ ๋ผ๊ณ ๋ทฐ๋ชจ๋ธ์ ๋ง๋ค์ด MVVM ๊ตฌ์กฐ๋ก ๋ง๋๋ ์ฐ์ต์ ํ๋ฒ ํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค..
[์ฐธ๊ณ ์๋ฃ]
'๐ iOS > iOS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[iOS] TDD์ Unit Test (3) | 2023.12.14 |
---|---|
[iOS] iOS ํ์ผ ์์คํ (1) | 2023.11.28 |
[iOS] ํค์ฒด์ธ(Keychain)์ ์ด์ฉํ ๋ฐ์ดํฐ ์ ์ฅ ๋ฐ ๊ด๋ฆฌ (0) | 2023.08.06 |
[iOS/Architecture] Coordinator Pattern (2) | 2022.12.11 |
[iOS] UserDefaults๋? (3) | 2022.01.06 |