무작정 ReactorKit 따라해보고 살펴보기 - 1

okstring·2021년 12월 17일
0
post-thumbnail

ReactorKit is a framework for a reactive and unidirectional Swift application architecture.

ReactorKit은 reactive 및 단방향 Swift 애플리케이션 아키텍처를 위한 프레임워크입니다.

ReactorKit을 말로만 많이 들었지 이번 기회에 README.md를 보면서 간단하게 github profile 이미지를 불러오는 예제를 만들어보고 이해해봤습니다.

install은 Podfile, Package.swift 편하신대로 사용하시면 됩니다. 참고로 Carthage는 공식적으로 지원하지는 않음

https://github.com/ReactorKit/ReactorKit

공식 repository에서 말하는 디자인 목표는 다음과 같다

  • Testability: ReactorKit의 첫 번째 목적은 뷰에서 비즈니스 로직을 분리하는 것입니다. 이것은 코드를 테스트 가능하게 만들 수 있습니다. 리액터는 뷰에 대한 종속성이 없습니다. 리액터를 테스트하고 뷰 바인딩을 테스트하기만 하면 됩니다.
  • Start Small:: ReactorKit은 전체 애플리케이션이 단일 아키텍처를 따를 필요가 없습니다. 하나 이상의 특정 보기에 대해 ReactorKit을 부분적으로 채택할 수 있습니다. 기존 프로젝트에서 ReactorKit을 사용하기 위해 모든 것을 다시 작성할 필요는 없습니다
  • Less Typing: ReactorKit은 간단한 것을 위해 복잡한 코드를 피하는 데 중점을 둡니다. ReactorKit은 다른 아키텍처에 비해 더 적은 코드가 필요합니다. 간단하게 시작하여 확장하십시오.

실제로 사용해보면서 느낀점은 익숙해지기만 한다면 불필요한 타이핑 없이 테스트 가능하게 확장이 쉽게 가능하다는 느낌을 받았습니다.

View

  • ReactorKit에서 View는 ViewController, Cell로 취급됩니다.
  • View에는 비즈니스 로직을 가지고 있지 않습니다. 즉 바인딩을 담당하는 곳
class ViewController: UIViewController, View { // 프로토콜 채택
    var disposeBag = DisposeBag()
  
    func bind(reactor: MainReactor) {
      
    }
}
  • 참고로 StoryboardView도 지원합니다. 유의할 점은 View(viewDidLoad)가 로드되고 StoryboardView가 바인딩 한다는 점
class MyViewController: UIViewController, StoryboardView { // 프로토콜 채택
  func bind(reactor: MyViewReactor) {
    // this is called after the view is loaded (viewDidLoad)
  }
}

Reactor

  • UI에서 독립적인 레이어이므로 테스트가 용이해집니다
  • 모든 View는 Reactor가 있고 Reactor는 비즈니스 로직을 담당합니다
  • 리액터 속성이 변경되면 bind(reactor:)가 호출된다. bind(reactor:)를 구현하여 작업 스트림과 상태 스트림의 바인딩을 정의
  • Reactor 프로토콜을 채택
class MainReactor: Reactor {
    let network = Network()
    
  // 사용자 작업을 나타냄
    enum Action {
        case touchButton(index: Int)
    }
    
  // 상태 변경을 나타냄
    enum Mutation {
        case setImage(image: UIImage?)
    }
    
  // 현재 View 상태를 나타냄
    struct State {
        var image: UIImage?
    }
    
    let initialState: State
    
    init() {
        self.initialState = State()
    }
}
  • Reactor 프로토콜을 채택하면 Action, MutationState의 세 가지 type과 initialState 속성을 정의해야 한다
  • 여기서 Mutation은 Action과 State의 연결 다리라고 보면 되겠습니다.

mutate()

  • mutate()Action을 수신하고 Observable을 생성합니다. 비동기 작업이나 API 호출과 같은 모든 side effects은 이 메서드에서 수행됩니다.
let network = Network()

private let URLs = ["https://avatars.githubusercontent.com/u/62657991?v=4"]

func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case .touchButton(let index):
        return Observable.just(URLs[index])
            .flatMap({ self.network.load(url: $0) })
            .map({ Mutation.setImage(image: $0) })
    }
}

reduce()

  • reduce()는 이전 StateMutation에서 새로운 State를 생성합니다.
  • 이 메서드는 순수 함수입니다. 동기식으로 새 State를 반환해야 합니다. 이 함수에서 어떠한 side effects를 일으키면 안됩니다
func reduce(state: State, mutation: Mutation) -> State {
    var state = state

    switch mutation {
    case .setImage(let image):
        state.image = image
    }

    return state
}

transform()

transform 함수들은 다음과 같다

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>
  • 다른 관찰 가능한 스트림을 변환하고 결합합니다.
  • 예를 들어, transform(mutation:)은 전역 이벤트 스트림을 돌연변이 스트림에 결합하는 데 가장 좋은 위치입니다. 디버깅 목적으로도 사용할 수 있습니다.
func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}

여기까지가 기본적인 내용! 전체 코드는 다음과 같습니다

//SceneDelegate

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        let window = UIWindow(windowScene: windowScene)
        let vc = ViewController()
        vc.view.backgroundColor = .white
        window.rootViewController = vc
        vc.reactor = MainReactor()
        self.window = window
        window.makeKeyAndVisible()   
    }
}
//ViewController

class ViewController: UIViewController, View {
    var disposeBag = DisposeBag()
    
    let loadButton: UIButton = {
        let button = UIButton()
        button.setTitle("load Image", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        return button
    }()
    
    lazy var centerImageView: UIImageView = {
        let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 350, height: 350))
        imageView.image = UIImage()
        imageView.center = view.center
        return imageView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        makeUI()
        
        loadButton.addAction(UIAction(handler: { _ in
            print("Hello")
        }), for: .touchUpInside)
    }
    
    func makeUI() {
        view.addSubview(centerImageView)
        view.addSubview(loadButton)
        
        loadButton.translatesAutoresizingMaskIntoConstraints = false
        loadButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        loadButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
    }
    
    func bind(reactor: MainReactor) {
        
        loadButton.rx.tap
            .map({ Reactor.Action.touchButton(index: 0) })
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        reactor.state
            .map({ $0.image })
            .bind(to: centerImageView.rx.image)
            .disposed(by: disposeBag)
        
    }
}
//MainReactor

class MainReactor: Reactor {
    let network = Network()
    
    private let URLs = ["https://avatars.githubusercontent.com/u/62657991?v=4"]
    
    enum Action {
        case touchButton(index: Int)
    }
    
    enum Mutation {
        case setImage(image: UIImage?)
    }
    
    struct State {
        var image: UIImage?
    }
    
    let initialState: State
    
    init() {
        self.initialState = State()
    }
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .touchButton(let index):
            return Observable.just(URLs[index])
                .flatMap({ self.network.load(url: $0) })
                .map({ Mutation.setImage(image: $0) })
        }
    }
    
    func reduce(state: State, mutation: Mutation) -> State {
        var state = state
        
        switch mutation {
        case .setImage(let image):
            state.image = image
        }
        
        return state
    }
}
//Network

class Network {
    func load(url: String) -> Single<UIImage?> {
        let request = URLRequest(url: URL(string: url)!)
        
        return URLSession.shared.rx.response(request: request)
            .map({ UIImage(data: $0.data) })
            .asSingle()
    }
}

구현 화면

간단하게 ReactorKit을 알아봤는데 Example을 보면서 좀 더 심화된 내용을 알아야 할 필요가 있다고 생각해 다음 포스트에 더 이어서 작성해보도록 합니다


reference

https://github.com/ReactorKit/ReactorKit

https://nsios.tistory.com/141

profile
step by step

0개의 댓글