[Design Pattern] ReactorKit이란?

2021. 10. 12. 01:34Design Pattern

반응형

그동안 개발을 하면서 MVVM + RxSwift의 Architecture을 사용해왔었는데요.

'또 새로운 것을 해봐야겠지~'하면서 ReactorKit이라는 프레임워크를 접하고 공부해보려고 합니다 ㅎㅎ

 

그래서 오늘은 ReactorKit을 간단하게 사용해보면서 느낀점과 어떤 Architecture인지를 포스팅해보겠습니다~

 

ReactorKit이란?

우선 ReactorKit은 제가 느끼기엔 어떤 Design Pattern이라 말하기는 조금 애매한 것 같구요. 

RxSwift + MVVM을 프레임워크화해서 앱의 Architecture를 통일화해준 프레임워크 인 것 같습니다(?)

 

이건 제가 생각한 느낌이구요 정확히는 단방향 앱을 위한 프레임워크라고 정의하고 있습니다.

여기서 단방향이라는 키워드가 ReactorKit의 핵심인데요. 

 

감이 잘 안올수도 있지만 RxSwift + MVVM으로 프로젝트를 진행해보셨다면 조금 이해가 수월할 것 같습니다 ㅎ

 

우선 모식도를 보면 다음과 같습니다.

오... 뭔가 화살표가 한 방향으로 흐르고 있죠..?!

 

ReactorKit에서는 기본적으로 View, Reactor라는 큰 레이어가 존재합니다. View의 레이어는 다른패턴과 동일하게 UIViewController들 또는 UIView에 해당하는 레이어입니다. 여기서 View는 사용자의 인터랙션을 Reactor로 전달하는 역할을 합니다. 여기서 Reactor로 전달하는 단위가 Action으로 이해하시면 됩니다. 그리고 ReactorView에서 들어온 Action에 따라 비즈니스 로직을 수행하고 그 상태를 State라는 단위로 View에 전달합니다.

 

어떤가요 단뱡향의 흐름이 보이나요?

View -> Action -> Reactor -> State의 순환구조로 전달하고 전달받는 흐름이 됩니다.

크게 MVVM과 비슷한데요. (거의 동일한 것 같습니다)
Reactor = ViewModel, View = View와 동일하다고 생각하면 될 것 같습니다. 

 

그렇다면, 이제 간단한 예제를 통해 ReactorKit으로 간단히 구현해보면서 알아보겠습니다.

예제는 이전에 VIPER, MVVM + RxSwift에 대해 포스팅 했을 때의 예제와 동일합니다.

 

[Design Pattern] RxSwift + MVVM Pattern - iOS Architecture

어제는 VIPER Pattern에 대해 글을 작성해보았는데, 오늘은 MVVM에 대해 작성을 해보려고 합니다. (+RxSwift로 구현하는 법을 알아보려고 해요.) 그리고 MVVM 패턴과 VIPER 패턴을 비교하면서 MVVM 패턴에 대

dongminyoon.tistory.com

 

Practice

View

우선 차의 리스트를 보여줄 View를 먼저 구성해보겠습니다. ReactorKit에서는 View에 해당하는 레이어는 View Protocol을 채택하여야 합니다. 준수하게 되면 필수적으로 DisposeBag, bind(reactor:) 메소드를 정의해야하게 됩니다.

final class CarListViewController: UIViewController, View {

    var disposeBag = DisposeBag()
    
    func bind(reactor: CarListReactor) {
        // Action(View -> Reactor 바인딩)
        
        // State (Reactor -> View 바인딩)
    }
    
}

이렇게 구성하게 되면, 우선은 ReactorKit의 프로토콜을 준수한 View에 해당하는 레이어가 완성되었습니다. 

ReactorKit의 주의사항을 보면 bind(reactor:) 부분을 직접호출하지 않는 것이 좋다고 언급되어 있는데요.

 

실제, 구현부를 보니 View의 요소중에 reactor: Reactor?가 있는데 View Protocol의 extension으로 Reactor에 값이 할당되면 자동으로 bind(reactor:) 구현부가 호출되게 정의되어 있습니다. 

 

나중에 이제 bind(reactor:)의 구현부에는 View의 Action을 Reactor로 바인딩, Reactor의 State를 View에 바인딩하는 코드들이 들어가게 될 예정입니다 ㅎㅎ. 우선은 비워주세요~

 

Reactor

다음은 Reactor입니다. 여기도 View와 마찬가지로 Reactor Protocol을 준수해주어야 합니다. 그리고 이 Reactor에서 View에서 받을 ActionView로 전달할 State를 정의하게 됩니다. 또한, Reactor 내에서 상태를 변경하는 단위인 Mutation을 내부에 정의합니다. 

final class CarListReactor: Reactor {

    var initialState: State = State()

    enum Action {
        case reloadCarList
    }
    
    enum Mutation {
        case setLoading(loading: Bool)
        case requestCarList(carList: [CarModel])
    }
    
    struct State {
        var loading: Bool = false
        var carList: [CarModel] = []
    }

}

우선 저는 이렇게 정의했습니다.

  • Action.reloadCarList : UITableView.refreshControl을 통해 전달받을 Action
  • Mutation.setLoading(loading:) : 내부에서 Loading Indicator의 State 전달을 위해 사용될 Mutation
  • Mutation.requestCarList(carList:) : View의 Action을 받아서 request를 하는 로직을 담당할 Mutation
  • State.loading : UITableView.refreshControl Indicator를 담당할 State
  • State.carList : UITableView에 차의 리스트를 보여줄 데이터 State

이렇게 각각의 단위들을 담당할 수 있는 Action, Mutation, State가 정의되었습니다~

아, 근데 여기서 처음 Reactor를 설명할 때와는 다른 개념이 나왔죠?

 

바로 Mutation입니다.

 

Mutation은 앞의 Action, State와는 다르게 외부로는 노출하지 않고 Reactor 레이어 내부에서 Action -> State를 연결하는 역할을 수행하게 됩니다.

위의 모식도가 ActionReactor로 들어왔을 때, State로 변경되는 과정을 보여주는 것입니다. 크게 2단계의 변형을 거치는 것 같죠?

우선 mutate()의 메소드에서 Action의 스트림을 Mutation 단위로 변경해주게 됩니다. 이 과정에서 대부분의 비즈니스 로직(네트워킹, 비동기 처리)들을 처리하게 됩니다. 그리고 나온 Mutation을 reduce() 메소드로 전달하고 reduce()에서 최종적으로 나온 값을 State에 반영하고 이를 반환합니다. 

 

그러면 Reactor의 위에서 정의했던 Action, Mutation에 따라 먼저 Reactor 내부 먼저 채워볼게요.

final class CarListReactor: Reactor {

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .reloadCarList:
            return Observable.concat([
                Observable.just(.setLoading(loading: true)),
                self.requestCarListRx(),
                Observable.just(.setLoading(loading: false))
            ])
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .requestCarList(let carList):
            newState.carList = carList
        case .setLoading(let loading):
            newState.loading = loading
        }
        return newState
    }
    
    private func requestCarListRx() -> Observable<Mutation> {
        return Observable.create { observer in
            let dummyCarList = [
                CarModel(brand: "BMW", name: "520d"),
                CarModel(brand: "Mercedes-Benz", name: "E-class"),
                CarModel(brand: "KIA", name: "K5"),
                CarModel(brand: "HYUNDAE", name: "AVANTE")
            ].shuffled()
            observer.onNext(Mutation.requestCarList(carList: dummyCarList))
            observer.onCompleted()
            return Disposables.create()
        }.delay(.seconds(2), scheduler: MainScheduler.instance)
    }

}

우선, reloadCarList Action이 들어오면 loading(true), requestCarList, loading(false)를 차례로 수행하게 됩니다. 

그리고 reduce에서 각각의 Mutation에 따라 변화되는 State를 적용하고 이를 반환합니다.

(확실히 RxSwift + MVVM을 적용했던 것보다 제 눈에는 좀더 선언적으로 보이고 알아보기가 좋네요 ㅎㅎ)

 

지금까지는 Reactor의 구현이었습니다. 아까 View에서 남겨놓은 부분이 있죠?

다시 그 부분으로 넘어가서 구현해보겠습니다.

 

View

이제 Action, State가 정의되었으니 이 각각의 값들을 바인딩 해보겠습니다.

final class CarListViewController: UIViewController, View {

    func bind(reactor: CarListReactor) {
        // Action
        self.tableView.refreshControl?.rx.controlEvent(.valueChanged)
            .map { CarListReactor.Action.reloadCarList }
            .bind(to: reactor.action)
            .disposed(by: self.disposeBag)
        
        // State
        reactor.state
            .map { $0.carList }
            .bind(to: self.tableView.rx.items) { tableView, index, item in
                let identifier = String(describing: CarListTableViewCell.self)
                let indexPath = IndexPath(row: index, section: 0)
                let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
                
                guard let carListCell = cell as? CarListTableViewCell else { return UITableViewCell() }
                carListCell.configure(carModel: item)
                return carListCell
            }
            .disposed(by: self.disposeBag)
        
        reactor.state
            .map { $0.loading }
            .filter { $0 == false }
            .observeOn(MainScheduler.instance)
            .bind(onNext: { [weak self] loading in
                self?.tableView.refreshControl?.endRefreshing()
            }).disposed(by: self.disposeBag)
        
        // View
        self.tableView.rx.itemSelected.subscribe(onNext: { [weak self] indexPath in
            guard let self = self else { return }

            let identifier = String(describing: CarDetailViewController.self)
            let viewController = self.storyboard?.instantiateViewController(withIdentifier: identifier)

            guard let currentState = self.reactor?.currentState else { return }
            guard currentState.carList.indices ~= indexPath.row else { return }
            guard let carDetailViewController = viewController as? CarDetailViewController else { return }
            guard let selectedCar = self.reactor?.currentState.carList[indexPath.row] else { return }
            carDetailViewController.setCarModel(selectedCar)

            self.navigationController?.pushViewController(carDetailViewController, animated: true)
        }).disposed(by: self.disposeBag)
    }

}

이렇게 View에서는 View -> ReactorAction을 전달하는 경우는 reactor.action으로 바인딩, Reactor -> ViewState를 전달하는 경우는 reactor.state를 바인딩 해주면 됩니다 ㅎㅎㅎㅎ

 

 

사용해보고 느낀점

우선, 기존에 사용하던 MVVM + RxSwift의 패턴이 기반이라고 생각되는 패턴입니다. 즉, Reactor = ViewModel, View = View의 역할을 하는 것 같습니다. 그렇기 때문에, 새롭게 습득할 때 러닝커브가 크지 않았습니다 🙃 

 

기존에 MVVM + RxSwift를 사용하셨던 분이라면 크게 시간을 들이지 않고도 금방 프로젝트에 적용하고 사용할 수 있을 것 같다는 생각이 들었습니다. 아, 그리고 얼마전에 Ribs에 대해서도 궁금해서 잠깐 찾아봤었는데요. Ribs는 적용하기 위해서는 앱의 전반적인 구조를 다시 설계해야하는 것 같더라구요.. 그러니깐 단편적으로는 적용이 불가능해 보였습니다(?). 반면 ReactorKit의 경우에는 현재 프로젝트가 다른 패턴을 기반으로 작성되어 있어도 뷰 단위로 일부만 적용이 가능합니다 ㅎㅎ 이것도 장점이겠죠~

 

무엇보다 좋았던 부분은 MVVM + RxSwift를 사용하면 사용하는 개발자마다 약간씩 구현 방법들이 달라서 팀 단위의 개발에서는 어느정도의 템플릿으로 통일하는 것이 필요합니다. ReactorKit을 사용하면 이를 템플릿화 해주기 때문에, 팀 내에서 어느정도의 구현의 통일이 가능합니다. 또, MVVM + RxSwift를 사용하다보면 Input, Output이 구분되지 않고 하나의 Subject에 View에서 onNext, accept의 로직이 들어가있고 이를 View에서 바로 구독하고 있는 경우들도 보입니다. 이런 부분들이 사실 코드를 읽기 힘들게 만들고 패턴을 살짝 모호(?)하게 하는 지점들이 생기게 됩니다. 근데 ReactorKit은 이를 단방향의 흐름으로 Action, State를 이용해서 잡아주고 있습니다. View에서 Action이 발생하면 이를 로직이 있던 없던 Reactor를 거치게 되고 View에서는 변화를 State만을 바라보고 업데이팅하게 됩니다. 템플릿화와 더불어 패턴의 모호함이 발생할 수 있는 지점도 잘 잡아주고 있습니다..!! 

 

사실, 팀에서도 개발하면서 MVVM + RxSwift를 사용하고 있는데 저희를 이런 흐름을 위해 꼭 accept, onNext를 하는 로직은 ViewModel로 숨기기로 약속이 되어 있거든요..!! 이런 니즈들을 ReactorKit에서 해소해주고 있다는 생각이 들었습니다.

 

다만, 물론 ReactorKit을 사용하지 않고도 위에서 말한 것과 같이 기존의 패턴에서도 이를 해결할 수 있기 때문에 꼭 ReactorKit이어야만해는 아니다는 생각도 들었습니다~

 

근데, 저는 한번 사이드 프로젝트에서 한 번 사용해보고 싶다는 생각이 들었습니다 ㅎㅎㅎ...

다음 진행하는 사이드에서는 꼭 사용해보려고 합니다.

 

오늘은 ReactorKit에 대해 간단한 예제를 통해 알아보고 작성해보았습니다 😄

 

 


참고 레퍼런스

 

ReactorKit 시작하기

오늘은 StyleShare에서 ReactorKit을 사용한지 딱 1년이 되는 날입니다. ReactorKit은 반응형 단방향 앱을 위한 프레임워크로, StyleShare와 Kakao를 비롯한 여러 기업에서 사용하고 있는 기술입니다.

medium.com

 

반응형