[Swift] ReactorKit 사용기 - 2

민경준·2022년 6월 27일
1

ReactorKit 사용기

목록 보기
2/3
post-thumbnail

3. How to Use?

3-1. Basic


View

View는 data를 Display하는 역할이다. View layer에는 비즈니스 로직이 없다. action stream에 user의 입력을 바인딩하고, view state를 view의 UI에 바인딩만 해주면 된다. 그리고 action과 state를 어떻게 맵핑할지만 정의해주면 된다.

View를 정의하기 위해서는 View 라는 Protocol을 따르도록 해야한다. 그러면 reactor 라는 이름으로 지정된 속성이 있는데 해당 속성은 외부에서 미리 정의해주어야 한다.

final class AppleViewController: UIViewController, View {
	internal var disposeBag = DisposeBag()
}

AppleViewController.reactor = AppleViewReactor() // 리액터 주입

reactor 속성이 변경 될 때, bind(reactor:) 메서드가 호출된다.

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

	// state (Reactor -> View)
	reactor.state
		.map { "\($0.currentValue)" }
		.bind(to: textLabel.rx.text)
		.disposed(by: self.disposeBag)
}

만약, 스토리보드를 통해 ViewController를 초기화 하는 경우에는 StoryboardView Protocol을 이용하면 된다. 모든 것이 똑같지만 View가 로드된 이후에 작동한다는것이 차이점이다.

final class OrangeViewController: ViewController, StoryboardView {
	internal var disposeBag = DisposeBag()

	func bind(reactor: MyViewReactor) {
    // this is called after the view is loaded (viewDidLoad)
  }
}

OrangeViewController.reactor = OrangeViewReactor()

Reactor

View의 상태를 관리하는 독립적인 계층이다. 가장 중요한 역할은 View에서 제어 흐름을 분리해주는 것이다. 모든 View에는 해당 Reactor가 있고 모든 로직을 Reactor에 위임해준다. (Reactor는 View에 종속되지 않아 개별로 테스트를 해주면 된다.)

reactor 속성에 값으로 주입하기 위해서는 Reactor Protocol 형식을 따라야한다.
이 Protocol을 사용하기 위해서는 Action, Mutation, State 세가지 유형을 정의해줘야 하고, initialState 속성을 필요로 한다.

final class AppleViewReactor: Reactor {
	
	enum Action {
	// 사용자의 입력과 상호작용하는 역할을 한다
		case increase
	}
	enum Mutation {
	// Action과 State 사이의 다리역할이다.
	// action stream을 변환하여 state에 전달한다.
		case executeIncrease
	}
	struct State {
	// View의 state를 관리한다.
		var currentValue: Int = 0
	}

	var initialState: State
	init() {
		self.initialState = State()
	}

mutation()

action 을 받고, Observable<Mutation> 을 생성한다.

이후에 다시 서술하겠지만 모든 side effect 및 API 호출과 같은 비동기 관련 작업들은 Service Provider에게 위임한다.

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
	case .increase: // receive an action
		return .just(.executeIncrease) // convert to Mutation stream
	}
}

reduce()

기존의 state 값과 Mutation을 가지고 newState를 생성한다.

func reduce(state: State, mutation: Mutation) -> State {
	var newState = state // create a copy of the old state
	switch mutation {
	case .executeIncrease:
		newState.currentValue = state.currentValue + 1 
		// manipulate the state, creating a new state
	}

	return newState // return the new state
}

transform()

다른 관찰 가능한 스트림과 mutation을 결합한다. 예를 들어 transform(mutation:)은 global event stream을 mutation에 결합하기에 가장 좋은 위치이다. 자세한 내용은 Global State 섹션을 참고하면 된다.

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

3-2. Advance


Global State

Redux와는 다르게 ReactorKit는 global state를 정의하지 않는다. 이것은 global state를 관리하기 위해 무엇이든 사용가능 하다는것을 의미한다. BehaviorSubject, PublishSubject 등.. 어떤 reactor 라도 사용가능 하기때문에 로직 특성에 맞게 global state를 정의할 수 있다.

Action → Mutation → State 의 흐름에는 global state가 없다. 반드시 transform(mutation:)를 사용하여 global state를 mutation으로 변환해줘야 한다.

현재 로그인된 유저의 정보를 저장하는 global state가 있다고 가정해보자, 해당 정보가 변경 될 때 Mutation.setUser(User?)를 리턴해주려면 다음과 같이 구현하면 된다.

var currentUser : BehaviorSubject<User>  // global state

func  transform ( mutation : Observable<Mutation>) -> Observable<Mutation> {
	return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}

View Communication

우리는 callback 패턴과 delegate 패턴에 익숙해질 필요가 있다. ReactorKit에서는 이러한 패턴을 Reactive Extension을 통해 ControlEvent으로 사용하는 방법을 알아보자.

AppleViewController가 있고 AppleViewController에서 present한 OrangeViewController가 있다고 가정하자, OrangeViewController에 있는 Custom UIButton을 tap 했을 때 AppleViewController에 대한 Reactor에 Action을 바인딩하기 위해서는 다음과 같이 구현하면 된다.

extension Reactive where Base: OrangeViewController {
	var increaseOranges: ControlEvent<Void> {
		let source = base.increaseButton.tap.withLatestFrom(...)
		return ControlEvent(events: source)
	}
}

이렇게 구현해둔것을 이제 AppleViewController 에서 다음과 같이 사용하면된다.

func bind(reactor: AppleViewReactor) {
	orangeViewController.rx.increaseOranges
		.map { Reactor.Action.Increase }
		.bind(to: reactor.action)
		.disposed(by: self.disposeBag)
}


Reference

profile
iOS Developer 💻

0개의 댓글