본문 바로가기

SwiftUI

SwiftUI 공부 with TCA

회사에서 진행중인 프로젝트는 이제 화면단위로 SwiftUI의 전환이 이루어지고있고
현재 SwiftUI 전환이 약 6개월정도 이루어졌다 현재 진행중인 피처들은 모두 SwiftUI개발을 진행하고있지만 개발을 하면 할수록 
과거 취준생 시절했던 SwiftUI 전혀 다르고 그때는 개념이 없이 사용만 했던 시즌이기에 새롭게 공부가 필요성을 느끼게 되었고

한국에서 가장 많이 사용되는 TCA에 대해 분석하고 왜 이렇게 만들어져있는지를 분석해보려고한다.

https://github.com/pitt500/OnlineStoreTCA

 

GitHub - pitt500/OnlineStoreTCA: A demo covering the basics of the Composable Architecture (TCA)

A demo covering the basics of the Composable Architecture (TCA) - pitt500/OnlineStoreTCA

github.com

분석에 사용할 프로젝트는 해당 프로젝트이고 이 프로젝트를 기반으로 실제 상용앱에서 TCA의 개념을 우리 앱에 적용 할수 있도록 할 예정이다

가능하다면 TCA를 적용 한다면 더 좋지만.

 

우선 TCA에 대해 간단하게 설명해보겠습니다.

 

TCA는 Point-Free에서 개발한 SwiftUI 애플리케이션을 위한 아키텍처 프레임워크로, 상태 관리와 사이드 이펙트 처리를 위한 일관된 접근 방식을 제공합니다. 이 아키텍처는 단방향 데이터 흐름 기반으로 하며, 다음과 같은 핵심 구성 요소들로 이루어져 있습니다:

  1. 상태(State): 애플리케이션의 데이터를 나타내는 단일 소스
  2. 액션(Action): 사용자 상호작용, 이벤트 발생
  3. 리듀서(Reducer): 현재 상태와 액션을 받아 새로운 상태를 반환
  4. 이펙트(Effect): 비동기 작업, 네트워크 요청 등의 사이드 이펙트를 처리하는 방법을 제공
  5. 스토어(Store): 상태를 저장하고, 액션을 받아 리듀서를 실행하며, 이펙트를 수행

TCA의 가장 큰 강점은 모듈화와 합성입니다. 복잡한 기능을 작은 단위로 나누고, 이를 조합하여 큰 시스템을 구축할 수 있습니다. 또한 상태와 액션이 명시적으로 정의되기 때문에 테스트가 용이하고, 디버깅이 쉽습니다.

예를 들어, 간단한 카운터 앱을 TCA로 구현한다면 다음과 같은 구조를 갖게 됩니다.

 

회사프로젝트에서 ReactorKit을 사용하고있기때문에 해당 개념이 그렇게 어색하거나 새롭게 느껴지지 않는다.
하지만 실제 코드를 보고 SwiftUI의 특성을 생각해보면 비슷하지만 전혀 다른느낌의 코드스타일이 나오게 되는걸 알수있다.

 

@main
struct OnlineStoreTCAApp: App {
    var body: some Scene {
        WindowGroup {
            RootView(
                store: Store(
                    initialState: RootDomain.State(),
					reducer: { RootDomain() }
                )
            )
        }
    }
}

 

코드의 시작부터 Store, Reducer 라는 처음보는 개념, State라는 친숙한 개념이보이게 된다 그럼 Store가 무엇인지
먼저 알아봐야 할것같다.

Store의 init 함수

Store의 init을 살펴보게 되면 시작부터 모르는 개념이 나온다 

func performIfTrue(_ condition: () -> Bool, _ action: () -> Void) {
    if condition() {
        action()
    }
}

performIfTrue({ 2 > 1 }) {
    print("조건이 참입니다")
}

func performIfTrue(_ condition: @autoclosure () -> Bool, _ action: () -> Void) {
    if condition() {
        action()
    }
}

// 호출 방법
performIfTrue(2 > 1) {
    print("조건이 참입니다")
}

@autoclosure  클로저를 사용할때 보통 아래와 같이 사용이 되는데 만약 오토클로저를사용한다면? 
아래와같이 { } 구문없이 사용이 가능하다 이는 코드를 간단하게 만들어주는 역할은 해주지만 코드가 어떻게 평가사용되는지 숨기는 역할을 하기때문에 무분별한 사용은 안좋다고합니다.

 

다시 TCA Store의 Init을 분석해보자 

/// 초기 상태와 리듀서로부터 스토어를 초기화합니다.  
/// - **매개변수**:
/// - initialState: 애플리케이션을 시작할 초기 상태입니다.
/// - reducer: 애플리케이션의 비즈니스 로직을 구동하는 리듀서입니다.
/// - prepareDependencies: 리듀서에 의해 접근될 의존성을 재정의하는 데 사용할 수 있는 클로저입니다.

파라미터를 살펴보면 앱의 초기상태, Reactor와 같은 역할을 하는 리듀서 그리고 외부 의존성을 주입받을수 있다고 한다 그럼
자연스럽게 화면을 만들때 Store라는 객체를 통해 화면을 만들고 그안에 State와 리엑터와 같은 역할을 하는 Reducer 그리고 의존성을 주입받을수있는걸로 확인할수있다.

 

그럼 주입을 받은 RootView를 한번 먼저 살펴봐야 할 것같다. 

import SwiftUI
import ComposableArchitecture

struct RootView: View {
    @Perception.Bindable var store: StoreOf<RootDomain>
    
    var body: some View {
        WithPerceptionTracking {
            TabView(
                selection: $store.selectedTab.sending(\.tabSelected)
            ) {
                ProductListView(
                    store: self.store.scope(
                        state: \.productListState,
                        action: \.productList
                    )
                )
                .tabItem {
                    Image(systemName: "list.bullet")
                    Text("Products")
                }
                .tag(RootDomain.Tab.products)
                ProfileView(
                    store: self.store.scope(
                        state: \.profileState,
                        action: \.profile
                    )
                )
                .tabItem {
                    Image(systemName: "person.fill")
                    Text("Profile")
                }
                .tag(RootDomain.Tab.profile)
            }
        }
    }
}

 

코드를 보면 새롭게 공부해야할 내용이 2가지나 보인다.

 @Perception.Bindable var store: StoreOf<RootDomain> 에서 보면 주입받은
store 객체를 Perception.Bindable로 관리하고있고 해당 스토어객체는 RootDomain을 사용하고있는것같다

public typealias StoreOf<R: Reducer> = Store<R.State, R.Action>

아 Store에는 Reducer가 필요한데 그 Reducer를 명확하게 RootDomain 이라고 해준거구나! 라는 사실을 알수있다.

그럼 @Perception.Bindable은 무엇일까? 

SwiftUI를 사용하게 되면 일반적으로 @StateObject / @ObservedObject 개념을 많이 사용하게되는데 

여기서는 @Perception.Bindable을 사용하고있는데 한번 자세히 살펴봐야겠다

우선 Perception 부터 찾아보게 되는데 Perception은 SwiftUI에서 제공하는 Observable을 iOS 16이전의 버전에서도 사용하기위해 제공한다 라고 되어있다 Perception을 공부하려고했더니 Observable이란것을 알아야 한다 Observable이 무엇인지 찾아보자

https://www.pointfree.co/blog/posts/129-perception-a-back-port-of-observable#how-the-perception-library-works


Observable에 대해 에플에서 설명해주는 WWDC의 자료가 있다 
https://developer.apple.com/videos/play/wwdc2023/10149/

 

Discover Observation in SwiftUI - WWDC23 - Videos - Apple Developer

Simplify your SwiftUI data models with Observation. We'll share how the Observable macro can help you simplify models and improve your...

developer.apple.com

다음에는 해당 내용을 정주행해서 정리할 예정이다

 

일단 오늘은 TCA의 맛보기만을 즐겼다 우선 ReactorKit과 같은단방향 플로우를 지향하고 Store를 사용하고 만들때는 State를 주입받아야하고 Reducer라는 것을 사용한다. 그리고 기본적인 코드를 바라보았을때 ReactorKit과의 이질감은 없어보였다.

내일은 Observable부터 다시 공부해볼예정이다.