일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- autoLayout
- 뷰 커스텀
- Swift
- imageView shadow
- Custom View
- 코코아팟 만들기
- Cocoapods
- DispatchQueue
- 델레게이트
- priority
- 비동기
- 리액터킷
- 코코아팟
- hugging
- CornerRadius
- compression resistance
- viewAppear
- Two Sum
- email regex
- UINavigationController
- view modifier
- Remote Url
- LeetCode 1
- Delegate Pattern
- onAppear
- 라이브러리
- Swift Package Manager
- 커스텀 뷰
- ios
- ReactorKit
- Today
- Total
Tong's Blog
[Swift] ReactorKit Framework 본문
안녕하세요.
오늘은 ReactorKit Framework에 대해 알아보겠습니다.
우선 ReactorKit이 어떤 프레임워크인지 알아보겠습니다.
https://github.com/ReactorKit/ReactorKit
ReactorKit is a framework for a reactive and unidirectional Swift application architecture.
ReactorKit은 반응형이고 단방향적인 Swift 아키텍처 구현을 위해 만들어진 프레임워크라고 나와있습니다.
그러면 우리가 ReactorKit을 써야하는 이유는 무엇일까요?
사실 최근에 공부를 시작했기 때문에 객관적인 설명보다는 주관적인 느낌이 우선시 될 수 있다는 것을 먼저 말씀드리고 설명드리겠습니다.
Swift, iOS 를 개발하기 위한 여러 아키텍처 중 많은 분들께서 사용하시는 것이 RxSwift + MVVM 입니다. (최근에는 Ribs 라던지 여러 아키텍처들도 많이 보이긴 합니다.) 해당 아키텍처는 난이도가 있는 편이지만 그만큼 많은 이점을 가져다 주기 때문에 많이 사용되고 있습니다. 하지만 MVVM 아키텍처는 ViewModel에 많은 역할을 부여하는 만큼 개발자마다 형태가 달라질수도 있고 그로 인해 협업하는 환경에서는 낭비되는 요소가 있다고 생각합니다.
그래서 ReactorKit을 통해 ViewModel(Reactor)가 구현되는 부분을 어느정도 통일시키고 서로의 가독성을 높이는데 도움을 주는 것이 이 프레임워크를 사용하는 이유가 아닐까 합니다.
그러면 이제 ReactorKit 이 어떤 모습인지 보겠습니다.
위 그림은 ReactorKit을 공부하다보면 빠지지 않고 보게되는 그림이고 ReactorKit의 모습을 가장 잘 설명한 그림입니다. 이 그림에서 볼 수 있는 ReactorKit의 특징은 단방향이라는 점입니다. View 에서 Action(Input)을 통해 Reactor에 이벤트를 전달하고 Reactor에서 Mutation을 거쳐 State(Output)로 View에 상태를 변경하는 형태로 동작하게 됩니다.
RxSwift + MVVM에서는 해당 모습을 위해 Input과 Output을 protocol로 구현해 viewModel형태를 잡기도 하지만 ReactorKit을 사용하면 해당 형태를 쉽게 구현할 수 있습니다.
ReactorKit 을 설치하는 법은 위에 공유드린 Github 링크를 통해 설치하시면 됩니다.
pod 'ReactorKit'
이제 workspace 파일을 열고 github에 있는 예제를 통해 ReactorKit 사용방법에 대해 알아보겠습니다.
예제는 +, - 을 통해 숫자를 카운트하는 간단한 counter를 구현하는 앱입니다.
ReactorKit에서는 MVVM 아키텍처를 구현하기 위해 View에 해당하는 부분에는 View protocol를 ViewModel에 해당하는 부분에는 Reactor protocol를 준수하도록 하면 됩니다.
View
import ReactorKit
final class SampleViewController: UIViewController, View{
typealias Reactor = SampleViewReactor
//MARK: Properties
var disposeBag: DisposeBag = DisposeBag()
//MARK: UI
private let increaseButton: UIButton = {
let aButton = UIButton()
aButton.setTitle("+", for: .normal)
return aButton
}()
private let decreaseButton: UIButton = {
let aButton = UIButton()
aButton.setTitle("-", for: .normal)
return aButton
}()
private let valueLabel: UILabel = {
let aLabel = UILabel()
aLabel.text = ""
aLabel.textColor = .white
return aLabel
}()
private let activityIndicatorView = UIActivityIndicatorView()
//MARK: View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: Configuration
func bind(reactor: Reactor) { ... }
}
View에서는 bind를 담당하는 func과 Reactor가 있어야 합니다.
bind는 Reactor의 Action과 State를 연결하는 부분이 들어가야 하며 연결하는 과정은 대부분 RxSwift로 작성합니다.
이 bind 함수는 View(여기서는 ViewController)에 Reactor를 연결(할당)하는 순간 실행됩니다.
Reactor(ViewModel)
import ReactorKit
final class SampleViewReactor: Reactor{
enum Action{ ... }
enum Mutation{ ... }
struct State{ ... }
let initialState: State
init(){ ... }
func mutate(action: Action) -> Observable<Mutation> { ... }
func reduce(state: State, mutation: Mutation) -> State { ... }
}
Reactor에서는 View로부터 이벤트를 받아오는 Action과 Action으로 부터 State 값을 변경 혹은 생성하기 위한 Mutation, View에 업데이트하기 위한 State가 구현되어야 하며 Action과 Mutation을 연결하는 mutate 함수, 이전 State와 Mutation을 통해 새로운 State를 만드는 reduce 함수가 있습니다.
일반적으로 네트워킹등 비동기적인 코드를 func mutate()에서 작성하게 됩니다.
func reduce()에서는 이전 State와 mutate()에서 받아온 mutation을 통해 새 state를 리턴하고 동기적인 코드들이 위치합니다.
이제 해당 요소들을 가지고 Counter 앱을 구현해보겠습니다.
우선 Reactor에서 View에서 받아올 Event는 버튼을 통한 increase 와 decrease가 있습니다.
enum Action{
case increase(Int)
case decrease(Int)
}
그리고 Mutation에서는 액션을 통한 값의 변화가 필요합니다.
enum Mutation{
case changeValue(Int)
}
State 에서는 View에 표현할 value가 필요합니다.
struct State{
var value: Int
}
이제 mutate()에서는 Action을 통한 Mutation(실제로는 Observable)을 반환해야 합니다.
increase라면 증가해야하고 decrease라면 감소해야 합니다.
func mutate(action: Action) -> Observable<Mutation> {
switch action{
case .increase(let value):
return
Observable.just(Mutation.changeValue(value))
case .decrease(let value):
return
Observable.just(Mutation.changeValue(-value))
}
}
reduce()에서는 이전 State와 Mutation을 통해 새로운 State를 만들어서 업데이트합니다.
우리는 계속 변경되는 value값을 그 때 그 때 + 혹은 - 해줘야 합니다.
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .changeValue(let value):
newState.value += value
case .setLoading(let isLoading):
newState.isLoading = isLoading
}
return newState
}
전체 코드는 다음과 같습니다.
//
// SampleViewReactor.swift
// ReactorKitPractice
//
// Created by Tong on 2021/05/12.
//
import Foundation
import RxSwift
import ReactorKit
final class SampleViewReactor: Reactor{
enum Action{
case increase(Int)
case decrease(Int)
}
enum Mutation{
case setLoading(Bool)
case changeValue(Int)
}
struct State{
var value: Int
var isLoading: Bool
}
let initialState: State
init(){
self.initialState = State(value: 0, isLoading: false)
}
func mutate(action: Action) -> Observable<Mutation> {
switch action{
case .increase(let value):
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.changeValue(value)).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false))
])
case .decrease(let value):
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.changeValue(-value)).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false))
])
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .changeValue(let value):
newState.value += value
case .setLoading(let isLoading):
newState.isLoading = isLoading
}
return newState
}
}
View에서는 bind() 함수에 Reactor에 Action과 State에 각 UI 요소들을 연결해주면 됩니다.
View는 bind() 함수만 알아보면 되니 바로 전체 코드를 보여드리겠습니다.
//
// SampleViewController.swift
// ReactorKitPractice
//
// Created by Tong on 2021/05/12.
//
import UIKit
import RxSwift
import RxCocoa
import SnapKit
import ReactorKit
final class SampleViewController: UIViewController, View{
typealias Reactor = SampleViewReactor
//MARK: Properties
var disposeBag: DisposeBag = DisposeBag()
//MARK: UI
private let increaseButton: UIButton = {
let aButton = UIButton()
aButton.setTitle("+", for: .normal)
return aButton
}()
private let decreaseButton: UIButton = {
let aButton = UIButton()
aButton.setTitle("-", for: .normal)
return aButton
}()
private let valueLabel: UILabel = {
let aLabel = UILabel()
aLabel.text = ""
aLabel.textColor = .white
return aLabel
}()
//MARK: View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
//MARK: Configuration
func setup(){
view.addSubview(increaseButton)
increaseButton.snp.makeConstraints{
$0.centerY.equalToSuperview()
$0.leading.equalToSuperview().offset(50)
$0.size.equalTo(50)
}
view.addSubview(decreaseButton)
decreaseButton.snp.makeConstraints{
$0.centerY.equalToSuperview()
$0.trailing.equalToSuperview().offset(-50)
$0.size.equalTo(50)
}
view.addSubview(valueLabel)
valueLabel.snp.makeConstraints{
$0.center.equalToSuperview()
}
view.addSubview(activityIndicatorView)
activityIndicatorView.snp.makeConstraints{
$0.center.equalToSuperview()
}
}
func bind(reactor: Reactor) {
//Action
increaseButton.rx.tap
.map{ Reactor.Action.increase(3) }
.bind(to: reactor.action)
.disposed(by: disposeBag)
decreaseButton.rx.tap
.map{ Reactor.Action.decrease(3) }
.bind(to: reactor.action)
.disposed(by: disposeBag)
//State
reactor.state
.map{ $0.value }
.distinctUntilChanged()
.map{ "\($0)" }
.bind(to: valueLabel.rx.text)
.disposed(by: disposeBag)
}
}
주석에서 Action과 State를 연결하는 부분을 구분했지만 함수로 구분하는게 좀더 나아보일거 같습니다.
각 버튼에 Action 이벤트를 연결하고 값을 표시할 Label에는 State를 연결해주면 나머진는 Reactor에서 알아서 해줍니다.
오늘은 이렇게 ReactorKit에 대해 알아봤습니다.
사실 RxSwift를 공부하면서 ReactorKit에 대한 호기심과 관심은 있었지만 RxSwift 공부하기도 벅차다고 생각했는데 개인적으로 오히려 RxSwift + MVVM을 만드는 것보다 ReactorKit을 공부하면서 RxSwift를 겸사겸사 공부하는 것도 좋은 방법이라고 생각이 들었습니다.
언제나 부족한 글 읽어주셔서 감사합니다.
'iOS > Swift' 카테고리의 다른 글
[Swift] 이메일(e-mail) 유효성 체크하는 정규식 만들기 (0) | 2022.10.14 |
---|---|
[iOS] UINavigationController 에 대해 (0) | 2021.08.31 |
[Swift] Delegate란? (0) | 2021.02.16 |
[Swift] Foundation 간단 정리 (0) | 2020.12.23 |
[Swift] UIImageView 기본 사용법과 CornerRadius 와 Shadow 넣기 (0) | 2020.12.15 |