김경록의 앱 개발 여정

[ReactorKit] 기본 예제 분석하고 내 프로젝트에 적용하기 본문

TIL

[ReactorKit] 기본 예제 분석하고 내 프로젝트에 적용하기

Kim Roks 2025. 1. 9. 19:16

Counter 앱의 Reacotr 분석

Reactor

final class CounterViewReactor: Reactor {
  • CounterView라는 뷰의 Reactor
  • 기본적으로 하나의 뷰의 하나의 리액터, 이렇게 함으로 개별 관리가 용이

Action

enum Action {
    case increase
    case decrease
  }
  • Action의 경우 사용자의 인터렉션에 기반한 행위를 정의
  • 실제 프로젝트에 적용함에 있어서 어느범위까지가 Action인가에 대한 고민이 있었는데,
    테이블 뷰 셀렉티드, 필요시 스크롤링하는 동작까지도 Action의 범주로 들어가서 정의해주면 된다.
  • 해당 예제에선 숫자의 증가와 감소 두개의 액션을 정의했다.

State

    struct State {
        var value: Int
        var isLoading: Bool
    }
  • State의 경우 화면상에서 가질 데이터의 모음이다
  • 이 앱의 경우 즉각 눈에 보이는 건 값 즉 Value
  • 일단 예제 코드엔 추가적으로 isLoading과 alertMessage가 존재했다.

mutation

enum Mutation {
    case increaseValue
    case decreaseValue
    case setLoading(Bool)
    case setAlertMessage(String)
  }
  • Action과 State의 연결다리
  • 그러니까 결국 State에 있는 Value를 어떻게 만들어줄까 라는 동작을 정의

Mutate is a state manipulator which is not exposed to a view
정확히는 Mutate는 뷰에 노출되지 않는 상태 조작자입니다. 라고 얘기하고 있다

  • 여기선 당연히 값의 증가와 감소, 더 나아가 로딩 상태의 변화, 알럿 메세지의 수정이 포함

initialState, init

let initialState: State

  init() {
    self.initialState = State(
      value: 0, // start from 0
      isLoading: false
    )
  }
  • 초기 State의 설정이 필요, 위에서 정의 한 State를 사용한다
  • init에서 State의 초기 값을 직접 지정할 수 있다.

여기까지가 Reactor의 기본 구성 요소

다시 한번 짚어보는 Reactor의 동작 사이클을 보며 필요 메서드들에 대한 분석

mutate() → Observable(Mutation)

func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case .increase:
      return Observable.concat([
        Observable.just(Mutation.setLoading(true)),
        Observable.just(Mutation.increaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
        Observable.just(Mutation.setLoading(false)),
        Observable.just(Mutation.setAlertMessage("increased!")),
      ])

    case .decrease:
      return Observable.concat([
        Observable.just(Mutation.setLoading(true)),
        Observable.just(Mutation.decreaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
        Observable.just(Mutation.setLoading(false)),
        Observable.just(Mutation.setAlertMessage("decreased!")),
      ])
    }
  }
  • 기본적으로 concat을 통해 여러 Observable을 순차적으로 연결하여 하나의 Observable로 묶어주고 있음
  • 로딩을 true로 바꾸고 값을바꾸고 다시 로딩을 false로 처리해주는데, 그 사이에 딜레이가 있다 이런 방식을 취하려해 isLoading이 있었나보다

reduce()

func reduce(state: State, mutation: Mutation) -> State {
    var state = state
    switch mutation {
    case .increaseValue:
      state.value += 1

    case .decreaseValue:
      state.value -= 1

    case let .setLoading(isLoading):
      state.isLoading = isLoading

    case let .setAlertMessage(message):
      state.alertMessage = message
    }
    return state
  }
  • 파라미터로 받아온 state를 변경하기 위해 함수 내부에서 변수로 선언해줬다
  • Value의 증가 감소량을 정의해줬다, 만약 변동 폭이 변경된다해도 수정하기 편해보인다

이론 공부와 예제 공부를 했으니 직접 적용해보기로 했다.

내 프로젝트에 직접 적용하기

  • 겪은 문제점 위주로 정리해보았다.

수정 전

  • 현재 프로젝트에선 Viewmodel를 주입받아 사용하는 중
final class HomeViewController: UIViewController { private let viewModel: TestViewModel init(viewModel: HomeViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) }
  • ViewModel을 Reactor로 바꾼다. 라는 생각을 가지고 진행했더니 이런 코드가 나옴
    위 코드의 큰 문제점은 VC가 View 프로토콜을 채택하여 가진 reactor 외에 다른 reactor 를 참조해서 사용하고 있다는 점.

수정 후

final class HomeViewController: BaseViewController {
    
    var disposeBag = DisposeBag()
    
    init(reactor: HomeReactor) {
        super.init(nibName: nil, bundle: nil)
        // View 프로토콜이 제공하는 reactor에 주입 시킴
        self.reactor = reactor
    }
    
	  override func viewDidLoad() {
	  // ViewDidLoad는 해당 작업과는 관련이 없어짐
		}
    
    
//MARK: Reactor

extension HomeViewController: View {
    func bind(reactor: HomeReactor) {
    // 기존 fetch 코드를 좀 더 행동에 알맞아 보이게끔 네이밍을 변경
    // 프로토콜의 메서드내에서 바로 Action을 emit하도록 수정
        self.reactor?.action.onNext(.viewDidLoad)
        
        reactor.state.map { $0.posts }
            .bind(....)

 

  1. 주입받는건 동일, 하지만 Viewmodel처럼 reactor 의 인스턴스 선언은 필요없게 되었다.
    View프로토콜을 채택하면 알아서 생기기 때문
  2. ViewDidLoad때 필요한 동작 자체(네트워킹 등)를 Action으로 정의하기로 했다.
  3. ReactorKit의 의도에 맞게 모든 reactor 관련 코드는 bind 함수 내에 위치하도록 변경했다
  4. 중구난방으로 관리되던 Reactor관련 코드를 전부 bind(reator: ) 내부에 몰아넣을 수 있게 되었다.