- Published on
๐ Swift - ReactorKit
- Authors
- Name
- ์ด์ฐฝ์ค
ReactorKit
์ฑ์คํ ์ด์ ๋ฑ๋ก๋ ์ฑ๋ค์ ์คํ์์ค๋ฅผ ํ์ณ๋ณด๋ฉด ๊ฝค๋ ์์ฃผ ๋ณด์๋ ReactorKit์ ๋๋ค.
์ฌ์ค ๋ง์ฐํ๊ฒ React Native๋ ๊ด๋ จ๋๊ฑด๊ฐ๋ณด๋ค~ ํ๊ณ ์ง๋์น๊ณค ํ์๋๋ฐ.. ์ ํ ์๋์์ฃ ? ๐
๊ฒจ์ธ ํ๋ก์ ํธ ๋์ ์ฌ์ฉํด์ผ ํ ๊ฒ ๊ฐ์์ ๊ณต๋ถํด๋ณด๊ฒ ๋์์ต๋๋ค!
ReactorKit
ReactorKit์ RxSwift๋ฅผ ์ ๋๋ก, ๋ ํธํ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด ๋์จ ํ๋ ์์ํฌ์ ๋๋ค.
๊ณต์ github์ ์ค๋ช ๋ ๊ธ์ ์ฝ์ด๋ณด๋ฉด, ReactorKit์ ๋ฆฌ์กํฐ๋ธํ๊ณ ์ผ๋ฐฉ์ฑ์ ๊ฐ๋ Swift ์ดํ๋ฆฌ์ผ์ด์ ์ ์ง์ํ๊ธฐ ์ํด ๋ง๋ค์ด์ก๋ค๊ณ ํฉ๋๋ค.
๋ฆฌ์กํฐ๋ธํ๊ฑฐ์ผ RxSwift ๊ธฐ๋ฐ์ด๋ ๊ทธ๋ ๋ค๊ณ ์น๊ณ , ์ผ๋ฐฉ์ฑ์ ์ฃผ๋ชฉํ๊ณ ๊ณต๋ถ๋ฅผ ํด๋ณด๋ฉด ๋๊ฒ ๋ค์!
๊ธฐ๋ณธ ์ปจ์
ReactorKit์ View
์ Reactor
๋ผ๋ ๊ฒ ์ฌ์ด๋ฅผ ์ ์ ์ Action
๊ณผ ๋ทฐ์ State
๋ฅผ Observable stream
์ ๋ง๋ค์ด ์ ๋ฌํ๋ค๊ณ ํฉ๋๋ค.
๋น์ต ๋ฌด์จ ๋ง์ธ์ง ์ ์๊ฐ ์๋ค์ ๐คฏ
ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ด ์๋ค.
View
View
๋ ์ฐ๋ฆฌ๊ฐ ์๊ฐํ๋ ๊ทธ View
์์ฒด์
๋๋ค.
ํ๋ฉด์ ํ์ํ๋ ๋ชจ๋ ์์๋ค์ View
๋ผ๊ณ ํ๊ณ ์ด View
๋ค์ ์ ์ ์ ์
๋ ฅ์ action stream์ผ๋ก bind
ํ๊ฑฐ๋ ๊ฐ๊ฐ์ UI Component๋ค์๊ฒ view states๋ฅผ bind
ํ๋ค๊ณ ํฉ๋๋ค.
๊ทธ๋ฌ๋๊น 1. ์ ์ ์ ์ธํฐ๋์ ์ ๋ฐ๊ฑฐ๋ 2. ํ์ UI Component๋ค์๊ฒ Reactor์ ์ํ๋ฅผ ์๋ ค์ค๋ค๋ ๋ป์ธ ๊ฒ ๊ฐ๋ค์.
์ด View
์๋ ๋น์ฆ๋์ค ๋ก์ง์ด ์๋ค๊ณ ํฉ๋๋ค. ๋จ์ง ์ ๋ฌ์์ ์ญํ ๋ง ํ๋ค๊ณ ๋ณด๋ฉด ๋ ๊ฒ ๊ฐ์์!
View Controller
๊ฐ View
๋ผ๋ ๊ฒ์ ๋ช
์ํ๊ธฐ ์ํด์ View
ํ๋กํ ์ฝ์ ์ฑํํ๋ผ๊ณ ํฉ๋๋ค.
class ProfileViewController: UIViewController, View {
var disposeBag = DisposeBag()
}
View
ํ๋กํ ์ฝ์ ์ฑํํ๋ ๊ฒ ๋ง์ผ๋ก ์ด ๋ทฐ์ปจ์ reactor
ํ๋กํผํฐ๋ฅผ ๊ฐ๊ฒ ๋ฉ๋๋ค.
profileViewController.reactor = UserViewReactor()
์์ ๊ฐ์ ์์ ์ด ์๋์ผ๋ก ์ด๋ฃจ์ด์ง๋ค๋ ์๋ฏธ์ ๋๋ค.
์ด๋ ๊ฒ ์ฃผ์ด์ง reactor
๋ผ๋ ํ๋กํผํฐ๊ฐ ๋ณ๊ฒฝ๋๊ฒ ๋๋ฉด, bind(reactor:)
๋ฉ์๋๊ฐ ์๋์ผ๋ก ํธ์ถ๋ฉ๋๋ค. action stream๊ณผ state stream์ ์ ์ํ๊ธฐ ์ํด ๋ทฐ์ปจ ์์ ์ด ํจ์๋ฅผ ๋ง๋ค์ด์ค์๋ค.
func bind(reactor: ProfileViewReactor) {
// action (View -> Reactor)
refreshButton.rx.tap.map { Reactor.Action.refresh }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
// state (Reactor -> View)
reactor.state.map { $0.isFollowing }
.bind(to: followButton.rx.isSelected)
.disposed(by: self.disposeBag)
}
์คํ ๋ฆฌ๋ณด๋๋ ์ง์ํ๋ค๊ณ ํฉ๋๋ค!
์คํ ๋ฆฌ๋ณด๋๋ฅผ ์ฌ์ฉํ๋ ๋ทฐ์ปจ์ View
๊ฐ ์๋๋ผ StoryboardView
๋ฅผ ์ฑํํด์ผ ํฉ๋๋ค!
class MyViewController: UIViewController, StoryboardView {
func bind(reactor: MyReactor) {
...
}
}
Reactor
Reactor
๋ UI์๋ ์ ํ ๊ด๋ จ์๋ ๋
๋ฆฝ๋ ๋ ์ด์ด ๊ณ์ธต์
๋๋ค.
ํ์ง๋ง View
์ ์ํ๋ฅผ ๊ฒฐ์ ํด์ฃผ๋ ์์ฃผ ์ค๋ํ ์ญํ ์ ํฉ๋๋ค.
๋ชจ๋ View
๋ ๊ฐ์์ Reactor
๋ฅผ ๊ฐ์ ธ์ผํ๊ณ , ๋ชจ๋ ๋ก์ง์ ๊ทธ Reactor
์ ์์(delegate)ํฉ๋๋ค.
๊ทธ๋ฌ๋ฉด์๋ Reactor
๋ View
์ ๋ํ ์์กด์ฑ์ด 1๋ ์๊ธฐ ๋๋ฌธ์ Unit Testํ๊ธฐ์๋ ์์ํ๋ค๊ณ ํ๋ค์!
Reactor
๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ View
์ ๋ง์ฐฌ๊ฐ์ง๋ก Reactor
ํ๋กํ ์ฝ์ ์ฑํํด์ผ ํฉ๋๋ค.
์ด ํ๋กํ ์ฝ์ ์ฑํํ ๊ฐ์ฒด๋ Action
, Mutation
, State
์ธ๊ฐ์ง ํ์
์ ๊ฐ์ ธ์ผํฉ๋๋ค.
๋ํ initialState
๋ผ๋ ํ๋กํผํฐ๋ ๊ฐ์ ธ์ผ ํ๋ค๊ณ ํฉ๋๋ค.
class ProfileViewReactor: Reactor {
// represent user actions
enum Action {
case refreshFollowingStatus(Int)
case follow(Int)
}
// represent state changes
enum Mutation {
case setFollowing(Bool)
}
// represents the current view state
struct State {
var isFollowing: Bool = false
}
let initialState: State = State()
}
Action ์ ์ ์ธํฐ๋์ ์ ์๋ฏธํฉ๋๋ค.
State View์ ์ํ๋ฅผ ์๋ฏธํฉ๋๋ค.
Mutation
Action
๊ณผState
๋ฅผ ์ด์ด์ฃผ๋ ์ญํ ์ ํฉ๋๋ค.
Reactor
๋ ๋ ๋จ๊ณ๋ฅผ ๊ฑฐ์ณ action stream์ state stream์ผ๋ก ๋ณํํฉ๋๋ค.
๋ฐ๋ก mutate()
์ reduce()
๋ฅผ ํตํด์์ฃ .
mutate()
๋ Action
์ ๋ฐ์ Observable<Mutation>
์ ์์ฑํฉ๋๋ค.
๋ชจ๋ ๋น๋๊ธฐ ์์ ๋ค์ด๋ API ํธ์ถ ๋ฑ์ด ์ฌ๊ธฐ์ ๋ค์ด๊ฐ๋ ์์ ์ด๋ผ๊ณ ํฉ๋๋ค.
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .refreshFollowingStatus(userID): // receive action
return UserAPI.isFollowing(userID) // create API stream
.map { (isFollowing: Bool) -> Mutation in
return Mutation.setFollowing(isFollowing) // convert to Mutation stream
}
case let .follow(userID):
return UserAPI.follow()
.map { _ -> Mutation in
return Mutation.setFollowing(true)
}
}
}
reduce()
๋ ์ด์ ์ State
์ Mutation
์ผ๋ก๋ถํฐ ์๋ก์ด State
๋ฅผ ๋ง๋๋ ๋ฉ์๋์
๋๋ค.
์๋ก์ด State
๋ฅผ ๋๊ธฐ์ ์ผ๋ก ๋ฐํํ๋ ๊ธฐ๋ฅ ์ธ์ ๊ธฐ๋ฅ์ ๋ฃ์ง ๋ง๋ผ๊ณ ํ๋ค์.
func reduce(state: State, mutation: Mutation) -> State {
var state = state // create a copy of old state
switch mutation {
case let .setFollowing(isFollowing):
state.isFollowing = isFollowing // manipulate the state, creating a new state
return state // return new state
}
}
transform()
์ ๊ฐ stream์ ๋ค๋ฅธ stream์ผ๋ก ๋ณํํ๋ ๊ธฐ๋ฅ์ ํ๋ ๋ฉ์๋์
๋๋ค.
์ฌํ
Global States
Redux์ ๋ค๋ฅด๊ฒ ReactorKit์ global app state๋ฅผ ์ ์ํ์ง ์๋๋ค๊ณ ํฉ๋๋ค.
์ ๋ Redux๊ฐ ๋ญ์ง ๋ชจ๋ฅด๋ ์ผ๋จ ๋์ด๊ฐ๋ณผ๊ฒ์..
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ global state๋ฅผ ์๋ฌด๊ฑธ๋ก๋ ๊ด๋ฆฌํ ์ ์๋ค๊ณ ํฉ๋๋ค. BehaviorSubject
๋ PublishSubject
, ํน์ reactor
๋ฅผ ์ฌ์ฉํด์ ๊ด๋ฆฌํ ์ ์๋ค๊ณ ํ๋ค์.
๋์์ global state๋ฅผ ์ฌ์ฉํ๋๋ก ๊ฐ์ ํ์ง๋ ์๋๋ค๊ณ ํฉ๋๋ค.
ReactorKit์๋ Action โก๏ธ Mutation โก๏ธ State Flow๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.
๊ทธ๋์ global state๋ฅผ Mutation
์ผ๋ก ๋ณํํ๋ ค๋ฉด transform(mutation:)
์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
var currentUser: BehaviorSubject<User> // global state
func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}
์ ์์๋ currentUser
๊ฐ ๋ฐ๋ ๋ Mutation.setUser
๋ฅผ ์คํํด๋ฌ๋ผ๋ ์ฝ๋์
๋๋ค.
๊ธฐ๋ณธ์ ์ธ ์คํธ๋ฆผ์ธ mutation
๊ณผ Global State์ currentUser
๋ฅผ ๋ณํฉ(merge
)์ํค๋ ์ฝ๋์ธ๋ฐ์,
currentUser
๋ Mutation.setUser
๋ก ์ ์๋ Mutation
์ผ๋ก ๋ณํํด์ ๋ค์ด๊ฐ๊ณ ์๋ค์.
View Communication
View๊ฐ์ ๋ฐ์ดํฐ ์ ๋ฌ์ด ์ด๋ฃจ์ด์ง ๋ ๋ณดํต delegate
ํจํด์ด๋ closure
๋ฅผ ์ฌ์ฉํ์ฃ ?
ReactorKit์ ๋ฐ์ดํฐ ์ ๋ฌ์ ์ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์ง ๋ง๊ณ reactive extension์ ์ฌ์ฉํ๋ ๊ฒ์ ์ถ์ฒํ๋ค๊ณ ํฉ๋๋ค.
// Read as "Reactive Extension where Base is a SomeType"
extension Reactive where Base: SomeType {
// Any specific reactive extension for SomeType
}
View A๋ฅผ ChatViewController
, View B๋ฅผ MessageInputView
๋ผ๊ณ ํฉ์๋ค.
View B๋ View A์ subview
์
๋๋ค.
View B๊ฐ View A์ ControlEvent
๋ฅผ ๋ณด๋ด๋ฉด View A์์ ๋ณธ์ธ์ reactor์ธ Reactor A์ Action
์ ์ ๋ฌํ๋ ์๋๋ฆฌ์ค์
๋๋ค.
์๋์ ๋ฐฉ์๋๋ก View B์์ View A๋ก ์ด๋ฒคํธ๋ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ค๋ฉด delegate
๋ closure
๋ฅผ ์ฌ์ฉํ์์ฃ .
๋์ ReactorKit์ด ์ถ์ฒํ๋ ๋ฐฉ์๋๋ก Reactive extension
์ ์ฌ์ฉํด์ ๊ตฌํ์ ํด๋ณด๋ฉด ์๋์ ๊ฐ์ต๋๋ค.
// MessageInputView.swift
extension Reactive where Base: MessageInputView {
var sendButtonTap: ControlEvent<String> {
let source = base.sendButton.rx.tap.withLatestFrom(...)
return ControlEvent(events: source)
}
}
// ChatViewController.swift
messageInputView.rx.sendButtonTap
.map(Reactor.Action.send)
.bind(to: reactor.action)