
안녕하세요, 별똥별🌠입니다!
이번 글에서는 The Composable Architecture(TCA)를 활용해 Effects를 처리하는 방법을 소개합니다.
예제에서는 카운터 증가/감소와, 음수일 경우 1초 후 자동으로 보정하는 효과, 그리고 네트워크 요청을 통한 숫자 팩트(number fact) 가져오기를 구현합니다.🔥
TCA에서는 단순 상태 변경 외에도 비동기 작업이나 지연, 네트워크 요청 등의 효과(Effects)를 Reducer에서 함께 처리할 수 있습니다.
예제에서는 아래와 같은 상황을 다룹니다.
- 카운터 감소 시, 음수 값이 되면 1초 후 자동으로 카운터를 보정하는 지연 효과
- 증가 버튼 클릭 시, 진행 중인 지연 효과를 취소
- 숫자 팩트 요청 버튼 클릭 시, 네트워크 요청을 통해 숫자 팩트를 가져오고, 결과에 따라 상태를 업데이트
아래 코드는 EffectsBasics Reducer와 SwiftUI View인 EffectsBasicsView로 구성되어 있습니다.
Reducer에서는 다양한 액션에 따라 상태를 업데이트하고, 필요한 경우 비동기 효과를 실행합니다.
@Reducer
struct EffectsBasics {
@ObservableState
struct State: Equatable {
var count = 0
var isNumberFactRequestInFlight = false
var numberFact: String?
}
enum Action {
case decrementButtonTapped
case decrementDelayResponse
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(Result<String, any Error>)
}
@Dependency(\.continuousClock) var clock
@Dependency(\.factClient) var factClient
private enum CancelID { case delay }
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .decrementButtonTapped:
state.count -= 1
state.numberFact = nil
// 카운트가 음수인 경우 1초 후 카운트를 다시 증가시키는 효과
return state.count >= 0 ? .none : .run { send in
try await self.clock.sleep(for: .seconds(1))
await send(.decrementDelayResponse)
}
case .decrementDelayResponse:
if state.count < 0 {
state.count += 1
}
return .none
case .incrementButtonTapped:
state.count += 1
state.numberFact = nil
return state.count >= 0 ? .cancel(id: CancelID.delay) : .none
case .numberFactButtonTapped:
state.isNumberFactRequestInFlight = true
state.numberFact = nil
return .run { [count = state.count] send in
await send(.numberFactResponse(Result { try await self.factClient.fetch(count) }))
}
case let .numberFactResponse(.success(response)):
state.isNumberFactRequestInFlight = false
state.numberFact = response
return .none
case .numberFactResponse(.failure):
state.isNumberFactRequestInFlight = false
return .none
}
}
}
}
감소/증가 처리
숫자 팩트 요청
SwiftUI View에서는 Form을 사용해 카운터 조작 및 숫자 팩트 요청 버튼을 배치하고, 상태에 따라 ProgressView 및 결과 텍스트를 표시합니다.
struct EffectsBasicsView: View {
let store: StoreOf<EffectsBasics>
@Environment(\.openURL) var openURL
var body: some View {
Form {
Section {
AboutView(readMe: readMe)
}
Section {
HStack {
Button {
store.send(.decrementButtonTapped)
} label: {
Image(systemName: "minus")
}
Text("\(store.count)")
.monospacedDigit()
Button {
store.send(.incrementButtonTapped)
} label: {
Image(systemName: "plus")
}
}
.frame(maxWidth: .infinity)
Button("Number fact") { store.send(.numberFactButtonTapped) }
.frame(maxWidth: .infinity)
if store.isNumberFactRequestInFlight {
ProgressView()
.frame(maxWidth: .infinity)
// SwiftUI의 안보이는 버그를 회피하기 위해 id를 명시적으로 부여
.id(UUID())
}
if let numberFact = store.numberFact {
Text(numberFact)
}
}
Section {
Button("Number facts provided by numbeersapi.com") {
openURL(URL(string: "http://numbersapi.com")!)
}
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderless)
.navigationTitle("Effects")
}
}
#Preview {
NavigationStack {
EffectsBasicsView(store: Store(initialState: EffectsBasics.State(), reducer: {
EffectsBasics()
}))
}
}
Form 구성
네비게이션
이번 예제에서는 TCA를 활용해 간단한 Effects 처리를 구현해 보았습니다.
이제 여러분도 TCA의 효과(Effects) 처리 방식을 활용하여 비동기 작업을 깔끔하게 관리해보세요! 🚀🔥
다음 글에서 더 흥미로운 TCA 활용 사례를 다뤄보겠습니다. 감사합니다! 😊