일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- CS
- gesture
- state
- UIKit
- Network
- Performance
- Concurrency
- Animation
- GCD
- iphone
- dataflow
- auth
- avsession
- 접근성제어
- 달력
- 네트워크
- swift
- withAnimation
- firebase
- combine
- SwiftUI
- stateobject
- authentication
- ios
- view
- RxSwift
- 최적화
- WWDC
- arkit
- toolbarvisibility
- Today
- Total
XLOG
[TCA] TCA 를 활용한 에러핸들링 전략 본문
회사에서 근무를 하며 백엔드에서 날려주는 상황별 Error Code 는 매우 다양했다.
하지만 Client 측에선 다양한 Error Code 라도 유저가 할 수 있는 Action 은 그리 다양하지 않았다.
그리고 회사에서 TCA 를 적용해서 프로젝트를 진행하였기 때문에 Error Handling 을 Composable 하게 구성할 수 없을까 고민을 했다. 그래서 Error Handling 을 위한 Redcuer 와 View 를 만들어 각 Feature 적용해야 겠다는 생각을 했다.
우선 내가 throw 할 수 있는 Error Case 와 해당 Error 마다 할 수 있는 액션들의 정의가 필요했다.
//
// Created by Sooik Kim on 5/15/24.
//
import SwiftUI
// API 통신 후 statusCode 에 따른 error 를 throw 해준다.
enum CustomErrorCase: Error {
case notAuth
case notFound
case timeOut
case etc
}
// Alert Button을 누를때 발생할 수 있는 Error Action
enum ErrorActionCase: String, CaseIterable {
case tryAgain
case signIn
case cancel
case dismiss
case confirm
}
extension ErrorActionCase {
// Alert Button 생성을 자동화 할 때 사용할 Button Role
var buttonRole: ButtonRole {
switch self {
case .tryAgain:
.destructive
case .signIn:
.destructive
case .cancel:
.cancel
case .dismiss:
.cancel
case .confirm:
.cancel
}
}
}
extension CustomErrorCase {
// 해당 Error에서 사용할 수 있는 Button 리스트
var actionCases: [ErrorActionCase] {
switch self {
case .notAuth:
[.cancel, .signIn]
case .notFound:
[.confirm]
case .timeOut:
[.cancel, .tryAgain]
case .etc:
[.confirm]
}
}
var alertTitle: String {
switch self {
case .notAuth:
"권한이 없습니다."
case .notFound:
"파일을 찾을 수 없습니다."
case .timeOut:
"시간 초과"
case .etc:
"알수없는 오류"
}
}
}
이제 Error 를 Handling 할 Reducer 와 View 가 필요하다.
import ComposableArchitecture
@Reducer
struct CommonError {
struct State: Equatable {
let error: CustomErrorCase
@BindingState var isPresented: Bool = true
init(error: Error) {
if let error = error as? CustomErrorCase {
self.error = error
} else {
self.error = .etc
}
}
}
enum Action: Equatable, BindableAction {
case errorAction(ErrorActionCase)
case binding(BindingAction<State>)
}
var body: some ReducerOf<Self> {
Reduce { _, action in
switch action {
case .errorAction(let errorAction):
switch errorAction {
case .signIn:
// Sign In Page Open
return .none
default:
return .none
}
default:
return .none
}
}
}
}
struct CommonErrorView: View {
let store: StoreOf<CommonError>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
// EmptyView 로 하게 되면 Alert 도 동작을 하지 않는다....
ZStack {
}
.alert(viewStore.error.alertTitle, isPresented: viewStore.$isPresented) {
ForEach(viewStore.error.actionCases, id: \.self) { actionCase in
Button(actionCase.rawValue, role: actionCase.buttonRole, action: { viewStore.send(.errorAction(actionCase) )})
}
}
}
}
}
이제 CommonError 모듈이 완성되었다.
각 Feature 에서 Error 가 발생하면 error 값을 받아 CommonError 를 Init 을 해주고 IfLetStore 로 CommonErrorView 를 build 하여 alert 가 발생하도록 하는게 핵심 이었다.
alert 에 버튼은 error 가 가질 수 있는 actionCases 를 통해 자동으로 생성이 된다.
이렇게 만든것을 각 Feature 에 적용하는 것이다.
import Foundation
import ComposableArchitecture
@Reducer
struct Root {
struct State: Equatable {
var commonError: CommonError.State?
}
enum Action: Equatable {
case resultFail(CustomErrorCase)
case commonError(CommonError.Action)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .resultFail(let error):
state.commonError = .init(error: error)
return .none
case .commonError(.errorAction(let errorAction)):
// alert 에서 액션이 발생했다는 얘기는 해당 view가 필요없기에 nil 로 만들어준다.
state.commonError = nil
switch errorAction {
case .tryAgain:
// return .request
// 필요한 api 통신을 다시 호출한다.
return .none
default:
return .none
}
default:
return .none
}
}
.ifLet(\.commonError, action: \.commonError) {
CommonError()
}
}
}
각 feature 의 리듀서는 scope 로 CommonError 를 가지고 있어 CommonError에서 발생하는 액션에 맞게 적절한 sideEffect 를 사용하여 Error 에 맞는 동작을 구현할 수 있다.
import SwiftUI
import ComposableArchitecture
struct ContentView: View {
let store: StoreOf<Root>
init() {
self.store = .init(initialState: Root.State(), reducer: { Root() })
}
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack (spacing: 20) {
Button(action: {
viewStore.send(.resultFail(.notAuth))
}, label: {
Text("Not Auth")
})
Button(action: {
viewStore.send(.resultFail(.notFound))
}, label: {
Text("Not Found")
})
Button(action: {
viewStore.send(.resultFail(.timeOut))
}, label: {
Text("Time out")
})
}
.overlay {
IfLetStore(self.store.scope(state: \.commonError, action: \.commonError), then: {
CommonErrorView(store: $0)
})
}
}
}
}
해당 파일에선 View에서 강제로 error 발생 상황을 만들어 error 를 전달하였는데, 실제 프로젝트에선 api 통신의 결과로 발생한 error를 받아서 동작하도록 했다. 사실 overlay 로 빈 View와 alert를 덮는 방식이 마음에 들진 않지만..우선 급한대로 이렇게 처리를 했다.
'Swift > Etc' 카테고리의 다른 글
ARKit + RealityKit 기초 및 예제 (0) | 2024.09.11 |
---|---|
[XCTest] TDD? Testable? 업무 효율 높이기 (0) | 2023.08.01 |
[ARKit] 은 무엇일까? 왜 발열이 심할까....? (0) | 2023.03.19 |
[Swift] RestApi, 구글 캘린더 Api 를 이용하여 대한민국 공휴일 가져오기 (0) | 2023.03.13 |
좋은 앱이란? - 디자인 챌린지 (Asia Pacific, 230307) 를 보고 (0) | 2023.03.07 |