본문 바로가기

swift 에러

[Swift error handling] "스파클" 예상 못한 에러는 어떻게 처리해줘야 할까?

반응형

"스파클" 앱을 리펙토링하는 과정에 있다..!

2주간 프로젝트를 진행하면서 마지막에 해결되지 않은 이슈가 있다. 

바로 "커플 연결에 혼선이 있으면 API 호출을 통해 받아오는 커플 정보를 정확히 받을 수 없다." 는 것이다. 

 

문제 상황

API 호출 전 indicator 실행 -> API 호출 -> 성공 시 indicator 그만

인 흐름에서 성공하지 못했기 때문에 indicator가 무한으로 도는 문제가 발생하고 있었다. 

우리팀은 슬랙채널을 활용해서 이슈를 정리한다.

 

사실 나는 그 전까지 "물론 에러 핸들링 중요하지만! 그 전에 이러한 에러가 나지 않아야 한다"라고 생각해서 에러 처리를 굳이 만들지 않았었다. 

그치만 이러한 현상을 겪으면 다시는 앱을 사용하지 않게 될 것 같다라는 생각에 우선 서버의 상태와는 별거로 iOS 앱에서 에러 핸들링을 해보고자 한다. 

 

저번 회고를 통해 문서화의 중요성을 깨닫고 회의를 거친 후 꼭 다시 한번 기록을 남기고 있다. 확실히 의사 전달과 협업에 도움이 되는 것 같다.

 

 

회의 끝에 홈 화면으로 이동하기 전, 커플을 연결하는 과정에서 예외 테스스를 두어서 겹치는 커플이 없게끔 만들게 되었다. 

 

이제 할일이 주어졌으니 개발을 해야한다!

 

에러 핸들링을 할 수 있는 방법은 여러가지가 있다. 
그 중 Result를 이용해서 에러 핸들링을 해보고자 한다. 

 

https://developer.apple.com/documentation/swift/result

 

Result | Apple Developer Documentation

A value that represents either a success or a failure, including an associated value in each case.

developer.apple.com

 

 

Result 란?

각각의 케이스와의 관련 값을 포함해서 성공이나 실패를 나타내는 값이다.

enum 타입이며 Success, Failure라는 두 개의 제네릭한 결과를 리턴한다. 또한 Failure 제네릭은 Error를 상속받은 타입이어야 한다. 

 

 

그럼 이제 실제 스파클 앱에서 사용해본 경우를 소개해보도록 하겠다. 

enum CoupleJoinError: String, Error {
    case badRequest = "UE1001"
    case invalidCoupleCode = "UE1007"
    case emptyToken = "UE1002"
    case expiredToken = "UE2001"
    case invalidToken = "UE2002"
    case connectedCoupleCode = "UE10002"
    case serverError = "UE500"
    case unknown
    func errorResponse() -> String {
        switch self {
        case .badRequest:
            return "잘못된 요청입니다."
        case .invalidCoupleCode:
            return "입력하신 코드 정보를 찾을 수 없어요"
        case .emptyToken:
            return "다시 로그인을 해야 합니다."
        case .expiredToken:
            return "다시 로그인을 해야 합니다."
        case .invalidToken:
            return "다시 로그인을 해야 합니다."
        case .connectedCoupleCode:
            return "이미 사용중인 코드에요"
        case .serverError:
            return "현재 서버에 오류가 생겼습니다. 잠시 후 다시 시도해주세요"
        case .unknown:
            return "예기치 못한 에러입니다."
        }
    }
}

에러 열거형은 Error라는 프로토콜을 채택한다. 

여기서 Error는 요구사항이 없는 프로토콜이지만 해당 타입이 에러 핸들링에 사용된다는 것을 가르킨다. 

(추가로 enum을 쓸 때 그에 따른 결과를 구하고 싶을 때 메서드를 함께 적어서 경우마다 결과를 쉽게 도출할 수 있도록 하는 편이다.!)

 

 

 

struct CoupleJoinRepository {
    static func postCoupleJoin(code: String, completion: @escaping ((Result<String, CoupleJoinError>) -> Void)) {
        let params: Parameters = [
            "inviteCode": code
        ]
        PostService.shared.postService(with: params, isUseHeader: true, from: Config.baseURL + "") { (data: PostCoupleJoinDataModel?, _) in
            guard let code = data?.code else {
                    completion(.success("성공"))
                    return
                }
                let joinError = CoupleJoinError(rawValue: code) ?? .unknown
                switch joinError {
                case .badRequest, .invalidCoupleCode, .emptyToken, .expiredToken, .invalidToken, .connectedCoupleCode, .serverError:
                    completion(.failure(joinError))
                case .unknown:
                    completion(.failure(.unknown))
                }
        }
    }
}

API 를 호출하는 메서드 부분이다. 

escaping 메서드에 Result 타입을 담아서 API의 성공과 실패를 쉽게 처리하고자 하였다. 

프로젝트를 하면서 서버의 환경에 따라 다르지만 이 API에서 성공했을 때는 아무것도 내려주지 않고 실패했을 경우, 각각의 에러 코드를 내려주고 있다. 

따라서 성공했을 경우는 guard 문을 쓰고 빈 값이기 때문에 성공을 시켜버리고 바로 return을 해주었다. 

반대로 실패했을 경우는 에러 코드가 있을 것이기 때문에 Error 열거형의 rawValue와 매칭시켜 넘겨주었다. 

 

 

 

private func postCoupleJoin(coupleCode: String?) {
        let code = coupleCode
        guard let code else { return }
        CoupleJoinRepository.postCoupleJoin(code: code) { [weak self] value in
            guard let self = self else { return }
            switch value {
            case .success(_):
                self.postCoupleJoinSuccessResponse()
            case .failure(let error):
                print(error.errorResponse())
                self.postCoupleJoinFailureResponse(message: error.errorResponse())
            }
        }
    }

API를 호출하는 메서드를 사용하는 부분이다. 

Result를 쓰면서 좋다고 느낀 부분은 성공 경우와 실패 케이스를 한번에 확인이 가능하다는 것이다.

다음과 같이 escaping 메서드에서 넘어온 Result 열거형 타입은 바로 switch 문으로 성공과 실패 케이스를 나눌 수 있기 때문에 
직관적으로 결과를 컨트롤할 수 있었다. 

(추가로 앞서 error 열거형을 정의할 때 각각의 경우에 따른 결과를 반환할 수 있도록 메서드를 만들었다. error.errorResponse()로 바로 에러 경고 메세지를 보낼 수 있도록 했다.) 

 

 

 

반응형

 

이건 에러 처리를 해주지 않았을 떄 코드이다. 

물론 이전의 코드는 2주안에 프로젝트를 완성시켜야 하는 상황이었기 때문에 많은 것을 패스하기도 했지만 확실히 다시 코드를 보는 리펙토링 과정에서 많은 것을 배우고 있는 것 같다.!!

서버 통신을 하는 코드

func postCoupleJoin(code: String, completion: @escaping ((String?) -> Void)) {
    let params: Parameters = [
        "inviteCode": code
    ]
    PostService.shared.postService(with: params, isUseHeader: true, from: Config.baseURL + "") { (data: PostCoupleJoinDataModel?, error) in
        completion(data?.code)

    }
}

ViewController에서 서버 통신에 따른 결과를 컨트롤 하는 코드

 private func postCoupleJoin() {
        let code = enterInvitationView.invitationTextField.sdsTextfield.text
        guard let code else { return }
        coupleJoinRepository.postCoupleJoin(code: code) { value in
            guard let value = value else { return }
            if let value = value {
                if value == "UE1007" {
                    /// 옳바르지 않은 코드
                    self.view.removeIndicator()
                    self.enterInvitationView.invitationTextField.errorLabel.text = "입력하신 코드 정보를 찾을 수 없어요"
                    self.enterInvitationView.invitationTextField.errorLabel.textColor = .red500
                    self.enterInvitationView.invitationTextField.sdsTextfield.layer.borderColor = UIColor.red500.cgColor
                    self.enterInvitationView.invitationTextField.errorLabel.isHidden = false
                } else {
                    self.view.removeIndicator()
                    print("예외")
                }
            }
            // 성공
            else {
                print("성공")
                self.view.removeIndicator()
                UserDefaultsManager.shared.save(value: true, forkey: .hasCoupleCode)
                let homeViewController = HomeViewController()
                self.changeRootViewController(UINavigationController(rootViewController: homeViewController))
            }
        }
    }

 

물론 이전의 코드는 2주안에 프로젝트를 완성시켜야 하는 상황이었기 때문에 많은 것을 패스하기도 했지만 확실히 다시 코드를 보는 리펙토링 과정에서 많은 것을 배우고 있는 것 같다.!!

반응형