Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- java
- MVVM-C
- Flutter Toy Project
- 영화리뷰
- 영화
- 백준
- 키노
- swift concurrency
- 영화후기
- 오블완
- SOPT
- 프로그래머스
- 독서일기
- 영화기록
- 자바 스터디
- 리뷰
- 코딩공부
- 자바
- 일기
- 티스토리챌린지
- 새벽녘 소소한 기록
- sopt ios
- Flutter
- 토이프로젝트
- 자바공부
- 플러터
- 영화일기
- SWIFT
- IOS
- sopt 35기
Archives
- Today
- Total
새벽의 기록
[iOS] TCA Zero-to-Hero #2 - Effect: 비동기 처리와 외부 통신 본문
앱은 단순히 더하기 빼기만 하지 않는다.
API 통신, 타이머, 데이터 저장 같은 복잡한 요구사항과 작업들을 해야 한다. TCA에서는 이를 Effect라고 부른다.
목표: 지난번 만든 카운터 앱에 "1초 뒤에 증가하기" 버튼 추가.
Reducer는 순수 함수여야 한다. 즉, 같은 입력(State, Action)이 들어오면 항상 같은 결과가 나와야 한다.
하지만 DispatchQueue.main.asyncAfter 같은 건 순수하지 않다.
그래서 TCA는 return .none 대신 return .run { send in ... } 을 사용해서 비동기 작업을 수행한다.
import SwiftUI
import ComposableArchitecture
@Reducer
struct CounterFeature {
@ObservableState
struct State: Equatable {
var count = 0
var isLoading = false
}
enum Action {
case incrementButtonTapped // 증가 버튼 클릭
case decrementButtonTapped // 감소 버튼 클릭
case delayedIncrementButtonTapped // 지연 증가 버튼 클릭
case incrementResponse // 1초가 지난 후 실제로 카운트를 올리라는 내부 액션
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .incrementButtonTapped:
state.count += 1
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .delayedIncrementButtonTapped:
state.isLoading = true // 로딩 시작
return .run { send in
// 복잡한 비동기 로직(API 호출, 타이머 등)을 수행.
try await Task.sleep(nanoseconds: 1_000_000_000) // 1초 대기
// 작업이 끝나면 다시 Action을 날려서 State 변경.
await send(.incrementResponse)
}
case .incrementResponse:
state.isLoading = false // 로딩 끝
state.count += 1
return .none
}
}
}
}
struct CounterView: View {
let store: StoreOf<CounterFeature>
var body: some View {
VStack {
if store.isLoading {
ProgressView()
.padding()
} else {
Text("\(store.count)")
.font(.largeTitle)
.padding()
}
HStack {
Button("-") {
store.send(.decrementButtonTapped)
}
.font(.largeTitle)
.padding()
.background(Color.black.opacity(0.1))
.cornerRadius(10)
Button("+") {
store.send(.incrementButtonTapped)
}
.font(.largeTitle)
.padding()
.background(Color.black.opacity(0.1))
.cornerRadius(10)
Button("1초 뒤 증가") {
store.send(.delayedIncrementButtonTapped)
}
.font(.largeTitle)
.padding()
.background(Color.black.opacity(0.1))
.cornerRadius(10)
}
}
}
}
#Preview {
CounterView(
store: Store(initialState: CounterFeature.State()) {
CounterFeature()
._printChanges()
}
)
}

'[iOS]' 카테고리의 다른 글
| [iOS] TCA Zero-to-Hero #4 - Dependencies: 의존성 주입과 통제 (0) | 2025.12.26 |
|---|---|
| [iOS] TCA Zero-to-Hero #3 - Binding: TextField, Toggle 등 UI 바인딩 처리 (0) | 2025.12.21 |
| [iOS] TCA Zero-to-Hero #1 - State, Action, Reducer의 단방향 데이터 흐름 이해 (0) | 2025.12.20 |
| [iOS] Swift Concurrency 박살내기 #4 - Sendable과 nonisolated (0) | 2025.12.06 |
| [iOS] 결합도와 응집도에 관해 + CoordinatorView에 대한 스스로의 판단 한 스푼 (1) | 2025.11.26 |
Comments
