- Total
꿈꾸는리버리
[Swift Concurrency 2 ] Async/Await, @escaping 본문
[Swift Concurrency 시리즈]
처음 개발을 시작했을 때 3개의 난관이 있었다. 제일 처음에는 for문이었고, 두번째는 네트워킹, 그리고 세번째는 아키텍쳐...
iOS로 네트워킹을 깊게 파 본적이 없어서 이번에 Concurrency에 대해 공부해야겠다는 마음을 먹었고, 그 시작으로 저번에는 에러처리 try-catch문 / Result에 대해 공부했다. 그리고 오늘은 Async/Await, @escaping !!
네트워킹을 하게 되면 요청 후 답이 올 때까지 기다려야 한다. 그리고 답이 온 후에 재요청을 보낼 수도 있고,,, 하면서 네트워킹을 시작하면 생각해야 하는 사항들이 많아진다. 이를 위해서 나온 게 swift에는 @escaping, Combine, Async/Await가 있다. 하지만.. Combine은 아직 공부를 제대로 안 해서 다음에 포스팅을 추가하도록 하고 오늘은 @escaping, Async/Await에 대해 알아보려 한다.
[ 시리즈 목록 ]
1️⃣ @escaping 에 대하여...
🌷 Closure의 종류
1) Non-Escaping Closure
아래의 closure의 경우에는 runClosure 함수가 종료되기 전에 closure 함수가 실행된다.
이러한 closure를 Non-Escaping Closure 라고 한다.
func runClosure(closure: () -> Void) {
closure()
}
2) Escaping Closure
이에 반면, 아래의 코드 같은 경우에는 fetchData 함수에서 fetchData 밖의 completionhandler에 값을 넣어두기 때문에 fetchData 함수가 종료되더라도 completion 클로져는 살아남아야 한다. 그래서 이름이 escaping이라고 붙어진 거다.. 이 함수를 떠나!! 도망쳐 !!
다시 말해서, completion 클로져는 fetchData 함수가 종료되더라도 "도망쳐서" 살아남아 있다.
class ViewModel {
var completionhandler: (() -> Void)? = nil
func fetchData(completion: @escaping () -> Void) {
completionhandler = completion
}
}
이러한 Escaping Closure는 HTTP Request CompletionHandler에서 많이 사용된다.
아래의 코드의 makeRequest() 함수에서 사용되는 completion 클로저는 함수 실행 중에 즉시 실행되지 않고, URL 요청이 끝난 후 비동기적으로 실행이 된다. 쉽게 말해서 http에서 받은 response 값이 바로 결정나는 게 아니라 네트워킹이 끝나고 실행되기 때문에 makeRequest 함수가 실행될 때 completion 함수가 바로 실행되는 것이 아니라, 네트워킹이 끝난 시점에 실행이 된다.
func makeRequest(_ completion: @escaping (Result<(Data, URLResponse), Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "http://룰랄루")!) { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data, let response = response {
completion(.success((data, response)))
}
}
}
🌷 예시로 알아보기
이미지를 불러와서 View에 보여지는 코드를 작성했다.
[참고] url image 사이트
import SwiftUI
class DownloadImageAsyncImageLoader {
let url = URL(string: "https://picsum.photos/seed/picsum/200/300")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard let data = data,
let image = UIImage(data: data),
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
return nil
}
return image
}
// @escaping 을 붙이는 이유는 completionHandler를 네트워킹이 끝난 후 downloadWithEscaping 함수 외부에서 사용할 것이기 때문
func downloadWithEscaping(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> ()) {
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
let image = self?.handleResponse(data: data, response: response)
completionHandler(image, nil)
}.resume()
}
}
class DownloadImageAsyncViewModel: ObservableObject {
@Published var Image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
func fetchImage() {
loader.downloadWithEscaping { [weak self] image, error in
// 네트워킹은 background에서 실행되기 때문에 UI 작업은 main으로 돌리기
DispatchQueue.main.async {
self?.Image = image
}
}
}
}
struct DownloadImageAsync: View {
@StateObject private var viewModel = DownloadImageAsyncViewModel()
var body: some View {
ZStack {
if let image = viewModel.Image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 250, height: 250)
}
}
.onAppear {
viewModel.fetchImage()
}
}
}
2️⃣ Async/Await
위의 코드를 Async/Await로 수정해보자.
🌷 예시 코드
import SwiftUI
class DownloadImageAsyncImageLoader {
let url = URL(string: "https://picsum.photos/seed/picsum/200/300")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard let data = data,
let image = UIImage(data: data),
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
return nil
}
return image
}
// 네트워킹 실행 후 실패시 실패 값을 던지므로 throws 추가
// async 함수임을 명시하기 위해서 async 추가
func downloadWithAsync() async throws -> UIImage? {
do {
// URLSession.shared.data가 실행되고 응답값은 천천히 오기 때문에 await 키워드 추가
let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)
return handleResponse(data: data, response: response)
} catch {
throw error
}
}
}
class DownloadImageAsyncViewModel: ObservableObject {
@Published var Image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
var cancellables = Set<AnyCancellable>()
// async 함수임을 명시하기 위해서 async 추가
func fetchImage() async {
// downloadWithAsync async 함수이기 때문에 await 붙여주기, 실패시 throw 날리기 때문에 try 붙여주기
let image = try? await loader.downloadWithAsync()
// 네트워킹은 background에서 일어나기 때문에 UI는 main으로 돌려주기 (MainActor에 대해서는 다음에 포스팅 하겠습니다!)
await MainActor.run {
self.Image = image
}
}
}
struct DownloadImageAsync: View {
@StateObject private var viewModel = DownloadImageAsyncViewModel()
var body: some View {
ZStack {
if let image = viewModel.Image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 250, height: 250)
}
}
.onAppear {
// async 함수를 호출할 때는 Task를 사용, fetchImage는 async 함수이기 때문에 await 붙여주기
Task {
await viewModel.fetchImage()
}
}
}
}
#Preview {
DownloadImageAsync()
}
🌷 Async/ Await의 장점
과거 Closure 및 completion handlers를 사용하는 asynchronous(비동기) 프로그래밍을 할 때, 많은 비동기 작업 / 오류 처리 / 비동기 호출간의 제어 흐름이 복잡할 때 문제가 되었고 한다. 왜냐면, [weak self]를 부르는 일, completionHandler를 호출하는 일이 개발자에게 맡겨졌기 때문이다.
아래와 같이 기존의 코드에서는 콜백지옥이 일어나는데,,, async/await를 사용한 코드를 확인하면 한결 보기 편안한 것을 알 수 있다.
🔮 출처
https://developer.apple.com/documentation/swift/updating_an_app_to_use_swift_concurrency
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/
'오뚝이 개발자 > SwiftUI' 카테고리의 다른 글
[SwiftUI] GeometryReader 뽀개기 (3) | 2024.04.21 |
---|---|
슬롯 머신 구현하기 (2) | 2024.04.17 |
[SwiftUI] View Modifier에서 분기처리하기 (0) | 2024.02.02 |
[Swift Concurrency 1] 에러처리 try-catch문 / Result (1) | 2024.02.02 |
[iOS] Sign In with Apple 구현하기 (0) | 2023.09.23 |