[SwiftUI][TCA] TCA Case Studies - 03. Effects Basic

별똥별·2025년 2월 13일

TCA

목록 보기
20/24

안녕하세요, 별똥별🌠입니다!
이번 글에서는 The Composable Architecture(TCA)를 활용해 Effects를 처리하는 방법을 소개합니다.
예제에서는 카운터 증가/감소와, 음수일 경우 1초 후 자동으로 보정하는 효과, 그리고 네트워크 요청을 통한 숫자 팩트(number fact) 가져오기를 구현합니다.🔥


🧐 Effects란?

TCA에서는 단순 상태 변경 외에도 비동기 작업이나 지연, 네트워크 요청 등의 효과(Effects)를 Reducer에서 함께 처리할 수 있습니다.
예제에서는 아래와 같은 상황을 다룹니다.

  • 카운터 감소 시, 음수 값이 되면 1초 후 자동으로 카운터를 보정하는 지연 효과
  • 증가 버튼 클릭 시, 진행 중인 지연 효과를 취소
  • 숫자 팩트 요청 버튼 클릭 시, 네트워크 요청을 통해 숫자 팩트를 가져오고, 결과에 따라 상태를 업데이트

🎯 예제: 카운터와 숫자 팩트 처리

아래 코드는 EffectsBasics Reducer와 SwiftUI View인 EffectsBasicsView로 구성되어 있습니다.

1️⃣ EffectsBasics Reducer 정의

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
            }
        }
    }
}
  • 감소/증가 처리

    • decrementButtonTapped에서 카운터를 감소시키고, 음수이면 1초 후 decrementDelayResponse를 보내 자동 보정합니다.
    • incrementButtonTapped는 카운터를 증가시키고, 보류 중인 지연 효과를 취소합니다.
  • 숫자 팩트 요청

    • numberFactButtonTapped에서는 네트워크 요청을 실행하여, 결과를 받아오면 numberFactResponse 액션으로 상태를 업데이트합니다.

2️⃣ EffectsBasicsView 정의

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 구성

    • 첫 번째 Section에서는 AboutView를 표시합니다.
    • 두 번째 Section에서는 카운터 버튼, 숫자 팩트 요청 버튼, 네트워크 요청 중 ProgressView, 그리고 요청 결과를 보여줍니다.
  • 네비게이션

    • 마지막 Section의 버튼을 통해 numbersapi.com 웹사이트를 열어줍니다.

✅ 정리

이번 예제에서는 TCA를 활용해 간단한 Effects 처리를 구현해 보았습니다.

  • 지연 효과 : 카운터가 음수일 때 1초 후 자동 보정
  • 네트워크 효과 : 숫자 팩트 요청 후 결과를 상태에 반영
  • 상태 업데이트와 효과 취소 : 증가 버튼 클릭 시 보류 중인 효과 취소

이제 여러분도 TCA의 효과(Effects) 처리 방식을 활용하여 비동기 작업을 깔끔하게 관리해보세요! 🚀🔥
다음 글에서 더 흥미로운 TCA 활용 사례를 다뤄보겠습니다. 감사합니다! 😊

profile
밍밍

0개의 댓글