반응형
LinkedIn 개발자로 성장하면서 남긴 발자취들을 확인하실 수 있습니다.
Github WWDC Student Challenge 및 Cherish, Tiramisul 등 개발한 앱들의 코드를 확인하실 수 있습니다.
개인 앱 : Cherish 내 마음을 들여다보는 시간, 체리시는 디자이너와 PM과 함께 진행 중인 1인 개발 프로젝트입니다.
10년 후, 20년 후 나는 어떤 스토리 텔러가 되어 있을지 궁금하다. 내가 만약에 아직 조금 더 탐구하고 싶은 게 있고, 궁금한 게 있다면, 그게 설사 지금 당장의 내 인생에 도움이 안 되는 것 같더라도 경험해보자. 그 경험들을 온전히 즐기며 내 것으로 만들고, 내 일에 녹여내고... 그러다보면 그 점들이 모여 나란 사람을 그려내는 선이 될 테니까.

Recent Posts
Recent Comments
Total
관리 메뉴

꿈꾸는리버리

[Swift Concurrency 2 ] Async/Await, @escaping 본문

오뚝이 개발자/SwiftUI

[Swift Concurrency 2 ] Async/Await, @escaping

rriver2 2024. 2. 3. 16:58
반응형

 [Swift Concurrency 시리즈] 

처음 개발을 시작했을 때 3개의 난관이 있었다. 제일 처음에는 for문이었고, 두번째는 네트워킹, 그리고 세번째는 아키텍쳐...

iOS로 네트워킹을 깊게 파 본적이 없어서 이번에 Concurrency에 대해 공부해야겠다는 마음을 먹었고, 그 시작으로 저번에는 에러처리 try-catch문 / Result에 대해 공부했다. 그리고 오늘은 Async/Await, @escaping !! 

 

네트워킹을 하게 되면 요청 후 답이 올 때까지 기다려야 한다. 그리고 답이 온 후에 재요청을 보낼 수도 있고,,, 하면서 네트워킹을 시작하면 생각해야 하는 사항들이 많아진다. 이를 위해서 나온 게 swift에는 @escaping, Combine, Async/Await가 있다. 하지만.. Combine은 아직 공부를 제대로 안 해서 다음에 포스팅을 추가하도록 하고 오늘은 @escaping, Async/Await에 대해 알아보려 한다.

 

[ 시리즈 목록 ]

1️⃣ 에러처리 try-catch문 / Result


 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://zeddios.tistory.com/1230

 

 🔮 출처 

https://developer.apple.com/documentation/swift/updating_an_app_to_use_swift_concurrency

https://picsum.photos/

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/

https://www.youtube.com/watch?v=9fXI6o39jLQ&t=202s

반응형
Comments