오늘은 Swift 5.5부터 도입된 Swift Concurrency라는 동시성 프로그래밍이 지원하는 async/await 비동기 프로그래밍에 대해서 알아보겠습니다.
기존 방식의 문제점: 콜백 지옥
Swift Concurrency가 등장하기 전, 우리는 주로 클로저(Closure)를 이용한 컴플리션 핸들러(Completion Handler) 방식으로 비동기 처리를 했습니다.
func fetchUserProfile(userId: String, completion: @escaping (Result<User, Error>) -> Void) {
// ... 네트워킹 코드 ...
// 성공하면 completion(.success(user))
// 실패하면 completion(.failure(error))
}
이 방식은 간단한 작업 하나에는 괜찮지만, 여러 개의 비동기 작업이 순차적으로 이어져야 할 경우 코드가 매우 복잡해집니다.
fetchUserId(for: "mini") { result in
switch result {
case .success(let userId):
fetchUserProfile(userId: userId) { result in
switch result {
case .success(let user):
fetchUserPosts(user: user) { result in
switch result {
case .success(let posts):
// 최종 성공
print("\n🎉 최종 결과: \(user.name)의 포스트 \(posts.count)개를 가져왔습니다.")
posts.forEach { print("- \($0.title)") }
case .failure(let error):
// 포스트 가져오기 실패 처리
print("\n❌ 에러 발생: \(error.localizedDescription)")
}
}
case .failure(let error):
// 프로필 가져오기 실패 처리
print("\n❌ 에러 발생: \(error.localizedDescription)")
}
}
case .failure(let error):
// ID 가져오기 실패 처리
print("\n❌ 에러 발생: \(error.localizedDescription)")
}
}
이런 코드는 가독성이 떨어지고, 에러 처리도 중복되며, 디버깅도 매우 어려워집니다.
이런 문제를 해결하기 위해 Swift는 async/await이라는 키워드를 도입했습니다. 핵심 아이디어는 "비동기 코드를 동기 코드처럼 순서대로 작성하자!" 입니다.
위의 코드를 async/await으로 바꾸면 아래와 같이 변경됩니다.
func fetchAllUserData() async {
print("🚀 async/await 버전으로 사용자 데이터 요청 시작")
do {
let userId = try await fetchUserId(for: "mini")
print(" ✅ ID 가져오기 성공: \(userId)")
let user = try await fetchUserProfile(userId: userId)
print(" ✅ 프로필 가져오기 성공: \(user.name)")
let posts = try await fetchUserPosts(user: user)
print(" ✅ 포스트 가져오기 성공: \(posts.count)개")
print("\n🎉 최종 결과: \(user.name)의 포스트 \(posts.count)개를 가져왔습니다.")
posts.forEach { print("- \($0.title)") }
} catch {
print("\n❌ 에러 발생: \(error.localizedDescription)")
}
}
Task {
await fetchAllUserData()
}
코드가 위에서 아래로 자연스럽게 읽기 쉬워지고, try-catch로 에러 처리를 해줍니다.
그럼 async/await에 대해서 차근차근 더 알아가보겠습니다
async
async는 함수 선언부에 붙여, 이 함수가 비동기적으로 동작하며 일시 중단 될 수 있음을 알립니다.
func fetchUserId(for username: String) async throws -> String {
try await Task.sleep(nanoseconds: 1_000_000_000) // 1초 대기 (네트워크 시뮬레이션)
if username == "mini" {
return "user-123"
} else {
throw NetworkError.userNotFound
}
}
await
await은 async 함수를 호출할 때 사용합니다. await을 만나면 프로그램은 다음과 같이 행동합니다.
- async 함수의 실행을 시작시킵니다.
- 결과가 돌아올 때까지 현재 함수의 실행을 일시 중단(suspend)합니다.
- 스레드를 막는 게 아니라, 스레드를 다른 작업이 쓸 수 있도록 양보합니다. 그래서 UI는 계속 부드럽게 움직일 수 있습니다.
- async 함수의 결과가 들어오면 중단됐던 지점부터 실행을 재개합니다.
즉, await은 비동기 작업이 끝날 때까지 스레드를 효율적으로 사용하며 기다리는 방법입니다.
오늘은 async/await으로 비동기 코드를 깔끔하게 만드는 법을 알아보았습니다. 하지만 여러 이미지를 동시에 다운로드 받아야 한다면 어떻게 해야 할까요? 다음 편에서는 여러 비동기 작업을 병렬로 처리하여 앱 성능을 향상시키는 방법에 대해 알아보도록 하겠습니다.
'iOS' 카테고리의 다른 글
| Swift Concurrency(Actor, @MainActor) (0) | 2025.08.28 |
|---|---|
| Swift Concurrency(async let, Task) (0) | 2025.08.27 |
| Swift의 비동기 프로그래밍 (Operation) (3) | 2025.08.22 |
| 클로저에서 순환참조 없애기 트러블 슈팅 (3) | 2025.08.21 |
| Swift의 비동기 프로그래밍 (GCD) (0) | 2025.08.20 |