Tori의 개발 공부

[iOS / Clean Architecture] 프론트엔드의 에러핸들링 본문

IOS App Programming/트러블 슈팅

[iOS / Clean Architecture] 프론트엔드의 에러핸들링

B_Tori 2025. 1. 1. 11:51

에러 핸들링은 소프트웨어 개발에서 피할 수 없는 과제입니다.
하지만 이전 프로젝트들에서는 에러 처리 방식은 단순히 “에러 발생 시 메시지를 출력하고 멈춘다”는 수준에 머무르는 경우가 많았습니다.
이렇게 단순화된 접근은 문제 해결에 필요한 정보를 제공하지 못하거나, 사용자 경험(UX)을 저해하는 결과를 초래할 수 있습니다.

이번 글에서는 클린아키텍처 구조에서 맥락(Contextual) 에러와 프레젠테이션(Presentation) 에러를 도입하여 에러 핸들링 체계를 개선한 사례와 그로 인한 효과를 공유합니다.

단순한 에러 처리의 한계

처음 프로젝트를 시작했을 때 저희는 DataError만을 사용해 에러를 처리했습니다.
예를 들어 네트워크 통신 에러, 디코딩 인코딩 에러 등 Data Layer에서 발생하는 에러들에 관해 정의하고 출력한 뒤 그대로 Presentation Layer까지 에러를 그대로 넘기도록 하였습니다.

문제점

  • 프레젠테이션 레이어에서는 자세한 API 오류에 대한 정보보다는 어떠한 상황에서 에러가 발생했는지 여부와 행동요령을 안내하는 것이 UX적으로 더 좋은 행동이었습니다.
  • 대응되는 DataSource에서 발생하는 오류들을 살펴보고 프로그래머가 해당 에러가 발생할 맥락을 임의로 파악한 뒤 프레젠테이션 레이어에서 많은 분기문을 통해 처리하게 되었고 이는 코드 길이도 길어질뿐더러 예상치 못한 에러에 대해 올바르게 대처하지 못하였습니다.
  • 사용자에게 “에러가 발생했습니다. 다시 시도해 주세요. “라는 막연한 메시지를 보여주는 것은 불친절할 뿐 아니라, 문제 해결에도 도움이 되지 않았습니다.
  • 같은 DataSource를 사용하지만 상황에 따라 토스트 메시지를 보여줄지 에러페이지를 보여줄지 다른 경우에는 이를 구분하는 방법이 없었습니다.
  • QA 모니터링 시 에러 발생 상황에 대해 이해하지 못하는 경우 로그를 보더라도 파악하기 힘든 경우가 많았습니다.

해결 방안 - DomainError와 PresentationError 도입

우선 프레젠테이션 레이어에서 보일 에러 핸들링 규칙을 정의했습니다.

  1. 페이지 로드와 같은 데이터 로드 상황의 에러 : 에러 페이지 보이기
  2. 버튼 액션 등과 같은 액션 상황의 요청 에러 : 토스트 메시지 보이기
  3. 사용자 정보에 접근할 수 없는 경우 (토큰 에러) : 로그아웃 안내 알럿 후 로그아웃
    이를 통해 저희는 3가지 맥락으로 에러를 간소화할 수 있었습니다.
    이는 사용자 상호작용을 의미하므로 Presentation Layer의 에러로 정의하였습니다.

하지만 문제점은 Data Layer에서 해당 맥락을 알 수 없다는 점이었습니다.

따라서 맥락을 전달할 중간다리 역할이 필요했고, DomainError를 추가하여 해당 에러에 전달받은 DataError와 맥락을 포함하도록 하였습니다.

맥락을 전달하는 역할을 유즈케이스에서 하도록 하였습니다.
유즈케이스의 경우 비즈니스 로직을 의미하므로 명확히 분리한다면 어떠한 상황의 비즈니스로직인지에 따라 충분히 맥락을 구분할 수 있다고 파악하였기 때문입니다.

Presentation Error

enum PresentationError: LocalizedError {
    case showErrorPage
    case showToastMessage(message: String)
    case unauthorized     // 인증 실패

}

Presentation Error에서는 에러 핸들링 규칙에 맞게 3가지로 나누어 정의했습니다.
토스트 메시지에서는 상황마다 알맞은 메시지를 제공할 수 있도록 message를 입력받을 수 있도록 하였습니다.

Domain Error

/// DomainError -> 맥락 + Error
struct DomainError: Error {
    let error: Error
    let context: ErrorContext
    let message: String?

    init(error: Error, context: ErrorContext, message: String? = nil) {
        self.error = error
        self.context = context
        self.message = message
    }

}


/// Error 발생 상황 맥락
enum ErrorContext {
    case action
    case pageLoad
    case tokenUnavailable
}

extension DomainError {
    func toPresentationError() -> PresentationError {
        let message = self.message
        switch context {
        case .action
            if let message {
                return .showToastMessage(message: message)
            } else {
                return .showToastMessage(message: "오류가 발생했습니다. 다시 시도해주세요.")
            }
        case .pageLoad:
            return .showErrorPage
        case .tokenUnavailable:
            return .unauthorized
        }
    }
}

Domain Error에서는 맥락과 오류, 오류메시지를 포함하여 구조체로 정의하였습니다.
Error에는 DataLayer에서 전달된 에러를 입력하고 유즈케이스에 맞게 맥락과 필요하다면 메시지까지 입력하여 Domain Error로 변환하도록 하였습니다.

또한 도메인 에러는 맥락에 따라 Presentation Error로 변환할 수 있도록 함수를 정의했습니다.

결론

  • 에러메시지 출력 시 단순 에러 디버깅 로그뿐만 아니라 상황에 대한 설명도 추가할 수 있어 모니터링 시 디버깅 속도도 향상
  • PresentationError로 사용자에게 적합한 메시지를 제공한 결과, “에러 메시지가 명확하지 않다”는 피드백 해결
  • 또한 명확한 규칙을 가지고 에러핸들링을 구현하여 에러 핸들링 통일성을 부여

DomainError(ContextualError)와 PresentationError를 도입한 에러 핸들링 체계는 단순히 에러 메시지를 출력하는 것을 넘어, 개발자 경험(DX)과 사용자 경험(UX) 모두를 크게 개선할 수 있었습니다.

앞으로도 프로젝트의 규모와 복잡도가 증가함에 따라, 에러 처리 체계는 더욱 중요한 역할을 할 것입니다.
특히 프론트 엔드의 경우 사용자 경험과 개발자 경험을 모두 중시해야 하기 때문에 더욱 어려웠던 것 같습니다. 계속해서 고민하고 개선해 가는 중이기에 이 방식이 정답이라고 할 수 없지만 이 글이 여러분의 프로젝트에서도 에러 핸들링 체계를 개선하는 데 도움이 되길 바랍니다!