
이전 포스팅인 " RIBs + ReactorKit을 활용한 RIBs 노드 中 Builder ", " RIBs + ReactorKit을 활용한 RIBs 노드 中 Router "를 먼저 살펴보고 와야 이해가 됩니다.
3. Interactor
import RIBs
import RxSwift
import ReactorKit
protocol SampleRouting: ViewableRouting {
}
protocol SamplePresentable: BasePresentable {
var listener: SamplePresentableListener? { get set }
}
protocol SampleListener: AnyObject {
func requestToInteractor()
}
final class SampleInteractor: PresentableInteractor<SamplePresentable>, SampleInteractable {
weak var router: SampleRouting?
weak var listener: SampleListener?
private let apiService: ApiServiceType
private let setting: Setting
// Reactor
typealias Action = SampleViewAction
typealias State = SampleViewState
let initialState: State
enum Mutation {
case setBusinessLogicResult(Bool)
}
init(
presenter: SamplePresentable,
apiService: ApiServiceType,
setting: Setting
) {
defer { _ = self.state }
self.apiService = apiService
self.setting = setting
self.initialState = State()
super.init(presenter: presenter)
presenter.listener = self
}
}
extension SampleInteractor: SamplePresentableListener {
// SampleInteractor에는 SampleViewController의 SamplePresentableListener protocol 에 대한 구현부가 있어야 함.
func requestToInteractor() {
listener?.requestToInteractor()
/* 여기서 listener는 SampleListener 를 듣고 있는 상위 Interactor ex) MainInteractor
MainInteractor에는 SampleInteractor protocol인 SampleListener에 대한 구현부가 있을것.
*/
}
}
extension SampleInteractor: Reactor {
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .requestLogin:
let reset = Observable.just(Mutation.setBusinessLogicResult(false))
let update = apiService.requestLogin()
.asObservable()
.trackActivity(presenter.activity)
.map { _ in Mutation.setBusinessLogicResult(true) }
.catchError { Observable.just(Mutation.setBusinessLogicResult(false)) }
return Observable.concat(update, reset)
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let .setBusinessLogicResult(result):
state.result = result
}
return state
}
}
Interactor의 샘플코드를 정의하였다.
- Interactor : 비즈니스 로직을 수행하여 Router로 Routing call 그리고 RIBs의 attach와 detach를 요청하거나, Presenter로 Data model를 전달합니다.
이번 포스팅에서는 Interactor와 Reactor의 개념을 같이 설명할겁니다.
RIBs 의 Interactor는 이전 포스팅들을 통해서 구체적이진 않지만 얼추 개념에 대해선 이해를 하셨을텐데
ReactorKit의 개념은 이번 포스팅에서 처음으로 등장하니 먼저 ReactorKit이 뭔지부터 설명하고자 합니다.
우선 ReactorKit은 반드시 RIBs 와 같이 쓰일 필요는 없습니다.
ReactorKit에서 reactor는 ViewController에 하나씩 매핑되어서, View의 Action 과 State 그리고 Action을 통해 발생되어지는 Mutation 을 정의하여 ViewController에서 비즈니스 로직을 분리하여 Massive ViewController 가 되는 것을 방지해주는 개념으로 이해 하면 됩니다.
즉,
1. ViewController에서 사용자의 이벤트를 받아 처리하는 Action을 정의.
2. Action을 통해서 발생하는 변이(변화, Mutation)을 정의.
3. View 의 State를 정의. (Mutation은 initial State → Mutated State을 일으킴)
4. State가 변화하면 ViewController가 View(UI)를 다시 그리게끔 공지.
저는 ReactorKit을 이렇게 이해하고 있습니다. 제가 회사에서 개발을 진행하면서 제가 나름대로 이해한 내용을 토대로 적다보니 당연히 잘못된 부분이 많이 있을 수 있으며, 이는 피드백을 주시면 감사하겠습니다.
ReactorKit만 사용하여도 ViewController 로부터 비즈니스 로직을 분리할 수 있지만, RIBs에서는 비즈니스 로직이 Interactor에 정의됩니다. 따라서 RIBs와 ReactorKit을 같이 사용하게 되면 reactor의 역할이 Action과 State를 정의하는 정도의 역할로 줄어들게 됩니다.
Mutation은 Interactor에 정의.
그럼 SampleInteractor의 코드를 분석해봅시다.
A. ViewableRouting protocol 정의
" RIBs + ReactorKit을 활용한 RIBs 노드 中 Router " 포스팅에서 Router는 Interactor의 라우팅 콜을 수행하기 위해 Interactor로부터 ViewableRouting프로토콜을 Get해오고, Get해온 ViewableRouting 프로토콜의 위임자가 된다.
라고 설명드렸습니다.
이 프로토콜을 정의하는 것이고, 실제로 Router에서 라우팅을 요청해야 할 비즈니스 로직이 생기면, Interactor는 예제의 경우 SampleRouting이라는 ViewableRouting프로토콜 안에 interface를 만들어 놓습니다.
interface에 대한 구현(implement)은 SampleRouter에 반드시 있어야 한다. (Router는 ViewableRouting 프로토콜을 준수하므로)
B. Presentable protocol 정의
Interactor 는 비즈니스 로직을 수행하는 역할을 한다고 했습니다.
비즈니스 로직을 수행한 결과로 Router에게 라우팅 콜을 요청할 수 있다는 설명 역시 드렸습니다.
하지만 모든 비즈니스 로직의 결과로 라우팅 콜만 일어나는건 아니겠죠?
또한 비즈니스 로직이 수행되기 위해서는 어떤게 전제되어야 할까요? 화면이 실행되자마자 자동으로 수행되어야 하는 코드를 제외하면 일반적으로 비즈니스 로직은 사용자의 이벤트에 의해 발생합니다.
그렇다면 사용자의 이벤트를 받아서, Interactor에게 비즈니스 로직을 수행해달라고 요청하는 역할은 누가 하게 될까요?
이때 등장하는 개념이 바로 Presenter와 Presentable protocol 입니다.
- View : UI화면을 생성하고 구성하여, UI Event를 Presenter로 전달하거나 View model을 받아서 UI를 업데이트합니다.
- Presenter : Interactor와 View간의 통신을 담당하여 Business model을 View model로 변환하는 역할로 상태를 가지고 있지 않은 클래스입니다. Presenter를 생략하는 경우 View model 변환의 책임은 View 또는 Interactor가 되어야합니다.
import Foundation
/// Base class of an `Interactor` that actually has an associated `Presenter` and `View`.
open class PresentableInteractor<PresenterType>: Interactor {
/// The `Presenter` associated with this `Interactor`.
public let presenter: PresenterType
/// Initializer.
///
/// - note: This holds a strong reference to the given `Presenter`.
///
/// - parameter presenter: The presenter associated with this `Interactor`.
public init(presenter: PresenterType) {
self.presenter = presenter
}
// MARK: - Private
deinit {
LeakDetector.instance.expectDeallocate(object: presenter as AnyObject)
}
}
SampleInteractor는 PresentableInteractor를 상속받고, PresentableInteractor는 Generic 으로 PresenterType을 받습니다.
사용자의 이벤트를 받은 View가 Interactor에게 비즈니스 로직을 수행해달라고 요청하기 위해서는 Interactor는 View의 이벤트를 Listen하고 있어야 합니다. 이 리스너가 바로 SamplePresentable 프로토콜에 있는 SamplePresentableListener입니다.
즉, SampleInteractor는 SampleViewController로 부터 이벤트를 받아 비즈니스 로직을 수행하기 위해 SamplePresentable 프로토콜을 통해 SamplePresentableListener을 Get 해오고, Get해온 SamplePresentable 프로토콜의 위임자가 된다는 것입니다!
샘플 코드에
presenter.listener = self
" RIBs + ReactorKit을 활용한 RIBs 노드 中 Router " 포스팅에도 똑같이 설명해드린 부분이 있습니다!!
해당 부분을 먼저 이해하시면, 이 부분도 이해가 훨씬 수월합니다.
그렇다면 SamplePresentableListener을 Get 해오기 위해선 SamplePresentable 프로토콜은 누가 준수해야 할까요?
→ SampleViewController 가 준수해야 합니다~~!! Router가 Interactor의 라우팅 콜 요청을 수행하기 위한 패턴이 또~옥같이 Interactor가 ViewController의 비즈니스 로직을 수행하기 위한 패턴에도 적용이 되어있습니다.
C. Listener protocol 정의
예제 코드에 SamplePresentableListener 와 SampleListener 두개의 리스너가 등장합니다.
여기서 머릿속이 아주 복잡해집니다.. 뭐가 다른거야,,
" B. Presentable protocol " 정의 에서 SamplePresentableListener에 대한 내용은 이해가 되셨길 바라며,
이제는 더 과거로 거슬러 올라가 " RIBs + ReactorKit을 활용한 RIBs 노드 中 Builder " 포스팅에
" C. Buildable Protocol 을 정의 " 내용을 보다보면 라우팅을 수행하는 두 가지 방식에 대한 소개를 드렸습니다.
ㄱ. 나를 트리에 attach 시킨 부모 RIBs 노드에 맡기는 방식 (부모 RIBs 노드에게 delegate)
ㄴ. 자기 자신이 자식 RIBs 노드를 attach 시키기 위해 router를 이용하는 방식
여기서 (ㄱ) 방식을 사용하기 위해선 부모 RIBs 노드가 SampleInteractor에 정의한 SampleListener protocol을 준수하여야 합니다.
즉, SampleRouter, SampleInteractor, SampleBuilder로 이루어진 RIBs 노드를 attach한 부모 RIBs노드가 SampleListener 프로토콜을 준수하여 자식 RIBs 노드의 라우팅 요청을 듣고 있을것이라는 뜻입니다.
부디 SamplePresentableListener 와 SampleListener의 차이를 이해하셨길 바랍니다.
D. Interactor 클래스 구현
weak var router: SampleRouting?
weak var listener: SampleListener?
약한참조로 선언된 router와 listener 변수가 보이시나요???
여태까지의 설명을 이해하셨다면 왜 약한참조로 선언되어 있는지 이해하실겁니다.
ㄱ. 나를 트리에 attach 시킨 부모 RIBs 노드에 맡기는 방식 (부모 RIBs 노드에게 delegate)
ㄴ. 자기 자신이 자식 RIBs 노드를 attach 시키기 위해 router를 이용하는 방식
여기서 (ㄱ) 방식을 사용하기 위해선 부모 RIBs 노드가 SampleInteractor에 정의한 SampleListener protocol을 준수하여야 합니다.
router 변수는 (ㄴ) 방식, listener 변수는 (ㄱ) 방식의 라우팅 요청을 위해 선언된 변수입니다.
둘 다 라우팅 요청을 누군가에게 위임했습니다.
강한 참조를 해놨다면, 강한 순환 참조로 인한 메모리 릭이 발생할 가능성이 있겠죠?
이제 드디어 Reactor와 관련한 코드들이 등장합니다!!
Reactor와 관련한 Action, State, Mutation과 샘플 코드 하단의 Reactor와 관련한 extension에 대한 설명은 잠시 뒤로 미루고
SamplePresentableListener 프로토콜과 관련한 extension을 보면 이곳에서 ViewController가 요청한 비즈니스 로직이 구현되는 곳입니다.
extension SampleInteractor: SamplePresentableListener {
// SampleInteractor에는 SampleViewController의 SamplePresentableListener protocol 에 대한 구현부가 있어야 함.
func requestToInteractor {
listener?.requestToInteractor()
/* 여기서 listener는 SampleListener 를 듣고 있는 상위 Interactor ex) MainInteractor
MainInteractor에는 SampleInteractor protocol인 SampleListener에 대한 구현부가 있을것.
*/
}
}
4. Reactor
import Foundation
enum SampleViewAction {
case requestBusinessLogic
}
struct SampleViewState {
var result: Bool = false
// 초기 상태값은 false
}
포스팅 상단의 ReactorKit에 대한 소개를 다시 살펴보자면
ReactorKit에서 reactor는 ViewController에 하나씩 매핑되어서, View의 Action 과 State 그리고 Action을 통해 발생되어지는 Mutation 을 정의하여 ViewController에서 비즈니스 로직을 분리하여 Massive ViewController 가 되는 것을 방지해주는 개념으로 이해 하면 됩니다.
즉,
1. ViewController에서 사용자의 이벤트를 받아 처리하는 Action을 정의.
2. Action을 통해서 발생하는 변이(변화, Mutation)을 정의.
3. View 의 State를 정의. (Mutation은 initial State → Mutated State을 일으킴)
4. State가 변화하면 ViewController가 View(UI)를 다시 그리게끔 공지.
ReactorKit만 사용하여도 ViewController 로부터 비즈니스 로직을 분리할 수 있지만, RIBs에서는 비즈니스 로직이 Interactor에 정의됩니다. 따라서 RIBs와 ReactorKit을 같이 사용하게 되면 reactor의 역할이 Action과 State를 정의하는 정도의 역할로 줄어들게 됩니다.
Mutation은 Interactor에 정의.
Reactor를 이렇게 소개하고 설명드렸습니다. 설명대로 SampleReactor의 코드는 Action과 State를 정의하는 정도의 역할만 하고 있습니다. 이말을 확장하면 SampleInteractor의 Reactor와 관련한 extension은 사실 RIBs와 ReactorKit이 같이 쓰이지 않았다면 SampleReactor안에 구현됨으로써 ReactorKit만 사용하여도 ViewController 로부터 비즈니스 로직을 분리할 수 있게끔 하는 것입니다.
하지만 예제의 경우 RIBs와 ReactorKit이 같이 쓰였기 때문에 비즈니스 로직을 Interactor에 구현하였고, 이렇게 변형하기 위하여 다음의 코드들이 SampleInteractor에 등장합니다.
// Reactor
typealias Action = SampleViewAction
typealias State = SampleViewState
let initialState: State
enum Mutation {
case setBusinessLogicResult(Bool)
}
이제 Reactor의 동장 방식에 관한 코드입니다.
extension SampleInteractor: Reactor {
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .requestBusinessLogic:
let reset = Observable.just(Mutation.setBusinessLogicResult(false))
let update = apiService.requestLogin()
.asObservable()
.trackActivity(presenter.activity)
.map { _ in Mutation.setBusinessLogicResult(true) }
.catchError { Observable.just(Mutation.setBusinessLogicResult(false)) }
return Observable.concat(update, reset)
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let .setBusinessLogicResult(result):
state.result = result
}
return state
}
}
- mutate 함수는 열거형(enum)으로 선언된 Action을 인자로 받아 switch 문을 통해 관찰가능한 Observable<Mutation>을 반환한다.
- reduce 함수는 State와 Mutation을 인자로 받아 Mutation으로 인해 바뀐 State를 반환한다.
mutate 함수와 reduce 함수는 Reactor 프로토콜에 정의된 함수이며,
구현(implement)는 SampleInteractor의 extension에 존재하는 것이다.
public protocol Reactor: class {
/// An action represents user actions.
associatedtype Action
/// A mutation represents state changes.
associatedtype Mutation = Action
/// A State represents the current state of a view.
associatedtype State
typealias Scheduler = ImmediateSchedulerType
/// The action from the view. Bind user inputs to this subject.
var action: ActionSubject<Action> { get }
/// The initial state.
var initialState: State { get }
/// The current state. This value is changed just after the state stream emits a new state.
var currentState: State { get }
/// The state stream. Use this observable to observe the state changes.
var state: Observable<State> { get }
/// A scheduler for reducing and observing the state stream. Defaults to `CurrentThreadScheduler`.
var scheduler: Scheduler { get }
/// Transforms the action. Use this function to combine with other observables. This method is
/// called once before the state stream is created.
func transform(action: Observable<Action>) -> Observable<Action>
/// Commits mutation from the action. This is the best place to perform side-effects such as
/// async tasks.
func mutate(action: Action) -> Observable<Mutation>
/// Transforms the mutation stream. Implement this method to transform or combine with other
/// observables. This method is called once before the state stream is created.
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
/// Generates a new state with the previous state and the action. It should be purely functional
/// so it should not perform any side-effects here. This method is called every time when the
/// mutation is committed.
func reduce(state: State, mutation: Mutation) -> State
/// Transforms the state stream. Use this function to perform side-effects such as logging. This
/// method is called once after the state stream is created.
func transform(state: Observable<State>) -> Observable<State>
}
이로써 Interactor와 Reactor의 개념에 대한 정리를 마친다.
다음 포스팅은 드디어 마지막인 " RIBs + ReactorKit을 활용한 RIBs 노드 中 ViewController " 입니다.
해당 포스팅에선 reduce함수를 통해 변한 State를 ViewController은 어떻게 인지하고 View를 다시 그리게 되는지를
설명하도록 하겠습니다.
'iOS_RIBs' 카테고리의 다른 글
| RIBs + ReactorKit을 활용한 RIBs 노드 中 ViewController (0) | 2022.02.11 |
|---|---|
| RIBs + ReactorKit을 활용한 RIBs 노드 中 Router (0) | 2022.02.08 |
| RIBs + ReactorKit을 활용한 RIBs 노드 中 Builder (0) | 2022.01.12 |