[iOS] 한 방향으로 흐르는 ReactorKit 알아보기

2023. 3. 14. 19:35·iOS&Swift

안녕하세요.

이번 글에선 반응형 애플리케이션의 데이터 흐름을 단반향으로 만들 수 있게 도와주는 프레임워크 ReactorKit에 대해 알아보겠습니다.

ReactorKit의 정의

ReactorKit의 정의는 ReactorKit Github에 설명되어 있었습니다.

ReactorKit은 Flux와 Reactive Programming의 조합입니다. 사용자 작업 및 보기 상태는 관찰 가능한 스트림을 통해 각 계층에 전달됩니다. 이러한 스트림은 단방향입니다. 뷰는 작업만 내보낼 수 있고 리액터는 상태만 내보낼 수 있습니다.

위와 같이 ReactorKit은 Flux 아키텍처를 채택하고 있으며, 데이터 흐름이 단 반향으로 흐르는 아키텍처를 Flux라고 칭합니다.

위 이미지와 같이 View에서 Action이 발생하면, Reator는 Action에 따른 State를 변경하고, 변경된 State는 그대로 View에 표시되게 됩니다.

정의는 비교적 어렵지 않은 것 같습니다.

Reactor 타입 알아보기

ReactorKit은 기존 MVVM 패턴에 사용되는 ViewModel의 역할을 이 Reactor가 대신하게 됩니다.

Reactor에 대해 알아보겠습니다.

Reactor 타입을 만드는 방법은 아주 간단합니다.

Reactor라는 프로토콜을 채택하면 끝입니다.

위 사진과 같이 타입을 만들고, Reactor 프로토콜을 채택하면, Reactor의 역할을 수행하는 타입을 만들 수 있습니다.

Reactor 프로토콜을 채택하면 Action과 State, initialState를 필수적으로 구현해야 합니다.

initialState는 말 그대로 초기 State 값입니다.

  • Action: Action은 View를 통해 받는 Action을 enum타입으로 케이스로 나열한 것입니다.
  • State: State는 현재 상태를 기록하고, View는 State의 값을 가지고 View에 원하는 데이터를 표시합니다.
  • Mutation: Mutation은 Action이 발생하면, 처리해야 하는 작업을 enum타입으로 케이스로 나열한 타입입니다.

⚠️ Mutation은 Action과 State와 다르게 필수 구현 요소가 아닙니다.
하지만, 구현하지 않을 경우 Action의 타입을 그대로 따라가게 됩니다.

이제 마지막으로 두 메서드를 구현해야 합니다.

  • func mutate(action:) -> Observable<Mutation>: 해당 메서드는 파라미터로 받은 action을 switch문을 통해 분기 처리하여, Action에 따른 Mutation을 Observable 타입으로 반환합니다.
  • func reduce(state:, mutation:) -> State: 해당 메서드는 파라미터로 받은 mutation을 switch문을 통해 분기 처리하여, Mutation에 맞게 State를 변경 후 리턴합니다. State는 구조체이기 때문에 새로운 변수에 파라미터로 받은 State를 저장하고, 변수를 반환해야 합니다.

여기까지 Reactor 타입에 대해 알아보았습니다.

이제는 View에서 Reactor로 Action을 보내는 방법과, Action에 따른 변경된 State를 어떻게 View에게 전달하는지 알아보겠습니다.

Reactor 사용법

사진과 같이 Button과 같은 Control View의 Control Event를 RxSwift의 map 오퍼레이터를 통해 원하는 Reactor Action으로 변환시키고, Reactor에 bind 하면, 위에서 언급한 mutate와 reduce 메서드가 순차적으로 실행되게 됩니다.

위 사진과 같이 원하는 State의 값을 구독해두면, State값이 변경될 때마다 값이 방출됩니다.

⚠️ .distinctUntilChanged() 오퍼레이터를 사용하는 이유는 불필요한 스트림이 발생하는 것을 막기 위함입니다. 예를 들어 State의 title만 변경되어도 State가 변경된 걸로 간주되어 모든 State 값의 스트림이 방출됩니다.

여기까지 Reactor 사용법의 사용법까지 알아보았습니다.

ReactorKit을 사용하며 느낀 점과 전체 코드를 마지막으로 마무리하겠습니다.

느낀 점

ReactorKit을 공부하게 된 계기는 기존 RxSwift만 사용하면서, MVVM 패턴으로 앱을 개발할 때 스트림의 흐름을 일관성 있게 디자인하는 것이 또 하나의 일이었습니다. 때문에 스트림의 흐름이 ViewModel에서 Dispose 되기도 하고, View에서 되기도 하는 일이 발생했었습니다.

하지만, ReactorKit 같은 경우 패턴이 정해져 있다 보니 사용하면서 편하고, 팀원들과 협업 시에 큰 강점이 있을 것 같습니다.

또한, State 타입을 통해 값을 저장해두고 있기 때문에, View에 필요한 상태 값 관리가 용이했습니다.

코드

해당 코드는 ReactorKit을 공부하며 토이 프로젝트로 진행했던 SearchBooks 프로젝트의 일부분입니다.

DetailReactor

import Foundation

import ReactorKit

final class DetailReactor: Reactor {
  let initialState: State
  let useCase: SearchBookUseCaseable

  init(useCase: SearchBookUseCaseable, book: Book) {
    self.useCase = useCase
    self.initialState = State(
      title: book.title,
      isFavorites: book.isFavorites,
      item: book
    )
  }

  enum Action {
    case favoritesButtonDidTap
  }

  enum Mutation {
    case favoritesValue(Bool)
  }

  struct State {
    var title: String
    var isFavorites: Bool
    var item: Book
  }

  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case .favoritesButtonDidTap:
      var newFavoriteValue = false
      if currentState.isFavorites {
        useCase.removeFavoritesBook(isbn: currentState.item.isbn)
      } else {
        useCase.addFavoritesBook(isbn: currentState.item.isbn)
        newFavoriteValue = true
      }
      return .just(.favoritesValue(newFavoriteValue))
    }
  }

  func reduce(state: State, mutation: Mutation) -> State {
    var newState = state
    switch mutation {
    case .favoritesValue(let isFavorites):
      newState.isFavorites = isFavorites
      return newState
    }
  }
}

DetailViewController

import UIKit

import RxCocoa
import RxSwift
import SnapKit

final class DetailViewController: UIViewController {
  private let mainView = DetailView()
  private let favoritesButton = UIBarButtonItem()
  private let disposeBag = DisposeBag()
  private let reactor: DetailReactor
  weak var coordinator: DetailCoordinator?

  init(reactor: DetailReactor) {
    self.reactor = reactor
    super.init(nibName: nil, bundle: nil)
  }

  deinit {
    coordinator?.removeCoordinator()
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    attribute()
    layout()
    bind(reactor)
  }

  private func attribute() {
    view.backgroundColor = .systemBackground
    navigationItem.rightBarButtonItem = favoritesButton
    favoritesButton.tintColor = .systemYellow
  }

  private func layout() {
    view.addSubview(mainView)
    mainView.snp.makeConstraints {
      $0.edges.equalTo(view.safeAreaLayoutGuide)
    }
  }

  private func bind(_ reactor: DetailReactor) {
    bindAction(reactor)
    bindState(reactor)
  }

  private func bindAction(_ reactor: DetailReactor) {
    favoritesButton.rx.tap
      .map { DetailReactor.Action.favoritesButtonDidTap }
      .bind(to: reactor.action)
      .disposed(by: disposeBag)
  }

  private func bindState(_ reactor: DetailReactor) {
    reactor.state.map { $0.item }
      .distinctUntilChanged()
      .bind(to: mainView.setContent)
      .disposed(by: disposeBag)

    reactor.state.map { $0.title }
      .distinctUntilChanged()
      .bind(to: navigationItem.rx.title)
      .disposed(by: disposeBag)

    reactor.state.map { $0.isFavorites }
      .distinctUntilChanged()
      .bind(to: setFavoritesButtonImage)
      .disposed(by: disposeBag)
  }
}

extension DetailViewController {
  private var setFavoritesButtonImage: Binder<Bool> {
    return Binder(self) { owner, isFavorites in
      let image = isFavorites ? UIImage(systemName: "star.fill") : UIImage(systemName: "star")
      owner.favoritesButton.image = image
    }
  }
}
저작자표시 (새창열림)

'iOS&Swift' 카테고리의 다른 글

Combine 뽀개기 1장: Publisher  (0) 2023.07.26
Appearance사용해 NavigationBar 커스텀하기(iOS 13.0+)  (0) 2023.07.19
[iOS] Clean Architecture 파헤치기  (0) 2023.01.16
[iOS] Coordinator Pattern  (0) 2023.01.09
[Swift] Swift Concurrency와 GCD  (1) 2022.12.14
'iOS&Swift' 카테고리의 다른 글
  • Combine 뽀개기 1장: Publisher
  • Appearance사용해 NavigationBar 커스텀하기(iOS 13.0+)
  • [iOS] Clean Architecture 파헤치기
  • [iOS] Coordinator Pattern
Esiwon
Esiwon
iOS 개발 블로그
  • Esiwon
    시원한 코드 기록
    Esiwon
  • 전체
    오늘
    어제
    • 분류 전체보기 (70)
      • iOS&Swift (24)
      • git & github (1)
      • 코테 (41)
      • 네부캠 회고 (4)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    비동기
    Property wrapper
    챌린지
    네부캠
    탐색
    이분탐색
    브루트포스 알고리즘
    Combine
    알고리즘
    다이나믹 프로그래밍
    노드
    구현
    Swift
    완전탐색
    코딩테스트
    그리디
    photos
    회고
    재귀
    백준
    PhotoKit
    코테
    ios
    실버
    photoUI
    dfs
    GCD
    동시성
    Race Condition
    BFS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Esiwon
[iOS] 한 방향으로 흐르는 ReactorKit 알아보기
상단으로

티스토리툴바