HIT해
[iOS/Swift] TCA 본문
TCA란?
TCA(The Composable Architecture)는 Swift와 SwiftUI를 사용하여 애플리케이션의 상태 관리와 비즈니스 로직을 다루기 위한 아키텍처.
TCA는 Point-Free라는 소프트웨어 개발 회사에서 만든 오픈 소스 라이브러리
구성요소
- State (상태): 애플리케이션의 현재 상태
- Action (액션): 상태를 변경하는 이벤트
- Reducer (리듀서): 액션에 따라 상태를 변경하는 함수
- Store (스토어): 상태, 리듀서, 및 액션을 관리하는 중앙 저장소
- Environment (환경): 외부 시스템과의 상호작용을 추상화한 것
구성요소 구체예시
- State (상태):
- 애플리케이션의 현재 상태를 나타내는 구조체
- 상태는 애플리케이션에서 중요한 데이터와 UI 상태를 포함
- 예시: 사용자 정보, UI의 현재 페이지, 로딩 상태 등
struct AppState : Equipable { var counter: Int var isLoading: Bool var Tmp : User }
- Action (액션):
- 상태를 변경하는 이벤트를 나타내는 열거형
- 액션은 사용자의 입력, 네트워크 응답 등 상태를 변화시키는 모든 이벤트를 포함
- 예시: 버튼 클릭, 데이터 로드 완료 등
enum AppAction { case increment case decrement case setLoading(Bool) }
- Reducer (리듀서):
- 액션에 따라 상태를 변경하는 순수 함수
- 리듀서는 현재 상태와 발생한 액션을 받아 새로운 상태를 반환
- 예시: 카운터 증가, 감소 등의 로직 처리
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in switch action { case .increment: state.counter += 1 return .none case .decrement: state.counter -= 1 return .none case .setLoading(let isLoading): state.isLoading = isLoading return .none } }
- Store (스토어):
- 상태, 리듀서 및 액션을 관리하는 중앙 저장소
- 스토어는 상태를 보유하고 있으며, 액션이 발생하면 리듀서를 통해 상태를 업데이트
- 또한, 스토어는 뷰와 연동되어 상태가 변경될 때 자동으로 UI를 업데이트
- 예시: ViewModel 역할 수행
let store = Store( initialState: AppState(counter: 0, isLoading: false), reducer: appReducer, environment: AppEnvironment() )
- Environment (환경):
- 외부 시스템과의 상호작용을 추상화한 구조체입니다. 여기에는 API 호출, 데이터베이스 액세스 등의 기능이 포함될 수 있습니다.
- 예시: 네트워크 서비스, 로컬 데이터베이스 등
struct AppEnvironment { var apiClient: APIClient }
MVVM을 대체해서 도입되는 이유
- 구조화된 상태 관리: TCA는 상태 관리와 비즈니스 로직을 명확하게 분리하여, 코드의 복잡성을 줄이고 유지 보수 용이
- 일관된 데이터 흐름: 단방향 데이터 흐름을 통해 데이터가 어떻게 변경되고 전달되는지 쉽게 추적
- 테스트 용이성: 리듀서와 액션은 순수 함수로 작성되므로 단위 테스트가 용이
- SwiftUI와의 자연스러운 통합: SwiftUI의 선언적 프로그래밍 패턴과 잘 맞아떨어지며, 뷰 업데이트를 자동으로 처리
- 확장성: 애플리케이션이 커질수록 모듈화와 확장이 용이
TCA는 보다 명확하고 구조화된 방법으로 상태 관리를 제공하여 대규모 애플리케이션에서도 일관성을 유지
TCA는 일관되고 포괄적으로 애플리케이션을 개발할 수 있도록 도와주는 라이브러리 입니다.TCA는 단방향 데이터 흐름 구조를 갖고 있어, 데이터 흐름을 파악하기가 쉽다보니 상태 변화를 추적하기가 쉽습니다.가장 큰 특징이라면 @Published 관련 속성을 사용하지 않아 코드 추적 및 테스트를 더 쉽다는 점입니다.TCA는 단방향 데이터 흐름을 강조하기에 State가 Reducer를 통해 업데이트되고 View에서 직접 변경될 수 없다는 것을 의미합니다.
TCA는 일관되고 포괄적으로 애플리케이션을 개발할 수 있음.
단방향 데이터 흐름 구조를 갖고 있고, 데이터 흐름을 파악하기 쉽다보니 상태변화를 추적하기 쉽다.
가장 큰 특징으로는 @Published 관련 속성을 사용하지 않아 코드 추적 및 테스트가 더 쉽다.
TCA는 단방향 데이터 흐름을 강조하기에 State가 Reducer를 통해 업데이트 되고 View에서 직접 변경될 수 있음
실제로 만들어보자
**카운터피처.스위프트
import ComposableArchitecture
@Reducer
struct CounterFeature {
@ObservableState
struct State {
var count = 0
var fact: String?
var isLoading = false
}
enum Action {
case decrementButtonTapped
case factButtonTapped
case factResponse(String)
case incrementButtonTapped
}
var body: some ReducerOf {
Reduce { state, action in
switch action {
case .decrementButtonTapped:
state.count -= 1
state.fact = nil
return .none
case .factButtonTapped:
state.fact = nil
state.isLoading = true
return .run { [count = state.count] send in
let (data, _) = try await URLSession.shared
.data(from: URL(string: "<http://numbersapi.com/\\(count)>")!)
let fact = String(decoding: data, as: UTF8.self)
await send(.factResponse(fact))
// send(.action명)으로 동작시킨다.
// run : 비동기 작업을 실행하고, 작업이 완료되면 특정 액션을 보내는 Effect를 생성
// API 호출 , 타이머, 데이터 처리 등 비동기 작업을 수행하고 그 결과에 따라 상태를 업데이트 헤야할때 사용
}
case let .factResponse(fact):
state.fact = fact
state.isLoading = false
return .none
// 왜 none 이냐 ObservableState로 감시하고있기때문애 값이 변경되면 자동으로 UI가 업데이트되기 때문이다.
// 액션에 비동기 작업이 필요 없을때 사용
case .incrementButtonTapped:
state.count += 1
state.fact = nil
return .none
}
}
}
}**
다른예시
if state.isTimerRunning {
return .run { send in
while true {
try await Task.sleep(for: .seconds(1))
await send(.timerTick)
}
}
.cancellable(id: CancelID.timer)
} else {
return .cancel(id: CancelID.timer)
}
.return .run { send in … } :
- send : 액션을 보내는 함수, 비동기 작업이 완료되거나 반복적으로 실행될때 이 함수를 사용해 액션을 디스패치함
Task.sleep(for:) : 지정된 시간 동안 비동기적으로 대기
try await : 비동기 작업이 완료될떄까지 기다림
cancellable(id : CancelID.timer)
- Effect에 취소 가능성을 추가.
return cancel
- 타이머가 실행중이지 않은 경우 이전에 설정된 타이머를 취소
- CancelID.timer를 사용해 타이머를 식별하고 취소
여러 store 구성방법
import ComposableArchitecture
import SwiftUI
struct AppView: View {
let store1: StoreOf<CounterFeature>
let store2: StoreOf<CounterFeature>
var body: some View {
TabView {
CounterView(store: store1)
.tabItem {
Text("Counter 1")
}
CounterView(store: store2)
.tabItem {
Text("Counter 2")
}
}
}
}
리듀서구성
@Reducer
struct AppFeature {
struct State: Equatable {
var tab1 = CounterFeature.State()
var tab2 = CounterFeature.State()
}
enum Action {
case tab1(CounterFeature.Action)
case tab2(CounterFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: \\.tab1, action: \\.tab1) {
CounterFeature()
}
Scope(state: \\.tab2, action: \\.tab2) {
CounterFeature()
}
Reduce { state, action in
// Core logic of the app feature
return .none
}
}
}
데이터 사전 입력
#Preview {
ContactsView(
store: Store(
initialState: ContactsFeature.State(
contacts: [
Contact(id: UUID(), name: "Blob"),
Contact(id: UUID(), name: "Blob Jr"),
Contact(id: UUID(), name: "Blob Sr"),
]
)
) {
ContactsFeature()
}
)
}
'Swift > Swift CS' 카테고리의 다른 글
[iOS/SwiftUI] TCA 프로젝트 뜯어보기 (0) | 2024.08.26 |
---|---|
[iOS/UIKit] UIKit 란 무엇일까 (0) | 2024.08.25 |
[iOS/Swift] Alamofire (0) | 2024.08.22 |
[iOS/Swift] UIViewRepresentable 이 무엇일까? (0) | 2024.08.20 |
[Swift 기초문법 - 37] 디자인패턴 Builder (0) | 2024.08.15 |