MVVM 패턴은 따로 규격화된 것이 없습니다.
그렇기 때문에 개발자의 개발 스타일에 따라 코드가 달라질 수 있습니다!
협업을 한다면 치명적인 문제가 될 수 있습니다.
코드를 이해하는 데 시간이 걸리고 의도와 다르게 파악할 수도 있기 때문이죠.
하지만 ReactorKit은 형식이 있기 때문에 다른 개발자가 작성한 코드도 쉽게 이해할 수 있습니다!!
그리고 보통 MVVM과 RxSwift를 같이 사용하는 경우가 많은데,
이때 상태 값을 관리하기가 어려워지는 문제가 있습니다.
이런 문제들을 빠샤! 하기 위해 등장한 것이 바로 ReactorKit입니다!
그리고 RxSwift 기반으로 만들어졌기 때문에 RxSwift의 메소드를 사용할 수 있습니다.
흐름은 MVVM과 비슷합니다.
사용자가 어떠한 액션을 보내면 Reactor(ViewModel)에서 각 액션에 맞게 Mutation를 통해서 상태(state)를 변경해 줍니다.
View는 이렇게 바뀐 상태를 통해서 UI를 업데이트 합니다!
Reactor에서는 View에서 발생할 수 있는 Action과 State를 정의합니다.
State는 음... View에서 사용할 데이터들? 이라고 생각하시면 될 것 같습니다!
Action은 input, State는 output...
그리고 각 Action과 State를 연결해주는 Mutation도 정의해 줍니다.
다리 역할을 한다고 보시면 돼요!
이제부터는 위에서 설명한 내용을 조금 고급스럽고(?) 자세하게 파악해 보겠습니다.
살짝 지루할 수 있어요... 흑흑
개념
- 반응형 단방향 앱을 위한 프레임워크입니다. (아래 사진을 보면 알 수 있죠?)
- 사용자의 상호작용과 뷰 상태가 관찰 가능한 스트림을 통해 단방향으로 전달됩니다. (이거는 RxSwift와 RxRelay(혹은 RxCocoa)를 함께 사용하니까!)
- 뷰와 비즈니스 로직을 분리할 수 있습니다.
- 따라서 모듈 간 결합도가 낮습니다. 그렇기 때문에 테스트하기에 좋겠죠?
- 또한 MVVM의 문제인 Observable이 많아지면 상태를 나타내는 변수와 행동을 나타내는 변수의 구분이 어려울 때가 있으며, 의도를 파악하기 어려울 수 있다는 문제를 해결할 수 있습니다. (Action, State, Mutation타입으로 관리)
로직을 보면 단방향인 것을 볼 수 있죠?!
컴포넌트
- View(UI)와 Reactor(UI에 반응하여 비즈니스 로직을 처리)로 구성되어 있습니다.
- Reactor에는 View의 Action을 미리 정의하고, 각 action에 맞는 Mutation을 정의하여 State를 업데이트를 한 뒤, View에게 이 업데이트된 state를 넘깁니다. (말이 어렵죠... 이해가 안 되신다면 다시 맨 위로...)
- View는 이벤트들을 Reactor의 Action 값으로 넘기고, Reactor의 State 값을 구독하여 UI를 업데이트합니다.
View
- UI들의 action을 reactor에 넘기고, reactor의 state를 구독하고 있는 형태입니다.
- View에는 비즈니스 로직이 없습니다!!
- 입력은 action 스트림에 바인딩하고, 뷰의 state를 각 UI 컴포넌트에 바인딩하는 역할을 합니다.
- Dispose와 bind(reactor:) 메소드를 필수로 정의해야 합니다!
- ReactorKit 내부적으로 호출되는 bind 메소드를 통해 바인딩을 수행합니다.
- bindAction : View에서 Reactor로 이벤트를 방출합니다.
- bindState : Reactor에서 바뀐 state들을 View에서 구독합니다
아래는 View의 예제입니다.
import UIKit
import RxSwift
import RxCocoa
import ReactorKit
class CounterViewController: UIViewController, StoryboardView {
var disposeBag = DisposeBag()
@IBOutlet weak var countLabel: UILabel!
@IBOutlet weak var increaseButton: UIButton!
func bind(reactor: CounterViewReactor) {
bindAction(reactor)
bindState(reactor)
}
// Reactior에 정의한 Action과 View의 Action을 바인딩 합니다!
private func bindAction(_ reactor: CounterViewReactor) {
increaseButton.rx.tap // increaseButton 클릭 이벤트를
.map { Reactor.Action.increase }
.bind(to: reactor.action) // increase 액션과 바인딩
.disposed(by: disposeBag)
}
// Reactor를 통해 업데이트된 값(State)을 바인딩해서 가져옵니다!
private func bindState(_ reactor: CounterViewReactor) {
reactor.state
.map { String($0.value) }
.distinctUntilChanged() // state 값을
.bind(to: countLabel.rx.text) // countLabel 텍스트에 바인딩
.disposed(by: disposeBag)
}
}
Reactor(ViewModel)
- View의 상태를 관리하며, UI로 부터 독립된 계층입니다. (그렇기 때문에 테스트에 용이!)
- 모든 View는 1:1로 대응되는 Reactor(ViewModel)를 가지고 있습니다.
- Action(Input)
- 사용자의 입력 및 상호작용입니다.
- View로부터 받을 Action들을 열거형(enum)으로 정의합니다.
- Mutation
- Action과 State를 연결해주는 다리 역할을 합니다.
- Action을 받았을 때, 처리해야 할 작업 단위를 열거형으로 정의합니다.
- State(Output)
- 현재 상태를 기록하고 있으며, View에서 해당 정보를 통해 UI를 업데이트합니다.
- 또한 Reactor에서 이미지를 가져올 때 페이지의 정보를 저장합니다.
- mutate(action:) → Observable<Mutation>
- Action이 들어온 경우, Mutation에서 정의한 작업 단위를 사용하여 어떤 처리를 할 것인지 Observable로 방출합니다.
- 이 부분에서 concat 연산자를 사용해서 비동기 처리를 유용하게 다룰 수 있습니다.
- concat : Observable의 배열이 주어지면 순서대로 실행하게 하는 연산자
- reduce(state:mutation) → State
- 현재 상태(state)와 작업 단위(mutation)를 받아 최종 상태를 반환합니다.
- mutate(action:) -> Observable<Mutation> 실행 후 바로 이 메소드가 실행됩니다.
- 즉, 내부적으로 mutate()와 reduce() 함수를 사용하여 action 스트림을 state 스트림으로 변환해주는 역할을 하는 계층입니다.
import Foundation
import RxSwift
import RxCocoa
import ReactorKit
class CounterViewReactor: Reactor {
let initialState = State()
// View로 부터 받을 action들을 열거형으로 정의
enum Action {
case increase
case decrease
}
// 작업 처리 단위를 열거형으로 정의
enum Mutation {
case increaseValue
case decreaseValue
case setLoading(Bool)
}
// 현재 상태를 기록
struct State {
var value = 0
var isLoading = false
}
// Action이 들어온 경우, 어떤 처리를 할건지 분기
// .concat은 순서대로 일을 처리하게 해주는 녀석으로 생각하세요!
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.increaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
case .decrease:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.decreaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
}
}
// 이전 상태와 처리 단위를 받아서 다음 상태를 반환하는 함수
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .increaseValue:
newState.value += 1
case .decreaseValue:
newState.value -= 1
case .setLoading(let isLoading):
newState.isLoading = isLoading
}
return newState
}
}
ReactorKit 함수 요약
- mutate() : Action을 받고 Observable을 생성합니다.
- reduce() : 이전 state와 mutation을 통해 새로운 state를 생성합니다.
- transform() : 각 stream을 변형시킵니다.
과정을 좀 더 자세히 도식화하면 위와 같습니다.
뷰에서 액션을 보내면 muate()로 해당 액션에 맞는 비즈니스 로직을 취합니다.
그 후 reduce를 통해서 변경된 상태(state)를 사용하여 View를 업데이트합니다.
이렇게 ReactorKit에 대해서 알아봤는데... 잘못된 부분이 있다면 무자비하게 혼내주세요...
'iOS > 기술?' 카테고리의 다른 글
swift - 네이버맵 연동하기 (지도 띄우기) (0) | 2023.05.02 |
---|---|
swift - SnapKit & Then (0) | 2022.08.10 |
swift - 카카오맵 API 연동하기 (3) | 2021.11.17 |
swift - CoreLocation을 사용해서 현재 위치(위도, 경도) 가져오기 (0) | 2021.11.15 |
swift - 컬렉션뷰 버튼 토글하기! (0) | 2021.11.11 |