[iOS] 내가 보려고 정리하는 TCA (The Composable Architecture) 기초

2025. 3. 5. 11:57Swift Architecture

오늘 글은 SwiftUI 환경에 적합한 아키텍처, TCA (The Composable Architecture)의 기초 개념을 소개하는 내용입니다.

이 글에서 TCA가 어떤 특징과 장점을 갖고 있고,
어떤 코드 구조로 Apple Platform의 소프트웨어를 설계하게 되는지를 최대한 쉽게 여러분들께 설명하고자 합니다!
오늘 살펴본 기초 개념을 토대로, 다음 글에서는 실제 SwiftUI 프로젝트에서 The Composable Architecture를 적용하는 리팩토링 설명까지 이어가보도록 할게요☺️

 

TCA (The Composable Architecture)란?

1️⃣ TCA는 오픈소스 아키텍처 라이브러리다.

TCA는 The Composable Architecture의 줄임말로,
Brandon WilliamsStephen Celis가 운영하는 Point-Free"오픈소스 아키텍처 라이브러리" 입니다.
다르게는 Swift Composable Architecture, SCA라는 이름으로 부르는 경우도 본 적이 있지만, 공식문서상에는 TCA로 설명하고 있네요.

TCA는 다시 한번 말하지만 "오픈소스"입니다.
실제 코드와 가이드, 그리고 영상이나 튜토리얼까지 좋은 퀄리티로 (위의 두 분이) 매우 자세하게 아래 링크에서 설명하고 있는 편입니다.
그러다 보니, 러닝 커브는 높은 편일지 몰라도, 실제 아키텍처 사용법을 배우는 것은 그리 어렵지 않을거라 (감히) 생각하는데요.

오늘 글도 TCA 문서에서 공식으로 제공하는 Meet the Composable Architecture 튜토리얼을 바탕으로 설명을 해볼 예정입니다!

 

GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way,

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. - pointfreeco/swift-composable-architecture

github.com

 

2️⃣ TCA는 SwiftUI 환경에서 적용하기 좋은 아키텍처다.

TCA는 Apple 생태계 (iOS, macOS, iPadOS, visionOS, watchOS 등)의 개발 아키텍처입니다.

TCA 내부적으로는 Combine이 활용되고 있고,
선언형 방식의 패턴과 잘 맞는 아키텍처라 SwiftUI 환경에서 TCA를 많이 적용하는 편이기도 하지만, UIKit 환경에서도 마찬가지로 The Composable Architecture를 활용할 수 있습니다.

SwiftUI 환경에서 TCA가 적합한 아키텍처로 활용되는 이유는, 아래 TCA 구조와 흐름을 설명하기 전에 더 자세하게 소개해보도록 할게요!
"일단 TCA가 Combine을 활용해 구현되어 있고,
SwiftUI와 UIKit을 가리지 않고 Apple Platform의 개발 아키텍처로 활용될 수 있구나!" 정도로만 알고 넘어가면 될 것 같습니다.

 

3️⃣ TCA는 Composable하다.

TCA가 이곳저곳에서 널리 채택되는 이유는 너무도 당연하게(?) 장점이 많기 때문일 겁니다.

  • Composable하다.
  • 데이터의 경로가 단방향이라 앱 흐름을 파악하기에 용이하다.
  • 사이드 이펙트 (Side-Effect)를 테스트할 수 있어 코드의 안정성을 높일 수 있다.
  • 대규모 프로젝트에서 복잡한 앱의 상태 관리나 비즈니스 로직 처리, 테스트 코드 등을 작성하는 데 있어 구조적 일관성을 유지하며 개발할 수 있도록 도와준다.

나머지 장점은 차차 살펴보도록 하고,
지금 이 부분에서는 The Composable Architecture의 "Composable"한 특성이 무엇인지에 대해서만 알아보도록 할게요.


Composable을 직역하면, "조합 가능한" "구성 가능한"과 같은 표현으로 나타낼 수 있습니다.

직역보다 조금 더 의미론적으로 번역을 해보면,
"어떤 작은 요소들 여러 개가 합쳐져 -> 어떤 하나의 큰 무언가를 이룬다"의 느낌으로 해석할 수도 있을 것 같네요!

어? 이 표현 어디서 들어본 것 같은데?

"Try Composition first."
"구성을 먼저 고려하라."

예전 글을 보신 분은 알겠지만, Apple이 Combine 프레임워크를 발표할 때, 핵심 설계 원칙이라고 소개했던 말입니다.

작은 Operator 여러 개를 결합함으로써 -> 복잡한 데이터 처리 과정을 더 작은 단계로 나눌 수 있게 되고 -> 이로 인해 더 간결하고 직관적으로 Combine 프레임워크를 활용할 수 있다는 의미로
Apple은 Compostion이라는 단어를 사용해 표현했었죠.

Composition은 Combine 프레임워크를 설계한 애플의 핵심 디자인 원칙입니다.


TCA가 Apple에서 공식으로 지원하는 아키텍처 라이브러리는 아니지만,
이 아키텍처를 설계한 두 개발자도 결국 Apple의 Composition에 대한 생각과 같은 마음이 아니었을까 싶습니다.

애플리케이션에서 필요한 기능을 "어떤 작은 단위로 쪼개어 분리하고"
그 분리된 작은 단위들을 "독립적으로 실행 가능하도록 만들고"
독립적으로 실행 가능한 모듈 하나하나를 "상위 레벨에서 결합해 핵심 기능을 구현"하도록 TCA라는 Composable한 아키텍처를 만든 것이죠.

이를 통해 각 작은 단위들은 서로 영향을 미치는 범위를 현저하게 줄일 수 있을 것입니다.

이는 대규모 프로젝트의 경우,
어떤 부분에 수정사항이 발생하더라도 -> 다른 부분에 있어 수정이 필요한 사항을 최소화할 수 있다는 의미기도 하구요.
새로운 기능을 추가해야하는 상황이 있더라도 -> 기존 부분에서 추가하는 부분 역시 최소화할 수 있다는 의미기도 할 것입니다.

그렇기 때문에 특히 TCA는
"대규모 프로젝트" 그리고 "복잡한 비즈니스 로직과 함께 데이터 공유가 많이 일어나는 상황", "독립적으로 테스트 요구사항을 수행하고 싶을 때" 적용하면 더욱 빛을 보게되는 아키텍처라 보면 됩니다!
*소규모 프로젝트에서 TCA를 적용하는 것은 불필요하게 앱의 규모를 늘리기만 할 수 있습니다. 모든 아키텍처가 그렇듯이 항상 설계 원칙에는 해당 패턴을 사용하는 타당한 이유를 갖고 있어야 한다는 점 잊지 마세요😊


지금부터는 TCA의 구조가 어떤 작은 요소들 하나하나로 이루어지는지, 

이 작은 요소들이 어떻게 Composable 하게 흐름을 이어가는지 등 다루지 못했던 다른 장점들에 대해서 살펴보도록 할게요!

 

 

TCA (The Composable Architecture) 구조와 기본 흐름 뽀개기

✔️ 잠깐! 일단 SwiftUI와 단방향 데이터 흐름과 관련된 이야기부터 하고 갑시다.

TCA 구조의 가장 큰 특징이자 장점은 "단방향 데이터 흐름 (Unidirectional Data Flow)"을 기반으로 설계되었다는 점입니다.

iOS 개발에서 가장 대표적이라고 볼 수 있는 MVVM 구조만 살펴보더라도
View와 ViewModel 사이, ViewModel과 Model 사이가 서로 양방향 데이터 흐름으로 이어지고 있다는 것을 확인할 수 있습니다.

"단뱡향 데이터 흐름보다 양방향 데이터 흐름이 무조건 좋다!"를 제가 주장하고 싶은 것은 아닙니다.

아무래도 양방향 데이터 흐름에서 데이터의 흐름을 파악하기가 단방향에 비해 상대적으로 어렵고,
양방향 관계에서는 강한 결합 문제가 발생할 수도 있다는 등 여러 근거도 일리있는 얘기이긴 하지만,
사실 이것들은 "꼭 단방향 흐름을 채택하는 아키텍처"를 사용함으로써 해결할 수 있는 문제들은 아니거든요.

충분히 양방향 흐름에서도 다른 우회적인 방법으로 해결할 수 있는 내용이기는 합니다.


그것보다는 기존 명령형 UI 환경인 UIKit에서 선언적 UI 환경인 SwiftUI로 최신 트렌드가 옮겨가는 과정에 있어,
과연 기존 MVC, MVVM과 같은 "양방향 데이터 흐름을 채택하는 아키텍처가 최선의 방식인가?"라는 질문에서 TCA의 선호 흐름을 봐야 할 필요가 있다는 주장을 하고 싶습니다.

Apple의 대표 아키텍처인 MVVM


흠... 위 질문에 대한 답을 하자면, 블로그 글 한 편이 뚝딱 나올 정도로 길어질 것 같네요. 간략하게만 하겠습니다.

이 내용이 TCA가 UIKit 환경과 SwiftUI 환경 모두 지원하는 라이브러리긴 합니다만,
선언형에 해당하는 SwiftUI에 더 적합하게 설계되었다는 위의 설명과 이어지는 내용이기도 해서.. 다루기는 해야 할 것 같습니다.

아무튼 UIKit만을 사용하던 과거와는 달리,
최근 흐름에 속하는 SwiftUI에서의 데이터 바인딩은 굳이 ViewModel을 두지 않더라도 View 자체에서 프로퍼티 래퍼를 사용하는 방식으로 구현할 수 있다는 점.

다만 "비즈니스 로직까지 View에서 처리해야 하는가?"라는 질문에 대한 답이 "No"라면,
이에 대한 해결책으로 등장하는 흐름이 "단방향 데이터 흐름 (Unidirectional Data Flow)"이라는 점.

여기서 데이터 바인딩이 아니라 비즈니스 로직을 View로부터 보다 효율적으로 분리하는 과정에서 떠올리게 된 단방향 데이터 흐름 구조가 Flux였고,
TCA는 이 단방향 데이터 흐름이라는 Flux 컨셉을 받아 발전시킨 아키텍처 라이브러리라고 이해하면 될 것 같습니다.

뭐 길게길게 설명했는데 그냥 읽기 싫다면 이것만 기억합시다.

✔️ 선언형 프로그래밍 방식 (SwiftUI)에서 적합한 데이터 흐름 구조를 떠올리다가 등장한 것이 바로 단방향 데이터 흐름 (Unidirectional Data Flow)이다.
✔️ 단뱡향 데이터 흐름을 제안한 대표 아키텍처가 Flux이고, 이 컨셉을 발전시킨 것이 TCA (The Composable Architecure)이다.

🤔 그래서 TCA의 단방향 흐름은 어떤데?

아래 보이는 것처럼 TCA에는 (MVVM과 같이 화살표가 양쪽을 주고받는 것 없이) 한방향으로만 가리키는 것을 확인할 수 있습니다.

먼저 TCA를 compose하고 있는 각 요소들이 각각 어떤 역할을 하는지 살펴보죠!

  • State : 말 그대로 애플리케이션의 상태 (State)를 나타내는 타입입니다. 여기서의 애플리케이션 상태란 UI를 그리기 위한 데이터나 비즈니스 로직이 해당될 수 있겠네요.
  • Action : 애플리케이션에서 발생하는 모든 이벤트를 정의한 열거형 (enum)입니다. 사용자의 버튼 탭, 네트워크 응답, 알림 등 모든 이벤트가 포함될 수 있죠.
  • Reducer : 입력된 Action에 따라 새로운 State 값을 업데이트하던지, API 요청과 같은 Effect를 반환하는 함수입니다.
  • Store : State, Action, Reducer를 실제 관리하고, 개발자가 정의한 애플리케이션의 모든 기능이 동작하는 핵심 공간이라고 생각하면 됩니다.
  • Environment : 애플리케이션 외부로부터 데이터를 받아올 때, 애플리케이션이 갖게 되는 의존성을 갖고 있는 타입입니다.

The Composable Architecture의 단방향 데이터 흐름


그럼 실제 TCA 동작 흐름이 어떻게 이루어지는지 자세하게 설명해 보겠습니다!

시작은 가장 왼쪽에 있는 View에서부터 출발합니다.
View(= UI)에서는 사용자가 버튼을 클릭하거나, 텍스트를 입력하는 등의 이벤트, 즉 Action을 발생시킬 수 있습니다.

View는 Store를 구독하고 있기에, View에서 발생한 Action은 Store로 즉시 전달됩니다.

Store는 전달받은 Action을 처리하기 위해 Reducer (함수)를 호출합니다.
*Action이 Store가 아니라 Reducer로 바로 이동하는 흐름으로 봐도 무방하지만, 정확하게는 Store 내부에서 Reducer 함수 부분을 호출하는 쪽으로 보는게 더 정확할 것 같아 구분해 설명합니다!

Reducer는 전달받은 Action의 종류에 따라 State 값을 업데이트하거나 / Effect를 반환하도록 결과를 반환합니다.
단순 View의 상태를 변화시키는 작업은 바로 State로,
앱 외부와 관련된 복잡한 비동기 처리 작업은 Effect로 Reducer에서 결과가 나뉘어진다고 생각하면 됩니다.

State로 바로 넘어가는 화살표를 따라가 보죠.
단순히 View로 보여지는 카운트 값을 증가시키는 경우에는 Reducer에서 State 값만 단순 업데이트하면 됩니다. (state.count += 1)
이렇게 업데이트된 State는 SwiftUI의 View와 바인딩되어 새롭게 UI를 그리는 데 반영되는 것이죠.

하지만 Effect로 넘어가는 경우는 이와는 조금 다릅니다.
대표적으로 비동기 네트워크 작업을 한다고 했을 때, State가 아닌 Effect 타입으로 결과를 반환하게 될 건데요.
비동기 네트워크 작업의 결과가 원하는 효과 (Good Effect)를 가져올 수도 있지만, 그렇지 않을 수도 있다는 점을 대비하고 있는 셈입니다.

이때 네트워크 작업 (= 비동기 작업)의 결과가 원하지 않은 효과를 가져왔을 때, 우리는 이것을 사이드 이펙트 (Side Effect)라고 부르는 것입니다.


아무튼 원하는 효과가 나타났던, 원하지 않은 사이드 이펙트가 발생을 했든 간에.
이 모든 Effect는 아무튼 다시 Action으로 취급되어 Reducer 함수에 전달됩니다.

위에서 TCA가 사이드 이펙트 (Side Effect)를 테스트하고, 처리하기 용이하다고 장점으로 언급했던 부분이 바로 이 맥락입니다.

원하는 효과가 나타났으면, 그에 따라 적절한 State 값을 업데이트해 View에 다시 전달해주면 되는 것이고요.
만약 원하지 않은 사이드 이펙트가 발생했다면,
<다시 네트워크 요청을 하던지 / 알럿을 띄워 사용자에게 안내를 해주던지>와 같은 예외 처리를 단방향 흐름으로 해줄 수 있다는 것이죠.


어떤가요?
단방향으로 화살표를 따라갈 수 있으니, 애플리케이션 작업의 흐름과 처리 방식을 더 명확하게 확인할 수 있지 않은가요?

자 이제는 TCA를 실제 Swift 코드로 만나볼 시간입니다!

 

 

The Composable Architecutre Tutorials : TCA 구조 코드로 살펴보기

우선 TCA를 사용하기 위해서는 SPM (Swift Package Manager)을 활용해 라이브러리 의존성을 추가해줘야 합니다.

먼저, Package URL 창에 https://github.com/pointfreeco/swift-composable-architecture를 입력해 의존성을 추가해 줍시다.


이제 본격적으로 TCA를 적용해 Tutorial에 나와있는 간단한 애플리케이션을 만들어보고자 합니다.

해당 Tutorial 앱의 기능을 구현하면서
기초적인 TCA의 상태 관리, 타이머 동작, 그리고 네트워크 요청과 사이드 이펙트 처리 방식까지 알아보도록 할게요!

  • +버튼과 -버튼을 활용한 간단한 카운트 상태 값의 증감 기능
  • Start timer 버튼과 Stop timer 버튼을 사용한 타이머 기능의 동작
  • Fact 버튼에서 현재 카운트 값과 관련된 API 요청을 보내고, 네트워크 통신 중에는 Progress View로 로딩 상태를 표출

 

구현할 애플리케이션 화면!


우선 앱의 상태를 State 구조체에 정의합니다.
SwiftUI 기반의 View는 @ObservableState로 정의된 해당 상태 값과 바인딩되어, UI를 실시간으로 렌더링할 수 있게 됩니다.

  • count : 현재 View에 표출되는 count 값
  • fact : 카운트와 관련된 정보 - API 통신 이후의 응답값을 담기 위해 만들어진 자료형
  • isLoading : 네트워크 요청이 현재 진행중인지 여부 - 해당 Bool 값을 바탕으로 ProgressView의 표출 여부가 결정될 것이죠!
  • isTimerRunning : 현재 타이머가 실행중인지 여부
@ObservableState
struct State: Equatable {
    var count = 0
    var fact: String?
    var isLoading = false
    var isTimerRunning = false
}


Action에는 사용자의 입력이나 시스템에서 발생할 수 있는 이벤트를 열거형 (enum)으로 정의합니다.
각 액션은 특정 State 값을 변경 (카운트 값의 증감)하거나, 특정한 로직 실행 (네트워크 요청, 응답, 타이머의 실행과 중지)을 처리하기 위해 만들어져 있죠.

enum Action {
    case decrementButtonTapped     // 사용자가 - 버튼을 탭했을 때
    case incrementButtonTapped     // 사용자가 + 버튼을 탭했을 때
    case factButtonTapped          // 사용자가 Fact 버튼을 탭했을 때
    case factResponse(String)      // Fact 네트워크 요청에 대한 응답이 왔을 때
    case timerTick                 // 타이머가 움직였을 때
    case toggleTimerButtonTapped   // 사용자가 타이머 토글 버튼을 탭했을 때
}


Reducer는 입력된 Action에 따라 상태(State)를 업데이트하거나 Effect를 반환하는 역할을 합니다.

Reducer로 입력될 수 있는 Action은 위에서 정의한 열거형 값이 해당되겠죠?
입력되는 action에 따라 switch-case문으로 나누어 단순 State만 업데이트하던지, Effect를 반환할지를 정하는 형태입니다.
해당 메서드의 반환 값은 Effect<Action>입니다.

카운트 값을 증감하는 incrementButtonTapped와 decrementButtonTapped 케이스의 경우에는,
단순 카운트 값만 증가하거나 감소하고, 기존 fact 데이터를 nil로 초기화하는 역할만 해줍니다.
여기서는 State 값만 업데이트해줄 뿐, 어떠한 Effect도 발생시키지 않기 때문에 .none을 반환하고 있는 것이죠.

반면, fact 버튼을 누르는 경우에는 네트워크 요청을 통해 숫자와 관련된 fact 정보를 가져와야 하기 때문에,
run을 반환해 비동기 작업 (numberFact.fetch)을 실행하고 응답을 .factResponse로 전달하는 것을 확인할 수 있습니다.
이때, State에 해당하는 로딩 상태 (isLoading)이나, fact String 값도 동시에 업데이트할 수 있다는 것도 확인할 수 있네요!

타이머도 네트워크 요청 부분과 유사합니다.
타이머 시작/정지 버튼을 탭하면 -> 타이머 실행 여부 (isTimerRunning)를 토글하고 -> 타이머가 실행 중이라면, 매 초마다 .timerTick 액션을 발생시켜 카운터 값을 증가 / 타이머가 정지되면, .cancel(id:)로 해당 작업을 취소하는 흐름입니다.

@Reducer
struct CounterFeature {    
    ...
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .decrementButtonTapped:
                state.count -= 1
                state.fact = nil
                return .none
                
            case .incrementButtonTapped:
                state.count += 1
                state.fact = nil
                return .none
                
            case .factButtonTapped:
                state.fact = nil
                state.isLoading = true
                return .run { [count = state.count] send in
                    try await send(.factResponse(self.numberFact.fetch(count)))
                }
                
            case .factResponse(let fact):
                state.fact = fact
                state.isLoading = false
                return .none
                
            case .timerTick:
                state.count += 1
                state.fact = nil
                return .none
                
            case .toggleTimerButtonTapped:
                state.isTimerRunning.toggle()
                if state.isTimerRunning {
                    return .run { send in
                        for await _ in self.clock.timer(interval: .seconds(1)) {
                            await send(.timerTick)
                        }
                    }
                    .cancellable(id: CancelID.timer)
                } else {
                    return .cancel(id: CancelID.timer)
                }
            }
        }
    }
}

 

앱 내부가 아니라, 외부에서 데이터를 받아오는 경우.
이 애플리케이션 기준 타이머나, API 같은 외부 의존성을 주입하기 위해 Environment 부분에 해당하는 Dependency를 이용했습니다.

@Dependency(\.continuousClock) var clock
@Dependency(\.numberFact) var numberFact


타이머 같은 경우는 continuousClock이라는 DependencyValues로 이미 등록되어 있어 사용할 수 있지만,
URL로 네트워크 요청을 보내는 numberFact는 직접 DependencyValues로 등록해 의존성을 형성가능하도록 만들어줘야 합니다.


NumberFactClient는 Int 값을 입력받고 -> String을 반환받는 비동기 호출 함수 fetch를 포함하고 있습니다.

해당 클라이언트 (NumberFactClient)는 DependencyKey 프로토콜을 채택합니다.
이곳에서는 liveValue라는
실제 애플리케이션이 실행될 때 사용할 기본 의존성을 정의하는데,

네트워크 요청을 수행할 수 있는 클라이언트의 fetch 비동기 함수의 구체적인 부분을 정의하고 있습니다.

마지막으로는, TCA에서 전역으로 외부 의존성을 사용하기 위해 DependencyValues에 앞에서 정의한 클라이언트를 등록합니다.

struct NumberFactClient {
    var fetch: (Int) async throws -> String
}

extension NumberFactClient: DependencyKey {
    static let liveValue = Self(
        fetch: { number in
            let (data, _) = try await URLSession.shared.data(from: URL(string: "http://numbersapi.com/\(number)")!)
            return String(decoding: data, as: UTF8.self)
        }
    )
}

extension DependencyValues {
    var numberFact: NumberFactClient {
        get { self[NumberFactClient.self] }
        set { self[NumberFactClient.self] = newValue }
    }
}

 

Store는 지금까지 말했던 State, Reducer, Action까지 모든 핵심 기능을 포함하는 곳이 Store입니다.

Store는 View에서 연결되어 UI 업데이트를 트리거하는 역할입니다.
이 코드에서는 Store가 View에 주입되어 상태와 액션 처리를 담당합니다. (위의 코드들이 CounterFeature라는 이름으로 정의되어 있는 것!)

let store: StoreOf<CounterFeature>


마지막 View 부분에서는 이 store를 통해 Action 값을 전달하고, State 값을 받아 화면을 그리고 있는 것을 확인할 수 있습니다!

import ComposableArchitecture
import SwiftUI

struct CounterView: View {
    let store: StoreOf<CounterFeature>
    
    var body: some View {
        VStack {
            Text("\(store.count)")
                ...
            HStack {
                Button("-") { store.send(.decrementButtonTapped) }
                Button("+") { store.send(.incrementButtonTapped) }
            }
            Button(store.isTimerRunning ? "Stop timer" : "Start timer") {
                store.send(.toggleTimerButtonTapped)
            }
            ...
            Button("Fact") { store.send(.factButtonTapped) }
            ...
            
            if store.isLoading {
                ProgressView()
            } else if let fact = store.fact {
                Text(fact)    
            }
        }
    }
}

#Preview {
    CounterView(
        store: Store(initialState: CounterFeature.State()) {
            CounterFeature()
        }
    )
}

 

 

마무리!

이번 글에서는 TCA의 기본 개념과 코드, Composition의 의미,
그리고 단방향 데이터 흐름이 선언형 SwiftUI에서 갖는 이점까지 자세하게 살펴봤습니다.

확실히 코드로 애플리케이션의 전체 흐름을 따라가다 보니
데이터가 어떻게 전달되고, 비동기 처리를 비롯한 비즈니스 로직이 어떤 식으로 처리되고, 사이드 이펙트는 어떤 방식으로 관리하는지 등을 쉽게 파악할 수 있었던 것 같습니다.

항상 제가 하는 말이 "아키텍처에 대한 절대적인 정답은 없다는 것"인데요.

본인이 사용하고자 하는 앱의 구조와, 확장성, 유지보수성을 고려해 "타당한 이유"를 갖고 아키텍처를 설계하는 것이 중요하다는 점!
잊지 마시길 바랍니다.
다음에 기회가 되면, 보다 구체적으로 TCA를 적용한 글로 돌아오도록 하겠습니다! 감사합니다 😊😊

 

 

Reference

 

Documentation

 

pointfreeco.github.io

 

[SwiftUI] The Composable Architecture(TCA) 입문

이 글을 작성하던 2022년 7월에는 TCA 가 0.38.X 버전대였습니다.

medium.com

 

Swift: The Composable Architecture(TCA) 맛보기

Swift 컨퍼런스에 자주 등장하는 TCA 아키텍쳐… 정체가 뭐야❓🤔 이제부터 정체를 파헤쳐보자(팍팍!)

medium.com

 

Riiid의 Swift Composable Architecture

by 이건석

medium.com

 

Swift Composable Architecture 를 도입하며 겪었던 문제와 해결법

TCA를 도입하며 겪었던 문제와 해결법

channel.io