Swift 5.5 Release(1)

유재경·2021년 6월 10일

iOS New Release

목록 보기
1/12
post-thumbnail

(※ hackingWithSwift의 글을 번역한 것으로 아래 출처를 남겨두었습니다. 약간의 오역이 있을 수 있으니 지적해주시면 감사드리겠습니다.)

Async/Await

매우 중요한 변화!!
동기인 것처럼 매우 복잡한 비동기 코드도 실행할 수 있도록 도입된 것으로 C#과Javascript에는 이미 있는 비슷한 기능인데 아주 간단하게 구현된다.
1. async 키워드 사용해서 함수 만듦
2. await 키워드를 사용해 호출

기존의 completion handler 방식

우리는 이미 completion handler를 사용해서 함수가 끝나고 값을 전달해주는 아주 복잡한 syntax를 구현해왔다. 이렇게 말이다..

func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) {
    // Complex networking code here; we'll just send back 100,000 random temperatures
    DispatchQueue.global().async {
        let results = (1...100_000).map { _ in Double.random(in: -10...30) }
        completion(results)
    }
}

func calculateAverageTemperature(for records: [Double], completion: @escaping (Double) -> Void) {
    // Sum our array then divide by the array size
    DispatchQueue.global().async {
        let total = records.reduce(0, +)
        let average = total / Double(records.count)
        completion(average)
    }
}

func upload(result: Double, completion: @escaping (String) -> Void) {
    // More complex networking code; we'll just send back "OK"
    DispatchQueue.global().async {
        completion("OK")
    }
}

위 함수들을 조합해서 서버로부터 100,000개의 날씨 기록을 가져와서, 평균 온도를 계산하여, 서버로 평균 온도 값을 전달하고 응답을 받도록 만드면 다음과 같다. call back 지옥

fetchWeatherHistory { records in
    calculateAverageTemperature(for: records) { average in
        upload(result: average) { response in
            print("Server response: \(response)")
        }
    }
}

기존 방식의 문제점은??

함수의 실행을 block하고 값을 즉각 받는 것이 아니라, 준비가 되면 값을 받도록 하는 completion closure를 사용하기 때문에 함수 하나 하나가 실행되기를 기다려야한다는 것이다.
또한, completion handler를 2번 이상 호출하거나 아예 호출하는 것을 잊어버리는 것, @escaping (String) -> Void 구문이 가독성이 떨어진다는 것, 들여쓰기 식으로 pyramid of doom 문제(콜백 문제), 이를 보완하기 위해 Result 라는 반환값 + Error를 리턴할 수 있는 타입을 도입하게 된 것 등이 있다.

이제는 다음과 같이 코드를 엄청나게 깔끔하게 작성할 수 있게 되었다!!!

func fetchWeatherHistory() async -> [Double] {
    (1...100_000).map { _ in Double.random(in: -10...30) }
}

func calculateAverageTemperature(for records: [Double]) async -> Double {
    let total = records.reduce(0, +)
    let average = total / Double(records.count)
    return average
}

func upload(result: Double) async -> String {
    "OK"
}

즉, completion: @escaping ([Double]) -> Void로 장황하게 썼던 코드를 async -> [Dobule]로 표현하면 되는 것이다.

함수를 호출할 때는 다음과 같다. 들여쓰기로 콜백콜백이 아닌 변수로 정의하여 넘겨주는 방식이다.

func processWeather() async {
    let records = await fetchWeatherHistory()
    let average = await calculateAverageTemperature(for: records)
    let response = await upload(result: average)
    print("Server response: \(response)")
}

단, async 함수를 호출 할 때 규칙이 있다

  1. 동기 함수는 비동기 함수를 직접 호출할 수 없다. (이건 말도 안되며, swift는 에러를 던질 것)
  2. 비동기 함수는 다른 비동기 함수를 호출할 수 있고, 필요에 따라 일반적인 동기 함수도 호출할 수 있다
  3. 같은 방식으로 호출될 수 있는 비동기 함수와 동기 함수가 각각 있을 때, swift는 함수가 호출된 맥락에 따라 적절한 것으로 호출할 것이다. 예를 들면, 현재 함수를 호출한 곳이 비동기 함수면 그 다음에도 비동기 함수를, 동기 함수였다면 그 다음에 동기 함수를 호출한다.

Async/Await는 try/catch 구문과 쓰면 아주 강력(?)하다

enum UserError: Error {
    case invalidCount, dataTooLong
}

func fetchUsers(count: Int) async throws -> [String] {
    if count > 3 {
        // Don't attempt to fetch too many users
        throw UserError.invalidCount
    }

    // Complex networking code here; we'll just send back up to `count` users
    return Array(["Antoni", "Karamo", "Tan"].prefix(count))
}

func save(users: [String]) async throws -> String {
    let savedUsers = users.joined(separator: ",")

    if savedUsers.count > 32 {
        throw UserError.dataTooLong
    } else {
        // Actual saving code would go here
        return "Saved \(savedUsers)!"
    }
}  
  
func updateUsers() async {
   do {
       let users = try await fetchUsers(count: 3)
       let result = try await save(users: users)
       print(result)
   } catch {
       print("Oops!")
   }
}

주의할 점은, 비동기 함수가 마법처럼 다른 코드와 동시적으로(concurrently) 맞물려 실행되는 것이 아니다. 오히려 다수의 비동기 함수들을 순차적으로 계속 실행하는 것이다 (이 부분의 번역은 조금 어려운 것 같네요)

AsyncSequence protocol

이건 언제 유용하는가?? 미리 결과를 한 번에 계산해놓지 않고 시퀀스로서 값을 처리하고자 할 때 유용하다!!
Swift의 Sequence와 일치하지만, 몇 가지 규칙이 있다.

1. AsyncSequence와 AsyncIterator를 준수해야 함
2. next() 메소드는 반드시 async를 붙여야 함
3. 시퀀스가 종료되려면 next()로부터 nil을 반환해야 함 (이건 Sequence와 같음)

예시를 살펴보자

struct DoubleGenerator: AsyncSequence {
  typealias Element = Int

  struct AsyncIterator: AsyncIteratorProtocol {
      var current = 1

      mutating func next() async -> Int? {
          defer { current &*= 2 }

          if current < 0 {
              return nil
          } else {
              return current
          }
      }
  }

  func makeAsyncIterator() -> AsyncIterator {
      AsyncIterator()
  }
}

활용 방법

1. for await 구문을 사용하여 값을 돌려볼 수 있다

func printAllDoubles() async {
  for await number in DoubleGenerator() {
      print(number)
  }
}

2. map(), compactMap(), allSatisfy() 등 다양한 메소드 지원

func containsExactNumber() async {
  let doubles = DoubleGenerator()
  let match = await doubles.contains(16_777_216)
  print(match)
}




포스팅이 길어지는 것 같아 다음 시간에 마저 알아보도록 하자..이번에 Swift 5.5에 변화가 꽤 많은 것 같아 몇 번에 걸쳐 작성해야할 것 같다.

(출처: https://www.hackingwithswift.com/articles/233/whats-new-in-swift-5-5,
https://www.youtube.com/watch?v=6C0SFPEy_0Y)

profile
iOS 개발

0개의 댓글