The Composable Architecture(TCA) 뜻
The Composable Architecture(TCA)는 일관되고 이해할 수 있는 방식으로 어플리케이션을 만들기 위해 탄생한 라이브러리입니다.
합성(Composition), 테스팅(Testing) 그리고 인체 공학(Ergonomics)을 염두에 둔 TCA는 SwiftUI, UIKit을 지원하며 모든 애플 플랫폼(iOS, macOS, tvOS, watchOS)에서 사용 가능합니다.
The Composable Architecture 구성 요소
- 상태(State) 관리 : 간단한 값 타입들로 어플리케이션의 상태를 관리하는 방법, 상태를 공유를 통해 화면에서 일어나는 변화(Mutation)를 다른 화면에서 즉시 관측(Observe)하는 방법을 제공합니다.
- 합성(Composition) : 기능을 여러 개의 독립된 모듈로 추출하는 방법, 이 모듈을 다시 합쳐서 거대한 기능을 작은 컴포넌트의 집합으로 구성하는 방법을 제공합니다.
- 사이드 이펙트(Side Effects) : 어플리케이션 바깥세상과 접촉하는 작업을 테스트할 수 있고 이해하기 쉽게 작성하는 방법을 제공합니다.
- 테스팅(Testing) : 아키텍처 내부의 기능을 테스트하는 방법뿐만 아니라 여러 파트로 구성된 기능의 통합 테스트를 작성하는 방법, 사이드 이펙트가 어플리케이션에 끼치는 영향에 대해 전체 테스트를 작성하는 방법을 제공합니다. 이 테스트 방식은 여러분의 비즈니스 로직이 예상대로 잘 작동하는지에 대한 강한 보증도 제공합니다.
- 인체 공학(Ergonomics) : 위의 내용을 가능한 한 적은 개념의 간단한 API로 이루는 방법을 제공합니다.
TCA 장점
- 단순히 로직을 관측 가능한 객체나 다양한 UI 컴포넌트의 클로저에 흩뿌리는 것보다, 상태 변경을 적용하는 것에 일관된 태도를 가지도록 해줍니다.
- 사이드 이펙트를 간결하게 표현하는 방법도 제공합니다.
- 추가적인 작업 없이 이펙트가 포함된 로직을 바로 테스트할 수도 있습니다.
TCA 사용법
TCA를 통해 기능을 만들기 위해서는 도메인 구성하는 몇 가지 타입을 정의해야 한다.
- 상태(State) : 비즈니스 로직을 수행하거나 UI를 그릴 때 필요한 데이터에 대한 설명을 나타내는 타입입니다.
- 행동(Action): 사용자가 하는 행동이나 노티피케이션 등 어플리케이션에서 생길 수 있는 모든 행동을 나타내는 타입입니다.
- 환경(Environment) : API 클라이언트나 애널리틱스 클라이언트와 같이 어플리케이션이 필요로 하는 의존성(Dependency)을 가지고 있는 타입입니다.
- 리듀서(Reducer) : 어떤 행동(Action)이 주어졌을 때 지금 상태(State)를 다음 상태로 변화시키는 방법을 가지고 있는 함수입니다. 또한 리듀서는 실행할 수 있는 이펙트(Effect, 예시: API 리퀘스트)를 반환해야 하며, 보통은 Effect 값을 반환합니다.
- 스토어(Store) : 실제로 기능이 작동하는 공간입니다. 우리는 사용자 행동(Action)을 보내서 스토어(Store)는 리듀서(Reducer)와 이펙트(Effect)를 실행할 수 있고, 스토어(Store)에서 일어나는 상태(State) 변화를 관측(observe)해서 UI를 업데이트할 수도 있습니다.
TCA 예시
구현 목표
- 화면에 숫자와 이 숫자를 증가 시킬 수 있는 + 버튼, 감소시킬 수 있는 - 버튼이 존재
- 더 다양한 행동을 위해 탭 하면 API 호출을 해서 숫자에 관한 무작위 사실을 알림창으로 보여주는 버튼도 추가
상태(State) : 화면의 숫자를 정수로 가짐, 알림창을 보여 줄 때 필요한 숫자도 동시에 가짐(알림창에 뜰 필요가 없는 경우 nil로 처리)
struct AppState: Equatable {
var count = 0
var numberFactAlert: String?
}
행동(Action) : 증가 버튼이나 감소 버튼을 누르는 행동, 알림창을 닫거나 무작위 사실 API 리퀘스트 결과를 받았을 때 발생하는 행동
enum AppAction: Equatable {
case factAlertDismissed
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(Result<String, ApiError>)
}
struct ApiError: Error, Equatable {}
환경(Environment) : 의존성(Dependency)을 관리하는 작업을 할 것입니다. 위의 예시를 따라가면 숫자에 관한 사실을 가져오는 경우 네트워크 리퀘스트를 요약해서 Effect값으로 만드는 작업이 존재! 이 작업의 의존성은 Int를 받아서 Effect<String, ApiError> 를 반환하는 함수가 되겠습니다. 여기서 String은 리퀘스트의 리스폰스를 요약한 값입니다. 이펙트는 통산적으로 백그라운드 스레드에서 작업을 처리 할 예정(Ex.URLSession)! 이제 이펙트 값을 메인 큐로 받아와야 할 필요가 있습니다.메인 큐 스케줄러를 사용해야 테스트를 작성할 수 있습니다. AnyScheduler를 사용해서 프로덕션에선 DispatchQueue 를 사용하고 테스트 시엔 테스트 스케줄러를 사용하기!
struct AppEnvironment {
var mainQueue: AnySchedulerOf<DispatchQueue>
var numberFact: (Int) -> Effect<String, ApiError>
}
리듀서(Reducer) : ****현재의 상태(State)를 변화시켜서 다음 상태를 만드는 설명과 어떤 이펙트(Effect)가 실행되는지 설명이 필요! , 만약 이펙트가 실행되지 않는 경우는 .none으로 처리하기
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
switch action {
case .factAlertDismissed:
state.numberFactAlert = nil
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
case .numberFactButtonTapped:
return environment.numberFact(state.count)
.receive(on: environment.mainQueue)
.catchToEffect()
.map(AppAction.numberFactResponse)
case .numberFactResponse(.success(fact)):
state.numberFactAlert = fact
return .none
case .numberFactResponse(.failure):
state.numberFactAlert = "Could not load a number fact :{"
return .none
}
}
View 정의하기 : Store<AppState, AppAction>가 있으면 모든 상태 변화를 관측하고 UI를 다시 한번 그리는 것이 있으며 사용자 행동을 보내서 상태를 변화할 수도 있습니다!
아래의 예시는.alert View Modifier가 요구하는 대로 숫자에 관한 사실을 구조체로 한 번 감싸서서 Identifiable을 따르게 만들기!
struct Appview: View {
let store: Store<Appstate, AppAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
HStack {
Button("−") { viewStore.send(.decrementButtonTapped) }
Text("\\(viewStore.count)")
Button("+") { viewStore.send(.incrementButtonTapped) }
}
Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
}
.alert(
item: viewStore.binding(
get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
send: .factAlertDismissed
),
content: { Alert(title: Text($0.title)) }
)
}
}
}
struct FactAlert: Identifiable {
var title: String
var id: String { self.title }
}
스토어(Store) : 디펜던시 제공, Api 리퀘스트를 생략하기 위한 문자열을 mock해서 바로 반환 해주는 Effect를 주입해주기!
let appView = AppView {
store: Store{
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
numberFact: { number in Effect(value: "\(number) is a good number Brent") }
)
)
)
'IOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] @StateObject (0) | 2022.06.17 |
---|---|
[SwiftUI] SwiftUI에서 UserDefaults를 사용 -> @AppStorage (0) | 2022.06.16 |
[SwiftUI] Coredata 사용해보기 (0) | 2022.05.04 |
[SwiftUI] VoiceViewModel 코드 분석하기 (0) | 2022.05.04 |
[SwiftUI] persistentContainer란 무엇인가? (0) | 2022.05.03 |