순진이의 하루/study정리

13. async/await_2022.09.19

순진이 2022. 9. 19. 19:39

async/await

 

async / await 사용이유

- 콜백 지옥

- 에러 처리에서 실수 가능성 있음

 

 

기본코드 ⬇️

더보기

model

import Foundation

// Codable = Encodable + Decodable
struct Quote: Decodable {
    let content: String
}

 error

import Foundation

enum NetworkError: Error {
    case badResponse
    case communicationError
    case decodeFailed
    case noData
}

controller

import UIKit

class ViewController: UIViewController {
    
    let networkService = NetworkService.shared
    
    let centerLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        setAttributes()
    }
}

// MARK: -URLSession
extension ViewController {
    func setUI() {
        view.addSubview(centerLabel)
        centerLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            centerLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
            centerLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
            centerLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            centerLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
        ])
    }
    
    func setAttributes() {
        centerLabel.textColor = .black
        centerLabel.textAlignment = .center
        centerLabel.numberOfLines = 0
    }
}

 


 

기존의 @escaping 방식

import Foundation

class NetworkService {
    static let shared = NetworkService()
    private init() {}
    
    let url = URL(string: "https://api.quotable.io/random")!
    
    func getQuote(completion: @escaping (Result<String, NetworkError>) -> Void) -> String {
        var result = ""
        URLSession.shared.dataTask(with: url) { data, response, error in
            // 1. error check
            if let error = error {
                print(error)
                completion(.failure(.communicationError))
                return
            }
            
            // 2. response check
            if let response = response as? HTTPURLResponse, !(200..<300).contains(response.statusCode) {
                completion(.failure(.badResponse))
                return
            }
            
            // 3. data check
            guard let data = data else {
                completion(.failure(.noData))
                return
            }
            
            do {
                let quote = try JSONDecoder().decode(Quote.self, from: data)
                result = quote.content
                completion(.success(result))
            } catch {
                completion(.failure(.decodeFailed))
                print(error)
            }
            
        }.resume()
        return result
    }
}
import UIKit

class ViewController: UIViewController {
    
    let networkService = NetworkService.shared
    
    let centerLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        setAttributes()
        getQuote()
    }
    
    // 🍎
    func getQuote() {
        networkService.getQuote { [weak self] result in
            switch result {
            case .success(let quote):
                print(quote)
                DispatchQueue.main.async {
                    self?.centerLabel.text = quote
                }
                
            case .failure(let error):
                print(error)
            }
        }
    }
}
// MARK: -URLSession
extension ViewController {
    func setUI() {
        view.addSubview(centerLabel)
        centerLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            centerLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
            centerLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
            centerLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            centerLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
        ])
    }
    
    func setAttributes() {
        centerLabel.textColor = .black
        centerLabel.textAlignment = .center
        centerLabel.numberOfLines = 0
    }
}

 


async / await 방식

정의

- 함수 정의 시 async 키워드 붙여주기

- data함수가 throws이므로 사용 시 try await 키워드 필요

- 결과 자체가 data, response로 옴

import Foundation

class NetworkService {
    static let shared = NetworkService()
    private init() {}
    
    let url = URL(string: "https://api.quotable.io/random")!
    
       
    func getQuoteWithAsync() async -> String {
        do {
            // try 뒤에 await 써야 함
            // 애초에 data와 response를 반환함
            let (data, response) = try await URLSession.shared.data(from: url)
            
            
            if let response = response as? HTTPURLResponse, !(200..<300).contains(response.statusCode) {
                return "응답 에러"
            }
            
            let quote = try JSONDecoder().decode(Quote.self, from: data)
            return quote.content
        } catch {
            return "Decode Error"
        }
    }
}

 

사용

- 반드시 Task 블록 안에서 사용해야 함! + await 키워드와 함께

- DispatchQueue에 넣지 않아도 됨

    func getQuote() {
        Task {
            let quote = await networkService.getQuoteWithAsync()
            self.centerLabel.text = quote
        }
    }

 


배열로 부르기

    func getQuotesArray1() async -> [String] {
        var quotes = [String]()

        for _ in 1...3 {
            quotes.append(await getQuoteWithAsync())
        }
        return quotes
    }
    func getQuotesArray() {
        var quoteArray = ""

        Task {
            let quotes = await networkService.getQuotesArray()
            
            quotes.forEach { quoteArray += "-\($0)\n" }
            self.centerLabel.text = quoteArray
        }
    }

 


배열로 부를 때 차이

1. async 키워드 붙일 때

        - 기다리지 않음 -> 순서가 보장되지 않으나 빠름
        - 리턴될 배열이 꽉차기를 기다릴 뿐

    func getQuotesArray2() async -> [String] {
        async let firstQuote = "1️⃣" + getQuoteWithAsync()
        async let secondQuote = "2️⃣" + getQuoteWithAsync()
        async let thirdQuote = "3️⃣" + getQuoteWithAsync()
        return await [firstQuote, secondQuote, thirdQuote]
    }
    func getQuotesArray() {
        var quoteArray = ""
        
        Task {
            let quotes = await networkService.getQuotesArray2()

            quotes.forEach { quoteArray += "-\($0)\n" }
            self.centerLabel.text = quoteArray
        }
    }

 

2. async 키워드 안 붙일 때

        - 무조건 순서대로 진행됨 (순서 보장되지만 시간 소요가 큼)
        - 각 작업이 10초가 걸린다면 이 함수는 무조건 30초가 걸림

    func getQuotesArray3() async -> [String] {
        let firstQuote = await "1️⃣" + getQuoteWithAsync()
        let secondQuote = await "2️⃣" + getQuoteWithAsync()
        let thirdQuote = await "3️⃣" + getQuoteWithAsync()
        return [firstQuote, secondQuote, thirdQuote]
    }
    func getQuotesArray() {
        var quoteArray = ""

        Task {
            let quotes = await networkService.getQuotesArray3()

            quotes.forEach { quoteArray += "-\($0)\n" }
            self.centerLabel.text = quoteArray
        }
    }

 

 

 


 

 

continuation 

- 내용을 알지 못하지만 async하게 쓰고 싶다!

- 회사에 갔는데 기존 함수가 패키지에 쌓여 있거나, 너무 복잡해서 건드릴 수 없을 때 등

- unsafe보다는 withCheckedContinuation를 쓸 것 (unsafe가 더 빠르다지만, 안전한게 중요!)

class NetworkService {
    static let shared = NetworkService()
    private init() {}
    
    let url = URL(string: "https://api.quotable.io/random")!
    
    
    // 🍎 기존 escaping 함수
    func getQuote(completion: @escaping (Result<String, NetworkError>) -> Void) -> String {
        var result = ""
        URLSession.shared.dataTask(with: url) { data, response, error in
            // 1. error check
            if let error = error {
                print(error)
                completion(.failure(.communicationError))
                return
            }
            
            // 2. response check
            if let response = response as? HTTPURLResponse, !(200..<300).contains(response.statusCode) {
                completion(.failure(.badResponse))
                return
            }
            
            // 3. data check
            guard let data = data else {
                completion(.failure(.noData))
                return
            }
            
            do {
                let quote = try JSONDecoder().decode(Quote.self, from: data)
                result = quote.content
                completion(.success(result))
            } catch {
                completion(.failure(.decodeFailed))
                print(error)
            }
            
        }.resume()
        return result
    }
    
    
    // 🍎 continuation
	func continuationGetQuote() async -> String {
        return await withCheckedContinuation { continuation in
            getQuote { result in
                switch result {
                case .success(let quote):
                    // resume은 반드시 한 번만 호출해야 함
                    continuation.resume(returning: quote)
                case .failure(let error):
                    print(error)
                }
            }
        }
    }
    func getQuote() {
        Task {
            let quote = await networkService.continuationGetQuote()
            self.centerLabel.text = quote
        }
    }