[Swift] Alamofire async / await 사용해보기

Alamofire 5.5[#] 버전부터 Concurrency를 지원한다. 공식 문서의 설명을 보면 아래와 같이 적혀있다.

Alamofire’s concurrency support requires Swift 5.5.2 or Xcode 13.2. These examples also include the use of static protocol values added in Alamofire 5.5 for Swift 5.5.

쓰기 위해서는 Xcode 13.2로 업데이트부터 해주자!

프로젝트의 최소 iOS 버전을 13으로 맞춰준다.

그리고 Alamofire의 5.5 버전을 받아주자. 필자는 Swift Package Manager를 통해 받음. (스샷에 있는 5.5.01는 올바른 버전이 아니므로 5.5.0으로 받으세요~!)

이제 사용할 준비는 끝났다. 이제 사용할 Session을 아래와 같이 정의해주자.

  private let session: Session = {
    let configuration = URLSessionConfiguration.default
    configuration.timeoutIntervalForRequest = 10
    configuration.timeoutIntervalForResource = 10
    return Session(configuration: configuration)
  }()

그 다음은 requestJSON 함수를 구현해보자. Alamofire을 사용하면 정말 간단하게 구현할 수 있다.

  func requestJSON<T: Decodable>(_ url: String,
                                 type: T.Type,
                                 method: HTTPMethod,
                                 parameters: Parameters? = nil) async throws -> T {
    
    return try await session.request(url,
                                     method: method,
                                     parameters: parameters,
                                     encoding: URLEncoding.default)
      .serializingDecodable()
      .value
  }

DataRequest -> DataTask -> value 속성 접근으로 DataResponse의 Success 값을 가져올 수 있다.

네트워크 통신 전체 코드

import Foundation
import Alamofire

final class AppNetworking {
  static let shared = AppNetworking()
  
  private init() { }
  
  private let session: Session = {
    let configuration = URLSessionConfiguration.default
    configuration.timeoutIntervalForRequest = 10
    configuration.timeoutIntervalForResource = 10
    return Session(configuration: configuration)
  }()
  
  func requestJSON<T: Decodable>(_ url: String,
                                 type: T.Type,
                                 method: HTTPMethod,
                                 parameters: Parameters? = nil) async throws -> T {
    
    return try await session.request(url,
                                     method: method,
                                     parameters: parameters,
                                     encoding: URLEncoding.default)
      .serializingDecodable()
      .value
  }
}

나는 간단하게 api.icndb.com/jokes/random 사이트의 응답값을 UITableView에 뿌리는 코드를 작성해보았다.

모델은 아래와 같이 정의했다.


struct Joke: Codable {
  let type: String
  let value: [Value]
  
  struct Value: Codable {
    let id: Int
    let joke: String
  }
}

이제 ViewController 코드를 아래와 같이 작성해주면 된다.

class ViewController: UIViewController {

  @IBOutlet weak var tableView: UITableView!
  
   private var jokes: [Joke.Value] = []
  
  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.dataSource = self
    
    Task {
      await retrieveJokes()
    }
  }
  
  private func retrieveJokes() async {
    do {
      let joke = try await AppNetworking.shared.requestJSON("https://api.icndb.com/jokes/random/50",
                                                             type: Joke.self,
                                                             method: .get,
                                                             parameters: nil)
      updateUI(jokes: joke.value)
    } catch {
      updateUI(error: error)
    }
  }
  
  @MainActor
  private func updateUI(error: Error) {
    let label = UILabel()
    label.numberOfLines = 0
    label.text = error.localizedDescription
    label.frame = view.bounds
    view.addSubview(label)
  }
  
  @MainActor
  private func updateUI(jokes: [Joke.Value]) {
    self.jokes = jokes
    tableView.reloadData()
  }
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
  func numberOfSections(in tableView: UITableView) -> Int {
     return 1
  }
  
  func tableView(_ tableView: UITableView,
                 numberOfRowsInSection section: Int) -> Int {
    return jokes.count
  }
  
  func tableView(_ tableView: UITableView,
                 cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = UITableViewCell()
    cell.textLabel?.text = jokes[indexPath.row].joke
    cell.textLabel?.numberOfLines = 0
    return cell
  }
}

코드를 간단하게 살펴보자면 requestJSON 함수는 retrieveJokes 함수에서 호출하도록 하였고 뷰가 로드된다면 retrieveJokes 함수를 호출하도록 하였다. viewDidLoad 의 경우 func viewDidLoad() async 형태로 사용할 수 없기 때문에 Task를 통해 async / await를 사용할 수 있도록 해주었다.

그리고 기본적으로 async / await를 통해 실행된 코드는 메인 스레드에서 실행되는 것이 아니라서, UI 업데이트 같은 작업을 수행할 땐 메인 스레드에서 실행할 수 있도록 해주어야 하는데(코드에서는 reloadData를 사용했지만, performBatchUpdates와 같은 함수를 호출할 땐 메인 스레드에서 접근하여야 한다), 코드마다 DispatchQueue.main.async를 사용하는 것 보단 MainActor 라는 attribute를 통해 좀 더 깔끔하게 구현할 수 있다. (+ 내용추가 : UIViewController가 MainActor로 정의되어 있어, 뷰 컨트롤러 안의 메소드들은 자동적으로 MainActor에서 실행됩니다 #)

iOS 13 에서도 실행되는걸 확인할 수 있다.

소스 코드: https://github.com/chorim/AlamofireConcurrencyDemo