Skip to content

Error Handling

JEONG edited this page Aug 24, 2026 · 2 revisions

Error Handling

세 가지 에러 표현 방식과 선택 기준. 상황에 맞는 도구를 고르는 것이 핵심입니다. 구현 위치: UMCApp/Core/Foundation/Sources/Error/ (UMCFoundation 모듈).

한눈에 보는 선택 기준

방식 용도 표현
Loadable 화면 내 인라인 상태 (리스트 로딩, 도메인 에러, 검증 실패) 뷰 내부 상태
ErrorHandler 흐름 중단형 전역 에러 (세션 만료, 권한, 네트워크 오류) 전역 Alert
AlertPrompt 확인/취소 다이얼로그 (파괴적 작업, 분기점) 모달 다이얼로그

타입 구성

Error/
├── Types/       AppError · NetworkError · RepositoryError · DomainError
│                AuthError · ValidationError · ErrorSeverity
├── Handler/     ErrorHandler · ErrorContext · PresentableError
├── Loadable/    Loadable
├── LocationError.swift
├── Error+Cancellation.swift      // error.isCancellation
└── Error+TransportFailure.swift  // error.isTransportFailure

AppError — 화면에 전달되는 단일 타입

public enum AppError: Error, LocalizedError, Equatable {
    case repository(RepositoryError)
    case network(NetworkError)
    case validation(ValidationError)
    case auth(AuthError)
    case domain(DomainError)
    case unknown(message: String)
}
멤버 설명
errorDescription 개발자용 상세 설명 (로깅)
userMessage 사용자에게 보여줄 문구
severity .info / .warning / .critical — Alert 제목 결정
isRetryable 재시도 버튼 노출 여부
AppError.from(_:) 임의 Error를 정규화. RepositoryError/NetworkError/AuthError를 각 케이스로 매핑, 그 외는 .unknown

catch 블록마다 매핑 로직을 다시 쓰지 말고 AppError.from(error) 를 사용합니다.

하위 에러 타입

타입 대표 케이스
NetworkError unauthorized, tokenRefreshFailed(reason:), noRefreshToken, requestFailed(statusCode:data:), invalidResponse, maxRetryExceeded, noNetwork, timeout
RepositoryError serverError(code:message:), decodingError(detail:), invalidResponse(detail:)
AuthError notLoggedIn, sessionExpired, invalidCredentials, socialLoginFailed(provider:reason:), accountSuspended(reason:), pendingApproval, rejected(reason:), notRegisteredMember, invalidVerificationCode, verificationCodeExpired
ValidationError empty(field:), invalidFormat(field:expected:), tooShort(field:minLength:), tooLong(field:maxLength:), invalidValue(field:reason:), mismatch(field1:field2:), alreadyInUse(field:)
DomainError 공지·출석·워크북·미션·커리큘럼·게시글 규칙 위반 (attendanceOutOfRange, workbookDeadlinePassed, insufficientPermission(required:), custom(message:) 등)
LocationError notAuthorized, locationFailed(_), timeout, geocodingFailed(_)

Loadable (로컬 / 인라인 에러)

public enum Loadable<T: Equatable>: Equatable {
    case idle              // 초기 상태
    case loading           // 로딩 중
    case loaded(T)         // 성공
    case failed(AppError)  // 실패 (인라인 표시)
}

편의 멤버: value · error · isLoading · isComplete · isIdle · map(_:)

switch viewModel.attendanceState {
case .idle:              Color.clear.task { await viewModel.fetch() }
case .loading:           LoadingView()
case .loaded(let data):  AttendanceContentView(data: data)
case .failed(let error): RetryContentUnavailableView(error: error) { await viewModel.fetch() }
}

ErrorHandler (전역 Alert 에러)

@Observable
public final class ErrorHandler {
    public private(set) var currentError: PresentableError?
    public func handle(_ error: Error, context: ErrorContext)
    public func clearError()
}
errorHandler.handle(error, context: ErrorContext(
    feature: "Activity",
    action: "attendanceBtnTapped",
    retryAction: { [weak self] in await self?.retry() }
))
  • 루트 뷰에 .globalErrorAlert(errorHandler: errorHandler) 한 번만 부착합니다.
  • handlePresentableError를 만들어 currentError에 싣고, Alert 제목은 AppError.severity, 재시도 버튼은 isRetryable + retryAction 유무로 결정됩니다.
  • 취소는 에러가 아닙니다. error.isCancellation인 경우 Alert를 띄우지 않습니다.

Loadable vs ErrorHandler

  • ErrorHandler: 작업 흐름 중단, 즉각적 사용자 액션 필요 (세션 만료, 권한 요청, 네트워크 오류)
  • Loadable: 화면 내 상태 표시 (리스트 로딩 실패, 도메인 에러, 검증 실패)

AlertPrompt (확인/취소 다이얼로그)

@Observable
final class SomeViewModel {
    var alertPrompt: AlertPrompt?

    func deleteButtonTapped() {
        alertPrompt = AlertPrompt(
            title: "삭제 확인",
            message: "정말 삭제하시겠습니까?",
            positiveBtnTitle: "삭제",
            positiveBtnAction: { [weak self] in self?.delete() },
            negativeBtnTitle: "취소",
            isPositiveBtnDestructive: true
        )
    }
}

// View
.alertPrompt(item: $viewModel.alertPrompt)
파라미터 설명
positiveBtnTitle / positiveBtnAction 긍정 버튼 (nil이면 미표시)
secondaryBtnTitle / secondaryBtnAction 3번째 선택지가 필요할 때만
negativeBtnTitle / negativeBtnAction 취소 버튼 (보통 액션 없음)
isPositiveBtnDestructive true면 긍정 버튼을 .destructive

AlertPrompt 사용 기준

  • 파괴적 작업 전 확인 (삭제, 초기화 등)
  • 사용자 선택이 필요한 분기점

에러 타입 빠른 판단

상황 타입
통신/토큰 재발급/타임아웃 NetworkError
서버 응답은 왔지만 비즈니스 실패/디코딩 실패 RepositoryError
로그인·가입·승인 상태 AuthError
입력값 검증 실패 ValidationError
도메인 규칙 위반 DomainError
화면 전달 타입 AppError

관련 문서: Architecture · Networking

Clone this wiki locally