Swift 5.0 이후로 등장한 결과 타입은 성공과 에러를 제네릭 파라미터로 받아서 처리할 수 있는 제네릭 열거형이다.
성공한 결과는 Result.success
연관값으로 저장되고, 실패한 결과는 Result.failure
연관값으로 저장된다.
어떤 에러를 어떤 케이스로 나눌 것인지에 대한 에러 타입 자체는 개발자가 do-catch
에서 하였듯 정의해주면 되고, 아래처럼 사용한다.
// Failure는 Error 프로토콜을 채택하는 요소, 즉 에러를 의미한다.
// 제네릭 파라미터의 이름은 Jaden Case 표기를 따를 때 커스텀이 가능하다.
@frozen enum Result<Success, Failure> where Failure : Error
결과 타입은 에러 처리에서 굉장히 유용하게 쓸 수 있다.
함수를 정의하는 동시에 어떤 에러를 어떻게 연관값으로 만들 것인지 분기 처리가 가능해진다.
무엇보다 코드 가독성을 매우 크게 높여주고 열거형 케이스를 활용하여 에러 처리도 비교적 접근이 쉬워진다.
실제 프로젝트에서 가져온 예시를 한 번 보자.
enum NetworkError: Error {
case networkingError
case dataError
case parseError
case requestError
}
final class NetworkManager {
static let shared = NetworkManager()
typealias NetworkCompletion = (Result<[APIResult], NetworkError>) -> Void
func fetchData(searchTerms: String, completion: @escaping NetworkCompletion) {
let urlString = "\(SomeApi.requestUrl)\(SomeApi.mediaParam)&term=\(searchTerms)"
performRequest(with: urlString) { result in
completion(result)
}
}
... code
}
NetworkManager
는 네트워킹을 담당하는 싱글톤 객체이다.
이스케이핑 컴플리션 핸들러를 통해 앞선 메소드의 데이터는 계속 전달될 수 있다.
그런데 이 객체 내에는 NetworkCompletion
이라는 함수 타입이 있다.
함수 타입의 파라미터에 명시된 Result<[APIResult], NetworkError>)
는 딱 보면 알겠지만, 제네릭 결과 타입이다.
[APIResult]
는 네트워킹의 결과를 연관값으로 받아오는 케이스일 것이고, NetworkError
열거형은 Error
프로토콜을 채택하는 에러를 자기 자신의 연관값으로 저장할 것이다.
이를 후행 클로저, 콜백 함수 형태로 사용하고 코드 가독성을 높이기 위해 typealias
를 활용한 것이다.
fetchMusic(searchTerms:, completion:)
이 실행되면, performRequest(with:,completion:)
를 호출하면서 completion
에 이 타입의 아규먼트가 전달될 것이다.
정의된 performRequest
메소드는 아래와 같다.
private func performRequest(with urlString: String, completion: @escaping NetworkCompletion) {
guard let url = URL(string: urlString) else { return }
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
guard error == nil else {
completion(.failure(.networkingError))
return
}
guard let safeData = data else {
completion(.failure(.dataError))
return
}
guard let response = response as? HTTPURLResponse, (200 ..< 299) ~= response.statusCode else {
completion(.failure(.requestError))
return
}
if let successData = self.parseJSON(safeData) {
completion(.success(successData))
}
else {
completion(.failure(.parseError))
}
}
task.resume()
}
performRequest(with:completion:)
는 fetchData(searchTerms:, completion:)
에서 정의한 completion(result)
콜백 함수를 실행할 것이다.
performRequest(with:completion:)
내에 정의된 task.resume()
가 실행되면서 각 경우에 해당하는 completion 콜백 함수를 실행할 것이다.
이때 파라미터에 전달되는 값이 error라면(Error 프로토콜을 채택한다면), .failure
케이스의 각 연관값으로 처리될 것이다.
반대로 콜백 함수 파라미터로 파싱 데이터가 제대로 전달된다면 .success
케이스에 저장될 것이다.
마지막으로 performRequest의 콜백 함수가 호출될 것인데, 이 콜백 함수는 아래와 같다.
networkManager.fetchMusic(searchTerms: term) { result in
switch result {
case .success(let result):
self.dataArray = result
DispatchQueue.main.async {
self.collectionView.reloadData()
}
case .failure(let error):
print(error.localizedDescription)
}
}
지금까지 호출한 콜백 함수의 파라미터 result의 타입은 열거형 제네릭 결과 타입이다.
이 콜백 함수를 마지막에 꺼내서 직접 쓰기 위해 각 케이스에 접근하고 있다.
한번 발생한 에러를 각 단계에서 처리하지 않고 열거형을 통해 에러 처리가 가능해졌다.
예시에서는 프린트만 하고 있지만, 더 많은 작업이 가능할 것이다.
다만 콜백함수가 많이 쓰이기 때문에 익숙해지기 전까지는 사용에 어려움이 있을 수 있겠다.