
안녕하세요.
대덕소프트웨어마이스터고등학교 2학년 Team Finda iOS의 하원입니다.
본 글은 프로젝트를 설계하기 위해 필요한 지식을 공부하여 정리한 글입니다.
부족한 점이나 틀린 부분이 있다면 편하게 말씀해 주시면 감사하겠습니다.
또한, 가독성을 위해 ~다 체를 사용하는 점 미리 양해 부탁드립니다. 🙇
기존에 UIKit 기반에서 MVVM + Clean Architecture를 많이 사용해왔다.
그러나 최근 iOS 개발 트렌드를 살펴보면,
반응형 UI를 중심으로 SwiftUI 사용이 점차 확대되고 있음을 알 수 있다.
이러한 변화 속에서 기존 개발 방식에도 변화가 필요하다고 느꼈고,
SwiftUI와 궁합이 좋다고 알려진 TCA(The Composable Architecture)를 프로젝트에 적용해보고자 한다.
Github 참고
https://github.com/pointfreeco/swift-composable-architecture
블로그 형식 및 내용 참고
What’s your ETA What’s your TCA ~Mmm-hmm (SwiftUI TCA를 시작하기 전에 보면 좋은 글)
TCA(The Composable Architecture)은 구성, 테스트 및 사용 편의성을 고려하여 일관되고 이해하기 쉬운 방식으로 애플리케이션을 구축하기 위한 라이브러리다.
SwiftUI, UIKit 등을 비롯한 다양한 프레임워크에서 사용할 수 있으나, SwiftUI랑 찰떡궁합으로 알고 있다. 👍
저 위에 있는 Github 링크에 따르면,
다양한 목적과 복잡성을 가진 애플리케이션을 구축하는 데 사용할 수 있는 몇 가지 핵심 도구를 제공한다고 한다.
또한 State management, Composition, Side effects, Testing, Ergonomics의 문제를 해결한다고 한다.
TCA는 크게 다음 네 가지 요소를 중심으로 구성된다.
State: 기능이 로직을 수행하고 UI를 그리는 데 필요한 상태를 정의한다.
Action: 사용자의 입력, 시스템 이벤트 등 기능에서 발생할 수 있는 모든 이벤트를 나타낸다.
Reducer: Action에 따라 State가 어떻게 변경되는지를 정의하는 역할을 한다.
Store: State와 Action을 연결하고, 실제로 기능을 실행하는 런타임 환경이다.
라고 간단한 설명을 할 수 있지만
프로젝트에 적용하기 위해선 흐름을 알아야 하기 때문에 Flow와 함께 보겠다.

출처: [Swift] TCA (The Composable Architecture)
그럼 이제 이 Flow를 기준으로 흐름을 하나하나 파헤쳐보겠다.
Action은 그냥 간단하게 화면에서 사용자가 발생시키는 event라고 생각하면 된다.
TCA에서는 사용자의 모든 Action에 대해 enum의 case로 정의를 해야 한다.
Action의 명칭에는 나름의 규칙이 있는데,
Button을 Tapped 했다 처럼 기술되어야 한다. (이건 나중에 문서화에 적어두어야 할 듯)
원래 그냥 SwiftUI라면
버튼을 눌러 State를 View에서 직접 바꾸어 줬겠지만, TCA는 좀 다르다.
직접적으로 바꾸지 않고 엄격하게 단방향으로 관리하기 위한 Reducer가 존재한다.
State는 그냥 말대로 UI의 상태인데
이제 그걸 관리하기 위해 State 값을 State 구조체로 관리한다.
그 구조체 안에 상태 변수가 선언되어 있겠지?
그 변수의 값이 변하면, View를 다시 그린다고 생각하면 편하다.
@Reducer
struct Feature {
struct State: Equatable { // State는 무조건 Equatable 프로토콜을 준수해야 함
var count = 0
}
enum Action: Equatable {
case plusButtonTap
case minusButtonTap
}
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .plusButtonTap:
// 더하기 작업 구현
state.count += 1
return .none
case .minusButtonTap:
// 빼기 작업 구현
state.count -= 1
return .none
}
}
}
출처: [iOS][SwiftUI] TCA란? TCA를 활용한 간단 예제
TCA에서 Reducer 하나의 단위를 Feature라는 접미사를 사용한다 .
(ex. struct AllCategoryListFeature: Reducer {} 또는 @Reducer struct AllCategoryListFeature {} )
Redcuer는 State와 Action을 인자로 받아
action으로 State의 count를 변경하는 것을 확인할 수 있다.
여기서 Reduce는 Effect를 반환하는데 (위에 return .none) 그건 아래서 살펴보겠다.
TCA에서는 Action에 의해 State 변경 뿐만 아니라 Side Effect를 발생시킬 수 있다.
(ex. 좋아요 버튼이 클릭 -> 서버에 상태값을 전달 -> UI에 이 변경사항의 성공 여부를 반영)
Effect는 다시 Action에 반영되고
이 Action이 별다른 side effect가 없다면 return으로 .none을 반환하고
해당 Action에 맞는 State를 변경하거나 비즈니스로직을 수행하면 된다.
즉 Effect는 비동기 작업을 수행한 뒤,
그 결과를 다시 Action으로 변환해 Reducer로 되돌린다.
자 이제 View → Action → Reducer → State, Effect → Action을 했으니
Reducer을 View와 연결해야 된다.
Store은 앱의 상태를 보관하고 관리하는 핵심 객체이며,
State와 Action을 연결하는 Reducer를 View와 연결하는 역할을 한다.
let store: StoreOf<Feature>
var body: some View {
// 직접 store를 사용
Text("\(store.count)")
Button("Plus") {
store.send(.plusButtonTap)
}
}
Store는 보통 부모가 생성해서 자식에게 전달한다.
자식 View는 전달받은 Store를 통해 State를 읽고 Action을 전달할 뿐,
직접 Store를 생성하지 않는다.
참고
구버전(TCA 1.0 이전)에서는 ViewStore를 별도로 사용했으나,
현재는 store를 직접 사용하는 방식으로 간소화되었다.
다 끝난줄 알았지만, 플로우에 Dependency가 남아있다..
Dependency는 Client라고도 하는데
이를 통해 네트워크 요청, 데이터베이스 작업, 또는 기타 외부 서비스와의 상호작용을 캡슐화하고 테스트 가능한 형태로 만들 수 있다.
TCA에서는 외부로 부터 전달 받는 값에 대해
의존성을 낮추기 위해 Dependency라는 유용한 기능을 제공한다.
그냥 Dependency를 정의하고 사용하는곳에서 dependency의 메소드 사용하라는데..
이 부분이 정말 어려웠다.
보통은 Protocol를 이용한다고 알고있다. (struct도 이용 가능)
쉬운 이해를 위해 지피티한테 예시 코드를 요청했다.
버튼 누르면 서버 대신 가짜 API에서 숫자를 받아와서 화면에 보여주는 구조의 예시다.
struct CounterClient {
var fetchCount: () async throws -> Int
}
이렇게 먼저 프로토콜을 설계한다.
import ComposableArchitecture
extension CounterClient: DependencyKey {
static let liveValue = CounterClient(
fetchCount: {
// 실제 서버 대신 가짜 값 원래 여기에 api 호출 메소드 구현해야 함
try await Task.sleep(for: .seconds(1))
return Int.random(in: 0...100)
}
)
}
extension DependencyValues {
var counterClient: CounterClient {
get { self[CounterClient.self] }
set { self[CounterClient.self] = newValue }
}
}
SwiftUI) TCA에서의 의존성 주입, dependency 블로그랑 유사한 코드다.
이렇게 안하고 fetchCount를 함수로 정의하여 struct로 하나하나 정의한 후에 DependencyKey할 때
static let liveValue: any CounterClient = struct
방식으로도 하던데 그게 더 깔끔해 보였다.
TCA - Dependency 설계 이 블로그가 그런 방식을 사용한다.
이건 직접 코드를 짜보고 결정해봐야 할 것 같다.
TCA를 학습하면서 한 가지 의문이 들었다.
TCA는 정말 아키텍처일까?
이름에는 분명 'Architecture'가 들어가지만,
실제로는 아키텍처보다는 패턴에 가깝다는 생각이 든다.
MVVM과 비교되는 것만 봐도 그렇다. MVVM이 패턴으로 분류되듯, TCA 역시 State 관리와 단방향 데이터 흐름을 강제하는 구체적인 구현 패턴에 가깝다고 느꼈다.
Clean Architecture나 레이어드 아키텍처처럼 전체 애플리케이션의 구조와 계층을 정의하는 것과는 결이 다르다. TCA는 Reducer, Effect, Dependency 등 매우 구체적인 구현 방식을 제시한다.
이러한 나의 생각을 다른 개발자분들과 공유하고 싶다.
끝까지 읽어주셔서 감사합니다. 🙇♀️
글에 대한 피드백은 언제나 환영입니다.
좋은 글 감사합니당
TCA 어떻게 사용할까? > Action → Reducer → State 에서 State는 UI의 상태라고 설명해주셨는데,
그러면 UI 외적으로 비즈니스 규칙을 표현하기 위한 상태를 표현하고자 할때는 TCA를 사용할 수 없는건가요?
(아니면 권장하지 않는다던가..)
가령 자동 로그인 기능을 구현한다고 가정한다면,
아이디랑 비번은 어디에 저장되어있어야 하는지 조금 애매할 것 같아서요...!
물론 자동로그인은 한번 하는거니까 DB에서 꺼내와서 실행할 법도 한데, State가 UI의 상태라면 그 외의 상태들은 앱 내 어디에서 관리되는것인지 궁금합니다