iOS/iOS 지식

Swift5에 도입된 Result Type

홍복치 2024. 5. 6. 21:33

ResultType

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

 

공식 문서에 따르면 ResultType은 성공이나 실패를 나타내는 값으로 각 케이스마다 연관 값을 포함한다고 한다.

Swift5에 도입되었으며 아래는 공식문서에 나와있는 ResultType의 선언이다.

 

이 선언에서 알 수 있는 것은 아래와 같다.

1. Result타입은 "열거형"이다.

2. 또한  Failure는 Error 타입을 채택해야 한다.

아래는 열거형의 각각의 케이스에 따른 연관값을 나타낸다.

success는 'Success' 값을 저장하고

failure는 'Failure'를 저장한다고 한다. 

/// A success, storing a `Success` value.
case success(Success)

/// A failure, storing a `Failure` value.
case failure(Failure)

아래는 공식문서에 작성된 예시인데 Result타입을 반환하는 getNextInteger함수가 있다.

이때 Result<Int, Error> 라고 되어 있기 때문에

success타입에는 Int 연관값, failure값에는 Error 연관값을 갖게 된다.

func getNextInteger() -> Result<Int, Error> { /* ... */ }
let integerResult = getNextInteger()
//integerResult == .success(5)
let stringResult = integerResult.map({ String($0) })
//stringResult == .success("5")

여기서 .success(5)까지는 이해가 됬다. success이면서 연관값 5를 가지는 구나

 

그렇다면 map함수를 이용해서 stringResult를 만드는건 뭐하는 걸까

Result타입에서 서술된 map함수에 대해 알아보기 전에 Array에 서술된 map함수에 대해 잠깐 알아봤다.

 

Array에서 map함수는 주로 형태 변환을 하는 함수이다.

func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]

A mapping closure. transform accepts an element of this sequence as its parameter and returns a transformed value of the same or of a different type.

Transform 매개변수는 매핑 클로저로써 sequence의 요소를 받아서 변형된 값이나 다른 타입의 값을 반환하는 거라고 한다. 

let cast = ["Vivien", "Marlon", "Kim", "Karl"]
let lowercaseNames = cast.map { $0.lowercased() }
// 'lowercaseNames' == ["vivien", "marlon", "kim", "karl"]
let letterCounts = cast.map { $0.count }
// 'letterCounts' == [6, 6, 3, 4]

위와 같이 배열의 요소를 map을 이용해 대문자를 모두 소문자로 변환하거나 각 단어의 글자 수를 받아서 반환한다.

 

다시 돌아와서 result타입에 기술된 map함수를 보면 매개변수인 transform: (succcess) -> NewSuccess가 보인다.

위의 배열의 map 함수와 같이 연관값을 받아서 사용자가 지정된 transform 클로저를 통해 변환시켜서 반환한다.

@inlinable
  public func map<NewSuccess>(
    _ transform: (Success) -> NewSuccess
  ) -> Result<NewSuccess, Failure> {
    switch self {
    case let .success(success):
      return .success(transform(success))
    case let .failure(failure):
      return .failure(failure)
    }
  }
stringResult는 resultType의  map함수를 통해 Int에서 String으로 변환된 값을 얻을 수 있다.
func getNextInteger() -> Result<Int, Error> { /* ... */ }
let integerResult = getNextInteger() 
//integerResult == .success(5)
let stringResult = integerResult.map({ String($0) })
//stringResult == .success("5")

이제는 failure인 경우 사용되는 mapError함수를 알아보자.

  @inlinable
  public func mapError<NewFailure>(
    _ transform: (Failure) -> NewFailure
  ) -> Result<Success, NewFailure> {
    switch self {
    case let .success(success):
      return .success(success)
    case let .failure(failure):
      return .failure(transform(failure))
    }
  }

아까 map함수랑 비슷하게 생겼다는 묘한 생각이 드는데. 아까는 성공값이라면 실패값을 변환할 때 사용된다.

아까 Result 타입의 선언에서 Failure는 Error를 상속받아야 한다고 했는데

transform을 해서 새로운 Failure를 만들 때도 당연히 Error를 상속받아야 한다. 

아래 예시에서 DatedError라는 사용자 정의 구조체는 Error를 상속받고 있으며 mapError 함수를 통해서 기존 Failure연관값을 사용자 정의 값으로 변경하고 있다.

struct DatedError: Error {
       var error: Error
       var date: Date

      init(_ error: Error) {
            self.error = error
            self.date = Date()
       }
}

   let result: Result<Int, Error> = // ...
   // result == .failure(<error value>)
    let resultWithDatedError = result.mapError { DatedError($0) }
   // result == .failure(DatedError(error: <error value>, date: <date>))

예시로 사용을 어떤식으로 하는지 알아보기로 했다.

https://github.com/twostraws/whats-new-in-swift-5-0.git

ResultType에 대한 여러 예시들을 소개 하고 있다.

 

아래는 url을 이용해서 통신을 하는 메소드를 작성한 예시이다.

fetchUnreadCount라는 함수를 작성할때 비동기 통신이 끝난 뒤의 completionHandler를 정의한다..

이 클로저는 Result<Int, NetWorkError>를 받아 처리하고 반환값은 없다.

 

진행되는 스탭을 보면

1) NetworkError는 Error를 상속받는 사용자 정의 오류 케이스를 정의한다.(failure일 경우의 연관값)

2) 비동기 통신이 완료되면 result는 success / failure 케이스에 따라 연관값을 갖는다.

3) completionHandler로 연관값이 전달

4) success / failure에 따라 결과 처리

enum NetworkError: Error {
    case badURL
}

func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {
    guard let url = URL(string: urlString) else {
        completionHandler(.failure(.badURL))
        return
    }

    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5))
}

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count):
        print("\(count) unread messages.")
    case .failure(let error):
        print(error.localizedDescription)
    }
}