---
# System prepended metadata

title: '[WWDC 2021] Meet Async/Await in Swift'

---

###### tags: 교육자료

# [WWDC 2021] Meet Async/Await in Swift
[TOC]

## 시작

### Async/Await 개념을 학습하는 이유
> CompletionHandler 를 계속 사용하다 보면 @escaping 클로저를 연속적으로 사용해야 할 때가 있습니다. 예를들면 HTTP GET 요청의 response로 deleteKey 값을 받아와 그 deleteKey 값으로 HTTP DELETE 요청을 할 때 가 있는데요, 문맥을 두번 들여쓰기를 해야하고 이 연속요청이 많으면 많을수록 마치 굼벵이 주름같은 들여쓰기 코드가 생깁니다. 가독성에 악영향을 미치죠. 이를 해결할 방법이 없을까요? 요청의 결과값을 반환할 수는 없을까 많은 궁금증을 가졌었습니다.

<br>

## 문제해결 예시

### @escaping 클로저를 사용하는 기본함수 예시
- UIKit의 기본적인 메서드인 `preparingThumbnail(of:)` 사용하면, 그 함수가 끝날 때 까지 thread 가 block 되어 아무것도 할 수 없게 됩니다.
- 반대로, completionHandler가 있는 비동기 함수인 `prepareThumbnail(of: completionHandler:)` 를 사용하면 thread 가 다른 일을 할 수 있게 됩니다. 이 함수가 끝나면 completionHandler로 해당 작업이 끝났음을 알려줍니다.
- 비동기 함수에는 이러한 장점이 있기 때문에, 네트워크 요청과 같이 비용이 많이들고 시간이 걸리는 작업을 비동기 함수로 처리하게 됩니다.

▼ 이미지를 가져올 때, 우리가 많이 사용하는 예시 코드입니다. 

```swift
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
    let request = thumbnailURLRequest(for: id)
    let task = URLSession. shared. dataTask(with: request) { data, response, error in
        if let error = error {
            completion (nil, error)
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion (nil, FetchError.badID)
        } else {
            guard let image = UIImage (data: data!) else {
                return
            }
            image.prepareThumbnail (of: CGSize (width: 40, height: 40)) { thumbnail in
                guard let thumbnail = thumbnail else {
                    return
                }
                completion (thumbnail, nil)
            }
        }
    }
    task.resume()
}
```

completion 이 3개가 사용되고 있네요! 이러면 완벽한 메서드가 되었을까요? 
자세히 보면 guard 문에서 생긴 에러 처리를 해주지 않고 있어요.

▼ guard 문에 대한 completion 2개를 추가하면 총 5개의 completion 이 사용 될 것입니다.

```swift
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
    let request = thumbnailURLRequest(for: id)
    let task = URLSession. shared. dataTask(with: request) { data, response, error in
        if let error = error {
            completion (nil, error)
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion (nil, FetchError.badID)
        } else {
            guard let image = UIImage (data: data!) else {
                completion(nil, FetchError.badImage) // 추가
                return
            }
            image.prepareThumbnail (of: CGSize (width: 40, height: 40)) { thumbnail in
                guard let thumbnail = thumbnail else {
                    completion(nil, FetchError.badImage) // 추가
                    return
                }
                completion (thumbnail, nil)
            }
        }
    }
    task.resume()
}
```

이렇게 하면 오류처리가 완벽하게 된 것으로 보입니다. <span style="background:lightpink">하지만, 우리는 Swift의 기본적인 에러 핸들링 메커니즘을 사용할 수 없게 됩니다. 문제 안에서 에러를 던질 수 없다는 것입니다. </span>

두개의 guard문을 처리해주지 않고도 컴파일 에러가 나지 않은 이유는 swift가 우리의 작업을 확인할 수 없기 때문입니다(컴파일 단계에서 에러를 잡아낼 수 없다는 뜻)

▼ ResultType을 써서 좀 더 안전하게 처리해줄 수도 있습니다. 하지만 가독성에 악영향을 미치죠..
```swift
func fetchThumbnail(for id: String, completion: @escaping (Result<UlImage, Error>) -> Void) {
    let request = thumbnailURLRequest(for: id)
    let task = URLSession. shared. dataTask(with: request) { data, response, error in
        if let error = error {
            completion(.failure (error))
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion(.failure(FetchError.badID))
        } else {
            guard let image = UIImage (data: data!) else {
                completion(.failure (FetchError.badImage))
                return
            }
            image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
                guard let thumbnail = thumbnail else {
                    completion(. failure (FetchError .badImage))
                    return
                }
                completion (.success (thumbnail))
            }
        }
    }
    task.resume ()
}
```


<br><br>
### Async/await 도입으로 해결

- `thumbnailURLRequest(for:)` 
- `dataTask(with:)`
- `UIImage(data:)`
- `prepareThumbnail(of:)`

▼ 위 네가지 스텝을 async/await 을 사용하여 구현 해보겠습니다.

```swift
func fetchThumbnail(for id: String) async throws -> UIImage {
    let request = thumbnailURLRequest(for: id) // 1.동기함수이기 때문에 thread block
    let (data, response) = try await URLSession.shared.data(for: request) // 2
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
    let maybeImage = UIImage(data: data) // 3 동기함수이기 때문에 thread block
    guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage } //4
    return thumbnail
}
```
- 위처럼 async/await 함수임을 나타내는 키워드 `async`와 에러를 던질 수 있음을 나타내는 키워드 `throws`가 있습니다. 
1. `thumbnailURLRequest(for:)` 메서드는 동기함수 이므로 thread block이 됩니다.
2. `data(for:)` 메서드는 `dataTask()`와 마찬가지로 Foundation에서 제공되는 비동기 함수입니다. 하지만 기존의 dataTask와는 달리 await 키워드를 통해 기다려줄 수 있습니다. 이 메서드는 스스로 중지시켜서 thread unblock을 합니다. 다른 작업들이 실행될 수 있는 것이죠. 이전에 dataTask 를 통해 생긴 에러를 모두 completion에 보내주었던 것 기억 하시나요? 여기선 try 키워드 하나로 처리를 해주게 됩니다 !! 데이터 다운로드가 끝나면 `data(for:)`메서드가 재개(resume) 되고 fetchThumbnail로 돌아옵니다. 이때 가져오는 에러는 이 함수에서 처리됩니다. 
3. 동기함수 이므로 thread block이 됩니다.
4. `thumbnail` 프로퍼티가 재개되고 fetchThumbnail로 돌아올 때 까지 다른 작업들이 실행될 수 있습니다.
<br>

completionHandler를 쓰던 이전버전과는 반대로, 여기서 <span style="background:lightpink">thumbnail이 받아와지지 않으면 에러를 던져주거나 값을 반환해주어야 합니다.</span> 그러지 않으면 컴파일 에러가 날 것입니다.(스위프트가 우리의 작업을 확인해줄 수 있다는 것입니다.)
<br>

이게 다입니다. 20줄 가량의 코드를 고작 6줄로 바꿨습니다. 이렇게 변환하게 되면 코드도 줄고 에러도 안전하게 처리할 수 있을 것입니다. 



▼ 함수 뿐만 아니라 프로퍼티 또한 async 키워드를 사용할 수 있습니다. `thumbnail` 프로퍼티의 구현부 입니다
```swift
extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}
```
- 오직 읽기전용 프로퍼티에만 async 키워드를 사용할 수 있습니다. (swift 5.5 이후부터 프로퍼티의 getter 또한 throw를 사용할 수 있습니다.)
<br>

▼ async는 for-in loop 에서도 사용할 수 있습니다. 

```swift 
for await id in staticImageIDsURL.lines {
    let thumbnail = await fetchThumbnail(for: id)
    collage.add(thumbnail)
}
let result = await collage.draw()
```

- 위 내용을 더 자세히 보고싶다면 
- [Meet AsyncSequence](https://developer.apple.com/videos/play/wwdc2021/10058) 와 
- [Explore structured concurrency in Swift](https://developer.apple.com/videos/play/wwdc2021/10134)를 참고해주세요

<br><br>

## 설명

### async 함수가 suspend 한다는 것은 무엇을 뜻할까요 ? [15:20]

![](https://i.imgur.com/WBLHFxP.png)

보통의 함수가 실행하고 끝나는 모습입니다. 함수가 끝나면 쓰레드 제어권을 포기하게 됩니다.
async 함수도 마찬가지로 실행이 끝나면 다시 상위 함수에게 제어권을 줍니다. 다만, 쓰레드 포기하는 방법이 전혀 다릅니다. 바로 `suspending` 입니다. 


![](https://i.imgur.com/sAo4xL5.png)

async 함수는 내부가 실행되고, await 할 수 있습니다. 이때 다시 함수에게 쓰레드 제어권이 가는 것이 아닌 system으로 갑니다. 이렇게 되면 상위 함수도 중지가 됩니다. 
suspending 은 함수가 시스템에게 "할 일이 많은 것을 알아. 가장 중요한 일을 결정해!" 라고 알리는 방법입니다.
함수가 한번 중지되면 시스템은 다른 작업을 하는데에 해당 쓰레드를 사용할 수 있게 됩니다. 
resume이 되면 suspend는 끝나고 다시 상위 함수로 돌아오게 됩니다.

```swift
func fetchThumbnail(for id: String) async throws -> UIImage {
    let request = thumbnailURLRequest(for: id)
    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { throws FetchError.badID}
    let maybeImage = UIImage(data: data)
    guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage}
    return thumbnail
}
```
위 fetchThumbnail 에서 data 메서드 에서 중지되는 동안 데이터를 업로드 하는 어떤 버튼이 눌렸다고 가정해봅시다. 이때는 이전에 큐에 들어가있던 작업보다 이 버튼작업이 먼저 실행됩니다. 버튼 작업이 끝나고 나서야 data 메서드가 resume 되거나, 다른작업을 실행합니다. data 메서드가 한번 끝나면 다시 fetchThumbnail 함수로 돌아옵니다. 
 이는 async 함수가 중지되어있는 동안 다른 작업(버튼 탭)이 실행될 수 있고, 그래서 await 키워드를 붙여주는 것이라고 할 수 있겠습니다. 
 
 async 함수는 suspend 된 동안 다른 작업이 실행될 수 있기 때문에 함수가 다른 스레드에서 실행될 수도 있습니다. 해당 이슈는 [Protect mutable state with Swift actors](https://developer.apple.com/videos/play/wwdc2021/10133) 여기서 확인할 수 있습니다. 
<br><br>

### Async/await facts

- `async` 키워드를 다는 것은 중지를 허용하는 것입니다.
- 함수를 중지한다면 그 함수의 호출자도 중지됩니다. 그래서 똑같이 호출자도 `async` 키워드를 써야만 합니다.
- 중지될 수 있는 `async` 함수의 앞에 `await` 키워드를 사용합니다.
- `await` 키워드가 무조건 중지한다는 뜻은 아닙니다
- `async` 함수가 중지되면, 쓰레드가 block 되지 않습니다. 그래서 system은 다른 작업들을 스케쥴링 할 수 있게 됩니다. (나중으로 미뤄진 작업이 첫번째로 실행될 수도 있습니다.)
- `async` 함수 호출이 한번 완료되면 `await` 이후에 실행이 재개됩니다.

<br><br>

### Adopting async/await [21:00]

#### 테스트에 적용 해보기

```swift
class MockViewModelSpec: XCTestCase {
    func testFetchThumbnails() throws {
        let expectation = XCTestExpectation(description: "mock thumbnails completion")
        self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
            XCTAssertEaqual(result?.size, CGSize(width: 40, height: 40))
            expectation.fulfill()
        }
        wait(for: [expectation], timeout: 5.0)
    }
}
```
▼ 기본 completionHandler 를 사용하여 비동기 처리를 했을 때, 테스트 방식입니다. 이젠 async를 이용해 간단하게 표현할 수 있습니다. 

```swift
class MockViewModelSpec: XCTestCase {
    func testFetchThumbnails() throws {
        let result = try await self.mockViewModel.fetchThumbnail(for: mockID)
        XCTAssertEaqual(result?.size, CGSize(width: 40, height: 40))
    }
}
```
<br>

#### Bridging from sync to async
Task 로 감싸는 것은 Global Dispatch Queue로 보내는 것과 같습니다.
- [Explore structured concurrency in Swift](https://developer.apple.com/videos/play/wwdc2021/10134)
- [Discover concurrency in SwiftUI](https://developer.apple.com/videos/play/wwdc2021/10019)
<br>

#### Async APIs in the SDK
swift 5.5 부터 기본내장 SDK에 async 코드가 추가되었습니다. 
[deprecated getCurrentTimelineEntry](https://developer.apple.com/documentation/clockkit/clkcomplicationdatasource/1628051-getcurrenttimelineentry)

>위 링크에서 특정 함수가 async로 적용된 형태를 설명하고 있습니다. 심지어 기존의 함수는 이제 사용되지 않을 것이라고 설명되어 있습니다. 

![](https://i.imgur.com/rm86i3l.png)

<br>

#### Async alternatives and continuations

```swift
func getPersistentPosts (completion: @escaping ([Post], Error?) -> Void) {
    do {
        let req = Post. fetchRequest ()
        req. sortDescriptors = [ NSSortDescriptor (key: "date", ascending: true) ]
        let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
            completion (result.finalResult ?? [1, nil)
        }
        try self.managedObjectContext.execute (asyncRequest)
    } catch {
        completion([], error)
    }
}
```

위 함수를 우리가 직접 async 함수로 변환하는 과정을 거쳐보겠습니다. 

```swift
func persistentPosts() async throws -> [Post] {
    self.getPersistentPosts { posts, error in
        
    }
}
```
![](https://i.imgur.com/js8N57k.png)

반환값을 어떻게 넘겨주어야 할까요? 
이러한 흐름은 모든 aync 적용에서 보이는 패턴입니다. 이를 continuation 이라고 합니다. 

```swift
func persistentPosts() async throws -> [Post] {
    typealias PostContinuation = CheckedContinuation<[Post], Error>
    return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
        self.getPersistentPosts { posts, error in 
            if let error = error {
                continuation.resume(throwing: error)
            } else {
                continuation.resume(returning: posts)
            }
        }
    }
}
```

completionHandler 를 async 함수로 만드는 과정을 보셨습니다. continuation 은 async함수의 실행을 수동제어 하기에 강력한 방법입니다.
그러나 주의할 점이 있습니다.  resume 은 모든 path에서 한번! 호출되어야 합니다. 0번 실행시 warning이 뜨고, 2번이상 호출시에는 compiler에서 더욱 심각한 에러를 발생시킵니다.
더 자세한 내용은 [Swift concurrency: Behind the scenes](https://developer.apple.com/videos/play/wwdc2021/10254)이 링크를 확인해주세요


빠진 내용이 있을 수 있습니다. 가장 좋은 것은 직접 WWDC 영상을 확인해 보시면 좋을 것 같습니다. 

### Reference
- [WWDC 2021 - Meet Async/Await in Swfit](https://developer.apple.com/videos/play/wwdc2021/10132/)
