[iOS] ReactorKit
안녕하세요, 띵지니어입니다. 😼
이번 글에서는 많은 회사에서 사용하고 있는 ReactorKit(리액터킷)에 대해 알아보려고 합니다.
실제로 전수열 님이 근무하셨던 스타일쉐어 뿐만 아니라 여러 곳에서도 사용되고 있습니다.
ReactorKit은 반응형 단방향 Swift 애플리케이션 아키텍처를 위한 프레임워크 라고 합니다!
장점
1. 테스트에 용이 합니다. (View와 Reactor 완전 분리해서 유닛테스트가 가능 해요)
2. 유지보수가 쉬움 (데이터가 단방향 흐름이기 때문에, 상태 값 관리를 편하게 할 수 있어요)
3. 코드의 일관성 (View와 Reactor의 프로토콜을 준수 하기 때문에 읽기 쉬운 코드가 됩니다.)
BasicConcept
ReactorKit(리액터킷)의 개념은 Flux와 RxProgramming의 조합이라고 해요!
User Action과 View State는 observable stream을 통해 각 계층에 전달(바인딩)됩니다.
이를 공부하기 위해서는 RxSwift와 Swift코드를 작성할 수 있는 능력이 필요해요.
아래는 ReactorKit의 데이터 흐름 이에요.
단방향으로 이루어져 있어요!
그림만 봐서는 이해가 당연히 안되겠죠!
요새 떠오르는 흑백요리사로 비유를 해보겠습니다.
혹시 밤 티라미수 드셔보셨나요?
어제 밤 티라미수를 먹었는데 맛있어서, 패자 부활전을 예시로 들겠습니다!
저는 이 미션의 나폴리 맛피아 라고 생각을 하겠습니다.
Action
먼저 백종원, 안성재(또는 손님)가 "요리를 해라"라는 미션을 제공했습니다.
여기서 "요리를 해라"라는 미션이 Action 에 해당합니다.
Reactor
Reactor(요리사-나폴리 맛피아)는 이 Action(요리를 해라)을 받아서 요리를 진행합니다.
Reactor 내부에는 mutate(과정)과 reduce(결과)의 두 가지 주요 단계가 포함되어 있습니다.
mutate (과정)
mutate() 는 요리의 각 단계를 처리하는 과정입니다. 요리의 진행 단계는 다음과 같습니다:
1. 밤 퓨레를 만든다
2. 밤 크림을 만든다
3. 베이스를 만든다
4. 토핑을 추가한다
5. 그래놀라를 추가한다
reduce (결과)
`reduce()`는 Mutation을 기반으로 최종 상태를 업데이트하는 역할을 합니다.
State
요리 과정을 통해 요리 미완성(State)에서 요리 완성(newState)으로 State가 바뀌었습니다.
View
마지막으로, 완성된 State(요리 완성)는 이제 심사위원의 접시(View)로 제공(Update)됩니다.
아래는 해당 과정을 손으로 그려볼게요!
쉽게 말하면, 유저가 터치해서 발생하는 이벤트가 Action이고, 그 이벤트로 인해 변경되는 것이 State라고 생각하면 돼요!
View와 Reactor에 대해서는 좀 더 아래에서 설명할게요.
하나 더 예를 들면, + 버튼을 클릭하면 숫자가 1 증가하는 Counter가 있다고 예를 들어 보겠습니다.
이 경우 + 버튼 터치가 Action이 되고, 숫자가 1 증가하는 결과가 State가 되는 거예요!
이제 Action과 State가 이해 됐을까요?
ReactorKit에는 뷰(View)와 리액터(Reactor)라는 개념도 존재합니다.
View
이제 View를 설명해보겠습니다.
A View displays data.
정의에서 View는 data를 표시한다고 되어있습니다.
View는 데이터를 보여주는 역할을 하며, ViewController와 Cell 도 View로 간주됩니다.
View는 사용자 입력을 Action stream 에 바인딩하고, State stream을 각각의 UI 컴포넌트에 바인딩합니다.
비즈니스 로직은 View 레이어에 포함되지 않고, 단지 액션 및 상태 스트림을 매핑하는 방식만 정의합니다.
View는 상태를 표현합니다. <손님처럼 결과물(완성된 상태)을 화면에 보여주는 역할>
View를 정의하기 위해서는, 기존 클래스가 View 프로토콜(Protocol)을 따르도록 해야 해요!
그러면 해당 클래스는 자동으로 reactor 라는 속성을 가지게 되며, 이 속성은 주로 View 외부에서 주입됩니다.
View 프로토콜은 DisposeBag 속성과 bind(reactor:) 를 필수로 정의해야 합니다.
class CounterViewController: UIViewController, View {
var disposeBag = DisposeBag()
}
counterViewController.reactor = CounterViewReactor() // 외부에서 reactor 주입
reactor 의 프로퍼티가 변경될 때 bind(reactor: ) 함수가 호출됩니다.
아래에 bind 함수에서 Action stream과 State stream을 정의해주면 됩니다.
func bind(reactor: CounterViewReactor) {
// Action (View -> Reactor)
increaseButton.rx.tap // tapEvent감지
.map { CounterViewReactor.Action.increase } // tap 이벤트가 발생했을 때, 이를 Reactor의 Action으로 변환합니다.
.bind(to: reactor.action) // Action을 reactor.action에 바인딩(bind) -> mutate(action:) 함수 호출
.disposed(by: disposeBag)
decreaseButton.rx.tap
.map { CounterViewReactor.Action.decrease }
.bind(to: reactor.action)
.disposed(by: disposeBag)
// State (Reactor -> View)
reactor.state // Reactor의 상태가 변경될 때마다 해당 스트림 전달
.map { String($0.value) } // String으로 변환
.distinctUntilChanged() // 다른 요소 오면 반환
.bind(to: countLabel.rx.text) // 바인딩
.disposed(by: disposeBag)
reactor.state
.map { $0.isLoading }
.distinctUntilChanged()
.bind(to: activityIndicatorView.rx.isAnimating)
.disposed(by: disposeBag)
}
이렇게 bind 메서드를 통해
View(손님)에서 -> Reactor(요리사) 로 이동하는 Action과
Reactor(요리사) -> View(손님)으로 업데이트를 해주는 State를
단방향으로 바인딩을 해줄 수 있습니다.
Reactor
A Reactor is an UI-independent layer which manages the state of a view.
리액터는 뷰의 상태를 관리하는 UI와 독립적인 레이어 라고 합니다.
Reactor는 Reactor 프로토콜을 따라야 합니다.
Reactor 프로토콜을 따르려면, 세 가지 타입(Action, Mutation, State)을 정의해야 하며,
initialState라는 초기 속성을 필수로 포함해야 합니다.
리액터는 뷰의 상태를 관리합니다. <요리사처럼 주문을 받아 요리를 준비하는 역할을 합니다>
주요 속성은 아래처럼 정의합니다.
Action
사용자가 버튼 클릭과 같이 어떤 행동을 했을 때, 어떤 형식으로 reactor에 전달할지 정의합니다.
- 사용자 인터랙션을 표현합니다.
ex) 요리해라, increase, decrease
Mutation
Action을 통해 들어온 행동으로 나타내는 결과의 행동을 정의합니다.
- 상태를 변경하는 가장 작은 단위입니다.
ex) 요리사가 요리를 만들기 위해 진행하는 작은 단계들, increaseValue, decreaseValue, setLoading
State
현재 상태를 정의하는 타입(값)을 정의해줍니다.
- 뷰의 상태를 표현합니다.
ex) 요리의 상태, value, isLoading
예시:
- 요리 전: 재료가 아직 준비되지 않은 상태.
- 요리 중: 물이 끓고 면이 삶아지는 중.
- 요리 완성: 파스타가 완성되어 손님에게 제공할 준비가 된 상태.
import RxSwift
import ReactorKit
class CounterViewReactor: Reactor {
let initialState = State()
// 사용자 인터랙션
enum Action {
case increase
case decrease
}
// 상태를 변경하는 단위
enum Mutation {
case increaseValue
case decreaseValue
case setLoading(Bool)
}
// 현재 상태를 기록
struct State {
var value = 0
var isLoading = false
}
}
Action이나 State와 달리 Mutation은 리액터 클래스 밖으로 노출되지 않습니다.
대신, 클래스 내부에서 Action과 State를 연결하는 역할을 수행합니다. Action이 리액터에 전달되면 두 단계를 거쳐서 뷰의 상태를 변경합니다.
두 단계는 mutate와 reduce입니다.
func mutate(action: Action) -> Observable<Mutation>
Action(사용자 입력)을 받아서 Mutation Observable(변동)을 리턴 시키는 함수입니다.
그 결과로 Mutation을 방출하면 그 값이 reduce() 함수로 전달됩니다.
// Action이 들어온 경우, 어떤 처리를 할건지 분기
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.increaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
case .decrease:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.decreaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
}
}
mutate() 함수에서는 Action 스트림을 Mutation 스트림으로 변환하는 역할을 합니다.
이곳에서 네트워킹이나 비동기로직 등의 사이드 이펙트를 처리합니다.
그 결과로 Mutation을 방출하면 그 값이 reduce() 함수로 전달됩니다.
func reduce(state: State, mutation: Mutation) -> State
State(이전 상태)와 Mutation(변동)을 받아서 다음 상태로 리턴해주는 함수입니다.
// 이전 상태와 처리 단위를 받아서 다음 상태를 반환하는 함수
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
}
reduce() 함수는 Mutation을 통해 상태(State)를 변경하는 부분으로,
리액터(Reactor) 내부에서만 호출됩니다. == (Reactor 클래스 밖으로 노출되지 않습니다.)
1. Mutation을 받아서 현재 상태(state)를 업데이트하고,
2. 그 결과로 새로운 상태(newState) 를 리턴합니다.
이 과정에서 Mutation은 외부에서 접근할 수 없고, 오직 Reactor 내부에서만 사용됩니다.
외부에서는 이 상태 변화 과정을 볼 수 없으며, 오직 Action을 통해 상태가 변경되고, State가 업데이트되는 결과만 접근할 수 있습니다.
마무리
전체 코드의 흐름은 아래와 같습니다.
전체 코드는 아래 Repo를 Clone하시면 편하게 볼 수 있습니다.
https://github.com/thingineeer/ReactorKitPractice
Xcode 16.0
iOS 18.1
MacOS Sequoir 15.0.1
환경에서 작성 한 글입니다.
감사합니다. 🤗
참고 자료
전수열님 블로그
https://medium.com/styleshare/reactorkit-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-c7b52fbb131a
공식문서
https://github.com/ReactorKit/ReactorKit
기타 블로그
https://oliveyoung.tech/blog/2023-05-20/OliveYoung-iOS-ReactorKit/
https://velog.io/@sso0022/iOS-ReactorKit-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0
https://www.slideshare.net/slideshow/hello-reactorkit/103433766#4