목차
티스토리 뷰
안녕하세요 🐾
iOS의 MVVM 디자인 패턴은 공식적으로 사용되는 표준이 없어 저는 주로 Kickstarter의 MVVM 디자인 패턴인 Input, Output 프로토콜을 이용한 방법을 사용했었습니다. 때문에 개발자들마다 MVVM 디자인 패턴 구현 방법이 조금씩 달라지곤하는데요.
ReactorKit
은 MVVM에서 뷰모델의 역할을 대신해 비즈니스 로직을 처리하고 뷰와 상호작용 하는 일련의 작업 과정을 정형화 된 구조로 제공하는 프레임워크입니다.
ReactorKit의 기본 개념 및 사용 방법을 간략히 정리해보았습니다. 혹시나 틀린곳이 있다면 알려주세요!
※ ReactorKit의 사용은 RxSwift에 대한 이해를 바탕으로 합니다.
1. 기본 개념
ReactorKit은 Flux와 Reactive 프로그래밍의 조합입니다.
간략히 요약하자면, Flux는 앱의 데이터 흐름을 단방향으로 관리하는 패턴이고, Reactive 프로그래밍은 데이터 흐름에 초점을 맞추어 반응형 코드 패턴을 설계합니다.
ReactorKit의 기본 구조를 살펴보면 아래와 같습니다.
데이터 흐름을 단방향으로, View
는 Action(작업)
을 보내고 Reactor
는 State(상태)
를 보냅니다.
View
는 사용자 UI 관련 코드, Reactor
에는 비즈니스 로직 관련 코드가 들어갑니다.
즉, 로직과 뷰가 분리되어 Reactor
는 View
에 대한 의존성이 사라집니다.
의존성이 낮을수록 테스트 가능한 코드 환경이 되고, 의존성이 높은 부분을 개선하면 프로젝트의 코드 품질이 향상됩니다.
1.1 View
View
는 데이터 흐름에서 데이터를 표시하는 부분이고 ViewController와 Cell은 View로 처리됩니다.
View
는 사용자 입력을 Action
에 바인딩하고 전달 받을 State
를 UI 구성 요소들에 바인딩 합니다.
class ProfileViewController: UIViewController {
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.reactor = ProfileViewReactor()
}
extension ProfileViewController: View {
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)
}
}
위 코드블럭처럼 뷰컨트롤러에서 ReactroKit의 View
프토토콜을 상속한 뒤 reactor
의 속성을 변경하면
extension func에서 정의한 bind(reactor:Reactor)
메소드가 실행됩니다.
View
의 Definition을 보면 그 이유가 아래와 같이 쓰여있네요.
지금까지 보신 예와 같이,
View
계층에는 비즈니스 관련 로직이 없습니다.
오직 Action
스트림과 State
스트림을 매핑하는 방법만을 정의했습니다.
이제 Reactor
레이어에 대하여 알아볼까요?
1.2 Reactor
Reactor
는 View
로 보낼State
를 관리하는 계층입니다.
Reactor
는 View
에 대한 종속성이 없어 이곳에 들어갈 비즈니스 로직 관련 코드에 대한 테스트가 쉽습니다.
다만, 간단한 프로젝트가 아니라면 아래의 이미지처럼 Reactor
에서 데이터 처리에 대한 직접적인 코드를 일체 사용하지는 않습니다.
(ex. 인터넷 연결이 끊어진 경우 이를 알리는 Service
에서 필요한 데이터 처리 후 Reactor
에 알리고 View
에 전달)
Action
과 연관된 사이드 이펙트나 API 호출과 같은 비동기 관련 작업들은 Service
를 통해 후 처리를 한 뒤,
Reactor
에 전달하고 View
에 반영합니다.
이 과정에서 Reactor
는 View
레이어와 좀 더 가까운 영역이 되겠네요!
(Service
를 이용하는 세부 사용방법은 다른 포스팅에서 소개하겠습니다.)
다시 소개로 돌아가, 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
는 뷰에 표시할 상태를 나타냅니다.
Mutation
은 Action
과 State
사이에 들어가는 브릿지 역할로 mutate()
- reduce()
메소드의 두 단계를 거쳐 Action
스트림을 State
스트림으로 변환합니다.
💡 mutate()
Action
을 전달받아 Observable<Mutation>
을 생성합니다.
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .refreshFollowingStatus(userID): // receive an action
return UserAPI.isFollowing(userID) // create an 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)
}
}
}
💡 recude()
기존의 State
와 Mutation
을 참조하여 newState를 View
에 반환합니다.
func reduce(state: State, mutation: Mutation) -> State {
var state = state // create a copy of the old state
switch mutation {
case let .setFollowing(isFollowing):
state.isFollowing = isFollowing // manipulate the state, creating a new state
return state // return the new state
}
}
💡 transform()
관찰 가능한 다른 스트림과 Mutation
을 결합하는 것을 말합니다. 자세한 내용은 링크 참고
func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>
2. 설치 및 예제
Cocoapod
target 'AssumReactorKitTest' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for AssumReactorKitTest
pod 'ReactorKit'
pod 'RxSwift'
pod 'RxCocoa'
pod 'SnapKit'
end
프로젝트 디렉토리
- podfile 수정
- pod install
예제
ViewController.swift
import UIKit
import ReactorKit
import RxSwift
import RxCocoa
import SnapKit
class ViewController: UIViewController {
var disposeBag = DisposeBag()
// MARK: - UI
let increaseButton: UIButton = {
let button = UIButton()
button.setTitleColor(.blue, for: .normal)
button.setTitle("+", for: .normal)
return button
}()
let numberLabel: UILabel = {
let label = UILabel()
label.textColor = .blue
label.textAlignment = .center
label.text = "0"
return label
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.reactor = ViewReactor()
setupUI()
}
// MARK: - UI Function
func setupUI() {
view.addSubview(increaseButton)
increaseButton.snp.makeConstraints {
$0.center.equalToSuperview()
$0.width.height.equalTo(20)
}
view.addSubview(numberLabel)
numberLabel.snp.makeConstraints {
$0.top.equalTo(increaseButton.snp.bottom).offset(12)
$0.left.right.equalToSuperview().inset(40)
$0.height.equalTo(20)
}
}
}
// MARK: - Reactor
extension ViewController: View {
func bind(reactor: ViewReactor) {
bindAction(reactor)
bindState(reactor)
}
private func bindAction(_ reactor: ViewReactor) {
increaseButton.rx.tap
.map { Reactor.Action.increase }
.bind(to: reactor.action)
.disposed(by: disposeBag)
}
private func bindState(_ reactor: ViewReactor) {
reactor.state
.map { String($0.value) }
.distinctUntilChanged()
.bind(to: numberLabel.rx.text)
.disposed(by: disposeBag)
}
}
클래스 내 코드를 보시면, 비즈니스로직 없이 Reactor에 보낼 Action과 전달 받을 State 스트림에 대해서만 매핑만 해놓은 것을 확인하실 수 있습니다.
ViewReactor.swift
import Foundation
import RxSwift
import RxCocoa
import ReactorKit
class ViewReactor: Reactor {
let initialState = State()
// 사용자 상호작용 구분
enum Action {
case increase
}
// 사용자 상호작용에 대한 처리 구분, mutate()와 reduce()의 두단계를 거쳐 Action 스트림을 State 스트림으로 변환
enum Mutation {
case increaseNumber
}
// 현재 상태, 뷰에 표시할 상태
struct State {
var value = 0
}
// Action을 전달받아 Observable<Mutation>을 생성
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case.increase:
return .just(.increaseNumber)
}
}
// 기존의 State와 Mutaion을 참조하여 newState를 View에 반환
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .increaseNumber:
newState.value += 1
}
return newState
}
}
Reactor에서는 mutate()
→ reduce()
를 거쳐, ViewController에서 들어온 Action 스트림을 State 스트림으로 반환하여
결과를 표시합니다.
3. 참고
- https://github.com/ReactorKit/ReactorKit
- https://en.wikipedia.org/wiki/Reactive_programming
- https://haruair.github.io/flux/
'iOS > Swift' 카테고리의 다른 글
[iOS] 구글 로그인 SDK 연동하기 (1) | 2023.12.26 |
---|---|
[iOS] 페이스북 로그인 SDK 적용 (0) | 2023.12.22 |
[iOS] 네이버 로그인 SDK 연동하기 (0) | 2023.06.21 |
[iOS] 카카오톡 로그인 SDK 연동하기 (0) | 2023.06.21 |
[iOS] Code Snippet(코드 스니펫), 코드 즐겨찾기 (0) | 2023.06.14 |
- Total
- Today
- Yesterday
- ios google
- Firebase Distribution
- swift google signin
- swift nimble
- swift 구글 sdk
- Quick
- ios mvvm
- iOS 유닛테스트
- ios 구글 로그인 sdk
- swift reactorkit
- swift xctest
- swift quick
- XCFramework
- iOS 테스트 코드
- Framework
- iOS Framework
- iOS Unit Tes
- swift google sdk
- iOS Quick
- XCTest
- ios reactorkit
- ios google signin
- nimble
- iOS 단위테스트
- swift google login
- iOS Nimble
- swift framework
- ios xcframework
- swift 구글 로그인
- swift google login sdk
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |