<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> 💭 가보자, 바꿔보자
func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
// .. perform data request
}
단점을 모아볼까요?
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
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
동시성을 지원하지 않는 동기 호출 환경에서 비동기 메서드를 호출하려고 할 때 오류가 발생합니다. 그래서 해당 메서드를 비동기식으로 정의해서 컨텍스트를 비동기적으로 만들어줍니다.
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에서 기존 클로저 기반 메서드를 비동기 지원 메서드로 리팩터링을 지원합니다.
struct ImageFetcher {
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
// .. perform data request
}
}
struct ImageFetcher {
func fetchImages() async throws -> [UIImage] {
// .. perform data request
}
}
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()")
이렇게 써주면, 기본 구현은 더 이상 사용되지 않도록 경고가 표시되게 만들 수 있습니다.
그럼 점진적으로 코드를 변환해갈 수 있습니다.
단순히 기존 코드를 사용하기 때문에 가장 쉬운 변환이 될 수 있습니다.
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 함수에 넘기는, 클로저 내부에서 호출하는 것이 바로 우리가 이전에 사용하던 레거시 함수입니다. 이 함수를 동일한 이름으로 감싸서 클로저 대신 비동기로 반환 값을 받을 수 있는 함수로 변신시킬 수 있습니다.
</aside>