<aside> 💭 비동기 함수를 정의해볼까요?

Swift 5.5에는 새로운 async/await 패턴이 도입되어 일반 동기 코드처럼 보이는 비동기 함수를 작성할 수 있습니다.

비동기 함수 정의

Defining an Asynchoronous Function

정의 방법

✅  파라미터 목록 뒤, 화살표(→) 앞에 키워드를 추가하도록 합니다.

// func fetchParticipants() async -> [Participant] {...}

class ViewModel: ObservableObject {
   @Published var participants: [Participant] = []

   func fetchParticipants() async -> [Participant] {...}
}

호출 방법

✅  await 키워드를 사용하여 비동기 함수를 호출할 수 있습니다. awaited(대기)중인 코드는 실행을 일시 중단할 수 있기 때문에, 비동기 함수의 본문 내부와 같은 비동기 컨텍스트에서만 사용할 수 있습니다.

class ViewModel: ObservableObject {
   @Published var participants: [Participant] = []

   func refresh() async {
      let fetchedParticipants = await fetchParticipants()
      self.participants = fetchedParticipants
   }
   func fetchParticipants() async -> [Participant] {...}
}

🔖  await 키워드를 사용하면, line 5의 fetchParticipants의 수행이 완료될 때까지 나머지 기능의 실행을 일시 중지할 수 있습니다. 함수가 일시 중지된 동안 스레드는 다른 작업을 자유롭게 수행할 수 있습니다. 완료되면 시스템은 함수의 다음 라인을 실행합니다.

struct ContentView: View {
   @StateObject var model = ViewModel()
 
   var body: some View {
      NavigationView {
         List {
            Button {
               Task {
                  await model.refresh()
               }
            } label: {
               Text("Load Participants")
            }
            ForEach(model.participants) { participant in
               ...
            }
         }
      }
   }
}

🔖  Task를 생성해서 동기 컨텍스트에서 비동기 함수를 호출할 수 있습니다.

struct ContentView: View {
   @StateObject var model = ViewModel()
 
   var body: some View {
      NavigationView {
         List {
            ForEach(model.participants) { participant in
               ...
            }
         }
         .task {
            await model.refresh()
         }
      }
   }
}

🔖  SwiftUI는 task뷰가 나타날 때 비동기 함수를 실행하는 데 사용할 수 있는 modifier를 제공합니다.

🔖  뷰가 사라지면 시스템이 자동으로 작업을 취소합니다.

</aside>

<aside> 💭 가보자, 바꿔보자

기존의 클로저 완료 콜백(Closure Completion Callbacks)

func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
    // .. perform data request
}

단점을 모아볼까요?

  1. 각 메서드의 종료 시점에서 완료 클로저를 호출해야 합니다.
  2. 클로저는 가독성을 떨어트립니다. 읽기가 어렵습니다.
  3. 약한 참조(weak references)를 사용해서 유지 주기(retain cycle)를 피해야 합니다.
  4. 구현자(Implementor)는 결과를 얻기 위해 결과를 전환해야 합니다. 구현 수준에서 try catch 문을 사용할 수 없습니다. (???)

Await, Async의 친구.

do {
    let images = try await fetchImages()
    print("Fetched \\(images.count) images.")
} catch {
    print("Fetching images failed with error \\(error)")
}

놀랍게도 위의 코드는 비동기 작업을 수행하고 있습니다. await 키워드를 사용해서 프로그램이 메서드의 결과를 기다리고 fetchImages()의 결과가 도착한 후에만 계속하도록 지시합니다.

구조적 동시성? 선형적 실행

async-await을 통한 구조적 동시성은 실행 순서를 쉽게 추론할 수 있도록 합니다.

클로저와 같이 앞뒤로 이동하지 않고 선형적으로 실행됩니다.

// 1. Call the method
fetchImages { result in
    // 3. The asynchronous method returns
    switch result {
    case .success(let images):
        print("Fetched \\(images.count) images.")
    case .failure(let error):
        print("Fetching images failed with error \\(error)")
    }
}
// 2. The calling method exits
// 1. Call the method
fetchImages { result in
    // 3. The asynchronous method returns
    switch result {
    case .success(let images):
        print("Fetched \\(images.count) images.")
        
        // 4. Call the resize method
        resizeImages(images) { result in
            // 6. Resize method returns
            switch result {
            case .success(let images):
                print("Decoded \\(images.count) images.")
            case .failure(let error):
                print("Decoding images failed with error \\(error)")
            }
        }
        // 5. Fetch images method returns
    case .failure(let error):
        print("Fetching images failed with error \\(error)")
    }
}
// 2. The calling method exits

async-await을 사용한 예제 코드

do {
    // 1. Call the method
    let images = try await fetchImages()
    // 2. Fetch images method returns
    
    // 3. Call the resize method
    let resizedImages = try await resizeImages(images)
    // 4. Resize method returns
    
    print("Fetched \\(images.count) images.")
} catch {
    print("Fetching images failed with error \\(error)")
}
// 5. The calling method exits

오류 발생?

Untitled

동시성을 지원하지 않는 동기 호출 환경에서 비동기 메서드를 호출하려고 할 때 오류가 발생합니다. 그래서 해당 메서드를 비동기식으로 정의해서 컨텍스트를 비동기적으로 만들어줍니다.

func fetchData() async {
    do {
        try await fetchImages()
    } catch {
        // .. handle error
    }
}
final class ContentViewModel: ObservableObject {
    
    @Published var images: [UIImage] = []
    
    func fetchData() {
        Task.init {
            do {
                self.images = try await fetchImages()
            } catch {
                // .. handle error
            }
        }
    }
}

Xcode에서 기존 클로저 기반 메서드를 비동기 지원 메서드로 리팩터링을 지원합니다.

Xcode에서 기존 클로저 기반 메서드를 비동기 지원 메서드로 리팩터링을 지원합니다.

기존 코드

struct ImageFetcher {
    func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
        // .. perform data request
    }
}

1. 함수를 비동기로 변환

struct ImageFetcher {
    func fetchImages() async throws -> [UIImage] {
        // .. perform data request
    }
}

2. 비동기 대안(Async Alternative) 추가

struct ImageFetcher {
    @available(*, renamed: "fetchImages()")
    func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
        Task {
            do {
                let result = try await fetchImages()
                completion(.success(result))
            } catch {
                completion(.failure(error))
            }
        }
    }

    func fetchImages() async throws -> [UIImage] {
        // .. perform data request
    }
}
@available(*, deprecated, renamed: "fetchImages()")

이렇게 써주면, 기본 구현은 더 이상 사용되지 않도록 경고가 표시되게 만들 수 있습니다.

Untitled

그럼 점진적으로 코드를 변환해갈 수 있습니다.

3. 비동기 래퍼(Async Wrapper) 추가

단순히 기존 코드를 사용하기 때문에 가장 쉬운 변환이 될 수 있습니다.

struct ImageFetcher {
    @available(*, renamed: "fetchImages()")
    func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
        // .. perform data request
    }

    func fetchImages() async throws -> [UIImage] {
        return try await withCheckedThrowingContinuation { continuation in
            fetchImages() { result in
                continuation.resume(with: result)
            }
        }
    }
}

Swift에 새롭게 도입된 메서드인 withCheckedThrowingContinuation 메서드를 통해서 클로저 기반 메서드를 변환할 수 있습니다. (cf. withCheckedContinuation)

line 8 : 지정된 클로저가 호출될 때까지 작업을 일시중지합니다. 결과적으로 원래의 이미지 가져오기 콜백에서 반환된 결과 값으로 호출하는 것으로 귀결됩니다.

withCheckContinuation 함수에 넘기는, 클로저 내부에서 호출하는 것이 바로 우리가 이전에 사용하던 레거시 함수입니다. 이 함수를 동일한 이름으로 감싸서 클로저 대신 비동기로 반환 값을 받을 수 있는 함수로 변신시킬 수 있습니다.

스크린샷 2022-04-05 오후 7.52.49.png

</aside>