[Swift] ReactorKit을 이용한 복잡한 UI 인터랙션 처리 방법: 여러 UI 컴포넌트와의 상호작용을 ReactorKit으로 관리하기

작성일 :

[Swift] ReactorKit을 이용한 복잡한 UI 인터랙션 처리 방법: 여러 UI 컴포넌트와의 상호작용을 ReactorKit으로 관리하기

ReactorKit은 Swift 기반의 애플리케이션에서 복잡한 상태 관리와 UI 인터랙션을 단순화해주는 라이브러리입니다. 이 글에서는 ReactorKit을 사용하여 여러 UI 컴포넌트 간의 상호작용을 어떻게 효과적으로 관리할 수 있는지 알아보겠습니다. 예제를 통해 ReactorKit의 기본 개념과 실제 적용 방법을 설명합니다.

ReactorKit의 기본 개념

ReactorKit은 Unidirectional Data Flow(UDF) 아키텍처를 기반으로 하고 있습니다. 이 아키텍처는 상태(state), 변환(mutation), 및 액션(action)의 흐름을 단방향으로 유지해 복잡한 UI 상태를 예측 가능하고 관리하기 쉽게 만듭니다. ReactorKit은 크게 세 가지 주요 컴포넌트로 구성됩니다:

  1. Reactor: 상태 변경 로직을 정의합니다. 주로 Action을 받아 Mutation을 일으키고, 최종적으로 State를 업데이트합니다.
  2. View: 상태(state)를 구독하여 UI를 업데이트하고, 사용자 입력을 통해 Action을 보냅니다.
  3. Action: 사용자의 입력이나 외부 이벤트를 정의합니다.

프로젝트 설정 및 기본 예제

ReactorKit을 사용하는 프로젝트를 설정하려면 먼저 Cocoapods나 Carthage를 사용하여 ReactorKit을 설치해야 합니다. 여기서는 Cocoapods를 이용한 설치 방법을 예제로 보여 드리겠습니다.

Cocoapods 설치

bash
# Podfile에 ReactorKit 추가
pod 'ReactorKit'

# 변경 사항 적용
pod install

기본 예제: 카운터 앱

다음은 ReactorKit을 이용한 간단한 카운터 앱 예제입니다. 이 예제를 통해 기본 개념을 이해해 보겠습니다.

Step 1: Reactor 정의

swift
import ReactorKit

class CounterReactor: Reactor {
    // Action 정의
    enum Action {
        case increase
        case decrease
    }

    // Mutation 정의
    enum Mutation {
        case increaseValue
        case decreaseValue
    }

    // State 정의
    struct State {
        var counter: Int
    }

    // 초기 상태 설정
    let initialState: State = State(counter: 0)

    // 상태 변환 로직
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increase:
            return Observable.just(.increaseValue)
        case .decrease:
            return Observable.just(.decreaseValue)
        }
    }

    // 변환된 상태를 적용
    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .increaseValue:
            newState.counter += 1
        case .decreaseValue:
            newState.counter -= 1
        }
        return newState
    }
}

Step 2: ViewController 설정

swift
import UIKit
import ReactorKit
import RxSwift
import RxCocoa

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

    // UI 컴포넌트 생성
    let increaseButton = UIButton()
    let decreaseButton = UIButton()
    let counterLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        // UI 초기화
        // 생략... (뷰 관련 코드)
    }

    func bind(reactor: CounterReactor) {
        // Action 바인딩
        increaseButton.rx.tap.map { Reactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

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

        // State 바인딩
        reactor.state.map { $0.counter }
            .distinctUntilChanged()
            .map { \"Counter: \($0)\" }
            .bind(to: counterLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

여러 UI 컴포넌트 간의 상호작용

ReactKit을 사용하면 여러 UI 컴포넌트 간의 상호작용을 보다 효과적으로 관리할 수 있습니다. 이 부분에서는 복잡한 상호작용 예제를 통해 다양한 UI 컴포넌트가 어떻게 상호작용하는지 살펴보겠습니다.

예제: 복잡한 폼 처리

이 예제에서는 여러 텍스트 필드와 버튼이 상호작용하는 복잡한 폼을 ReactKit으로 관리하는 방법을 다룹니다.

Step 1: Reactor 정의

swift
import ReactorKit

class FormReactor: Reactor {
    enum Action {
        case updateName(String)
        case updateEmail(String)
        case submit
    }

    enum Mutation {
        case setName(String)
        case setEmail(String)
        case setSubmitting(Bool)
        case setError(String?)
    }

    struct State {
        var name: String = ""
        var email: String = ""
        var isSubmitting: Bool = false
        var errorMessage: String?
    }

    let initialState = State()

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .updateName(let name):
            return Observable.just(.setName(name))
        case .updateEmail(let email):
            return Observable.just(.setEmail(email))
        case .submit:
            if currentState.name.isEmpty || currentState.email.isEmpty {
                return Observable.just(.setError("모든 필드를 입력하세요."))
            }
            return Observable.concat([
                Observable.just(.setSubmitting(true)),
                Observable.just(.setError(nil)),
                Observable.just(.setSubmitting(false))
            ])
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setName(let name):
            newState.name = name
        case .setEmail(let email):
            newState.email = email
        case .setSubmitting(let isSubmitting):
            newState.isSubmitting = isSubmitting
        case .setError(let error):
            newState.errorMessage = error
        }
        return newState
    }
}

Step 2: ViewController 설정

swift
import UIKit
import ReactorKit
import RxSwift
import RxCocoa

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

    let nameTextField = UITextField()
    let emailTextField = UITextField()
    let submitButton = UIButton()
    let errorLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        // UI 초기화
    }

    func bind(reactor: FormReactor) {
        // Action 바인딩
        nameTextField.rx.text.orEmpty.map { Reactor.Action.updateName($0) }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        emailTextField.rx.text.orEmpty.map { Reactor.Action.updateEmail($0) }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

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

        // State 바인딩
        reactor.state.map { $0.errorMessage }
            .compactMap { $0 }
            .bind(to: errorLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

ReactorKit은 복잡한 UI 상태를 명확하고 간결하게 관리할 수 있는 강력한 툴입니다. 여러 UI 컴포넌트가 상호작용하는 애플리케이션에서도 일관된 상태 관리를 보장할 수 있습니다.