https://developer.apple.com/videos/play/wwdc2021/10132/
Meet async/await in Swift - WWDC21 - Videos - Apple Developer
Swift now supports asynchronous functions — a pattern commonly known as async/await. Discover how the new syntax can make your code...
developer.apple.com
1. 동기 함수(synchronous) vs 비동기 함수 (asynchronous)(0:59)


- preparingThumbnail: 동기 함수
- prepareThumbnail: 비동기 함수
- 동기 함수, 즉 흔한 일반 함수를 호출하면 해당 스레드는 그 함수가 끝날 때까지 차단되어 작업이 끝날 때까지 다른 일을 할 수 없음
- 반대로, 비동기 버전인 prepareThumbnail 함수는 실행되는 동안 스레드는 다른 일을 할 수 있고, 작업이 끝나면 completion handler를 호출해 완료 사실을 알려줌
→ 이러한 비동기 함수는 호출되면 작업을 시작하고 빠르게 스레드 차단을 해제하여, 오래 걸리는 작업이 끝나는 동안 스레드는 다른 일을 할 수 있다는 장점이 있음
2. async / await은 왜 나왔을까? (3:43)
다음의 예시를 살펴보자

- 첫 번째 작업의 입력으로 쓰일 문자열과, 결과를 호출자에게 돌려주기 위한 completion handler를 인자로 받음
- 문자열(id)를 URLRequest로 만듦
- URLSession의 dataTask 메서드가 그 요청에 대한 데이터를 가져옴
- 받아온 데이터를 UIImage로 변환
- UIImage의 prepareThumbnail 메서드로 원본 이미지로부터 썸네일을 렌더링
→ 이 작업들은 각각 이전 결과에 의존하기 때문에 순서대로 수행되어야 함
→ 문자열에서 URLRequest를 만드는 것과 데이터에서 UIImage를 만드는 부분은 동기 호출이어도 됨 (빨라서)
그러나, 데이터 다운로드, 썸네일 렌더링은 오래 걸리기 때문에 비동기 호출이어야 함
비동기 프로그래밍을 Completion Handler로 작성하면
- 코드가 장황하고 중첩되어 의도 파악이 어려움
- 모든 경로에서 콜백을 호출해야 하는데 누락되기 쉬움 (guard문에서 콜백 호출을 잊는 경우)
- 에러 전파가 분산되고, 컴파일러가 정적 검증을 충분히 못함
→ 콜백은 그저 클로저이기 때문에 throw 사용 불가하고, guard에서 콜백 호출 없이 return 해도 컴파일 오류 발생 x
Result 타입으로 개선 (7:53)
아래와 같이 Result 타입으로 보완 가능하지만, 그만큼 절차(보일러플레이트)가 늘어나 코드가 복잡하고 길어짐
func fetchThumbnail(for id: String, completion: @escaping (Result<UIImage, 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()
}
async/await 사용 (8:30)
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 handler를 받지 않고 함수 자체가 async 키워드가 붙음
- 이미지의 썸네일이 성공적으로 만들어졌다면, 썸네일 리턴, 오류가 발생하면 throw로 간단한 구조
- thumbnailURLRequest은 동기 함수이브로 스레드 블록
- dataTask와 달리 data 메서드는 await이 가능하여 함수는 빠르게 자기 자신을 일시 중단하여 스레드 차단을 해제, 스레드는 다른 작업 가능
- try가 있는 이유는 data 메서드가 throws로 표시되어 있는 에러를 던질 수 있는 함수이기 때문
- 데이터 다운로드가 끝나면 data 메서드는 재개되어 fetchThumbnail로 돌아옴
async/await를 사용한다면 (11:48)
- async 함수는 필요시 일시 중단(suspend)되고, 결과가 준비되면 재개 (resume)
- 직선적이고 간결한 코드 구현 가능
- throw/try를 사용하여 정상 / 실패 경로를 컴파일러가 강하게 검증해, 누락 가능성이 없어짐
- 호출자는 반드시 값 또는 오류를 받을 수 있음
→ 더 안전하고 짧고, 개발자의 의도를 더 잘 반영한 코드
3. 스레드 관점에서의 차이 (15:19)
- 동기 호출: 호출 스레드를 블록
- Completion Handler 기반 비동기: 호출 즉시 언블록(백그라운드에서 작업), 나중에 콜백
- async/await: 호출 지점에서 스레드를 블록하지 않고 함수 실행을 suspend
- 시스템이 스레드를 다른 작업에 재활용 가능
- 재개 시점/스레드는 시스템이 결정 (다른 스레드에서 재개 될 수 있음 → 처음 스레드와 재개되는 스레드 다를 수 있음)
- await 표기는 ‘이 사이에 앱 상태가 바뀔 수 있음’을 개발자에게 분명히 표시
중요: async라고 해서 반드시 suspend 되는 것이 아님, await이 있어도 상황에 따라 즉시 반환할 수 있음
- suspend는 “결과가 아직 준비되지 않은 경우”에만 발생
- 결과가 이미 준비되어 있으면 await은 즉시 반환 → 스레드 재활용 X.
await 키워드: async 함수가 그 지점에서 일시 중단 될 수 있음을 나타냄
동기 함수의 스레드 제어권
어떤 함수를 호출하든 함수가 실행 중인 스레드 제어권을 그 함수에 넘겨줌
만약 호출한 함수가 일반(동기)함수일 경우, 그 스레드는 함수가 끝날 때까지 해당 작업만 수행됨

호출된 함수는 값을 반환하거나, 오류를 던짐으로써 종료되고, 해당 함수는 제어권을 호출한 함수로 돌려줌
→ 일반 함수가 스레드 제어권을 넘겨줄 수 있는 유일한 방법은 끝나느 것뿐
그리고, 제어권을 넘겨받는 대상은 함수를 호출한 함수 뿐
비동기 함수의 스레드 제어권 (18:07)
만약 async 함수를 호출하게 된다면
일반 함수처럼 일이 끝나면 종료하고 제어권을 호출한 함수로 돌려주긴 하지만,
완전히 다른 방식으로 스레드 제어권을 넘길 수 있음 → 일시 중단 (suspend)
일반 함수와 마찬가지로 async 함수를 호출하면 스레드의 제어권을 그 함수에 넘김
async 함수는 일시 중단 될 수 있는데, 일시 중단되면 스레드의 제어권을 포기하는 것
하지만, 호출한 함수로 제어권을 돌려주는 대신, 시스템에 제어권을 넘기게 되어 호출한 함수도 일시 중단됨
함수가 스스로 일시 중단되면, 시스템은 그 스레드를 다른 작업에 사용할 수 있고,
어느 순간 시스템은 가장 중요한 작업이 이전에 스스로 일시 중단했던 async 함수를 계속 실행하는 것이라고 판단하면 재개하고, 그 async 함수는 다시 스레드의 제어권을 얻고 작업을 계속할 수 있음
→ 작업의 우선순위를 시스템에게 맡기는 것
중요한 것은 아예 일시 중단할 필요가 없을 수 있음
async 함수는 일시 중단할 수 있지만, async로 표시되었다고 해서 반드시 일시 중단하는 것은 아님
마찬가지로, await을 사용한다고 해서 그 지점에서 함수가 반드시 일시 중단되는 것도 아님
단 한 번도, 일시 중단하지 않았든지, 마지막으로 재개된 이후든지 함수는 종료하면서 스레드의 제어권을 여러분의 함수에 돌려주고, 값이나 오류를 함께 넘김

- URLSession의 async data 메서드를 호출하면 data 메서드는 스레드에서의 실행을 멈추고 (일시 중단), 스레드의 제어권을 시스템에게 넘겨 data 메서드에 대한 스케쥴링 요청
- 이 시점에서는 시스템이 제어권을 쥐고 있으면 해당 작업이 바로 시작되지 않을 수 있으며, 다른 일을 먼저 할 수 있음
예를 들어, fetchThumbnail 호출된 이후 사용자가 어떤 데이터를 업로드하는 버튼을 탭했다고 가정해본다면,
시스템은 이미 대기열에 있던 작업보다 사용자의 반응을 전송하는 작업을 실행할 수 있음
이후, 전송 작업이 완료되면 URLSession의 data 메서드가 재개될 수 있음
→ 함수가 일시 중단되는 동안 다른 작업이 수행될 수 있는 사실이 async 호출에 await이 강제되는 이유!
→ 함수가 일시 중단 되는 동안 앱의 상태가 급격히 바뀔 수 있는 사실을 인지해야 함!!
async가 될 수 있는 것은 함수만이 아님
프로퍼티가 될 수 있고, 이니셜 라이저도 될 수 있음
4. Async properties (13:16)
UIImage Extension에 thumbnail 프로퍼티 추가
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
// print(await image.thumbnail) // 접근 후, 기다렸다가 값이 생기면 출력
- CGSize를 만들어 byPreparingThumbnail(ofSize:)에 전달한 결과를 await함
- async property 주의점
- 명시적 getter 필요
- 프로퍼티를 async로 표시하려면 명시적으로 getter가 필요함
- Swift 5.5부터는 프로퍼티 getter도 throw 가능
- setter 사용 불가능 - 읽기 전용 (read-only) 프로퍼티에만 async가 될 수 있음
- setter는 값을 설장하는 기능인데 이 작업은 일반적으로 동기적으로, 즉 즉시 수행되어야 함
- setter가 비동기적이라면 속성 값 설정 후, 실제 값이 반영되기까지 다른 코드가 실행될 수 있고, 예측 불가능하게 되어 디버깅이 안 됨
- 명시적 getter 필요
5. Async sequences (14:05)
for 루프에서 async 시퀀스를 순회할 때 await 사용 가능
async 시퀀스는 일반 시퀀스와 같지만, 요소를 비동기적으로 제공함
따라서 다음 항목을 가져오는 동작은 async임을 나타내기 위해 await 키워드를 사용
for await id in staticImageIDsURL.lines {
let thumbnail = await fetchThumbnail(for: id)
collage.add(thumbnail)
}
let result = await collage.draw()
- 함수가 async 시퀀스를 반복하는 동안 다음 요소를 기다리는 동안 스레드 차단을 해제할 수 있고, 그 다음 요소와 함께 루프 본문으로 재개 되거나, 더 이상 요소가 없으면 루프 이후로 재개 됨
https://developer.apple.com/videos/play/wwdc2021/10058/
https://developer.apple.com/kr/videos/play/wwdc2021/10134/
Explore structured concurrency in Swift - WWDC21 - 비디오 - Apple Developer
When you have code that needs to run at the same time as other code, it's important to choose the right tool for the job. We'll take you...
developer.apple.com
6. Testing async code (20:24)
XCTest는 기본적으로 async를 지원함
Testing using XCTestExpectation
이전에는 XCTestExpectation을 설정하여 테스트 대상 API를 호출하고, expectation을 fulfill, wait 해야하는 번거로운 과정이 필요했었음

Testing using async/await
테스트 함수에 async 표시하는 형태로 간단하게 구현 가능해짐

7. SwiftUI에서의 async/await (22:13)


- completion handler를 제거하고 try, await을 추가하면 빌드 오류 발생
- async가 아닌 컨테스트에서는 async 함수를 호출할 수 없다는 오류!
- onAppear modifier는 일반 클로저를 받기 때문에 동기에서 비동기를 연결할 방법이 필요
→ async task 함수 사용
async task

- async task는 클로저 안의 작업을 포장하여 다음 사용 가능한 스레드에서 즉시 실행되도록 시스템에 보냄
- 마치 글로벌 디스패치 큐에서 async 함수를 사용하는 것과 같음
- 동기 컨텍스트 내부에서 비동기 코드를 호출할 수 있음
https://developer.apple.com/videos/play/wwdc2021/10019/
Discover concurrency in SwiftUI - WWDC21 - Videos - Apple Developer
Discover how you can use Swift's concurrency features to build even better SwiftUI apps. We'll show you how concurrent workflows interact...
developer.apple.com
8. Completion Handler에서 async/await으로의 전환 (24:27)
Swift 5.5부터는 Swift 컴파일러가 Objective-C에서 import된 completion handler 코드를 자동으로 살펴보고 async 대안을 제공함
많은 delegate API도 completion handler 코드를 async로 사용 가능해짐
예시로, ClockKit의 Complication Data Source 메서드를 보면
기존 completion handler

- 모든 경로에서 handler를 호출해야하므로 실수가 잦음
- 클로저 중첩과 guard 처리로 코드가 지저분함
변경 후 async/await

- 클로저가 사라져 코드 흐름 단순화
- return으로 결과를 바로 반환하여 실수 여지 줄음
- 네이밍 규칙 변화
- completion handler 기반 함수는 종종 ‘get’ 접두사를 붙여 ‘이 함수가 호출 시 바로 값을 반환하지 않는다’는 걸 강조함
- 하지만 async 함수는 결과를 await 하면 자연스럽게 반환되도록 접두사를 불필요하게 됨
9. Continuations (26:59)
Swift가 기존의 completion handler를 async로 변환해주는 API를 살펴봤지만, 이미 구현된 우리의 코드를 직접 async로 변환해야 함
예를 들어, Core Data에 저장해둔 값을 가져오기 위해 getPersistentPosts 함수를 사용한다고 생각해보자

이 함수는 많은 곳에서 호출하기 때문에 바로 전체를 async로 바꾸기에는 변화가 너무 큼
따라서, 새로운 async 함수를 만들어 기존의 getPersistentPosts를 호출하여 연결하는 메서드를 만드는 것이 효율적

- 콜백에서 나온 결과를 persistentPosts를 호출하고 await하고 있는 곳으로 돌려줘야 함
- 그 호출자들은 현재 일시 중단된 상태이므로 적절한 시점과 올바른 데이터로 재개되도록 보장해야 나머지 작업을 이어나갈 수 있음
일시 중단/재개 과정

- persistentPosts의 async 버전이 호출되면 CoreData로 호출을 전달함
- 어느 시점에 Core Data가 completion handler를 호출하여 fetch 요청의 결과를 getPersistentPosts에 전달함
- 현재 부족한 것은 persistentPosts에서 getPersistentPosts를 호출한 completion handler를 await하고 fetch 요청의 결과로 재개할 수 있게 해주는 브릿지가 필요함
→ Continuation 사용
Checked continuation
- 호출자는 함수 호출의 결과를 await 하고 다음에 무엇을 할지를 지정하기 위해 클로저를 제공
- 함수 호출이 완료되면 그 함수는 completion handler를 호출하여 호출자가 결과로 하려고 했던 일을 재개함

- withCheckedThrowingContinuation 함수는 오류를 갖는 completion 블록을 throwing async Swift 함수를 끌어올려 줌
- 오류를 안 던지면 withCheckedContinuations 사용
- 이 함수들은 일시 중단된 async 함수를 재개하는 데 사용할 수 있는 continuation 값에 접근하는 방법
- 또한, getPersistentPosts 호출을 await할 수 있게 해줌으로써 브릿지 구축
- completion handler로 받은 결과를 continuation.resume을 이용하여 전달하여 중단된 호출을 재개하는 역할
- resume은 반드시 정확히 한 번 호출되어야 함!!!!
- 한 번도 호출되지 않으면 함수가 절대 재개되지 않아 런타임 오류 발생
- 여러 번 재개하면 데이터를 손상시킬 수 있어 오류 발생됨
- withUnsafeContinuation 사용하면 에러나지만, CheckedContinuation은 내부적으로 오류는 안 나게 걸러줌)
https://developer.apple.com/videos/play/wwdc2021/10254/
마무리 정리
- 기존 Completion Handler 기반 비동기 코드는 장황하고 실수 유발
- async/await은 동기 코드처럼 읽히는 직선 흐름으로 비동기를 작성하게 해주며 컴파일러가 안정성 보장
- SDK(UIKit, Foundation 등)에 수백 개의 awaitable API가 이미 제공/자동 변환됨
- 테스트, SwiftUI에서도 곧바로 사용 가능
async
- 함수/프로퍼티/이니셜라이저가 비동기임을 명시하는 키워드
- suspend될 수 있을을 의미 (항상 되는 것은 아님)
- 함수가 중단되면, 호출자도 함께 일시 중단 (호출자도 async 여야 함)
- 시그니처 규칙: async는 throws 앞에 위치
func load() async throws -> Data
var thumbnail: UIImage? { get async throws }
init(path: String) async
await
- async 함수가 한 번 혹은 여러 번 일시 중단될 수 있는 위치를 가리키기 위해 await 키워드 사용
- 비동기 결과를 기다리는 지점 (suspend point) 표시
- 일시 중단되어 있는 동안 스레드는 다른 작업을 스케줄할 수 있음
- 같은 표현식에 여러 async 호출이 있어도 await 한 번이면 충분
- 함수는 처음 스레드와 다른 스레드에서 재개될 수 있음
let (data, response) = try await URLSession.shared.data(for: request)
let image = await loader.thumbnail
Task
- 비동기 작업 실행 단위
- sync 컨텍스트에서 async 호출을 가능하게 하는 브리지
- 클로저 안의 작업을 포장하여 다음 사용 가능한 스레드에서 즉시 실행되도록 시스템에 보내짐
- Task는 현재 context(메인 액터라면 메인, 백그라운드라면 백그라운드)를 따름
Task {
let v = try await api()
}'WWDC' 카테고리의 다른 글
| WWDC 2021 - Protect mutable state with Swift actors 정리 (0) | 2025.09.09 |
|---|---|
| WWDC 2021 - Meet AsyncSequence 정리 (0) | 2025.09.08 |
| WWDC 2021 - Use async/await with URLSession 정리 (0) | 2025.09.05 |
| WWDC 2021 - Discover concurrency in SwiftUI 정리 (0) | 2025.09.04 |
| WWDC 2021 - Explore structured concurrency in Swift (1) | 2025.09.03 |