[수위프트UI/번역] SwiftUI에서 에러를 표시하는 가장 간결한 방법

에러 메세지는 사용자에게 실패 상황을 알려줄 때 꼭 필요한 요소 중 하나입니다. 보통 앱을 만들면 예상한대로만 흘러가는 행복한 흐름에 집중하는 경우가 있는데요, 하지만 예상하지 못 한 상황이 생겼을 때를 대비하는 것도 정말 중요합니다. 그렇다고 모든 에러 메세지를 하나씩 띄우는 것을 하자니 이것도 쉽지 않은 일입니다.

글쓴이가 앱을 만들 때는 미래의 프로젝트에서도 쓸 수 있는 지속 가능한 방법을 꾸준히 고민했고, 이런 고민의 결과로 만든 익스텐션들은 역시나 미래의 나를 더 생산적이고 쉽게 개발할 수 있게 만들었다고 합니다. 오늘은 그 중 Localized Error를 유저에게 쉽게 보여줄 수 있는 방법을 소개하려고 합니다.

This article was originally written by Antoine van der Lee.
이 글은 Antoine van der Lee가 작성한 글의 번역본입니다. 원문 링크

Localized Error란 무엇인가요?

얼럿을 띄우는 것에 대해 알아보기 전에, 먼저 보통의 에러와 Localized Error의 차이가 무엇인지 알고 가는 것이 좋겠습니다. Localized Error는 에러가 발생한 이유와 함께 현지화된 메세지를 제공합니다. 이 메세지를 사용해서 유저가 읽을 수 있는 에러 메세지를 제공하려고 합니다.

Swift의 에러 핸들링에 대해 궁금하시다면 글쓴이의 다른 글인 Try Catch Throw: Error Handling in Swift with Code Examples를 참고하시기 바랍니다.

제네릭한 메소드 생성하기

에러 메세지를 띄우는 데에 들이는 노력이 줄어들수록 이상적인 해결책에 가까워질 것입니다. 메소드가 에러를 보여줄지 결정할테니 우리가 할 일은 뷰와 잠재적인 에러의 바인딩을 만들어 두는 것입니다.

아래의 예시의 ArticleView는 기사를 발행(publish)할 수 있게 해주는 뷰입니다. 이 과정에서 에러가 발생할 수 있으니 적절한 에러 메세지를 띄우면 되겠네요.

struct ArticleView: View {

    @StateObject var viewModel = ArticleViewModel()

    var body: some View {
        VStack {
            TextField(text: $viewModel.title, prompt: Text("Article title")) {
                Text("Title")
            }
            Button {
                viewModel.publish()
            } label: {
                Text("Publish")
            }
        }.errorAlert(error: $viewModel.error)
    }
}

에러 메세지를 보여주기 위해 옵셔널 에러 바인딩을 인자로 받는 errorAlert(error: Error?) 뷰 모디파이어를 사용했습니다. 에러는 뷰 모델에 아래와 같이 정의되어 있습니다.

final class ArticleViewModel: ObservableObject {
    enum Error: LocalizedError {
        case titleEmpty

        var errorDescription: String? {
            switch self {
            case .titleEmpty:
                return "Missing title"
            }
        }

        var recoverySuggestion: String? {
            switch self {
            case .titleEmpty:
                return "Article publishing failed due to missing title"
            }
        }
    }

    @Published var title: String = ""
    @Published var error: Swift.Error?

    func publish() {
        error = Error.titleEmpty
    }
}

위 코드에선 에러 바인딩을 Error.titleEmpty로 업데이트 했습니다. 이 에러는 desription과 recoverySuggestion을 제공하고 있으니 Localized Error 프로토콜에도 맞겠네요. 그러면 이제 이 메타데이터를 이용해서 얼럿을 띄우는 제네릭한 뷰 익스텐션을 만들어봅시다.

extension View {
    func errorAlert(error: Binding<Error?>, buttonTitle: String = "OK") -> some View {
        let localizedAlertError = LocalizedAlertError(error: error.wrappedValue)
        return alert(isPresented: .constant(localizedAlertError != nil), error: localizedAlertError) { _ in
            Button(buttonTitle) {
                error.wrappedValue = nil
            }
        } message: { error in
            Text(error.recoverySuggestion ?? "")
        }
    }
}

메소드가 인자로 받은 옵셔널 에러 바인딩을 통해 에러를 보여준 뒤, 얼럿이 닫힐 땐 nil로 만들어줍니다.

LocalizedAlertErrorLocalizedError 프로토콜에 맞게 만들지 않는다면 다음과 같은 에러가 발생합니다.

Protocol ‘LocalizedError’ as a type cannot conform to the protocol itself

다음과 같은 구조를 만들면 문제없이 잘 작동합니다.

struct LocalizedAlertError: LocalizedError {
    let underlyingError: LocalizedError
    var errorDescription: String? {
        underlyingError.errorDescription
    }
    var recoverySuggestion: String? {
        underlyingError.recoverySuggestion
    }

    init?(error: Error?) {
        guard let localizedError = error as? LocalizedError else { return nil }
        underlyingError = localizedError
    }
}

LocalizedAlertError를 초기화할 때 LocalizedError가 아니면 nil을 반환하기 때문에 현지화된 에러가 아니면 에러 메세지를 보여주지 않습니다. constant 바인딩 조건 덕분에 nil일 경우 에러 메세지를 안보여줄 수 있습니다.

.constant(localizedAlertError != nil)

앞의 코드를 다 합치니 아래와 같은 간단한 구현이 에러 얼럿을 보여줄 수 있게 되었네요.

struct ArticleView: View {

    @StateObject var viewModel = ArticleViewModel()

    var body: some View {
        VStack {
            TextField(text: $viewModel.title, prompt: Text("Article title")) {
                Text("Title")
            }
            Button {
                viewModel.publish()
            } label: {
                Text("Publish")
            }
        }.errorAlert(error: $viewModel.error)
    }
}

어떤 Localized Error를 넣어도 아래와 같은 결과가 나오는 것을 확인하실 수 있습니다.

Localized Error를 표시하는 얼럿


결론

최종 사용자에게 에러를 보여주는 것은 예상하지 못 한 상황이 발생했을 때 꼭 필요합니다. 범용적인 해결책을 사용하면 이러한 종류의 구현을 간결화할 수 있습니다. 그리고 이 해결책은 LocalizedError 프로토콜을 사용했기 때문에 써드파티 프레임워크에서 발생하는 에러에 대해서도 처리할 수 있습니다!

글 내용이 마음에 드시고 SwiftUI 지식을 더 키우고 싶으시다면 원본 블로그의 SwiftUI 카테고리 페이지를 참고해보세요. (물론 저의 수위프트UI도 있습니다.)

감사합니다!