Reference
Async/Await 등장 배경
우선 Swift Concurrency를 사용하려면, async-await 를 알아야한다.
그러려면 동기vs비동기 코드의 차이를 알아야 한다.
동기 코드와 비동기 코드는 iOS를 개발해봤다면 모를 수가 없다.
가장 일반적인 코드 작성 방식이 동기 코드를 작성하는 방법이고, 나중에 이벤트가 발생하면 호출할 코드는 따로 closure 의 형태로 작성하는데 이게 비동기 코드이다. 그래서 보통 오래 걸리는 작업은 비동기 코드를 사용해 언젠지 모르지만 이벤트가 발생하면 그때 처리하도록 하고, 나머지의 일반적인 케이스에서는 동기 코드로 작성하곤한다.
아래처럼 URLSession 의 dataTask 함수가 대표적인 비동기 함수이다. (API response 가 와야 completionHandler 가 호출된다)
func dataTask(
with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, (any Error)?) -> Void
) -> URLSessionDataTask
그런데... completionHandler 와 같은 구조의 비동기코드를 짜면서 이슈가 있었던게...
에러가 나는등의 강제성이 없다보니 아래 예시처럼 completion 을 누락하기가 쉽다는 점이었다...
completion 이라는 건, 애초에 비동기 작업이 완료되면 호출되도록 설계되었다는 건데, 이게 누락되면 문제인게 아래 코드 예시로 설명하면 fetchThumbnail 의 completion이 호출되면(= 썸네일 로드가 완료/실패하면) 로딩 인디케이터를 화면에서 제거해라.. 라는 로직이 있었다고 치면, 아래 예시에서는 로딩 인디케이터가 평생 제거되지 않을 수 있게 된다는 뜻이기 때문이다..!
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 {
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 else {
return // ⚠️ 누락!!
}
completion(thumbnail, nil)
}
}
}
}
그리고 뭣보다 closure 구조를 쓰면, 비동기 코드에서 발생하는 에러를 함수를 통해 Error Throwing 으로 전달할 수가 없게 되고, 결국 completion 에 Error 타입을 추가해 전달할 수 밖에 없다는 불편함이 있었다.
그래서..!
비동기 작업이 종료되었음을 누락없이(= 안전하게) 전달하고, 함수의 throw 기능도 활용 가능하게 한 방법이!
Swift Concurrency 의 async-await 구조이다!!
(추가로 비동기 로직을 여러개 연결했을 때 이전에는 closure 안에 closure 안에 closure... 이렇게 구현했어야했는데 async-await 을 쓰면 nested 방식이 아니어서 가독성도 훨씬 좋아진다!)
자 이제 본격적으로 async-await 이 어떻게 사용되는지 확인해보자
아까 closure로 구현했었던 fetchThumbnail 함수를 async-await 을 사용해 구현하면 아래와 같아진다.
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 {
throw FetchError.badID
}
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage }
return thumbnail
}
completion 를 개발자가 의식적으로 호출하는 것이 아닌 비동기 작업이 종료되면 함수 return을 통해 전달 받을 수 있게 되면서 누락되는 케이스가 제거되었고, completion 파라미터에 포함되어 있던 Error 는 try-throws 로 전달하게된걸 볼 수 있다.
(그리고 nested 구조가 아니어서 가독성도 훨씬 좋다)
Await
그렇다면 await 은 뭘까?
await 라는 건 await 키워드가 있는 위치에서 코드가 일시중단(suspend)될 수 있음을 의미한다.
아래 예시를 통해 await 키워드가 없을 때(동기 코드)와 있을 때(비동기 코드)의 차이점을 비교하면서 일시중단된다는게 어떤 의미인지 확인해보자.
우선 아래와 같이 동기 코드였다면, 아래 순서로 중단없이 쭉쭉 진행될 것이다.
1. doSomething() 을 실행하던 하던 스레드가 A 작업을 한다.
2. doMore() 함수로 스레드 제어권을 넘긴다.
3. doMore() 함수의 B 작업이 완료되면 해당 함수가 return 되면서 스레드가 doSomething() 함수로 반환된다.
4. C 작업을 한다.
func doSomething() {
// A...
doMore()
// C...
}
func doMore() {
// B...
}
하지만 await 키워드를 사용한 비동기 코드는 아래와 같이 동작한다.
1. doSomething() 을 실행하던 하던 스레드가 A 작업을 한다.
2. doMore() 함수는 to-do list 에 저장되고, doSomething() 함수는 중단(suspend)된다.
3. 시스템은 to-do list에서 우선순위가 높은 작업 순으로 스레드를 할당하고, 언젠가 B 작업이 완료되면 doMore() 함수가 return 되면서 doSomething() 함수는 재개(resume)된다.
4. C 작업을 한다.
func doSomething() async {
// A...
await doMore()
// C...
}
func doMore() async {
// await B...
}
따라서 await 은 suspend/resume 될 수 있음을 명시하고 각각의 의미는 아래와 같다.
- 중단(suspend): thread 를 시스템에 넘겨줌
- 재개(resume): thread 를 다시 넘겨 받음
단, 아래를 주의하자.
- await 을 만났다고 반드시 suspend 되는건 아니다. (suspend 없이 진행될 수도 있다)
- suspend 되기 전에 코드를 실행하던 스레드와 resume 된 후에 코드를 실행하는 스레드가 다를 수 있다.
- suspend 된다는 건 스레드가 block 되는게 아닌, 스레드가 다른 작업을 할 수 있도록 풀어준다는 뜻이다.
- 비동기 작업은 언제 완료 될지 알 수 없으며, 다른 비동기 작업이 먼저 종료될 수도 있다. (비동기 완료 순서 보장 X)
추가로 위처럼 함수에서 async-await 을 사용하는 방법도 있지만 다른 활용법들도 있다.
Async properties
setter 없이 getter 만 정의되어있는 프로퍼티는 async 로 구현될 수 있다. Swift 5.5 부터는 async throws 도 가능하다.
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await byPreparingThumbnail(ofSize: size)
}
}
}
AsyncSequence
for await 를 사용해 AsyncSequence에서 값을 하나씩 꺼내면서, 각 값이 준비될 때까지 기다리는 반복문을 구현할 수 있다.
import StoreKit
func observeTransactions() async {
for await result in Transaction.updates {
// result 를 이용해 처리...
}
}
유익했다면 댓글/공감 남겨주세요~~ 작성자에게 큰 힘이 됩니다 ☺️
(이전 글) 👉 Swift Concurrency - 시작하기 전에 (0)
(다음 글) 👉 Swift Concurrency - Structured Concurrency & Task (2)
'Swift' 카테고리의 다른 글
| Swift Concurrency - Structured Concurrency & Task (2) (0) | 2025.10.01 |
|---|---|
| Swift Concurrency - 시작하기 전에 (0) (0) | 2025.09.27 |
| Attribute Wrapper (0) | 2024.08.01 |
| [Swift Standard Library] (0) | 2024.08.01 |