맛동산이

SwiftUI) TCA 장점과 기본 개념 본문

앱/SwiftUI

SwiftUI) TCA 장점과 기본 개념

진ddang 2024. 8. 8. 15:15
💡 TCA는 단방향 아키텍쳐로, 상태관리가 용이하고, 데이터 흐름이 명확하게 정의되어 있기 때문에 단방향으로 이해하기 쉽습니다.

TCA 의 장점

  • 모듈화: TCA는 애플리케이션을 작은 모듈로 나누어 관리할 수 있도록 도와줍니다. 각 모듈은 독립적으로 테스트하고, 유지보수할 수 있어 코드의 가독성과 재사용성이 향상됩니다.
  • 예측 가능성: 상태의 변화가 명확하게 정의되어 있어, 애플리케이션의 동작을 예측하기 쉽습니다. 모든 상태 변화는 액션을 통해 이루어지며, 이는 디버깅과 유지보수를 용이하게 만듭니다.
  • 테스트 용이성: TCA는 테스트를 쉽게 할 수 있도록 설계되었습니다. 상태, 액션, 리듀서를 독립적으로 테스트할 수 있어, 각 모듈의 동작을 검증하는 데 유리합니다.
  • 상태 관리: TCA는 중앙 집중식 상태 관리를 제공하여, 애플리케이션의 상태를 효율적으로 관리하고, 상태의 변경을 쉽게 추적할 수 있습니다.
  • 효율적인 Side Effects 처리: 비동기 작업이나 외부 API 호출과 같은 부수 효과(side effect)를 쉽게 처리할 수 있는 구조를 제공합니다. 이를 통해 복잡한 비즈니스 로직을 깔끔하게 관리할 수 있습니다.
  • SwiftUI와의 통합: TCA는 SwiftUI와 잘 통합되어 있어, 선언형 UI와 반응형 프로그래밍 패러다임을 자연스럽게 사용할 수 있습니다. 이를 통해 UI의 상태를 쉽게 동기화할 수 있습니다.
  • 서로 다른 플랫폼 간의 일관성: TCA는 iOS, macOS 등 다양한 플랫폼에서 사용할 수 있어, 코드의 일관성을 유지할 수 있습니다.

상태관리가 쉽고 데이터플로우가 일정하다.

swiftUI에서는 @state, @environmentObject @Bind 등등 기본적으로 제공하는 property wrapper를 사용하지만, State를 하위 화면으로 내려주어야 하고, EnvironmentObject주입, Bind주입 등등에서 발생하는 런타임에러와, 흐름을 파악하기 힘든 점이 존재함

TCA에서는 일관된 구조로 개발이 가능하고, 단방향이기 때문에 뷰의 영향이 가는 데이터를 쉽게 파악할수 있음

이를 통해 기존의 SwiftUI 보다 해당 뷰에 액션과 상태를 이해하기 쉽기때문에, 코드 유지 보수에도 많은 도움이 됨

RxSwift + ReactorKit, SwiftUI + TCA

두개다 단방향 아키텍쳐이며, 비슷한 형태를 띄지만, rxswfit, reactorKit은 정확하게는 UIkit + rxSwift, reactorkit의 3개의 프레임워크의 조합이며 이를 사용하기 위해서는 3개의 프레임워크와 라이브러리에 대해서 높은 이해도를 필요로 함

반면 TCA는 swiftui와 적은 외부 라이브러리, 프레임워크 의존도를 가진다는 장점과, rxswift, reactorkit두개의 라이브러리, 프레임워크에 대한 러닝커브또한 비교적 짧은 장점

TCA 기본형태

TCA는 ReactorKit에서의 Mutation과 Action이 한데 합쳐져 있는 형태 와 비슷하다.

따라서 연쇄적으로 Action을 통해서 새로운 Action을 방출하는것과 같은

예를 들어, 버튼을 누르고 api상태를 바꾸고, 다시 화면전환을 하는것과 같은 동작은 계속해서 Action안에서 선언되어야 한다.

ex)

enum Action: Equatable { 
	case tapButton
	case changeSomeState(Bool)
	case showSomeview(Bool)
}

이렇게 리액터킷과 Action은 Mutation과 섞여 있는 형태가 된다. 

기본형태

/// Reducer 프로토콜은 추상 타입으로 State와 Action을 요구한다.
struct AFeature: Reducer {
    /// State는 구조체이다. 어플리케이션의 UI를 그리기 위해 필요한 상태를 보관한다.
    struct State: Equatable { /* code */ }

    /// Action은 열거형이다. 어떤 Action이 유저의 상호작용에 의해 트리거 되는지,
    /// 혹은 어떤 Effect로 인해 되먹임 될 것인지를 정의한다.
    enum Action: Equatable { /* code */ }
    
    var body: some ReducerOf<AStore> {

    /// body 혹은 func reduce(into:action)에서
    /// State가 Action에 의해 어떻게 변형되는지 구체화한다.
    /// 각각 프로토콜을 채택한 타입의 State와 Action을 클로저의 아규먼트로 전달한다.
        Reduce<State, Action> { state, action in }
    }
}

기본적으로 State, Action, Reducer가 존재하며, Reducer에서 State가 변경되는것을 Effect로서 방출한다.

Effect는 state를 변경하고, State는 Observable을 통해서 화면에 변경사항을 적용하게 된다 .

위에서의 body는 모든 리듀서를 순차적으로 실행하고, 이를 하나의 Action으로 merge하는 프로퍼티 래퍼이다.

State

SwiftUI는 View의 상태가 변경되었을 때 해당 view를 자동으로 업데이트한다.

  • Equtable을 채택하는것으로, 이전의 값과 상이한 값만을 업데이트 하여서 효율을 높인다.

Reducer

Reducer는 크게 body와 func으로 나뉘는데, body는 모든 리듀서를 순차적으로 실행하고, 이를 하나의 Action으로 merge하는 프로퍼티 레퍼 이다.

reducer가 기존의 reactorkit과 다른점은, reducer에서 비동기를 처리하고 나면 해당 비동기를 다시 action을 태워서 값이 변경되도록 하는 과정을 거치게 된다.

action에 따라서 Effect를 방출한다.

  • none: 아무런 이펙트도 발생시키지 않는 경우
  • run: 비동기 처리를 하고, 이팩트를 발생시키는 경우
    var body: some ReducerOf<Self> {
        Scope(state: \\.login, action: \\.login) {
            Login()
        }
        Reduce<State, Action> { state, action in
            switch action {
            case .onAppear:
                return .none

            case .task:
                state.isLoading = true
                return .run { send in
                    for try await result in try await self.authenticationClient.listenAuthState() {
                        await send(.listenAuthStateResponse(.success(result)))
                    }
                } catch: { error, send in
                    await send(.listenAuthStateResponse(.failure(error)))
                }

            case let .listenAuthStateResponse(.success(user)):
                state.isLoading = false
                state.appUser = user
                return .none

            case let .listenAuthStateResponse(.failure(error)):
                state.isLoading = false
                print(error.localizedDescription)
                return .none

            case let .login(.delegate(.userUpdated(user))):
                state.appUser = user
                return .none

            case .login:
                return .none
            }
        }
    }
}
반응형