저는 최근 Clean architecture를 프로젝트에 도입하기 위해서 VIP 패턴을 사용하여 개발을 하고 있습니다, 그러다 요즘은 너무나 많이 사용하는 TCA가 궁금해져서 공부를 해보았는데요
TCA란?
iOS 개발에서 언급되는 TCA는 The Composable Architecture의 약자로, Swift 언어를 사용한 iOS 앱 개발에서 점점 더 많이 사용되고 있는 아키텍처 패턴입니다. The Composable Architecture는 Point-Free라는 개발자가 설계한 라이브러리로, 애플리케이션의 상태 관리, 사이드 이펙트 처리, 모듈성, 테스트 가능성을 개선하는 데 중점을 둡니다.
TCA를 사용하는 이유:
- 상태 관리의 일관성: TCA는 앱의 상태를 중앙에서 관리합니다. 이를 통해 여러 UI 컴포넌트가 동일한 상태를 일관되게 공유하고 업데이트할 수 있습니다. Redux와 유사한 패턴으로, 상태 변화가 단방향으로 이루어지며 예측 가능해집니다.
- 사이드 이펙트 관리: 네트워크 요청이나 데이터베이스 접근 등 사이드 이펙트를 명확하게 처리할 수 있도록 도와줍니다. 사이드 이펙트는 Effect라는 타입으로 캡슐화되며, 이를 통해 코드의 가독성과 유지보수성을 높일 수 있습니다.
- 모듈화 및 재사용성: TCA는 애플리케이션의 기능을 작은 모듈로 나눌 수 있게 해줍니다. 이러한 모듈은 독립적으로 개발, 테스트, 유지보수할 수 있으며, 다른 프로젝트나 앱에서도 재사용할 수 있습니다.
- 테스트 가능성: TCA는 순수 함수와 데이터 기반 상태 관리를 중심으로 설계되어 있어, 테스트가 용이합니다. 애플리케이션의 상태와 액션을 쉽게 시뮬레이션하고, 사이드 이펙트를 테스트할 수 있습니다.
- SwiftUI와의 높은 호환성: TCA는 SwiftUI의 데이터 흐름과 자연스럽게 어우러집니다. SwiftUI의 선언적 UI 패턴과 TCA의 상태 관리 및 사이드 이펙트 처리 메커니즘이 잘 맞아 떨어집니다.
요약
TCA는 iOS 앱 개발에서 복잡한 상태 관리, 모듈화, 테스트 가능성을 개선하고자 하는 개발자들에게 유용한 도구입니다. 특히 SwiftUI와의 결합에서 강력한 효과를 발휘해, 점점 더 많은 개발자들이 TCA를 선택하고 있습니다.
Architecture와 Design Pattern 차이를 아시나요? 해당 개념을 정리하지 않고 공부를 하다보니 머리가 아프고 햇갈리는게 만더라구요 (저도 알고 싶지 않았습니다...)
아키텍처와 디자인 패턴: 개념, 차이점, 그리고 이유
소프트웨어 아키텍처와 디자인 패턴은 소프트웨어 설계의 핵심 개념이지만, 그 역할과 적용 범위는 다릅니다. 이 두 가지를 이해하는 것은 고품질 소프트웨어를 설계하고 유지보수하는 데 필수적입니다.
개념을 상세하게 설명했지만 제가 생각하는 주요 키워드 위주로 형광펜을 치겠습니다. 해당 부분 위주로 봐주세요.
1. 소프트웨어 아키텍처 (Software Architecture)
아키텍처는 소프트웨어 시스템의 전반적인 구조를 정의하는 고수준(한 차원 더 높은 단계)의 설계입니다. 이는 시스템의 주요 구성 요소와 그 상호 작용 방식을 정의하며, 프로젝트 전반에 걸쳐 사용됩니다. 아키텍처는 시스템의 구조, 의사소통 방식, 모듈화, 의존성, 확장성 등을 고려합니다.
주요 특징:
- 전반적인 구조: 아키텍처는 시스템의 전반적인 구조와 큰 그림을 그립니다. 예를 들어, 클린 아키텍처, 레이어드 아키텍처, 마이크로서비스 아키텍처 등이 있습니다.
- 모듈 간의 관계: 시스템을 구성하는 주요 모듈들이 어떻게 상호작용하는지 정의합니다.
- 유지보수성: 시스템이 변경되거나 확장될 때 아키텍처는 그 영향도를 최소화하도록 설계됩니다.
- 성능과 확장성: 아키텍처는 시스템의 성능 및 확장성을 고려하여 설계됩니다.
사용 이유:
- 유지보수와 확장성: 시스템의 복잡성을 관리하고, 향후 확장 및 변경을 용이하게 합니다.
- 의사소통의 명확성: 팀 간의 협업을 촉진하며, 모든 팀원이 시스템의 큰 그림을 이해할 수 있도록 돕습니다.
- 품질 향상: 일관된 아키텍처는 시스템의 안정성과 품질을 높여줍니다.
2. 디자인 패턴 (Design Pattern)
디자인 패턴은 소프트웨어 설계에서 자주 발생하는 문제를 해결하기 위한 반복 가능한 해결책입니다. 이는 특정 문제를 해결하기 위한 구체적인 설계 방법을 제공합니다. 디자인 패턴은 아키텍처보다 낮은 수준에서, 코드의 구조와 구현에 집중합니다.
주요 특징:
- 구체적인 설계 방법: 디자인 패턴은 특정 상황에서의 문제 해결을 위한 구체적인 코딩 방법을 제공합니다. 예를 들어, 싱글톤 패턴, 팩토리 패턴, 옵저버 패턴 등이 있습니다.
- 재사용 가능성: 패턴은 반복적으로 사용 가능한 해결책을 제공하여, 코드의 재사용성을 높입니다.
- 명확한 용어: 패턴은 소프트웨어 설계에 대한 공통 언어를 제공하여, 개발자 간의 의사소통을 용이하게 합니다.
사용 이유:
- 반복적인 문제 해결: 자주 발생하는 문제를 해결하기 위한 검증된 방법을 제공합니다.
- 코드 품질 향상: 디자인 패턴을 사용하면 코드의 명확성과 유지보수성을 높일 수 있습니다.
- 효율성: 디자인 패턴은 이미 검증된 해결책이므로, 개발 시간을 절약하고 오류를 줄일 수 있습니다.
3. 아키텍처와 패턴의 차이점
- 적용 범위:
- 아키텍처는 시스템 전체의 구조를 다루며, 전반적인 설계와 관련된 의사결정을 포함합니다. 이는 시스템의 모든 모듈과 그 관계를 포괄합니다.
- 디자인 패턴은 특정 문제를 해결하기 위한 특정 모듈이나 코드 수준의 방법을 다룹니다.
- 수준의 차이:
- 아키텍처는 높은 수준의 개념입니다. 이는 시스템의 전반적인 설계와 목표에 초점을 맞추고, 시스템이 어떻게 구조화되고 동작해야 하는지를 정의합니다.
- 디자인 패턴은 상대적으로 낮은 수준의 구현 세부 사항에 초점을 맞춥니다. 코딩 과정에서의 문제 해결 방법을 제공하며, 구현에 직접적으로 영향을 미칩니다.
- 유연성:
- 아키텍처는 시스템의 큰 그림을 설정하기 때문에, 초기 단계에서 결정되고 잘 변하지 않습니다.
- 디자인 패턴은 다양한 시점에서 필요에 따라 적용될 수 있으며, 특정 문제에 대한 해결책을 제공합니다.
4. 요약 및 결론
아키텍처는 소프트웨어 시스템의 전반적인 구조와 설계를 다루며, 디자인 패턴은 특정 문제에 대한 반복 가능한 설계 방법을 제공합니다. 아키텍처는 시스템의 큰 그림을 그리고, 패턴은 그 안에서 개별적인 문제를 해결하는 구체적인 방법을 제시합니다.
이 둘은 함께 사용되며, 좋은 아키텍처는 적절한 패턴을 활용하여 시스템의 품질을 높이고, 유지보수성을 향상시킵니다.
TCA도 앱을 개발하는데 있어 전반적인 구조와 큰 그림을 그리기 위해 설계되었다고 보시면 됩니다!
클린 아키텍처와 TCA를 비교해볼까요?
TCA(The Composable Architecture)와 같은 아키텍처는 명확한 구조와 문서가 제공되지만, 클린 아키텍처(Clean Architecture)는 추상적인 개념을 다루기 때문에 이를 구체화하는 방법으로 VIP(VIPER) 패턴이나 다른 디자인 패턴을 사용합니다. 이를 통해 클린 아키텍처의 원칙을 실제 코드로 구현할 수 있습니다.
즉 클린 아키텍처를 구체화 하기 위해서는 개발자가 다양한 디자인 패턴을 비교하고 채택하여 구현을 합니다.
하지만 TCA는 공식문서가 명확하게 있기때문에 보고 이에 맞춰 구현을 하면 됩니다. TCA도 단방향 데이터 흐름 (Unidirectional Data Flow)이라는 것을 사용하는데 밑에서 상세하게 설명하겠습니다.
이제 이를 좀 더 구체적으로 설명해드릴게요.
https://github.com/pointfreeco/swift-composable-architecture
클린아키텍처는 명확한 가이드는 따로 없습니다.
1. 클린 아키텍처의 개념
클린 아키텍처는 소프트웨어 설계 원칙을 기반으로, 애플리케이션을 모듈화하고 책임을 명확하게 나누어 유지보수성과 확장성을 높이는 것을 목표로 합니다. 이 아키텍처는 **의존성 역전 원칙(DIP)**과 단일 책임 원칙(SRP) 등을 중심으로 설계되며, 애플리케이션의 각 레이어가 서로 독립적으로 동작할 수 있도록 합니다.
클린 아키텍처의 주요 레이어:
- 엔티티(Entity): 비즈니스 로직과 핵심 규칙을 담고 있습니다. 이들은 애플리케이션의 핵심 기능을 구현하며, 외부 세계와 독립적으로 존재합니다.
- 유스케이스(Use Case, 또는 인터랙터): 애플리케이션의 특정 기능이나 작업 흐름을 담당합니다. 이 레이어는 엔티티와 상호작용하며, 비즈니스 로직을 실행합니다.
- 인터페이스 어댑터(Interface Adapters): 유스케이스와 외부 시스템(예: UI, 데이터베이스, 웹 API) 간의 변환을 담당합니다.
- 프레임워크 및 드라이버(Frameworks & Drivers): 실제 구현(예: UI, 데이터베이스 등)을 담당하며, 시스템의 외곽에 위치합니다.
이러한 레이어 구조는 모듈화된 코드를 작성할 수 있게 해주며, 각 레이어는 독립적으로 테스트 및 유지보수할 수 있습니다. 그러나 클린 아키텍처 자체는 추상적인 개념이기 때문에, 이를 구체적으로 구현하려면 디자인 패턴이 필요합니다.
2. 클린 아키텍처는 추상적이다!!! 구현을 위한 디자인 패턴: VIP와 VIPER
클린 아키텍처의 추상적인 개념을 실제 코드로 구현하기 위해, 다양한 디자인 패턴이 사용됩니다. 특히, iOS 개발에서 VIP와 VIPER 패턴이 클린 아키텍처를 구현하는 데 자주 사용됩니다.
VIP 패턴 (View-Interactor-Presenter):
- View: 사용자 인터페이스(UI)를 담당합니다. 사용자의 입력을 받아 인터랙터에게 전달하고, 프레젠터로부터 전달받은 데이터를 화면에 표시합니다.
- Interactor: 애플리케이션의 비즈니스 로직을 처리합니다. 유스케이스를 구현하며, View와 Presenter 간의 데이터를 중계합니다.
- Presenter: 데이터를 포맷하고, 이를 View에 전달합니다. 또한, 인터랙터로부터 받은 데이터를 View에 알맞게 변환합니다.
VIP 패턴은 각 모듈에 명확한 역할을 부여하여, 코드의 책임을 분리하고 테스트 가능성을 높입니다. 이는 클린 아키텍처의 원칙인 모듈화와 책임 분리(SRP)를 잘 반영합니다.
VIPER 패턴 (View-Interactor-Presenter-Entity-Router):
VIPER은 VIP 패턴을 확장하여, 더 세분화된 구조를 제공합니다.
- View: UI 요소와 사용자 인터랙션을 처리합니다.
- Interactor: 비즈니스 로직을 담당합니다.
- Presenter: 데이터를 View에 전달하고, UI 로직을 처리합니다.
- Entity: 비즈니스 엔티티를 나타내며, 데이터 모델을 정의합니다.
- Router: 화면 간의 내비게이션과 모듈 간의 전환을 담당합니다.
VIPER 패턴은 클린 아키텍처의 각 레이어를 구체화하여, 모듈 간 의존성을 더 명확히 분리하고, 코드의 구조를 체계적으로 유지할 수 있도록 합니다.
3. 클린 아키텍처와 디자인 패턴의 관계
클린 아키텍처는 소프트웨어의 전반적인 설계 철학과 구조를 다루는 반면, 디자인 패턴(VIP, VIPER 등)은 이 철학을 실질적으로 구현하는 구체적인 방법론입니다.
관계 요약:
- 클린 아키텍처는 시스템의 전체적인 설계를 정의하며, 이 설계를 구현하기 위한 가이드라인을 제공합니다.
- 디자인 패턴은 클린 아키텍처의 원칙을 실현하기 위한 도구입니다. VIP, VIPER 패턴은 클린 아키텍처의 모듈화, 책임 분리, 의존성 관리 등의 원칙을 iOS 개발에서 구현할 수 있도록 도와줍니다.
4. 왜 사용하는가?
- 모듈화와 유지보수성: 클린 아키텍처와 디자인 패턴을 사용하면 애플리케이션을 모듈화하여, 코드의 재사용성과 유지보수성을 크게 향상시킬 수 있습니다.
- 테스트 가능성: 각 모듈이 독립적으로 동작할 수 있도록 설계되어, 유닛 테스트와 같은 테스트가 용이해집니다.
- 확장성과 유연성: 새로운 기능을 추가하거나 기존 기능을 변경할 때, 전체 시스템에 미치는 영향을 최소화할 수 있습니다.
- 책임 분리: 코드의 각 부분이 명확한 역할을 가지고, 이로 인해 시스템의 복잡성을 줄이고 이해하기 쉽게 만듭니다.
TCA(The Composable Architecture)에서는 주로 단방향 데이터 흐름(Unidirectional Data Flow) 패턴을 사용합니다. 이 패턴은 앱의 상태 관리와 비즈니스 로직을 명확하게 구조화하는 데 중점을 둡니다. TCA는 이 패턴을 기반으로 상태(state), 액션(action), 리듀서(reducer), 그리고 환경(environment)을 사용하여 앱의 로직을 구성합니다.
TCA에서 사용하는 주요 패턴:
- 단방향 데이터 흐름(Unidirectional Data Flow)
- 상태(State): 애플리케이션의 모든 데이터를 포함하는 구조체입니다.
- 액션(Action): 상태 변경을 일으키는 이벤트입니다.
- 리듀서(Reducer): 액션에 따라 상태를 변경하는 순수 함수입니다.
- 환경(Environment): 외부 의존성을 캡슐화하여 애플리케이션 로직과 분리합니다.
이 패턴은 상태 관리의 복잡성을 줄이고 예측 가능성을 높이는 데 중점을 둡니다. Redux에서 영감을 받은 이 패턴은 TCA의 핵심이 됩니다.
TCA의 주요 특징 :
- 의존성 주입(Dependency Injection)
- TCA에서는 Environment를 통해 외부 의존성을 관리합니다. 이는 테스트 가능성과 모듈화를 촉진 : TCA의 특징
- 컴포지션(Composition)
- TCA는 리듀서와 상태를 합성하여 더 복잡한 로직을 만들 수 있게 합니다. 이는 시스템을 모듈화하고 재사용성을 높이는 방식 : TCA의 설계 철학
- 이펙트 관리(Effect Management)
- Effect는 비동기 작업이나 부작용을 처리하는 TCA의 메커니즘입니다. Combine 프레임워크와 함께 사용되며, 비동기 작업을 선언적이고 관리 가능한 방식으로 처리합니다. : TCA의 기능적 특징
요약
- 패턴: TCA의 핵심 패턴은 단방향 데이터 흐름입니다. 이 패턴은 앱의 상태와 비즈니스 로직을 예측 가능하고 명확하게 관리할 수 있게 합니다.
- 특징: TCA의 주요 특징으로는 의존성 주입, 컴포지션, 이펙트 관리가 있습니다.
자 이제 코드로 이해를 해볼까요?
저도 해당 강의를 보고 대략적인 감을 잡았습니다. 해당 강의를 보시면서 아래 코드 주석을 읽어 주세요
우선 간단한 카운트 앱을 만들어 보겠습니다.
Counter App
도메인 별로 상태, 액션, 환경, 라우터, 리듀서를 만들어줘야 합니다.
도메인(무언가를 만들때 어떠한 데이터) + 상태
// 도메인(무언가를 만들때 어떠한 데이터) + 상태
// 카운터 화면이라 이러한 구조를 가짐
struct CounterState: Equatable {
var count = 0
}
// 도메인 + 액션
enum CounterAction: Equatable {
case addCount // 카운트를 더하는 액션
case subtractCount // 카운트를 빼는 액션
}
// 도메인 Environment(환경)
struct CounterEnvironment {}
// Reducer : 액션과 상태를 연결 시켜주는 역할 - Effect를 반환함
let counterReducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, environment in
// 들어온 액션에 따라 상태를 변경
switch action {
case .addCount:
state.count += 1
return Effect.none // 이펙트를 반환하지는 않지만 상태를 바꿔준것임
case .subtractCount:
state.count -= 1
return Effect.none
}
}
이팩트는 다시 따로 설명하겠습니다.
이제 뷰는 스토어라는 상태랑 액션을 가지는 친구를 이용하여 소통합니다. 약간 기존의 MVVM을 사용하셨다면 비슷하게 생각하시면 편합니다.
struct CounterView: View {
// 스토어는 상태랑 액션을 가지고 있음 선언만 한거고 넣어주는건 외부 에서
let store : Store<CounterState, CounterAction>
var body: some View {
// 스토어를 받는 방법 WithViewStore : 스토어를 받고 할 수 있도록하는 옵져버블이 되는애
// 스토어랑 스유뷰랑 연결해주는 친구임
WithViewStore(self.store) { viewStore in
VStack {
Text("count: \(viewStore.state.count)") // 스토어가 상태도 가지고 있어서 변경이 됨
.padding()
HStack{
// 스토어 == 사령관 : 사령관님 저 이제 더하기 할거에요 하고 알려줘야함
Button("더하기", action: { viewStore.send(.addCount) }) // enum으로 다 정의함
Button("뺴기", action: { viewStore.send(.subtractCount) })
}
}
}
}
}
전체코드
import SwiftUI
import ComposableArchitecture
// 도메인(무언가를 만들때 어떠한 데이터) + 상태
// 카운터 화면이라 이러한 구조를 가짐
struct CounterState: Equatable {
var count = 0
}
// 도메인 + 액션
enum CounterAction: Equatable {
case addCount // 카운트를 더하는 액션
case subtractCount // 카운트를 빼는 액션
}
// 도메인 Environment(환경)
struct CounterEnvironment {}
// Reducer : 액션과 상태를 연결 시켜주는 역할 - Effect를 반환함
let counterReducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, environment in
// 들어온 액션에 따라 상태를 변경
switch action {
case .addCount:
state.count += 1
return Effect.none // 이펙트를 반환하지는 않지만 상태를 바꿔준것임
case .subtractCount:
state.count -= 1
return Effect.none
}
}
struct CounterView: View {
// 스토어는 상태랑 액션을 가지고 있음 선언만 한거고 넣어주는건 외부 에서
let store : Store<CounterState, CounterAction>
var body: some View {
// 스토어를 받는 방법 WithViewStore : 스토어를 받고 할 수 있도록하는 옵져버블이 되는애
// 스토어랑 스유 뷰랑 연결해주는 친구임
WithViewStore(self.store) { viewStore in
VStack {
Text("count: \(viewStore.state.count)") // 스토어가 상태도 가지고 있어서 변경이 됨
.padding()
HStack{
// 스토어 == 사령관 : 사령관님 저 이제 더하기 할거에요 하고 알려줘야함
Button("더하기", action: { viewStore.send(.addCount) }) // 이넘으로 다 정의함
Button("뺴기", action: { viewStore.send(.subtractCount) })
}
}
}
}
}
//struct CounterView_Previews: PreviewProvider {
// static var previews: some View {
// CounterView()
// }
//}
store는 외부에서 주입해주는 방식으로 사용하면 됩니다.
import SwiftUI
import ComposableArchitecture
@main
struct TCA_Simple_tutorialApp: App {
// 외부에서 Store를 생성
let counterStore = Store(initialState: CounterState(),
reducer: counterReducer,
environment: CounterEnvironment())
var body: some Scene {
WindowGroup {
CounterView(store: counterStore)
}
}
}
Memo App (서버와 통신 및 Environment(환경) 사용하기)
import Foundation
// MARK: - MemoElement
struct Memo: Codable, Equatable, Identifiable {
let createdAt, title, viewCount, id: String
}
typealias Memos = [Memo] // 별칭 주기
API 호출시 해당 구조로 날아온다고 생각
API 통신 작업을 정의
https://elisha0103.tistory.com/26
- 환경(Environment): API 클라이언트나 애널리틱스 클라이언트와 같이 어플리케이션이 필요로 하는 의존성(Dependency)을 가지고 있는 타입 결국 환경 쪽에서 해당 MemoClient를 가지게 됩니다. (외부에서 소통을 하는 HTTP 통신과 같은 애들을 따로 구현한다음 환경에서 가지고 있다가 Reducer가 해당 환경을 통해 Effect를 받는 구조 입니다.)
Effect는 Composable 밖에서 외부에서 일어나는것들을 다시 Composable로 들어와서 Composable에서 상태를 변경할때
SideEffect, Effect 라고함 보통 사이드 이펙트라고 함 (보통은 데이터 처리)
쉽게 외부에서 일어난 일을 다시 스토어로 가져와 상태를 변경시킬때 이팩트를 쓰는 거군아 라고 생각하면 됨
import Foundation
import ComposableArchitecture
// API 통신
// 통신쪽에 대한 행위들을 정의
struct MemoClient {
/// 단일 아이템 조회
var fetchMemoItem: (_ id: String) -> Effect<Memo, Failure>
var fetchMemos: () -> Effect<Memos, Failure>
struct Failure : Error, Equatable {} // 에러 정의
// ComposableArchitecture를 사용할때 거의 대부분 데이터들 Equatable로 되어 있음
}
// 실제 로직처리
extension MemoClient {
/*
live는 MemoClient 자체를 반환 하는 것임
live = MemoClient() 두 문법은 같음
live = Self()
*/
static let live = Self(
// fetchMemoItem 클로저로 정의 했음 id를 받음
fetchMemoItem: { id in
Effect.task{
let (data, _) = try await URLSession.shared
.data(from: URL(string: "https://603fca51d9528500176060fc.mockapi.io/api/01/todos/\(id)")!)
return try JSONDecoder().decode(Memo.self, from: data) // 디코딩
}
.mapError { _ in Failure() } // 에러가 나면 내가 정의한 형태로 변환
.eraseToEffect() //퍼블리셔 랑 똑같음 어떤것이든 Effect로 만들어줌
}, fetchMemos: {
Effect.task{
let (data, _) = try await URLSession.shared
.data(from: URL(string: "https://603fca51d9528500176060fc.mockapi.io/api/01/todos/")!)
return try JSONDecoder().decode([Memo].self, from: data)
}
.mapError { _ in Failure() }
.eraseToEffect()
}
)
}
그래서 현제 API 통신이라 반환값이 Effect임 Effect들어가서 보면 퍼블리셔임 결과, 에러가 있음
Effect<Output, Failure: Error>: Publisher
이제 아까와 같이 도메인 별로 상태, 액션, 환경, 라우터, 리듀서를 만들어줘야 합니다.
//
// MemoView.swift
// TCA_Simple_tutorial
//
// Created by Jeff Jeong on 2022/08/04.
//
import Foundation
import SwiftUI
import ComposableArchitecture
// 도메인 + 상태
struct MemoState: Equatable {
var memos : [Memo] = []
var selectedMemo : Memo? = nil
var isLoading : Bool = false
}
// 도메인 + 액션 (약간 뷰모델을 정의하는 듯한 느낌인데)
enum MemoAction: Equatable {
case fetchItem(_ id: String) // 단일 조회 액션
case fetchItemResponse(Result<Memo, MemoClient.Failure>) // 단일 조회 액션 응답
case fetchAll // 모두 조회 액션
case fetchAllResponse(Result<[Memo], MemoClient.Failure>) // 모두 조회 액션 응답
}
// 환경설정 주입 (여기서 주입이 됨)
struct MemoEnvironment {
var memoClient : MemoClient // API 호출위해 필요한 녀석
var mainQueue: AnySchedulerOf<DispatchQueue> // 어떤 스레드에서 할건지
}
// 상태와 액션을 가지고 있는 리듀서
// 액션이 들어왔을때 상태를 변경하는 부분
let memoReducer = Reducer<MemoState, MemoAction, MemoEnvironment> { state, action, environment in
// 들어온 액션에 따라 상태를 변경
switch action {
case .fetchItem(let memoId): // 액션이 들어왔을때 행동을 정의
enum FetchItemId {} // 이건 정대리도 모름 하나의 사용방법
state.isLoading = true // 상태 넣어주기
return environment.memoClient
.fetchMemoItem(memoId) // api 호출 환경설정을 통해
.debounce(id: FetchItemId.self,
for: 0.3,
scheduler: environment.mainQueue) // 스레드 설정
.catchToEffect(MemoAction.fetchItemResponse) // 다른 행위 액션으로 이동 (즉 액션간의 이동 처리 방법)
// Result 타입이라 성공, 실패 각각 정의
case .fetchItemResponse(.success(let memo)):
state.selectedMemo = memo
state.isLoading = false
return Effect.none
case .fetchItemResponse(.failure): // 상태로 들어와 변경 : 이팩트 = 외부에서 일어나는 결과로 상태를 변경
state.selectedMemo = nil
state.isLoading = false
return Effect.none
case .fetchAll:
enum FetchAllId {}
state.isLoading = true
return environment.memoClient
.fetchMemos()
.debounce(id: FetchAllId.self,
for: 0.3,
scheduler: environment.mainQueue)
.catchToEffect(MemoAction.fetchAllResponse)
case .fetchAllResponse(.success(let memos)):
state.memos = memos
state.isLoading = false
return Effect.none
case .fetchAllResponse(.failure):
state.memos = []
state.isLoading = false
return Effect.none
}
}
struct MemoView: View {
let store : Store<MemoState, MemoAction>
var body: some View {
WithViewStore(self.store) { viewStore in
ZStack {
if viewStore.state.isLoading {
Color.black.opacity(0.3)
.edgesIgnoringSafeArea(.all)
.overlay{
ProgressView().tint(.white)
.scaleEffect(1.7)
}.zIndex(1)
}
List{
Section(header:
VStack(spacing: 8) {
Button("메모 목록 가져오기", action: {
viewStore.send(.fetchAll, animation: .default)
})
Text("선택된 메모정보")
Text(viewStore.state.selectedMemo?.id ?? "비어있음")
Text(viewStore.state.selectedMemo?.title ?? "비어있음")
}
,
content: {
ForEach(viewStore.state.memos) { aMemo in
// 하나씩 나온 memo 누르면 개별 호출을 하기위해 버튼 생성
Button(aMemo.title, action: {
viewStore.send(.fetchItem(aMemo.id), animation: .default)
})
}
})
}.listStyle(PlainListStyle())
}
}
}
}
import SwiftUI
import ComposableArchitecture
@main
struct TCA_Simple_tutorialApp: App {
// 외부에서 Store를 생성
let counterStore = Store(initialState: CounterState(),
reducer: counterReducer,
environment: CounterEnvironment())
let memoStore = Store(initialState: MemoState(),
reducer: memoReducer,
environment: MemoEnvironment(
memoClient: MemoClient.live, // 클라이언트 만들어서 넣어줘야함 (스테틱으로 정의된)
mainQueue: .main
))
var body: some Scene {
WindowGroup {
//CounterView(store: counterStore)
MemoView(store: memoStore)
}
}
}
추천 자료 (공식 git 예제)
https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples