Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
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 31
Archives
Today
Total
관리 메뉴

cchanmi

[iOS] 운영 앱 서비스 Network Error handling 본문

iOS

[iOS] 운영 앱 서비스 Network Error handling

cchanmi 2024. 1. 28. 13:21

안녕하세요.

제가 작년부터 배포를 시작해서 운영하고 있는 서비스가 있는데, 앱에서 에러가 발생했을 때의 에러 핸들링이 잘되어 있지 않아서 QA 때마다 에러가 발생하면 그 원인을 찾기 위해 애를 먹었던 기억들이 종종 있었습니다...

 

최근, 팀원들과 서비스 업데이트 방향을 재정비하면서 에러가 발생했을 때, 유연한 대처가 되지 못하고 앱이 불친절하게 멈춰 버리는 현상을 유저에게 노출시켜서는 안된다는 의견이 나왔습니다.

의논 끝에 세 가지 에러 상황을 정의해 놓고, 해당 에러가 발생한다면 기존처럼 앱이 묵묵부답으로 멈춰 버리는 것이 아니라, 토스트 메시지를 띄어 주어서 상황을 알리고, 재시도를 요청하는 방향으로 기능을 추가하게 되었습니다.

 

그렇게 저희 서비스에서 정의 내린 에러 상황 세 가지는 다음과 같습니다.

 

- 네트워크 연결 실패 ( 유저 에러 )

- 시스템 에러 ( 클라이언트 에러 )

- 데이터 에러 ( 서버 에러 )

 

서버에서 내려오는 response statuscode를 통해서 해당 에러를 처리해야 했고, 현재 네트워크 라이브러리는 Moya를 사용해 왔었는데 기존의 코드에서 많은 문제점을 발견했었습니다.

 

일단 기존의 코드의 첫 번째 문제는 에러 처리가 전혀 되어 있지 않았다는 것이었습니다. 그동안은 개인 프로젝트만 해 와서 에러 데이터를 따로 처리한다기보다 print문을 통해 콘솔로 확인하는 정도로만 구현해 왔었는데, 실제 운영앱에서는 그런 식으로 구현되어 있으면 에러가 발생해도 원인을 찾을 수 없다는 것을 깨달았습니다. ㅠㅠ...


다시 본론으로 돌아와서 다시 생각해 보자면, 일단 네트워크 자체에 성공은 했지만, 에러가 발생했을 때 (클라이언트, 서버)의 에러 처리를 먼저 해 주어야겠다는 생각이 들었습니다.

    static func statusCodeErrorHandling(statusCode: Int) -> SmeemError {
        switch statusCode {
        case 400..<500:
            return .clientError
        case 500...:
            return .serverError
        default: break
        }
        return .userError
    }

 

먼저 statusCode에 따라 어떤 에러가 발생했는지를 판단하고, 해당 에러에 맞는 에러 메시지를 내보낼 수 있는 statusCodeErrorHandling 메서드를 하나 생성했습니다.

 

 

그럼 현재 loginAPI 메서드를 호출했을 때의 결과값은 성공, 실패 두 가지로 나뉠 것입니다.

성공했을 때는 response를 그대로 return 해 주고, 에러가 발생했을 때는 에러를 return 해야 합니다. 즉, 성공과 실패가 다른 타입을 return 해야 하는 상황입니다. 현재 completion 탈출 클로저로 성공했을 때의 response 타입만 반환해 주고 있는데, 이 부분을 Result 타입을 사용해서 수정해 주었습니다.

 

func planList(completion: @escaping (Result<PlanListResponse, SmeemError>) -> ()) {}

 

Result 타입으로 성공했을 때는 LoginResponse를, 실패했을 때는 Error 프로토콜을 채택한 SmeemError 타입을 받습니다.

 

그럼 어디서 에러를 던질까요?

서버에서 온 Response 타입을 지정된 타입으로 mapping 해 주는 map 메서드에서 에러 발생시 에러를 throw로 던져 주고 있었습니다.

 

해당 map 메서드를 호출할 때 do-catch문을 호출해서 throw 하는 에러를 처리해 주면 되겠네요

do {
   guard let data = try result.map(GeneralResponse<PlanListResponse>.self).data else { return }
   completion(.success(data))
} catch {
   let error = NetworkManager.statusCodeErrorHandling(statusCode: statusCode)
   completion(.failure(error))
}

 

 

전체 코드는 이렇습니다.

    func planList(completion: @escaping (Result<PlanListResponse, SmeemError>) -> ()) {
        onboardingProvider.request(.planList) { response in
            switch response {
            case .success(let result):
                let statusCode = result.statusCode
                
                do {
                    guard let data = try result.map(GeneralResponse<PlanListResponse>.self).data else { return }
                    completion(.success(data))
                } catch {
                    let error = NetworkManager.statusCodeErrorHandling(statusCode: statusCode)
                    completion(.failure(error))
                }
                
            case .failure(_):
                completion(.failure(.userError))
            }
        }
    }

case .failure는 아예 서버 통신 성공, 실패 유무를 떠나서 네트워크가 연결이 안 된 상황처럼 서버 통신 요청 자체에 실패한 경우이기 때문에 서비스 요구사항에 맞게 .userError 타입을 보내도록 했습니다.

 

extension HowOnboardingViewController {
    func detailPlanListGetAPI(tempTarget: String) {
        SmeemLoadingView.showLoading()
        
        OnboardingAPI.shared.detailPlanList(param: tempTarget) { result in
            
            switch result {
            case .success(let response):
                self.planName = response.name
                self.planWay = response.way
                self.planDetailWay = response.detail
                self.configurePlanData()
                
            case .failure(let error):
                self.showToast(toastType: .smeemErrorToast(message: error))
            }
            
            SmeemLoadingView.hideLoading()
        }
    }
}

이렇게 되면 ViewController에서는 해당 API를 호출하고 성공하여 데이터가 온다면 데이터를 사용하고, 실패하여 에러가 온다면 어떤 에러인지는 알 필요 없이 그저 전달받은 에러 메시지를 토스트 메시지로 띄워 주는 행위에만 집중할 수 있습니다.

만약 에러가 발생한다면 위와 같이 토스트 메시지를 통해 유저에게 재접속을 요청하게 됩니다.

현재는 네트워트 연결 에러가 뜨는 모습이네요!


 

 

서비스 운영을 하면서 유저에게 좀 더 친절한 UI에 대해서 많이 고민하게 되는 것 같아요.

이번에 추가하게 된 토스트 메시지 또한 그런 의도로부터 시작되었구요.

단순히 프로젝트를 개발만 하다 보면 경험하지 못했을 것들을 서비스 운영하면서 많이 배우는 것 같습니다.

 

Comments