
안녕하세요, 별똥별🌠입니다!
이번 포스트에서는 TCA를 활용해 Refreshable Effect를 구현하는 방법을 알아보겠습니다.
이 예제에서는 사용자가 화면을 당겨서 새로고침(Pull to Refresh)할 때 API 요청을 보내고, 응답을 받아 화면을 업데이트하는 방식을 다룹니다.
사용자가 데이터를 갱신하고 싶을 때, 흔히 당겨서 새로고침(Pull to Refresh) 기능을 사용합니다. TCA에서는 .run { ... }을 사용하여 비동기 요청을 처리하고, .refreshable 뷰 수정자를 활용해 자연스럽게 적용할 수 있습니다.
API 요청이 오래 걸릴 수 있기 때문에, 사용자가 취소 버튼을 눌러 요청을 중단할 수 있도록 .cancellable(id:)을 적용할 수 있습니다.
@Reducer
struct Refreshable {
@ObservableState
struct State: Equatable {
var count = 0
var fact: String?
}
enum Action {
case cancelButtonTapped
case decrementButtonTapped
case factResponse(Result<String, any Error>)
case incrementButtonTapped
case refresh
}
@Dependency(\.factClient) var factClient
private enum CancelID { case factRequest }
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .cancelButtonTapped:
return .cancel(id: CancelID.factRequest)
case .decrementButtonTapped:
state.count -= 1
return .none
case let .factResponse(.success(fact)):
state.fact = fact
return .none
case .factResponse(.failure):
return .none // 오류 처리 가능
case .incrementButtonTapped:
state.count += 1
return .none
case .refresh:
state.fact = nil
return .run { [count = state.count] send in
await send(
.factResponse(Result { try await self.factClient.fetch(count) }),
animation: .default
)
}
.cancellable(id: CancelID.factRequest)
}
}
}
}
- 1️⃣ 상태(State)
- count: 현재 숫자 값.
- fact: 서버에서 받아올 랜덤한 문장.
- 2️⃣ 액션(Action)
- .refresh: 새 데이터를 불러오는 액션.
- .factResponse: API 응답을 받아 상태를 업데이트.
- .cancelButtonTapped: 네트워크 요청 취소.
- .incrementButtonTapped & .decrementButtonTapped: 숫자 증감.
- 3️⃣ Effect 실행
- .run 내부에서 factClient.fetch(count) 호출.
- .cancellable(id:)을 사용하여 요청을 취소 가능하게 함.
struct RefreshableView: View {
let store: StoreOf<Refreshable>
@State private var isLoading: Bool = false
var body: some View {
List {
Section {
AboutView(readMe: readMe)
}
HStack {
Button {
store.send(.decrementButtonTapped)
} label: {
Image(systemName: "minus")
}
Text("\(store.count)")
.monospacedDigit()
Button {
store.send(.incrementButtonTapped)
} label: {
Image(systemName: "plus")
}
}
.frame(maxWidth: .infinity)
.buttonStyle(.borderless)
if let fact = store.fact {
Text(fact)
.bold()
}
if self.isLoading {
Button("Cancel") {
store.send(.cancelButtonTapped, animation: .default)
}
}
}
.refreshable {
isLoading = true
defer { isLoading = false }
await store.send(.refresh).finish()
}
}
}
- 1️⃣ 뷰 구성
- List 안에 숫자 조작 버튼과 API 응답을 표시하는 Text 포함.
- .refreshable을 사용해 당겨서 새로고침 지원.
- 2️⃣ Pull to Refresh
- 사용자가 화면을 아래로 당기면 .refresh 액션을 호출하여 새로운 데이터를 요청.
- isLoading 상태를 활용해 로딩 상태 관리.
- 3️⃣ 취소 버튼
- API 요청 중 취소할 수 있도록 버튼을 제공.
- 1️⃣ 사용자가 화면을 당기면 .refresh 액션 실행
- 2️⃣ .run 내부에서 factClient.fetch(count) 호출
- 3️⃣ API 응답이 오면 .factResponse 액션 실행
- 4️⃣ 상태가 업데이트되면서 화면에 새 데이터 표시
- 5️⃣ 요청 도중 취소 버튼을 누르면 .cancel(id:)로 요청 취소
- TCA에서 .refreshable을 활용해 쉽게 새로고침 기능을 구현할 수 있습니다.
- .run을 사용해 비동기 API 요청을 관리하며, .cancellable(id:)을 적용해 취소 기능도 추가할 수 있습니다.
- 사용자가 당겨서 새로고침할 때마다 새로운 데이터를 가져와 UI를 업데이트합니다.
이 예제를 통해 TCA의 Refreshable Effect와 비동기 처리 개념을 쉽게 이해할 수 있길 바랍니다. Happy Coding! 🚀