맛동산이

UIKit) ReactorKit 을 알아보자(feat. SwiftUI) 본문

앱/Swift

UIKit) ReactorKit 을 알아보자(feat. SwiftUI)

진ddang 2024. 1. 1. 21:25

Untitled.png

리액터킷은 Flux와 Reactive Programming의 조합으로 만든 프레임워크이다.

유저액션과, 뷰 상태는 옵저버블(rx)스트림을 통해서 각각의 레이어에 전달된다.

여기에서 중요한점!

  • 뷰는 액션만을 방출한다.
  • 리액터는 상태만을 방출한다.

이것을 통해서 단방향 구조를 형성한다.

리액터킷의 특징

  • 테스터블하다
    • 뷰에서 로직을 분리하기 때문에(리액터를 통해서) 테스터블하다.
  • 특정뷰에서만 채택이 가능
    • 전체 아키텍쳐를 사용하지 않고 필요한 부분만 채택가능하다는 장점
  • 타이핑이 적다
    • 간결하게 코딩할수있다는 장점이 있다.

View

리액터킷에서 뷰는 뷰 컨트롤러이다.

역할은 다음과 같다.

  • 유저인풋을 받아들이고 이를 액션스트림에 추가한다.
  • view state를 각 UI component에 추가한다.(bind)

뷰는 어떠한 로직을 가지지도 않으며 action stream과 state stream을 어떻게 매핑할지만 가지고 있는것이다.

class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // inject reactor

리액터는 뷰컨트롤러 외부에서 주입해줘야한다.

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

해당 bind라는 메소드는 reactor가 주입되면 바로 실행된다.

따라서 init시점에서 (viewDidLoad)가 실행되기 전에 ui와 함께 실행해주면 된다.

Reactor

리액터는 뷰와 독립된, 레이어로, 가장 중요한점은 비즈니스 로직을 뷰에서 완벽하게 분리해냈다는 점이다.

리액터는 3개의 상태가 있으며, initalState라는 프로퍼티를 가지고 있다.

class ProfileViewReactor: Reactor {
  // represent user actions
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // represents the current view state
  struct State {
    var isFollowing: Bool = false
  }

  // represent state changes
  enum Mutation {
    case setFollowing(Bool)
  }

  let initialState: State = State()
}

initalState는 그냥 초기 뷰가 가질 상태값을 의미한다.

Action은 유저의 인풋(액션)을 의미하고, State는 뷰에 보여질 상태값이다.

이사이에 mutation이라는 기능이 있는데, 이것을 통해서 Action과 State를 연결시켜준다.a

Mutation을 한번 보자

  enum Mutation {
    case setFollowing(Bool)
  }

mutation은 state가 변할거를 보여준다. 위의 예시에서는 setFollowing(Bool)이다.

위의 enum에서는 어떠한 mutation이 있을지, AssociatedType을 작성해준다.

하나의 mutation case내부에서 다양한 실행이나, 값의 변화가 생길수도 있다.

Mutation은 이후 mutation메소드 안에서 발행될 Observable이다.

mutate, reducer

mutate

mutate는 Action을 받아서 Observable으로 발행해주는 과정이다.

func mutate(action: Action) -> Observable<Mutation>
func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }
///혹은
		let request = UserAPI.isFollowing(userId)
		let success = 
				request.success
								.compactmap { $0.data }
								.map { $0.isFollowing }
								.map(Mutation.SetFollowing)
		let failure = 
				request.failure
							.catch(using: errorHandler)
							.flatMap { _ -> Observable<Mutation> in .empty() }
		return .merge([success, failure])
  }
}

실제 구현부는 다음과 같다.

  • 모든 sideEffect는 여기에서 관리 된다.
  • 어떠한 Api콜링, 혹은 값의 변경과 같은 과정들이 담기는 곳이다.
  • 맨 마지막에 리턴해주면 된다.

reducer

reducer는 앞의 Mutation과 기존의 state를 받아서 newState화면에 뿌려주는 역할을 하는 로직이다.

func reducer(state: State, mutation: Mutation) -> State
unc reducer(state: State, mutation: Mutation) -> State { 
	var newState = state
	switch mutation {
		case .setFollowing(let isFollowing)
//혹은 case let .setFollowing(isFollowing)
		newState.isFollowing = isFollowing
	}
	return newState
}

전체적인 플로우에 대한 설명

SwiftUI 가 나는 편한데 그것에 대입하여 비교하자면,

State

우선 state는 SwiftUI에서 @State와 동일하다. 화면에 영향을주는 변화 값들을 의미한다.

State값이 변화하면 화면이 변한다고생각하면 된다.

이 state를 화면에 연결할때 하는 행위가 2개가 있다.

reactor.state
	.map { $0.옵저빙하길 원하는 상태 }
	.distinctUntilChange()
	.asDriver(onErrorJustReturn: something)
	.drive(onNext: { [weak self] in ... }
	.disposed(by: disposeBag)

이 과정을 통해서 원하는 곳으로 drive할수 있다.

action

action은 SwiftUI로 치자면, onChange, onTapGeture, onAppear ….등등 action들이다.

해당 액션을 통해서 state에 바인딩 해줘야한다

과정은 다음과 같다.

something.rx.tap
	.map { _ in Reactor.Action.만들어둔 액션 }
	.bind(to: reactor.action)
	.disposed(by: disposeBag)

mutation

mutation은 SwiftUI로 치자면 onChange에 뒤에오는 trailing closure이다.

즉 액션을 받고 어떠한 변화를 할것인지 작성하는 부분.

reduce

reduce는 mutate함수에서 보내주는 Mutation의 값을 받아서 currentState를 newState로 변경해주는 부분이다.

과정은 다음과 같다.

reduce(state: State, mutation: Mutation) -> State {
	var newState = state
	switch mutation {
		case .somecase(let data): 
				newState.변화할state = data
	}
	return newState
}

reactorkit처음에는 어렵게 생각했지만, SwiftUI에 대응해서 생각해보자면, State와 action을 생성하기 쉽다!!

다들 파이팅

반응형