본문 바로가기

iOS/PROJECT

[두깃] 에러처리 (Result)

async, await 처리 실패 후 completion 클로저를 사용하여 api 통신하는 코드로 다시 바꿨었는데,

에러처리를 임시방편으로 마음대로 해놨다가 enum 을 사용하여 에러를 처리하기로 했다.

 

요리조리 시간이 더 걸리는 것 같지만.. 공부 목적이니까..!

 

원래 작성했던 코드는 isFetchUserData 라는 enum 을 만들어서 [ api 통신에 성공 & 입력한 아이디가 있을 경우 성공 ] 하면 success 실패하면 failed 를 클로저로 반환했다.

그리고 실행해서 결과를 받아 success 일때 모달창을 닫고, 실패하면 alert 창을 띄워줬다.

 

코드

더보기
enum IsFetchUserData {
    case success
    case failed
}

struct UserDataManager {
    
    private let githubUserURL = "https://api.github.com/users/"
    
    func fetchUser(userId: String, completion: @escaping (IsFetchUserData) -> Void) throws {
        
        guard let url = URL(string: githubUserURL + userId) else { return }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            if error != nil {
                completion(.failed)
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
                completion(.failed)
                return
            }
            
            guard let data = data else {
                completion(.failed)
                return
            }
            
            if let name = self.userJSONDecode(data: data) {
                let realm = try! Realm()
                let user = User(name: name)
                try! realm.write {
                    realm.add(user)
                }
                
                completion(.success)
            } else {
                completion(.failed)
            }
        }.resume()
    }
    
    private func userJSONDecode(data: Data) -> String? {
        
        let decoder = JSONDecoder()
        
        do {
            let decodeData = try decoder.decode(GithubUserData.self, from: data)
            return decodeData.name
        } catch {
            print("decode failed")
            return nil
        }
    }
}

// 사용
@objc func didPressDoneButton(_ sender: UIButton)  {
    guard let name = self.nameTextField.text else { return }
    userDataManager.fetchUser(userId: name) { result in
        switch result {
        case .success:
            DispatchQueue.main.sync {
                self.dismiss(animated: true)
            }
        case .failed:
            self.hapticNotification.notificationOccurred(.error)
            DispatchQueue.main.sync {
                self.showAlert()
            }
        }
    }
}

 

이제 이 코드를 직접 만든 DoGitError 로 던져주고 핸들링 하도록 바꿀 것이다.

enum DoGitError: Error {
    
    case failedConnectServer
    case userNameNotFound
    
    var errorDescription: String? {
        switch self {
        case .failedConnectServer:
            return String("서버연결에 실패했어요.")
        case .userNameNotFound:
            return String("존재하지 않는 이름이에요.")
    }
}

 

원래 에러처리에 대해 아는 지식은 이정도였다. 가장 기본적인 에러처리

1. error를 enum으로 선언하고

2. 에러가 발생한 부분에서 throw 던진다..

3. 실행은 do try 로 실행한다.

4. 에러를 catch 에서 받아서 처리한다.

 

그런데 우아한 형제들 api 통신 글을 보면서 Result 타입을 클로저에 넘겨주는 코드를 발견했다.

 

그래서 Result 타입이 뭔지 찾아봤는데, api 호출시 발생하는 여러가지 예외 상황에 대처하기 어려워

Swift5 에서 에러를 유연하게 처리할 수 있는 Result<Value, Error> 문법을 지원한다고 한다.

 

여러가지 예외상황이란, 아래 코드를 보면 알 수 있다.

load { (data, error) in
    guard error == nil else { handleError(error!) }
    
    guard let data = data else { return }
    
    handleData(data)
}

처리에 필요한 결과는 성공 or 실패 인데 ( enum으로 success, failed 로 나눴듯이 )

위의 코드에서는 error 가 nil 인지 아닌지, data 가 nil 인지 아닌지 해서 총 4가지 예외 (모호한) 상황이 발생할 수 있다는 것이었다.

 

따라서 이 Result 타입을 가지고 예외처리 할 수 있도록 코드를 바꿔보앗다.

 

Result<String, Error> 

    ->  성공하면 String, 실패하면 Error를 넘겨줄 것이다.

completion(.failure(DogitError.userNameNotFound)) 

    -> 커스텀 에러를 넘겨줄 수 있다.

completion(.success(name))

    -> 성공 시 name 을 넘겨준다.

private let githubUserURL = "https://api.github.com/users/"

func fetchUser(userId: String, completion: @escaping (Result<String, Error>) -> Void) {

    guard let url = URL(string: githubUserURL + userId) else { return }

    URLSession.shared.dataTask(with: url) { data, response, error in
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            completion(.failure(error ?? DoGitError.userNameNotFound))
            return
        }

        guard let data = data else {
            completion(.failure(DoGitError.userNameNotFound))
            return
        }

        if let name = self.userJSONDecode(data: data) {
            let realm = try! Realm()
            let user = User(name: name)
            try! realm.write {
                realm.add(user)
            }
            completion(.success(name))
        } else {
            completion(.failure(DoGitError.userNameNotFound))
        }
    }.resume()
    }

 

이 에러를 처리하는 부분의 코드는 작성했던 코드와 유사했고,

에러를 받아서 alert 창에 던져주는 부분만 변경하였다.

case .failure(let error):
    self.hapticNotification.notificationOccurred(.error)
    DispatchQueue.main.sync {
        self.showAlert(error.localizedDescription)
    }
}

 

결론

api 통신에서 에러의 비동기 처리는 Result 타입을 사용하자. (클로저 사용할 때)

 

왜?

에러의 상황을 명확하게 처리하기 위해 (success, failure)

 

 

 

참조

https://techblog.woowahan.com/2704/

 

iOS Networking and Testing | 우아한형제들 기술블로그

{{item.name}} Why Networking? Networking은 요즘 앱에서 거의 필수적인 요소입니다. 설치되어 있는 앱들 중에 네트워킹을 사용하지 않는 앱은 거의 없을 겁니다. API 추가가 쉽고 변경이 용이한 네트워킹

techblog.woowahan.com

https://jusung.github.io/Result-%ED%83%80%EC%9E%85/

 

[Swift] Result 타입

개 요 작업(Task) 중에는 실패할 수 있는 작업이 있습니다. 디스크에 파일을 쓰거나, API를 호출해 네트워크를 통해 데이터를 가져온다거나, 특정 URL에 있는 데이터를 불러오는 작업이 이 경우에

jusung.github.io