티스토리 뷰

728x90

안녕하세요 이번엔 전 포스팅에서 설명했었던 ReactorKit을 간단하게 사용해보는 튜토리얼 포스팅을 해보겠습니다. 혹시 이전 포스팅을 못보신 분이 계시다면 아래 링크를 참고해주세용

ReactorKit 알아보기(링크)

만들어 볼 프로젝트 미리보기

ReactorKit 작업순서 미리보기

  1. 외부 프레임워크 설치(처음에만)

    • ReactorKit
    • RxCocoa
    • RxSwift
  2. ViewController 작업

    • 외부 프레임워크 import
    • View 프로토콜 적용
    • bind 메소드 작성
  3. ViewModel(reactor) 작업

    • 외부 프레임워크 import

    • 기본적인 틀 작성

    • 비즈니스 로직 작성

  4. ViewController의 bind 메소드 추가작성

이정도가 되겠네요.. 그럼 지금부터 차근차근 설명해보도록 하겠습니다.

ReactorKit에선 ViewController와 ViewReactor(ViewModel)이 한 쌍이라고 생각하시면 되요.

뷰와 제약사항 정의 부분을 스킵하고 싶으신 분은 아래 repo에서 스타트킷 안에 있는 프로젝트를 다운받아서 시작해주시면 되겠습니다. 하지만 초보자분이시라면 처음부터 따라하시는걸 추천드려용

  1. 프로젝트 다운
  2. 터미널에서 스타트킷 프로젝트 디렉토리에서 pod install
  3. xcworkspace 파일 실행

스타트킷 repo 링크

1. 외부 프레임워크 설치

CocoaPods 혹은 Package manager를 통해 외부 프레임워크를 설치해줄 수 있는데, 저는 CocoaPods을 사용해서 외부 프레임워크를 설치해 줄 거에요. 혹시 코코아팟 설치 밑 사용방법을 모르시는 분은 이 포스팅을 참고해주세요.

코코아팟 관련 포스팅 링크

터미널을 통해 프로젝트 디렉토리로 이동 후에

  1. Pod init
  2. vi Podfile 명령어를 통해 밑의 내용 추가
 # Pods for {프로젝트이름}
...
pod 'ReactorKit'
pod 'RxSwift', '~> 5'
pod 'RxCocoa', '~> 5'
...
end
  1. pod install
  2. open *.xcw* 명령어를 통해 xcworkspace 파일 실행

이렇게 하면 ReactorKit을 사용할 준비물은 갖춰졌습니다.

2. ViewController 작업

저는 스토리보드를 사용하지 않고 코드로 작성하는게 직관적이기 때문에 코드로 작업을 할거에요. 혹시 스토리보드 없이 코드로 작업하는 방법을 모르시는 분은 아래 포스팅을 참고해주세용

스토리보드 없이 코드로 개발하기 관련 포스팅 링크

외부 프레임워크 설치 & 코드로 개발할 준비가 된 상태라면 기본세팅이 완료된 상태에요. 기본세팅이 완료된 상태라면, 프로젝트를 만들 때 기본적으로 생성되는 ViewController를 기준으로 작업을 해줄꺼에요.

우선 첫번째로 외부 프레임워크 사용할 부분들을 import 해줄 거에요.

// ViewController
import UIKit

import ReactorKit
import RxCocoa
import RxSwift
class ViewController: UIViewController {
  ...
}

class ViewController 부분을 보시면 UIViewController부분만 있고 View라는 프로토콜이 채택이 안되어 있는데, 채택을 해주는게 맞습니다.. 우선 설명을 위해 이부분은 조금있다가 ReactorKit을 사용할 때 추가해주겠습니당. (ReactorKit 포스팅 참고)

다음으로 해줄 부분은 Reactorkit의 ViewController부분은 View와 bind 메소드로 이루어 져 있기 때문에, 우선 View 부분을 선언해 주겠습니다.

// ViewController
class ViewController: UIViewController {
...
// MARK: - Property
        let decreaseButton: UIButton = {
      let decreaseButton = UIButton()
        decreaseButton.setImage(UIImage(systemName: "minus"), for: .normal)
        decreaseButton.tintColor = .black
        decreaseButton.translatesAutoresizingMaskIntoConstraints = false
        return decreaseButton
    }()

    let increaseButton: UIButton = {
        let increaseButton = UIButton()
        increaseButton.setImage(UIImage(systemName: "plus"), for: .normal)
        increaseButton.tintColor = .black
        increaseButton.translatesAutoresizingMaskIntoConstraints = false
        return increaseButton
    }()

    let valueLabel: UILabel = {
        let valueLabel = UILabel()
        valueLabel.text = "0"
        valueLabel.translatesAutoresizingMaskIntoConstraints = false
        return valueLabel
    }()

    let activityIndicator: UIActivityIndicatorView = {
        let statusIndicator = UIActivityIndicatorView()
        statusIndicator.translatesAutoresizingMaskIntoConstraints  = false
        return statusIndicator
    }()

ReactorKit에선 UI요소들을 모두 View로 봅니다.

뷰를 선언했으면 View에 넣어주고, 제약사항을 걸어줘야겠죠?? 저는 View 요소들을 넣어주는 부분은 LoadView 부분에서, 제약사항 부분은 ViewDidLoad부분에서 정의를 해주겠습니다.

뷰 생명주기: LoadView() -> ViewDidLoad() -> ... 순서로 진행

// ViewController
class ViewController: UIViewController {
...
  // MARK: - View Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        initConstraint()
    }

  // 뷰 넣어주기
override func loadView() {
        let view = UIView()
        self.view = view
        view.backgroundColor = .systemBackground

        [decreaseButton, valueLabel, increaseButton, activityIndicator].forEach {
            self.view.addSubview($0)
        }
    }

  // 제약사항 추가
  func initConstraint() {
        NSLayoutConstraint.activate([
            decreaseButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
            decreaseButton.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
            decreaseButton.heightAnchor.constraint(equalTo: decreaseButton.widthAnchor),

            valueLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
            valueLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),

            increaseButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
            increaseButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
            increaseButton.heightAnchor.constraint(equalTo: increaseButton.widthAnchor),

            activityIndicator.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            activityIndicator.topAnchor.constraint(equalTo: valueLabel.safeAreaLayoutGuide.bottomAnchor, constant: 10),
        ])
    }

addSubView 부분을 저렇게 선언해주니 깔끔하죠?? 이번에 프로젝트를 진행하면서 알게 됬습니다 ㅠㅠ 이래서 서브 프로젝트를 계속 해야하나봐요

이제 뷰 부분은 잘 보이니 ReactorKit을 적용해보도록 할게요!

  1. View 프로토콜 적용해주기
// ViewController
class ViewController: UIViewController, View {
  ...
  1. Rx를 사용할 것이기 때문에 DisposeBag 추가 && ViewReactor typealias 추가
// ViewController
class ViewController: UIViewController, View {
      var disposeBag: DisposeBag = DisposeBag()
    typealias Reactor = ViewReactor
  ...
  1. 이니셜라이즈 추가
// ViewController
class ViewController: UIViewController, View {
  ...
  // MARK: - Init
    init(reactor: Reactor) {
        super.init(nibName: nil, bundle: nil)
        self.reactor = reactor
    }

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

ReactorKit은 reactor값이 변해야 bind메소드가 작동되기 때문에 reactor를 넣어줘야 작동해요. 그래서 사용하기 편하게 이니셜라이즈를 추가 작성해줬습니다.

init 부분을 추가해 줬기 때문에 ViewController를 호출해주는 SceneDelegate.swift 부분도 수정을 해줘야 합니다.

//  SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
      ...
      let reactor = ViewReactor()
      window?.rootViewController = ViewController(reactor: reactor)
      ...

요런식으로.. reactor 부분에 ViewReactor를 넣어줍시다.

  1. bind 메소드 작성
// ViewController
class ViewController: UIViewController, View {
  ...
  func bind(reactor: ViewReactor) {
        // action

        // State
    }
  ...

이부분이 ViewReactor에 액션을 보내서 비즈니스 로직 및 state값을 변경해서 그 값에 따라 뷰가 그려지는 부분을 구현해주는 부분이에요. 아직 ViewReactor(ViewModel)가 구현되지 않았기 때문에 이제 ViewReactor를 구현해주도록 하겠습니다.

ViewReactor(ViewModel) 작성

우선 new file을 통해 swift 파일을 새로 만들어 줍니다. 이름은 땡땡 ViewReactor 또는 땡땡ViewModel로 편하신걸로 만들어주시면 되용. 저는 ViewModel이 편해서 이렇게 작성하는데 설명을 위해 ViewReactor로 만들겠습니다.

  1. 외부 프레임워크 import
import ReactorKit
import RxSwift
// ViewModel
class ViewReactor: Reactor {
  ...
  1. 기본적인 틀 작성
import ReactorKit
import RxSwift

// ViewModel
class ViewReactor: Reactor {
    enum Action {

    }

    enum Mutation {

    }

    struct State {

    }

    let initialState: State = State()

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {

        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {


        return newState
    }
}

여기서 Mutation은 State값을 바꾸는 가장 작은 단위이기 때문에 Mutaion와 State는 한쌍이라고 생각하시면 되요. 즉 State값을 1 증가하는 로직을 할 경우 증가하는 값이 있어야 하고(Mutation), 증가 전 후 상태값인 State Value값이 있어야 하죠 하지만 Action은 꼭 있어야 할 필요는 없어요. Action같은 경우는 ViewController에서 액션을 가져오는 부분을 선언해주는 것이기 때문에 ViewController의 액션과 관련있을 때만 잘 구분해서 작성해주면 됩니다.

Mutation과 State는 한 쌍이다. 하지만 Action은 ViewController와 관련있을 때 작성

우선 기능이 잘 돌아가는지 테스트를 위해 - 버튼을 눌렀을 경우만 작성을 해보도록 할게요. 어떤걸 추가하면 될까요?

  • -버튼을 누르는 Action이 있어야겠죠? decrease 라고 네이밍 해볼게요
  • -버튼을 눌렀을 때 값이 변해야 하니깐 state값이 있어야 겠죠? value 라고 네이밍 해볼게요
  • state와 mutaion은 한쌍이라고 했죠?? -버튼을 눌렀을 때 값이 변하는 값의 최소단위를 decreaseValue 라고 네이밍 해볼게요.

이 부분을 코드로 작성해보면

import ReactorKit
import RxSwift

// ViewModel
class ViewReactor: Reactor {
    enum Action {
        case decrease
    }

    enum Mutation {
        case decreaseValue
    }

    struct State {
        var value: Int = 0
    }

    ...
}

이렇게 되겠네요. 이제 mutate(), reduce() 메소드를 추가로 작성해줘야 하는데 ReactorKit은 단방향 데이터 흐름이라고 했죠?? 데이터 흐름 순서를 한번 설명한 다음에 다음 메소드를 작성해볼게요.

  1. ViewController의 bind 메소드에서 tap 같은 Action이 생기면 ViewReactor(Reactor에요)의 Action에 bind 해줍니다.
  2. Reactor가 호출되면 맨 처음으로 mutate()가 실행되는데, mutate()는 action을 받아서 Observable를 반환하는 메소드에요. 그래서 ViewController의 bind된 action에 따라 case문을 통해 Observable을 반환하는데 쉽게 생각해서 Muation값을 가져온다고 생각하시면 되요.
  3. mutate() 다음에 reduce()가 실행되는데, reduce()는 현재 state값과 아까 mutate()에서 반환한 mutation 값을 받아와서 새로운 상태값인 state값을 반환하는 메소드에요. 즉 받아온 mutation값을 case문을 통해 현재상태값 + mutaion의 값을 적용해서 새로운 state를 반환해주는 메소드 입니다.
  4. ViewReactor에서 위 작업이 끝나면 결국 reduce() 메소드에서 state값이 변경되는걸 알 수 있는데요. 이 state값은 Reactor의 state값이 변한 것이기 때문에 결국 Reactor값이 변한거라고 생각하실 수 있어요. ReactorKit은 Reactor값이 변할때 bind 메소드가 호출되기 때문에 새로운 state값을 가지고 ViewController의 bind 메소드에서 뷰를 그려주거나 하는 상태값을 적용시켜주면 되겠습니다.

정리: bind() -> mutate() -> reduce() -> bind()

TIP: mutate()에서 Observable값이 concat을 통해 여러 Observable가 리턴 된다면 그만큼 reduce() -> state값 변화 -> bind() 메소드가 호출됩니다.

그럼 mutate() 메소드를 작성해볼게요.

import ReactorKit
import RxSwift

// ViewModel
class ViewReactor: Reactor {
...
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .decrease:
            return Observable.just(Mutation.decreaseValue)
        }
    }
}

bind()에서 bind 된 action을 가져와서 case문을 통해 해당 action일 경우 Observable.just(Mutation)을 반환해주면 됩니다. Observable 부분은 rx 문법이라 추후 rxSwift 관련 포스팅을 작성하게 되면 설명해드리도록 하겠습니다.

이제 마지막 reduce() 메소드를 작성해볼게요.

import ReactorKit
import RxSwift

// ViewModel
class ViewReactor: Reactor {
...
    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .decreaseValue:
            newState.value -= 1
        }

        return newState
    }
}

현재 값을 바꿀 것이기 때문에 var 형으로 선언해주고

mutate()에서 받아온 mutation값을 case문을 통해 구분해서 state값을 변경해주고, 변경된 newState값을 return해주면 되겠습니다.

reduce() 메소드가 끝나면 상태값이 변해서 결국 Reactor값이 변한 것이기 때문에 ViewController에 있는 bind() 메소드가 호출된다고 했죠?? 그럼 ViewController의 bind() 메소드에 작성해줘야 할 부분들이 크게 2가지가 있네요.

  1. ViewReactor에 action을 bind 해줄 action 부분
  2. state값이 변했을 때 state값에 따라 작업해주는 state 부분

그럼 두가지 경우를 작성해보도록 다시 ViewController로 이동해서 bind() 메소드를 작성해보도록 할게요.

ViewController의 bind 메소드 추가작성

첫번째로 action 부분을 작성해보도록 할게요.

  1. -버튼이 tap 되면
  2. Reactor의 Action 중 decrese를
  3. reactor의 action에 bind 해줘서
  4. disposBag에 적용..? (저도 rx를 배우는중이라 이부분을 잘 모르겠지만 마지막에 적용해주더라구요 ㅠㅠ)

이부분을 코드로 구현해보면

// ViewController
class ViewController: UIViewController, View {
  ...
  func bind(reactor: ViewReactor) {

        // action
        decreaseButton.rx.tap // tap 시
            .map{Reactor.Action.decrease} // decrease Action을
            .bind(to: reactor.action) // reactor의 action에 bind 해줌
        .disposed(by: disposeBag)
    ...

그럼 reactor 부분에서 muate() -> redcue() 이후 state값이 변경되서 다시 bind() 메소드가 작동되겠죠?? 이제 state값을 가져와서 state값에 따라 특정부분을 변경해주는 state 부분을 작성해볼게요.

  1. reactor의 state값 중 value값을 가져와서
  2. 이 값이 변경됬을경우에만
  3. 내려온 값이 Int형이기 때문에 String으로 형변환 후
  4. 변경된 값을 label에 적용해주고
  5. 다시 disposBag에 적용해줍니다..
// ViewController
class ViewController: UIViewController, View {
  ...
  func bind(reactor: ViewReactor) {

        // action
        decreaseButton.rx.tap // tap 시
            .map{Reactor.Action.decrease} // decrease Action을
            .bind(to: reactor.action) // reactor의 action에 bind 해줌
        .disposed(by: disposeBag)
    ...

    // State
        reactor.state.map{$0.value} // reacor의 state값 중 value값을 가져와서
        .distinctUntilChanged() // 값이 변경될 경우에만(조건)
        .map{String($0)} // Int형이라 String 형변환
            .bind(to: valueLabel.rx.text) // label의 text값에 적용
        .disposed(by: disposeBag)

이렇게해서............ ViewController의 action이 ViewReactor로 가서 작업을 한 후에 부메랑처럼 다시 ViewController로 돌아오는 단방향 데이터 흐름인 ReactorKit을 사용해봤습니다........... 복잡하죠..? 이걸 굳이 왜 이렇게 써야하나 싶죠..? 그런데 이렇게 작성해보니 ReactorKit의 사용이유는 알겠네요..(아직 모를지도..)

  1. 상태관리가 간결해지고..
  2. 단방향 데이터 흐름이기 때문에... 흐름만 이해한다면 흐름이 간결해지는 것 같고..
  3. RxSwift 기반으로 만든 것이기 때문에 RxSwift를 사용할 수 있다는것..
  4. 뷰와 비즈니스 로직이 분리되어 테스트하기 좋고 ViewController가 가벼워진다는 것..

저도 이 예제 프로젝트만 작성했을 때까진 크게 안와닿았는데 현재 진행중인 프로젝트에 ReactorKit을 적용해보니 이래서 쓰는구나라는걸 알게됬어요..

그럼 이런식으로 +버튼 또한 작성해보도록 하겠습니다. ViewController의 bind() 부분과 ViewReactor 부분을 추가 작성해주면 되겠죠??

// ViewController
class ViewController: UIViewController, View {
  ...
  func bind(reactor: ViewReactor) {

        // action
        decreaseButton.rx.tap 
            .map{Reactor.Action.decrease} 
            .bind(to: reactor.action) 
        .disposed(by: disposeBag)

    increaseButton.rx.tap
            .map{Reactor.Action.increase}
            .bind(to: reactor.action)
        .disposed(by: disposeBag)

    // State
        reactor.state.map{$0.value}
        .distinctUntilChanged()
        .map{String($0)}
            .bind(to: valueLabel.rx.text)
        .disposed(by: disposeBag)

        reactor.state.map{$0.isLoading}
        .distinctUntilChanged()
            .bind(to: activityIndicator.rx.isAnimating)
        .disposed(by: disposeBag)
    ...

// ViewModel
class ViewReactor: Reactor {
    enum Action {
        case increase
        case decrease
    }

    enum Mutation {
        case increaseValue
        case decreaseValue
    }

    struct State {
        var value: Int = 0
    }
    ...

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .decrease:
            return  Observable.just(Mutation.decreaseValue)

        case .increase:
            return Observable.just(Mutation.increaseValue)
        }
    }

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

        return newState
    }
}

추가1. 버튼 누를 때마다 딜레이 줘보기

이렇게까지만 하면 reactorKit을 사용하는것 보다 그냥 버튼에 #selector로 액션을 적용하는게 더 간편해보이는데 굳이 왜 이렇게 복잡하게 해야 하는지 이해가 안되기 때문에.. 버튼을 클릭시 딜레이를 주는 이벤트를 추가해보도록 하겠습니다..

어떻게 해야 할까요?

  1. 우선 ViewController 부분엔 비즈니스 로직이 들어가면 안되니 bind() 부분은 그대로 두고.. ViewReactor에서 작성해줘야 하는데..
  2. ViewController에서 action을 받아서 Observable를 return 해주는 muate() 부분에 Observable.concat([])를 사용해주면 여러 Observable을 반환할 수 있기 때문에 약간 작업큐 같은 느낌이니 이부분에서 1초 딜레이를 추가해보도록 하겠습니다.
  3. 우선 state값에 isLoading이라는 Bool값 추가해주고 기본은 false로 선언해주겠습니다.
  4. state값이 있으면 mutation값도 있어야겠죠? 한쌍이니깐.. setLoading(Bool) 으로 case를 추가할게요.
  5. 로딩이 되는 부분은 이미 버튼이 클릭하는 increase, decrease 액션이 있기 때문에 따로 추가할 필요가 없어보여요.

지금까지를 코드로 작성해보면 이렇습니다.

// ViewModel
class ViewReactor: Reactor {
  ...
  enum Mutation {
    ...
        case setLoading(Bool)
    }

  struct State {
    ...
    var isLoading: Bool = false
  }
  ...
  1. ViewController에서 action이 bind 되면 맨 처음으로 muate()메소드가 실행되므로 mutate() 부분에 로딩중 false -> 작동 -> 로딩중 true로 변경해볼게요.
// ViewModel
class ViewReactor: Reactor {
  ...
  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
        case .decrease:
      return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.decreaseValue)
                    .delay(1, scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false))
            ])
      // increase 부분도 같은방법으로..
      ...

굳이 setLoading을 true로 변경했다가 false로 변경하는 이유가 있을까요? 그 이유는 isLoading이라는 state값이 변해야 다시 ViewController의 bind() 메소드가 호출되고 state부분에서 로딩중이라는 빙글빙글 효과를 넣어줄것이기 때문에 넣어줬습니다.

1초 딜레이를 해주는 부분은 .delay(1, scheduler: MainScheduler.instance) 이부분입니다.

TIP: Observable이 3개죠?? 즉 muate() -> reduce() -> bind() 과정이 3번 일어납니다. 처음 isLoading값이 false에서 true로 변해서 ViewController에서 빙글빙글 효과를 줄거고, 다음 value값이 변하는 과정에서 1초 후에 value값을 -1 해주고, 다시 isLoadingr값을 false로 돌려놔줍니다.

  1. mutate() 이후 reduce() 부분에서 setLoading(Bool) 부분일 경우 state값을 변경하는 부분을 추가해줍니당.
// ViewModel
class ViewReactor: Reactor {
  ...
  func reduce(state: State, mutation: Mutation) -> State {
    case let .setLoading(isLoading):
            newState.isLoading = isLoading
    ...

case let 입니당

  1. 마지막으로.. state값이 변경됬으니... 부메랑처럼 bind() 메소드에서 state값에 따라 할 작업을 작성해줘야겠죠..?
// ViewController
class ViewController: UIViewController, View {
  ...
  func bind(reactor: ViewReactor) {
    ...
    // State
    reactor.state.map{$0.isLoading} // reactor의 state값 중 isLoading을
        .distinctUntilChanged() // 이 값이 바뀔경우
            .bind(to: activityIndicator.rx.isAnimating) // activityIndicator 빙글뱅글 효과를 줍니다..
        .disposed(by: disposeBag)

여기까지 해주면.. +- 버튼을 클릭시 빙글뱅글 인디케이터효과가 잘 작동하는걸 확인할 수 있습니다..

추가2. 기본 tap시 메소드 실행하기

앱개발 하면서 대부분 꼭 쓰는 기능 중 하나가 뷰 이동이죠..? 대부분 뷰 이동은 self.present()나 네비게이션을 사용해서 이동하는데, 단순 tap 시 #selector 처럼 특정 메소드를 실행하는 과정을 reactorKit에서 구현하려면 어떻게 해야 할까요?? 이부분은 자주 쓰는 부분인데 강의영상엔 없고 프로젝트를 진행하면서 알게되서 작성해봐요(포스팅이 너무 길어져서 손이 아프네요 ㅠㅠ..)

우선 특정 버튼을 클릭시 alert창을 띄워주는 alertButton을 넣어줄게요.

// ViewController
class ViewController: UIViewController, View {
  ...
  let alertButton: UIButton = {
        let button = UIButton()
        button.setTitle("show alert", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
  ...
  override func loadView() {
        ...
    // alertButton 추가
        [decreaseButton, valueLabel, increaseButton, activityIndicator, alertButton].forEach {
            self.view.addSubview($0)
        }
  }
  ...
  func initConstraint() {
    ...
    alertButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            alertButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -20)
        ])
    }

이제 이 버튼을 reactorKit에 적용할 방법을 생각해봅시다.

  1. 기본적으로 reactorKit은 reactor값이 변해야 부메랑처럼 다시 돌아와서 변경된 state값에 따라 작업을 해줄 수 있습니다.
  2. 그렇기 때문에 단순 tap시 뷰 이동이나 alert창을 띄워주는 부분이라도 reactor값에 변화를 주는 state값과 mutation 부분을 작성해줘야 해요. 요런식으로..
// ViewModel
class ViewReactor: Reactor {
    enum Action {
      ...
        case showAlertAction
    }

    enum Mutation {
            ...
        case alertButtonTapped(Void?)
    }

    struct State {
      ...
        var isAlertShow: Void?
    }
  1. mutation과 state값이 있으니 mutate()와 reduce()부분을 작성해줘야 겠죠?
// ViewModel
class ViewReactor: Reactor {
  ...
func mutate(action: Action) -> Observable<Mutation> {
        switch action {
          ...
          case .showAlertAction:
            return Observable.concat([
                Observable.just(Mutation.alertButtonTapped(Void())),
                Observable.just(Mutation.alertButtonTapped(nil))
            ])
        }
}

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

mutate() 와 reduce() 부분에서 결과적으로 isAlertShow라는 state값을 Void()값으로 변경했다가 nil로 state값을 변경했기 때문에 reactor값이 변경되었기 때문에 ViewController의 bind() 부분에서 재호출 될 수 있게 됩니다.

이부분은 저도 이해가 잘 안되는데.. Void()도 값이라고 하네요..

  1. 아무튼 state값이 변해서 결과적으로 reactor값이 변경했기 때문에 ViewController의 bind() 부분이 재호출되어 state부분을 추가로 작성할 수 있게 됬습니다. 작성해줍시다.
// ViewController
class ViewController: UIViewController, View {
  ...
func bind(reactor: ViewReactor) {
  ...
  // state
  reactor.state.map{$0.isAlertShow}
            .compactMap {$0} // 조건, compactMap은 nil만 필터링 됩니다.
        .bind(onNext: {[weak self] in
            self?.showAlert("알람참", "개발자아라찌")
            }).disposed(by: disposeBag)
}

  // alert창을 띄우기 위한 메소드
  func showAlert(_ title: String, _ message: String) {
        let alertViewController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let okayAction = UIAlertAction(title: "네에..", style: .default)
        alertViewController.addAction(okayAction)
        self.present(alertViewController, animated: true)
    }

과정을 살펴보면 조건 부분에서 compactMap을 사용했는데, 그 이유는 compactMap은 nil만 필터링되기 때문입니다. 그래서 두번 호출되지 않게 되는거죠.

그 다음에 bind(onNext: )를 사용해서 안에 클로저를 통해 사용하려는 메소드를 넣어주는식으로 구현을 했습니다.

클로저 안에 [weak self]가 들어간 이유는 혹시나 하는 메모리 누수현상을 방지하기 위해 self을 weak으로 해주도록 추가 작성을 했어요.

클로저 안에 self를 사용할 경우가 생길 경우 [weak self]부분을 추가해주는게 메모리 누수현상을 방지할 수 있기 때문에 작성해주는게 좋다고 하네요. 자세한 내용은 Swift ARC(Aoutomatic Reference Counting) 부분을 참고해주세용

swift ARC 관련 포스팅

이런식으로 기본 tap 시 reactorKit을 사용해서 특정 메소드를 사용하는 방법에 대해 알아봤습니다....요....9손아프네요..

지금까지...

ReactorKit 사용법을 튜토리얼 프로젝트를 만들어보는 과정을 통해 알아봤습니다. 저도 배워가는 중이고.. 알게된 내용을 확실하게 알기 위해 포스팅을 자세하게 작성해봤는데.. 작성하고 보니 내용이 길어졌네요... ReactorKit은 토스, 네이버 라인, 카카오, 하이퍼커넥트 등 유명한 곳에서 사용하고 있는 프레임워크라고 해요.. 그리고 사용해보니 RxSwift와 찰떡인 것 같습니다..(아직 배워가는 중이지만..) 그래서 이 튜토리얼을 따라해보시고.. 나중에 간단한 프로젝트나 예전 진행했던 프로젝트들을 ReactorKit + RxSwift 를 적용하는 리팩토링을 해보시는것도 좋은 경험이 될 것이라는 생각이 들어요(제가 지금 그러고 있습니다....) 그럼 ReactorKit을 처음 시작하시는분이나 공부하시는분에게 이 포스팅이 도움이 되셧으면 좋겠고... 마지막으로 지금까지 진행한 프로젝트 완성본 repo를 링크로 남기면서 마무리를 해보도록 할게요.... 다들 코로나 조심하시고.. 좋은하루 되세요..

완성 프로젝트 repo 링크

728x90

'개발 블로그 > iOS' 카테고리의 다른 글

CollectionView 알아보고 사용해보기  (0) 2020.08.18
PageViewController 사용해보기(1탄)  (0) 2020.08.12
CocoaPods(코코아팟) 설치 && 사용법  (0) 2020.07.10
ReactorKit 알아보기  (0) 2020.07.09
WebKit 사용해보기  (0) 2020.07.01
댓글