Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meet async/await in Swift #4

Open
Taehyeon-Kim opened this issue May 31, 2022 · 0 comments
Open

Meet async/await in Swift #4

Taehyeon-Kim opened this issue May 31, 2022 · 0 comments
Assignees
Labels
Swift WWDC Topic: swift wwdc21 wwdc2021

Comments

@Taehyeon-Kim
Copy link

Taehyeon-Kim commented May 31, 2022

스크린샷 2022-05-31 오후 6 17 02

WWDC21) Meet async/await in Swift

Meet async/await in Swift

*본 글은 WWDC 를 보고, 번역 및 요약 그리고 실행해보는 스터디 프로젝트의 일환입니다.

들어가며

비동기 프로그래밍은 많은 사람들에게 Regular한(일상적인) 활동입니다. 이런 상황에서 장황 · 복잡 · 부정확한 비동기 코드를 작성하는 것은 너무 쉽습니다. Swift의 async/await을 이용하면 일반적인 코드를 작성하는 것처럼 쉽게 비동기 코드를 작성할 수 있습니다.

  • 아이디어를 더 잘 반영할 수 있습니다.
  • 더 안전하게 코드를 작성할 수 있습니다.
  • SDK에는 사용할 수 있는 수백 가지의 awaitable한 메서드가 있습니다.

on top of that - 덧붙이자면

thumbnail - 축소판 (사진의 축소판을 의미)

UIKit - UIImage

  • iOS 15.0 이상부터 UIImage의 축소판(Thumbnail)을 리턴해주는 인스턴스 메서드를 제공합니다.
  • 동기 및 비동기 기능을 모두 제공하고 있습니다.

실험) preparingThumbnail 함수를 이용했을 때, 메모리 오버헤드를 얼마나 줄일 수 있을까요? 매우 큰 이미지를 전체 크기로 디코딩 했을 때 발생하는 오버헤드를 줄여봅시다.

iOS 15부터 생긴 메서드를 이용해보기 위해서 프로젝트를 하나 만들었습니다. 그리고 간단한 테스트를 위해서 Image를 하나 넣어줬어요.
스크린샷 2022-05-31 오후 7 03 33

Image 사이즈는 보시다시피 매우 큽니다.
스크린샷 2022-05-31 오후 7 04 25

휴대폰 사이즈와는 비교도 안 되는 픽셀 사이즈를 가지고 있죠. 그래서 분명 사이즈 조정이 필요할 것입니다. 간단하게는 ImageView의 ContentMode를 조정하여 View의 크기에 맞춰버리는 방식이 있을텐데요. 그것은 나의 휴대폰에게 너무나 미안한 일입니다. 메모리 어떡해 🥲

근데 너무나 고맙게 위에서 소개한 메서드를 이용하면 메모리 오버헤드를 줄일 수 있다고 합니다. View의 bounds를 넘어가는 엄청난 크기의 원본 이미지가 있다면 thumbnail 메서드를 이용해서 특정 사이즈로 줄일 수 있다고 하네요. 바로 코드를 작성해보았습니다.

간단하게 UIImageView를 올렸구요. 코드도 이어서 보여드리겠습니다. 간단한 테스트를 위해서 그냥 원본 이미지를 디코딩 해줬을 때랑, Thumbnail 함수로 사이즈를 줄인 이미지를 디코딩 해주었습니다.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let originImage = UIImage(named: "sunrise")
        let thumbnail = originImage?.preparingThumbnail(of: CGSize(width: 1000, height: 500))

        // imageView.image = originImage        
        imageView.image = thumbnail
    }

}

스크린샷 2022-05-31 오후 7 09 56

원본이미지를 넣었을 때입니다.

스크린샷 2022-05-31 오후 7 10 10

Thumbnail(축소판)이미지를 넣었을 때 입니다. 이미지 1개인데 메모리 사용량 3배 차이 실화입니까..? 아찔하네요. 정말 풀 사이즈의 이미지를 디코딩하는 오버헤드를 많이 줄일 수 있네요. 실험 성공!

Functions: synchronous and asynchronous

스크린샷 2022-05-31 오후 6 32 23

함수를 동기적으로 호출하면, 스레드가 Blocked(차단)되어 해당 함수가 완료되기를 기다립니다. 예를 들어 우리가 만든 fetchThumbnail 함수가 UIKit이 제공하는 prepareThumbnail이라는 동기 함수를 호출하면 이 작업이 완료될 때까지 스레드는 다른 작업을 수행할 수 없게 되는 거죠.

스크린샷 2022-05-31 오후 6 34 28

반대로, prepareThumbnail의 비동기 버전도 있는데요. 작업이 진행되는 동안 스레드는 자유롭게 다른 작업을 수행할 수 있습니다. 완료되면 Completion Handler를 호출하여 알려줍니다.

SDK는 많은 비동기 기능을 제공합니다. 일부는 Completion handler를 사용하거나, 다른 것들은 Delegate Callback에 의지합니다. 그리고 많은 것들이 비동기로 표시되고 값만 반환합니다.

이러한 비동기 함수의 공통점은 함수를 호출하면 스레드가 빠르게 unblock(차단 해제)되어 작업이 시작됩니다. 그렇게 하면 장기간 실행되는 작업(예를 들어 네트워크 통신, 다운로드 등의 작업)이 완료되는 동안 다른 작업을 수행할 수 있습니다.

Use Async/Await

Safer, Simpler, Reflect your intent
다음 3가지 키워드로 장점을 이야기하고 있습니다.

문법정리

  • throws ↔️ async : 함수가 에러를 던질 수 있다는 의미 ↔️ 함수가 비동기적으로 실행, 일시 중단 될 수 있음을 의미
  • try ↔️ 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 { return FetchError.badImage }
    return thumbnail
}
  • throws 앞에 async 키워드를 붙여서 비동기 처리를 해줍니다.
  • data(for: request)메서드를 호출함으로서 데이터 다운로드를 시작합니다.
  • dataTask 처럼 Foundation에서 제공하는 메서드이며 비동기적으로 작동합니다.
  • dataTask와는 다르게 data 메서드는 awaitable(대기 가능)합니다.
  • 비동기 함수를 호출하기 위해서는 await 키워드가 필요합니다.
  • throws하는 비동기 표현식을 다룰 때에는 await 앞에 try 키워드를 붙여주어야 합니다.

Async Properties

함수, 프로퍼티, 생성자에서도 await이 사용될 수 있습니다.

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}
  • UIImage의 thumbnail 프로퍼티는 async합니다.
  • SDK의 일부는 아니고, 예제에서 추가한 프로퍼티입니다.
  • 프로퍼티도 비동기적으로 만들 수 있다는 것을 보여줍니다.
  • Swift 5.5부터 속성 getter도 throw할 수 있습니다.
  • read-only 프로퍼티만 async할 수 있습니다. (getter)

Async Sequence

  • Sequence도 await을 사용하여 for-loop를 비동기로 동작할 수 있습니다.
  • 요소 앞에 await 키워드를 선언하면 됩니다.
for await id in staticImageIDsURL.lines {
  let thumbnail = await fetchThumbnail(for: id)
  collage.add(thumbnail)
}
let result = await collage.draw()
  • AsyncSequence의 요소인 문자열을 기다리는 동안 fetchThumbnail(for:)함수가 일시 중단했다가 요소가 오면 재개하여 함수를 시작합니다.
  • 함수가 비동기 시퀀스를 반복하면서 다음 요소를 기다리는 동안 스레드의 차단을 해제한다음 다음 루프 본문에 다음 요소를 사용하거나 요소가 남아 있지 않은 경우 루프 뒤에 다시 시작할 수 있습니다.

동작 원리

1️⃣ 일반 함수의 동작 원리 : 비동기 기능은 일시 중단될 수 있다.

스크린샷 2022-06-07 오후 1 47 38

함수를 호출할 때 함수가 실행 중인 스레드의 제어권을 해당 함수에 넘깁니다.

  • thumbnailURLRequest와 같이 일반 함수를 호출하는 경우엔 스레드는 완료될 때까지 해당 함수를 대신하여 작업을 수행하는데 완전하게 사용됩니다.
  • 함수가 값을 반환하거나 오류를 발생시켰을 때 완료되게 됩니다. 이 때 우리가 작성한 함수(fetchThumbnail)에 다시 제어권을 넘겨줍니다.

2️⃣ 비동기 함수의 동작 원리

스크린샷 2022-06-07 오후 1 51 03

비동기 함수의 경우에도 마찬가지로 완료 시 종료되고 함수에 대한 제어가 반환되는데 다른 방식으로도 스레드 제어를 포기할 수 있습니다. Suspend(일시 중단)입니다.

일시 중단되었을 때 스레드의 제어권을 함수에 반환하는 것이 아닌 시스템에 반환하게 됩니다. 시스템이 우선 순위를 따져서 다른 작업을 수행하거나 다시 비동기 함수를 실행하도록 결정합니다. 필요한 만큼 스스로 일시 중단할 수 잇습니다.

중요한 것은 await 키워드가 있다고 해서 반드시 일시 중단되는 것은 아닙니다.

Async/await facts

  1. 함수를 비동기로 표시하면 일시 중단됩니다. 함수가 자신을 일시 중단하면 호출자도 일시 중단됩니다. 호출자도 비동기식이어야 합니다.
  2. 비동기 함수에서 한 번 또는 여러 번 일시 중단될 수 있는 위치를 지적하기 위해 await 키워드가 사용됩니다.
  3. 비동기 기능이 일시 중단되는 동안 스레드는 차단되지 않습니다. 따라서 시스템은 다른 작업을 자유롭게 예약할 수 있습니다. 기능이 일시 중단되는 동안 앱의 상태가 크게 변경될 수도 있습니다.
  4. 비동기 함수가 다시 시작되면 호출한 비동기 함수에서 반환된 결과가 원래 함수로 다시 흐르고 중단된 곳에서 바로 실행이 계속됩니다.

실습해보기

Unsplash Fetch Photo List
Photo Example

// ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Task {
            guard let fullImageURLString = await APIManager.shared.fullImageURLs.randomElement() else { return }
            guard let imageURL = URL(string: fullImageURLString) else { return }
            let imageData = try Data(contentsOf: imageURL)
            let originImage = UIImage(data: imageData)
            let thumbnailImage = originImage?.preparingThumbnail(of: CGSize(width: 1000, height: 1000))
            imageView.image = thumbnailImage
        }
    }
}
// APIManager.swift
import Foundation

enum MyNetworkingError: Error {
    case invaildServerResponse
}

class APIManager {
    private init() {}
    
    static let shared = APIManager()
    
    var fullImageURLs: [String] {
        get async {
            try! await fetchPhotoLists()
        }
    }

    private func fetchPhotoLists() async throws -> [String] {
        let url = URL(string: APIConstants.baseURL + "/photos")!
        var urlRequest = URLRequest(url: url)
        urlRequest.setValue("Client-ID " + Secrets.accessKey, forHTTPHeaderField: "Authorization")
        
        let (data, response) = try await URLSession.shared.data(for: urlRequest)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw MyNetworkingError.invaildServerResponse
        }

        let decodedData = try JSONDecoder().decode([Photo].self, from: data)
        return decodedData.map { $0.urls.raw }
    }
}
// Photo.swift
import Foundation

struct Photo: Decodable {
    let id: String
    let width: Int
    let height: Int
    let urls: PhotoURLDescription
    let likes: Int
}

struct PhotoURLDescription: Decodable {
    let raw: String
    let full: String
}
@Taehyeon-Kim Taehyeon-Kim added wwdc21 wwdc2021 Swift WWDC Topic: swift labels May 31, 2022
@Taehyeon-Kim Taehyeon-Kim self-assigned this May 31, 2022
@Taehyeon-Kim Taehyeon-Kim pinned this issue May 31, 2022
@hyun99999 hyun99999 unpinned this issue Jun 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Swift WWDC Topic: swift wwdc21 wwdc2021
Projects
None yet
Development

No branches or pull requests

1 participant