J
[SwiftUI] TCA 공식문서로 시작하기_Getting started 본문
시작하기
Composable Architecture를 프로젝트에 통합하고 첫 번째 애플리케이션을 작성하는 방법을 알아봅시다.
Composable Architecture를 프로젝트에 추가하기
SwiftPM 프로젝트에서 Composable Architecture를 사용하려면 Package.swift의 dependencies 섹션에 추가하고, 해당 라이브러리가 필요한 타겟에서 ComposableArchitecture를 지정해야 합니다.
let package = Package(
dependencies: [
.package(
url: "<https://github.com/pointfreeco/swift-composable-architecture>",
from: "1.0.0"
),
],
targets: [
.target(
name: "",
dependencies: [
.product(
name: "ComposableArchitecture",
package: "swift-composable-architecture"
)
]
)
]
)
첫 번째 기능 구현하기
Composable Architecture를 사용하여 기능을 만들려면 다음과 같은 개념을 정의해야 합니다.
- State (상태): 기능이 동작하기 위해 필요한 데이터를 표현하는 타입입니다.
- Action (액션): 사용자 입력, 알림, 이벤트 등 다양한 동작을 표현하는 타입입니다.
- Reducer (리듀서): 현재 상태를 주어진 액션에 따라 어떻게 변경할지 정의하는 함수입니다. 또한, API 요청과 같은 비동기 작업을 수행하고, 그 결과를 다시 시스템에 전달하는 역할을 합니다.
- Store (스토어): 기능의 실제 실행 환경입니다. 모든 사용자 액션을 스토어에 전달하면, 스토어는 리듀서를 실행하고 Effect를 처리하며, 상태 변화를 관찰하여 UI를 업데이트합니다.
이 방식의 장점은 기능의 테스트 가능성을 높이고, 복잡한 기능을 작은 도메인 단위로 나누어 유지보수하기 쉽게 만든다는 것입니다.
예제: 숫자 증가/감소 및 랜덤 숫자 정보 가져오기
다음과 같은 UI를 구현해 보겠습니다.
- 숫자를 표시하고, +, - 버튼을 눌러 값을 변경할 수 있습니다.
- "Number fact" 버튼을 누르면 현재 숫자에 대한 랜덤 정보를 가져와서 알림으로 표시됩니다.
1️⃣ Reducer 정의
Reducer를 사용하여 상태, 액션, 동작을 정의합니다.
struct Feature: Reducer {
}
State 정의
현재 숫자를 저장하는 count와, 랜덤 숫자 정보를 저장하는 numberFactAlert(알림에 표시할 문자열)을 포함합니다.
struct Feature: Reducer {
struct State: Equatable {
var count = 0
var numberFactAlert: String?
}
}
Action 정의
사용자가 수행할 수 있는 동작을 enum으로 정의합니다.
struct Feature: Reducer {
struct State: Equatable { /* ... */ }
enum Action: Equatable {
case factAlertDismissed
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(String)
}
}
2️⃣ reduce(into:action:) 메서드 구현
이제 상태를 변경하고 Effect를 반환하는 리듀서를 구현합니다.
struct Feature: Reducer {
struct State: Equatable { /* ... */ }
enum Action: Equatable { /* ... */ }
func reduce(into state: inout State, action: Action) -> Effect {
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 .run { [count = state.count] send in
let (data, _) = try await URLSession.shared.data(
from: URL(string: "<http://numbersapi.com/\\(count)/trivia>")!
)
await send(.numberFactResponse(String(decoding: data, as: UTF8.self)))
}
case let .numberFactResponse(fact):
state.numberFactAlert = fact
return .none
}
}
}
3️⃣ SwiftUI View 구현
Composable Architecture에서는 StoreOf<Feature>를 이용하여 상태를 관찰하고 액션을 보낼 수 있습니다.
struct FeatureView: View {
let store: StoreOf<Feature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { 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 }
}
4️⃣ 스토어 생성 및 앱 실행
이제 Store를 생성하여 애플리케이션을 실행할 수 있습니다.
@main
struct MyApp: App {
var body: some Scene {
FeatureView(
store: Store(initialState: Feature.State()) {
Feature()
}
)
}
}
5️⃣ 테스트 작성
Composable Architecture의 TestStore를 사용하면 상태 변화와 액션을 쉽게 테스트할 수 있습니다.
@MainActor
func testFeature() async {
let store = TestStore(initialState: Feature.State()) {
Feature()
}
await store.send(.incrementButtonTapped) {
$0.count = 1
}
await store.send(.decrementButtonTapped) {
$0.count = 0
}
await store.send(.numberFactButtonTapped)
await store.receive(.numberFactResponse("???")) {
$0.numberFactAlert = "???"
}
}
6️⃣ 의존성 주입 및 테스트 개선
API 요청을 테스트하기 위해 Dependency를 주입하면 보다 안정적인 테스트가 가능합니다.
1. 의존성 정의
struct NumberFactClient {
var fetch: (Int) async throws -> String
}
2. 기본 구현 등록
private enum NumberFactClientKey: DependencyKey {
static let liveValue = NumberFactClient(
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[NumberFactClientKey.self] }
set { self[NumberFactClientKey.self] = newValue }
}
}
3. Reducer에서 의존성 사용
struct Feature: Reducer {
@Dependency(\\.numberFact) var numberFact
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .numberFactButtonTapped:
return .run { [count = state.count] send in
let fact = try await self.numberFact.fetch(count)
await send(.numberFactResponse(fact))
}
}
}
}
4. 테스트에서 Mock 데이터 사용
let store = TestStore(initialState: Feature.State()) {
Feature()
} withDependencies: {
$0.numberFact.fetch = { "\\($0) is a great number!" }
}
await store.send(.numberFactButtonTapped)
await store.receive(.numberFactResponse("0 is a great number!")) {
$0.numberFactAlert = "0 is a great number!"
}
'SwiftUI' 카테고리의 다른 글
[SwiftUI] TCA 공식문서로 시작하기_Reducer (0) | 2024.08.20 |
---|---|
[SwiftUI] SwiftUI + TCA: Environment vs. DependencyValues (0) | 2024.07.09 |
[SwiftUI] SwiftUI + TCA: Effect? DependencyValues? (예제 포함) (0) | 2024.07.08 |
[SwiftUI] SwiftUI + TCA: Client 훑고 가기… (0) | 2024.07.05 |
[SwiftUI] SwiftUI + TCA: 최신 버전 TCA의 동작 과정 (예제 포함) (0) | 2024.07.05 |