<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>j_aion.log</title>
        <link>https://velog.io/</link>
        <description>JUST DO IT</description>
        <lastBuildDate>Sat, 31 Dec 2022 17:15:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>j_aion.log</title>
            <url>https://images.velog.io/images/j_aion/profile/1ce15a88-1e44-4256-89b4-d01c3c68dec7/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. j_aion.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/j_aion" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Conference] Swift Concurrency Under the Hood]]></title>
            <link>https://velog.io/@j_aion/Conference-Swift-Concurrency-Under-the-Hood</link>
            <guid>https://velog.io/@j_aion/Conference-Swift-Concurrency-Under-the-Hood</guid>
            <pubDate>Sat, 31 Dec 2022 17:15:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=wp5vIVxABFk">Swift Concurrency Under the Hood - iOS Conf SG 2022
</a></p>
</blockquote>
<h1 id="swift-concurrency-under-the-hood">Swift Concurrency Under the Hood</h1>
<h2 id="concurrency">Concurrency</h2>
<ul>
<li>스레드</li>
<li>GCD</li>
<li>모던 스위프트 컨커런시</li>
</ul>
<h2 id="async-await">Async Await</h2>
<ul>
<li>비동기 컨텍스트에서 실행되는 함수를 컴플리션을 통해 실행하지 않더라도 실행 가능하도록 지원</li>
<li><code>Task</code> 내부 또는 <code>async</code>를 따르는 함수에서 <code>do catch</code> 등을 통해 <code>await</code>로 비동기적으로 종료되는 태스크의 실행을 캐치 가능</li>
</ul>
<pre><code class="language-swift">import SwiftUI

struct MyStorage {
    static func fetchID() async {
        print(&quot;Async Work&quot;)
    }

    static func fetchID() {
        print(&quot;Sync Work&quot;)
    }
}

struct ConcurrencyView: View {
    var body: some View {
        Text(&quot;Hello, World!&quot;)
            .onAppear(perform: MyStorage.fetchID)
            .task {
                await MyStorage.fetchID()
            }
    }
}</code></pre>
<ul>
<li><p>태스크 모디파이어를 통해 비동기 함수 실행</p>
<pre><code class="language-swift">Sync Work
Async Work</code></pre>
</li>
<li><p>비동기/동기 함수가 실행되는 구문 확인 가능 </p>
<pre><code class="language-swift">struct ConcurrencyView: View {
  var body: some View {
      Text(&quot;Hello, World!&quot;)
          .onAppear(perform: MyStorage.fetchID)
          .task {
                 MyStorage.fetchID()
          }
  }
}</code></pre>
</li>
<li><p>태스크 모디파이어 내부에서 <code>await</code>를 쓰지 않고 <code>fetchID()</code>를 호출했을 때 컴파일러는 동일한 이름의 동기 함수를 실행</p>
<pre><code class="language-swift">Sync Work
Sync Work</code></pre>
</li>
<li><p>동일한 동기 함수가 실행되는 것을 확인 가능 → <code>await</code> 키워드 필수</p>
<h2 id="task-groups">Task Groups</h2>
</li>
<li><p>인풋이 동적으로 들어오는 비동기 리퀘스트를 병렬적으로 실행한 뒤 결과를 종합해서 리턴/사용해야 할 때 사용 가능한 방식</p>
<pre><code class="language-swift">func loginAndAskAndConnet() {
  try await withThrowingTaskGroup(of: LoginTaskResult.self, body: { group in
      group.addTask(operation: fetchLocation)
      group.addTask(operation: askForUserPermission)
      group.addTask(priority: .high) {
          return .client(try await APIClient.connect())
      }

      var client: APIClient!
      for try await result in group {
          if case LoginTaskResult.client(let api) = result {
              client = api
              break
          }
      }
      let tracer = client.traceEvent

      group.addTask(operation: client.logUserIn)
      group.addTask {
          try await tracer(.login)
      }
      try await group.waitForAll()
      _ = try await tracer(.completedLoginSequence)
  })
}
</code></pre>
</li>
</ul>
<pre><code>* 로그인 태스크에서 발생한 가능한 타입이 `of`를 통해 주어진 태스크 그룹
* 세 가지 태스크가 그룹에 추가 → 위치를 가져오면서 유저의 허락을 구하고 API 클라이언트와 연결하는 작업
* `for try await`를 통해 `AsyncSequence`를 다루면서 태스크 그룹이 리턴하는 클라이언트 api 데이터를 가져온 뒤 클라이언트의 로그인 태스크 및 이벤트 추적을 동시적으로 진행
* `group.waitForAll()`을 통해 그룹 내에 추가된 모든 태스크가 종료될 때까지 보장
* 위 컨커런트한 태스크 그룹 → 해당 태스크 종료 후에야 특정 태스크 실행 가능한 흐름일 때 도중 특정한 태스크에서 에러를 스로우한다면 → 자동으로 그룹 내 에러가 발생한 태스크 이외의 (종료되지 않은) 태스크는 자동으로 캔슬
* 태스크 그룹에 대한 자동 스레드 할당 방법: 컨커런트 디스패치 큐인 `user-initiated-qos.cooperative` 스레드 풀에서 멀티 스레드를 통해 실행
* 각 스레드 풀에서 실행되는 스레드 개수는 디바이스의 코어 개수에 따라 상이(아이폰, 맥 등)

## Actors
* 레이스 문제를 해결하는 가장 간단한 방법 중 한 가지
```swift
actor MyActor {
    private var counter = 0

    public func increment() {
        counter += 1
    }
}</code></pre><ul>
<li>해당 <code>increment()</code>를 실행하는 호출 순서가 액터가 주관하는 큐에서 실행<pre><code class="language-swift">struct MyModel {
  @MainActor func updateItems() {}
  @MainActor func clearAllItems() {}
}
</code></pre>
</li>
</ul>
<p>struct Logger {
    @MainActor var logs: [String]
}</p>
<p>@MainActor struct UIHelpers {
    // members
}</p>
<pre><code>* 일반적인 액터 클래스 이외에도 해당 함수, 변수 등이 `@MainActor`와 같은 형식으로 선언된다면 해당 함수 실행이 곧 `isolated`된다는 뜻
* 구조체, 클래스 자체를 `@MainActor`와 같은 형식으로 선언했다면 내부의 모든 데이터, 함수 등이 `isolated`되는 게 디폴트이기 때문에 그렇지 않은 대상에 ``nonisolted`로 별도로 선언해 주어야 함

## Actors Protocol
* 프로토콜을 통해 액터를 따를 때 인스턴스 프로퍼티로 `unownedExecutor`가 존재
* 해당 익스큐터는 `unowned` 참조로서 해당 액터를 실행하는 익스큐터를 반환하는 프로퍼티 변수(익스큐터란 해당 액터가 주관하는 동작을 순차적으로 실행하도록 보장하는 큐)
* 해당 익스큐터를 커스텀한다면 보다 많은 일을 할 수 있을 거라 기대

## Distributed Actor

```swift
distributed actor Database {
    func allUsers() -&gt; [User] { ... }
}

let database = try Database.resolve(id: IP(&quot;111.222.333&quot;), using: HTTPSTransport())
let users = try await database.allUsers()</code></pre><ul>
<li><code>distributed</code>를 사용한 액터 클래스<blockquote>
<p>해당 강의가 나온 시점에 <code>distributed</code> 키워드는 아직 도입되지 않은 상황이었는데, 해당 방법을 통해 액터를 사용할 때의 이점을 설명하고 있다</p>
</blockquote>
</li>
<li>앱 내의 여러 프로세스가 존재할 때 특정 프로세스를 외부 데이터센터에서 실행할 수 있는 방법을 모색하고 있는 듯</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Runloop.Main vs DispatchQueue.Main]]></title>
            <link>https://velog.io/@j_aion/UIKit-Runloop.Main-vs-DispatchQueue.Main</link>
            <guid>https://velog.io/@j_aion/UIKit-Runloop.Main-vs-DispatchQueue.Main</guid>
            <pubDate>Sat, 31 Dec 2022 16:14:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=0Gk6GDQDiJ8">Common Mistake while using @Published | RunLoop.Main vs DispatchQueue.Main | Combine
</a></p>
</blockquote>
<h1 id="runloopmain-vs-dispatchqueuemain">Runloop.Main vs DispatchQueue.Main</h1>
<h2 id="published">@Published</h2>
<ul>
<li><p>컴바인을 사용해 간단한 테이블 뷰를 그리기</p>
</li>
<li><p>특정 버튼을 통해 테이블 뷰의 데이터 소스를 갱신</p>
<pre><code class="language-swift">class TableViewModel {
  @Published var data = [String]() {
      willSet {
          print(&quot;Willset executed&quot;)
      }
      didSet {
          print(&quot;Didset executed&quot;)
      }
  }

  init() {
  }

  func fetchData() {
      var data = [String]()
      for _ in 1...100 {
          let rand = Int.random(in: 1...100)
          data.append(rand.description)
      }
      self.data = data
  }
}
</code></pre>
</li>
</ul>
<pre><code>* `@Published`을 따르는 데이터 소스는 해당 값이 업데이트될 때를 감지
```swift
class TableViewController: UIViewController {
    private lazy var refreshButton: UIButton = {
        let button = UIButton()
        var config = UIButton.Configuration.filled()
        config.baseBackgroundColor = .systemGreen
        config.baseForegroundColor = .white
        config.title = &quot;Refresh&quot;
        button.configuration = config
        button.addTarget(self, action: #selector(didTapRefresh), for: .touchUpInside)
        return button
    }()
    private lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: &quot;tableViewCell&quot;)
        tableView.dataSource = self
        tableView.delegate = self
        return tableView
    }()
    private var cancelables = Set&lt;AnyCancellable&gt;()
    private let viewModel = TableViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        bind()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        refreshButton.frame = CGRect(x: (view.frame.size.width - 100) / 2, y: view.safeAreaInsets.top, width: 100, height: 50)
        tableView.frame = CGRect(x: 0, y: refreshButton.frame.origin.y + 50 + 10, width: view.frame.size.width, height: view.frame.size.height)
    }

    private func setUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
        view.addSubview(refreshButton)
    }

    @objc private func didTapRefresh() {
        viewModel.fetchData()
    }

    private func bind() {
        viewModel
            .$data
            .sink { [weak self] _ in
                print(&quot;TableView reload!&quot;)
                self?.tableView.reloadData()
            }
            .store(in: &amp;cancelables)
    }
}
</code></pre><ul>
<li><p>뷰 모델이 가지고 있는 데이터 퍼블리셔가 관찰될 때마다 테이블 뷰를 갱신</p>
<pre><code class="language-swift">extension TableViewController: UITableViewDelegate, UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {
      viewModel.data.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: &quot;tableViewCell&quot;, for: indexPath)
      var content = cell.defaultContentConfiguration()
      let model = viewModel.data[indexPath.row]
      content.text = model
      content.textProperties.alignment = .center
      cell.contentConfiguration = content
      return cell
  }
}
</code></pre>
</li>
</ul>
<pre><code>* 현 시점의 뷰 모델의 데이터 소스가 가지고 있는 값을 통해 테이블 뷰 UI 구성

![](https://velog.velcdn.com/images/j_aion/post/cc2c76d7-ba4d-418b-9eea-b17b4bbd52fd/image.gif)
* 하지만 리프레시 버튼을 두 번 클릭해서야 이전의 값을 확인 가능

```swift
TableView reload!
Willset executed
TableView reload!
Didset executed
Willset executed
TableView reload!
Didset executed</code></pre><ul>
<li>퍼블리셔의 <code>Willset</code>이 호출된 뒤 퍼블리셔 변경을 감지하여 뷰 컨트롤러에서 구독한 <code>tableView.reloadData()</code>가 호출되는 데, 이 시점에서는 실제 데이터가 들어가기 전이므로 이전 값이 계속 유지됨. 즉 업데이트될 것은 체크했으나 업데이트되기 전의 값을 통해 UI를 그리기 때문에 두 번 클릭해야 하는 불상사 발생<pre><code class="language-swift">private func bind() {
      viewModel
          .$data
          .receive(on: DispatchQueue.main)
          .sink { [weak self] _ in
              print(&quot;TableView reload!&quot;)
              self?.tableView.reloadData()
          }
          .store(in: &amp;cancelables)
  }</code></pre>
</li>
<li>위 상황을 퍼블리셔를 구독하는 <code>receive</code> 메소드를 통해 해결 가능</li>
<li><code>DispatchQueue.main</code> 또는 <code>Runloop.main</code>을 통해 해당 데이터 퍼블리셔의 변경 사항을 구독할 경우 올바른 행동을 기대 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/j_aion/post/d85c8dd1-ead9-4e76-b2bc-9a8433256cb4/image.gif" alt=""></p>
<pre><code class="language-swift">TableView reload!
Willset executed
Didset executed
TableView reload!
Willset executed
Didset executed
TableView reload!</code></pre>
<ul>
<li>리프레시 버튼을 누른 직후 퍼블리셔가 변경, <code>didSet</code>이 호출된 이후에야 제대로 업데이트된 값을 통해 테이블 뷰 UI를 그리기 때문에 올바른 타이밍에서 호출</li>
</ul>
<h2 id="runloopmain-vs-dispatchqueuemain-1">Runloop.Main vs DispatchQueue.Main</h2>
<ul>
<li>기본적으로 같은 개념</li>
<li>런루프는 특정 태스크 스케줄링에 사용하는 이벤트 처리 루프</li>
<li>각 스레드(커스텀 스레드 또는 시스템에 의한 디폴트 스레드 모두)는 각자의 런루프를 가지는 데, 메인 스레드와 연관된 모든 런루프가 곧 런루프 메인</li>
<li>디스패치 큐는 메인 스레드와 연관 된 디스패치 큐로 메인 스레드와 연관된 시리얼 큐가 곧 디스패치 큐 메인 스레드로 UIKit의 모든 뷰 드로우 사이클이 해당 스레드와 연관</li>
<li>런루프는 <code>perform selector</code>를 사용하는 반면 디스패치 큐는 일반적인 GCD 메소드를 사용하면서 실행</li>
<li>런루프는 디폴트 모드에서 사용될 때에만 콜백 가능 → 유저 인터렉션과 함께 백그라운드 태스크를 작업해야 할 경우 사용 불가능(유저가가 테이블 뷰를 스크롤할 때 해당 셀에서 내부적으로 이미지를 다운로드받아야 하는 백그라운드 태스크를 실행할 때 런루프 메인을 사용할 때에는 해당 다운로드 작업이 유저 인터렉션이 끝날 때까지 실행되지 않음) → 런루프를 일종의 스케줄러로 사용하기 때문에 유저 인터렉션을 다루는 중이기 때문</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Opaque Type vs Protocol & Associated Type & Generics]]></title>
            <link>https://velog.io/@j_aion/UIKit-Opaque-Type-vs-Protocol-Associated-Type-Generics</link>
            <guid>https://velog.io/@j_aion/UIKit-Opaque-Type-vs-Protocol-Associated-Type-Generics</guid>
            <pubDate>Sat, 31 Dec 2022 15:35:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=SPhATsEQR74">https://www.youtube.com/watch?v=SPhATsEQR74</a></p>
</blockquote>
<h1 id="opaque-type-vs-protocol--associated-type--generics">Opaque Type vs Protocol &amp; Associated Type &amp; Generics</h1>
<h2 id="generics">Generics</h2>
<ul>
<li>어떤 타입이더라도 적용 가능한 유연성을 보장하는 방법</li>
</ul>
<pre><code class="language-swift">func swapTwoValues&lt;T&gt;(_ a: inout T, _ b: inout T) {
        let tempA = a
        a = b
        b = tempA
    }</code></pre>
<ul>
<li>들어오는 데이터의 타입을 모르더라도 받아들일 수 있음</li>
<li>특정 프로토콜을 따르는 데이터 타입만을 받아들일 수도 있음(<code>Decodable</code> 등을 따르는 데이터 타입만을 파라미터로 받아들여 디코딩한 결과를 리턴하는 게 대표적인 예시)</li>
</ul>
<pre><code class="language-swift">    class someClass { ... }

    protocol someProtocol { ... }  

    func someFunction&lt;T: someClass, U: someProtocol&gt;(someT: T, someU: U) {
        // handle T and U
    }</code></pre>
<ul>
<li>스위프트가 제공하는 거의 모든 유용한 공통 오퍼레이터가 제네릭을 통해 구현<pre><code class="language-swift">public init&lt;H&gt;(_ base: H) where H: Hashable</code></pre>
</li>
<li>위와 같이 <code>Hashable</code> 프로토콜을 따르는 타입을 통해 이니셜라이즈한다는 뜻</li>
</ul>
<h2 id="associated-types">Associated Types</h2>
<ul>
<li><p>프로토콜을 보다 유연하게 사용하고자 할 때 사용 가능</p>
<pre><code class="language-swift">protocol someProtocol {
  associatedtype SomePlaceholder

  func someFunctionUsing(type: SomePlaceholder)
  func someOtherFunctionReturning() -&gt; SomePlaceholder
}</code></pre>
</li>
<li><p>특정 프로토콜 내부에서 <code>associatedtype</code>을 통해 연관 타입을 선언한다면, 해당 프로토콜을 따르는 구조체/클래스에서 해당 타입을 명확하게 지정 가능</p>
</li>
<li><p>현 시점에서는 불분명한 타입에 지나지 않음</p>
<pre><code class="language-swift">struct someStruct: someProtocol {
  typealias SomePlaceholder = Int

  func someOtherFunctionReturning() -&gt; Int {
      // return SomePlaceholder as Int
  }

  func someFunctionUsing(type: Int) { }
}</code></pre>
</li>
<li><p><code>typealias</code>를 통해 해당 프로토콜이 지정한 <code>associatedtype</code>을 구체적으로 지정</p>
</li>
<li><p>해당 타입을 리턴/파라미터로 사용하는 함수에 현 시점의 구체적인 타입을 그대로 적용 가능</p>
<pre><code class="language-swift">protocol stackable {
  associatedtype stackType
  var stack: [stackType] { get set }
  func pop() -&gt; stackType
  func push(_ item: stackType)
}
</code></pre>
</li>
</ul>
<p>struct stringStack: stackable {
    var stack: [String]</p>
<pre><code>func pop() -&gt; String {
    // return pop
}
func push(_ item: String) {
    // handle push
}
typealias stackType = String</code></pre><p>}</p>
<pre><code>* 연관 타입을 해당 프로토콜을 따르는 구체적인 구조체/클래스에서 `typealisas`를 통해 직접 설정 가능하다는 게 핵심
```swift
protocol Constainer {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -&gt; Item { get }
}</code></pre><ul>
<li>컨테이너 프로토콜은 <code>Equatable</code>을 따르는 타입을 연관 타입으로 받아, <code>mutating</code>, <code>subscript</code> 등 특정 함수를 내재하는 프로토콜</li>
</ul>
<h2 id="opaque--associatedtype">Opaque + associatedtype</h2>
<ul>
<li>프로토콜이 사용한 연관타입은 오파크와 함께 사용할 시 큰 시너지<pre><code class="language-swift">enum CardType {
  case gold
  case platinum
}
</code></pre>
</li>
</ul>
<p>protocol CardNumberProtocol { }</p>
<p>extension String: CardNumberProtocol { }</p>
<pre><code>* 특정한 프로토콜이 존재한다고 가정
```swift
protocol Card {
    associatedtype CardNumber: CardNumberProtocol
    var type: CardType { get set }
    var limit: Int { get set }
    var number: CardNumber { get set }
    func validateCardNumber(number: CardNumber)
}</code></pre><ul>
<li><p>카드 프로토콜의 연관 타입으로 선언된 <code>CardNumber</code>는 위의 <code>CardNumberProtocol</code>을 준수하는 타입임</p>
</li>
<li><p>타입, 리미트, 넘버 등 프로토콜 내 변수 및 <code>validateCardNumber</code>와 같은 연관 타입을 인자로 쓰는 함수 역시 존재</p>
<pre><code class="language-swift">struct VisaCard: Card {
  typealias CardNumber = String
  var type: CardType = .gold
  var limit: Int = 100_000
  var number: String = &quot;1111 2222 3333 4444&quot;
  func validateCardNumber(number: String) {

  }
}
</code></pre>
</li>
</ul>
<p>struct MasterCard: Card {
    typealias CardNumber = String
    var type: CardType = .platinum
    var limit: Int = 500_000
    var number: String = &quot;1111 2222 3333 4444&quot;
    func validateCardNumber(number: String) {</p>
<pre><code>}</code></pre><p>}</p>
<pre><code>* 위의 프로토콜을 따르는 구체적인 구조체
* 연관타입이 각 구조체 내부에서 `typealias`로 선언되어 구체화되어 있음
```swift
func getLoadEligibility() -&gt; Bool {
    getUserCard().limit &gt;= getLoanEligibilityCard().limit
}

func getUserCard() -&gt; some Card {
    MasterCard()
}

func getLoanEligibilityCard() -&gt; some Card {
    VisaCard()
}</code></pre><ul>
<li>함수의 리턴 타입으로 카드 프로토콜 자체를 리턴하려면 컴파일러가 연관타입으로 감춰놓은 실제 타입을 알지 못하기 때문에 컴파일러 에러가 발생</li>
<li><code>some</code>이라는 오파크 타입 키워드를 통해 해당 타입을 &#39;감싸&#39;줄 때 비로소 리턴할 수 있는데, 실제로 제네릭의 완전히 반대 방향으로 작동</li>
<li>실제 리턴하는 구체적인 실제 타입을 <code>Card</code>로는 알아차리지 못하기 때문에(연관 타입의 실제 표현은 각 구조체 내부에서 하도록 설정되었음), <code>some</code> 키워드를 통해 어떤 타입인지는 모르겠으나 해당 프로토콜을 따르는 구조체라서 인지 가능</li>
<li>SwiftUI 프레임워크의 모든 UI를 그리는 구조체가 <code>some View</code>를 따르는 연산 프로퍼티인데, 이 또한 같은 맥락</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Conference] A crash course of async await]]></title>
            <link>https://velog.io/@j_aion/Conference-A-crash-course-of-async-await</link>
            <guid>https://velog.io/@j_aion/Conference-A-crash-course-of-async-await</guid>
            <pubDate>Sat, 31 Dec 2022 13:53:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=uWqy5KZXSlA">A crash course of async await (Swift Concurrency) - Shai Mishali - Swift Heroes 2022
</a></p>
</blockquote>
<h1 id="a-crash-course-of-async-await">A crash course of async await</h1>
<h2 id="concurrency">Concurrency</h2>
<ul>
<li>시리얼 큐에 할당도힌 작업보다 더 적은 시간을 소모한 채 동일한 결과물을 가져올 수 있다는 장점 → 이슈는 타이밍</li>
</ul>
<h2 id="history">History</h2>
<ul>
<li>GCD: 디스패치 그룹, 세마포어, 워크 아이템 등을 통해 특정 비동기 작업의 실행 순서를 지정</li>
<li>completion handler: 이스케이핑 클로저를 통해 비동기적으로 갱신되는 데이터를 할당하는 구조, 하지만 클로저의 개수가 많아지고 예외 케이스 등 처리가 복잡해질 때 코드 복잡성이 많아지는 문제점 발생</li>
<li>Combine</li>
</ul>
<h2 id="sync-vs-async">Sync vs Async</h2>
<ul>
<li>동기: (1). 코드 가독성이 좋고 순서대로 실행됨을 보장. 예측 가능. 간단한 에러 핸들링 (2). 시리얼적이고, 블로킹이 발생. 싱글 스레드</li>
<li>비동기: (1). 컨커런시를 구현 가능. 멀티 스레드 환경에 적합. 리소스 최적화에 있어 뛰어남, 블로킹이 일어나지 않음. (2). 따라가기 어렵고 예측 불가능할 때가 있음. 에러 핸들링이 필수적</li>
</ul>
<h2 id="async-await">Async Await</h2>
<ul>
<li>기존의 컴플리션 핸들러 스타일의 비동기 리턴 방식을 동기적 리턴 방식으로 감싸기</li>
</ul>
<pre><code class="language-swift">private func loadImage(completion: @escaping (Result&lt;UIImage, Error&gt;) -&gt; ()) {
        // ... return result using completion
    }</code></pre>
<ul>
<li>컴플리션 핸들러 내부에 리턴할 값을 감싸야 함<pre><code class="language-swift">private func loadImage() async throws -&gt; UIImage {
    // ... return image</code></pre>
</li>
<li><code>async</code>를 따른다고 선언한 뒤 해당 파라미터를 리턴</li>
<li><code>throws</code>는 에러를 스로우할 수 있음을 보여줌<pre><code class="language-swift">private func testWithCompletion() {
      loadImage { result in
          switch result {
          case .success(let image):
              // handle image
              break
          case .failure(let error):
              // handle error
              break
          }
      }
  }</code></pre>
</li>
<li>컴플리션 핸들러를 통해 리턴받은 값을 클로저 내부에서 관리해야 하는 이전 방식</li>
</ul>
<pre><code class="language-swift">private func testWithAsync() async {
        do {
            let image = try await loadImage()
        } catch {
            // error handling
        }
    }</code></pre>
<ul>
<li><code>async</code>를 따른다고 선언해야 <code>async</code>, 비동기적으로 리턴하는 위의 함수를 사용 가능</li>
<li><code>throws</code>를 통해 에러를 내보낼 수 있기 때문에 <code>do catch</code>를 통해 에러 핸들링</li>
<li><code>await</code>를 통해 비동기적 함수의 리턴 값이 나올 때까지 기다린 뒤 원하는 태스크를 실행 가능<pre><code class="language-swift">private func testWithAsyncAndTask() {
      Task {
          do {
              let image = try await loadImage()
          } catch {
              // error handling
          }
      }
  }</code></pre>
</li>
<li><code>async</code>를 통해 리턴하는 비동기 구문을 사용할 수 있는 또 다른 방법은 <code>Task</code> 블럭 내부에서 사용하는 것</li>
</ul>
<pre><code class="language-swift">private func testWithAsyncAsSync() async {
        do {
            let firstImage = try await loadImage()
            let secondImage = try await loadImage()
            let thridImage = try await loadImage()
        } catch {
            // error handling
        }
    }</code></pre>
<ul>
<li>비동기 태스크를 연속적으로 실행하는 (것처럼 보이는) 구문</li>
<li>실제로 비동기 함수를 위의 순차적 태스크를 하도록 하는 블럭 내부에서는 각 구문에서 서스펜션이 걸린 뒤, <code>await</code>를 통해 리턴을 받을 때까지 기다림. 이후 <code>resuming</code>이 가능한 구조</li>
</ul>
<h2 id="task">Task</h2>
<ul>
<li>태스크는 비동기적 작업의 유닛</li>
<li>다른 태스크의 자식이 될 수 있음</li>
<li>새로운 비동기 컨텍스트를 생성하도록 하는 일련의 이니셜라이저임</li>
<li>태스크 실행 시 실행되는 컨텍스트의 스레드를 상속할 수도, 벗어날 수도, 독립적으로 작동하는 것도 가능</li>
<li><code>Success</code>와 <code>Failure</code>를 모두 제네릭하게 설정 가능</li>
</ul>
<h2 id="cooperative-thread-pool">Cooperative Thread Pool</h2>
<ul>
<li>스레드 풀: 비동기 함수를 실행할 수 있는 스레드 모음</li>
<li>태스크는 언제나 앞으로 진행되는 프로그레스를 따르거나 서스펜션에 걸려 멈춰 있어야 함</li>
<li>GCD와 달리 CPU 코어 하나 당 한 개 이상이면 안 됨</li>
<li>스레드 과포화 상태인 <code>thread exploision</code>을 방지</li>
<li>스레드 스위칭으로 인한 오버헤드 및 퍼포먼스 패널티를 방지</li>
<li>여러 개의 태스크를 정지할 수 있고, 멈추 뒤 리줌을 해당 태스크 우선순위에 따라 실행 가능</li>
<li><code>Continuation</code>: 스레드 스위칭 없이 특정 작업을 정지, 재개하는 일이 경량화됨</li>
</ul>
<pre><code class="language-swift">private func getAmazingItem() async -&gt; UIImage? {
        // ... return data
    }

private func getFoo() async {
        let tiem = await getAmazingItem()
    }</code></pre>
<ul>
<li><code>getFoo()</code> 함수는 <code>getAmazingItem()</code> 비동기 함수가 데이터를 리턴할 때까지 현재 스레드 상태를 정지한 뒤, 데이터가 리턴되면 그때 태스크를 재개</li>
<li>즉 현재 스레드를 &#39;포기&#39;한다는 것을 알려주는 것 → 스레드를 멈추거나 스레드 가용 상황과 우선순위에 따라 즉각적으로 실행할 수 있도록 설정</li>
<li>스레드가 멈춘 시점에 해당 리소스를 다른 태스크가 점유할 수 있기 때문에 최적화</li>
<li><code>getAmazingItem()</code> 비동기 함수가 종료된다면, <code>getFoo()</code> 함수는 다시 작업을 재개하는 데, 이 싲머에서는 어느 종류의 스레드에서라도 리줌될 수 있음</li>
<li>스레드 과포화 상태를 방지하고 리소스 최적화를 손쉽게 달성 가능</li>
</ul>
<h2 id="error-handling">Error Handling</h2>
<ul>
<li><code>async throws</code>를 통해 발생 가능한 에러를 스로우하는 구문을 다른 태스크 / 비동기 구문에서 사용할 때 <code>try catch</code>를 통해 손쉽게 에러 핸들링 가능</li>
</ul>
<h2 id="complier-check">Complier Check</h2>
<pre><code class="language-swift">    private func testWithCompletionCorrect(urlString: String, completion: @escaping ((Result&lt;UIImage, Error&gt;) -&gt; Void)) {
        guard let url = URL(string: urlString) else {
            completion(.failure(URLError(.badURL)))
            return
        }
        ...
    }

    private func testWithCompletionWrong(urlString: String, completion: @escaping ((Result&lt;UIImage, Error&gt;) -&gt; Void)) {
        guard let url = URL(string: urlString) else {
            return
        }
        ...
    }</code></pre>
<ul>
<li>컴플리션 핸들러를 통한 에러 핸들링의 경우 <code>return</code> 문과 별도로 컴플리션 클로저 내부에 실패 상황을 작성하지 않아도 컴파일러가 통과시킴<pre><code class="language-swift">  private func testWithAsyncThrows(urlString: String) async throws -&gt; UIImage? {
      guard let url = URL(string: urlString) else {
          throw URLError(.badURL)
      }
      ...
  }</code></pre>
</li>
<li><code>async</code> 구문을 통해 특정 데이터를 리턴하는 경우에는 <code>throw</code> 또는 리턴 데이터에 맞춰 값을 리턴해야 하기 때문에 코드를 잘못 작성하는 케이스가 감소</li>
</ul>
<h2 id="async-properties">Async Properties</h2>
<ul>
<li>Async getters: 함수 이외의 프로퍼티에도 적용 가능. </li>
</ul>
<pre><code class="language-swift">struct Person {
    let url: URL

    var image: UIImage? {
        get async throws {
            let (data, _) = try await URLSession.shared.data(from: url)
            return UIImage(data: data)
        }
    }
}</code></pre>
<ul>
<li>연산 프로퍼티의 <code>getter</code>를 비동기적으로 작성한 구문</li>
<li>저장 프로퍼티의 URL을 인자로 받아 <code>URLSession</code>의 <code>async</code>를 따르는 <code>data</code>를 비동기적으로 리턴<pre><code class="language-swift">private func testAsyncGetter() {
  Task {
      let person1 = Person(url: URL(string: &quot;&quot;)!)
      let imge = try await person1.image
  }
}</code></pre>
</li>
<li>해당 값을 사용하기 위해서는 동일한 방법으로 <code>Task</code> 또는 <code>async</code>를 따르는 구문 아래에서 <code>await</code> 작성</li>
</ul>
<h2 id="asnyc--await-scenarios">Asnyc / Await Scenarios</h2>
<ul>
<li>토큰 만료 이전 갱신 필요 체크: (1). 필요하다면 토큰 갱신 시도 (2). 필요없다면 그대로 놔두기 → 데이터 패치 및 리턴하기</li>
<li>위 상황에서 발생 가능한 에러는 거의 모든 상황에 존재(토큰 갱신 체크, 토큰을 갱신하는 과정 등)</li>
<li>컴플리션 핸들러를 통한 비동기 사용 → 재귀 호출이 필수 (리프레시 체크 → 그렇지 않다면 새롭게 리프레시 → 인증 토큰 발급 등이 순차적으로 이루어져야 하는데, 클로저 내부에서 또 다른 클로저를 사용해야 하기 때문에 재귀 호출을 해야 함)</li>
<li><code>async await</code>를 통한 비동기 사용 → 재귀 호출을 사용하지 않음 (만료가 되었다면 갱신을 하는 과정이 비동기적으로 이루어지는 데, 해당 과정이 <code>await</code>를 통해 해당 태스크 전반을 대기시키기 때문에 그 과정이 보장이 된 시점에 해당 토큰을 곧바로 사용할 수 있기 때문)</li>
</ul>
<h2 id="async-let-bindings">Async let Bindings</h2>
<ul>
<li><p><code>async</code>하게 리턴받는 과정 여러 개의 그룹을 순차적으로 실행하는 게 아니라, 병렬적으로 처리하고 싶을 때 사용 가능한 방법</p>
</li>
<li><p>기존 디스패치 그룹을 통한 <code>enter</code>, <code>leave</code>에서 벗어나 코드 가독성 및 단축</p>
<pre><code class="language-swift">private func testWithDispatchGroup() {
  let group = DispatchGroup()
  let person1 = Person(url: URL(string: &quot;&quot;)!)
  let person2 = Person(url: URL(string: &quot;&quot;)!)
  let person3 = Person(url: URL(string: &quot;&quot;)!)
  var personImages: [UIImage] = []

  group.enter()
  person1.loadImage { result in
      defer { group.leave() }
      switch result {
      case .success(let image): personImages.append(image)
      case .failure(let error): break
      }
  }
  group.enter()
  person2.loadImage { result in
      defer { group.leave() }
      switch result {
      case .success(let image): personImages.append(image)
      case .failure(let error): break
      }
  }
  group.enter()
  person3.loadImage { result in
      switch result {
      case .success(let image): personImages.append(image)
      case .failure(let error): break
      }
  }
  group.notify(queue: DispatchQueue.main) {
      handleImages(images: personImages)
  }
}</code></pre>
</li>
<li><p>컴플리션 핸들러를 통해 비동기 데이터를 사용한다면 디스패치 그룹을 사용할 수 있음</p>
</li>
<li><p><code>notifiy</code>를 통해 특정 작업이 완료되었음을 감지 가능 → 해당 블럭에서의 <code>images</code> 배열은 각 비동기 블럭이 완료된 이후임을 보장할 수 있음</p>
</li>
<li><p>에러 핸들링이 어렵다는 단점</p>
<pre><code class="language-swift">func loadImage(completion: @escaping((Result&lt;UIImage, Error&gt;) -&gt; Void)) {
      URLSession.shared.dataTask(with: url) { data, _, error in
          if let error = error {
              completion(.failure(error))
          } else if
              let data = data,
              let image = UIImage(data: data) {
              completion(.success(image))
          }
      }
  }</code></pre>
</li>
<li><p>이스케이핑 클로저를 통해 결과를 리턴하는 전형적인 비동기 구문</p>
</li>
</ul>
<pre><code class="language-swift">private func testWithAsyncLetBinding() async {
    let person1 = Person(url: URL(string: &quot;&quot;)!)
    let person2 = Person(url: URL(string: &quot;&quot;)!)
    let person3 = Person(url: URL(string: &quot;&quot;)!)

    async let image1 = person1.image
    async let image2 = person2.image
    async let image3 = person3.image

    do {
        try await handleImages(images: [image1, image2, image3])
    } catch {
        // error handling
    }

}</code></pre>
<ul>
<li>각 비동기 구문을 병렬적으로 처리하되, 오로지 모든 태스크가 완료된 이후에 <code>handleImages</code> 함수가 실행되는 게 보장 (<code>await</code>를 통해 해당 <code>handleImages</code>로 들어오는 <code>image1</code>... 들이 들어올 때까지 기다림)</li>
</ul>
<h2 id="cancellation">Cancellation</h2>
<ul>
<li>비동기적으로 시작된 특정 태스크를 중도 취소할 수 있음</li>
<li><code>Cooperative Cancellation</code> 메커니즘을 사용</li>
<li>특정 비동기 요청 중간에 해당 태스크를 취소할 수 있어야 함 → 리소스를 절약하고 불필요한 네트워킹을 방지<pre><code class="language-swift">try Task.checkCancellation()</code></pre>
</li>
<li>해당 구문을 통해 해당 태스크가 중도 취소되었다면 에러를 스로우<pre><code class="language-swift">if Task.isCancelled {
              // .. default value returned
          }</code></pre>
</li>
<li>디폴트 값을 리턴하도록 캔슬 여부를 체크할 수도 있음</li>
<li>캔슬 상황에서 특정 구문을 실행하도록 핸들러를 제공<pre><code class="language-swift">var image: UIImage? {
      get async throws {
          try await withTaskCancellationHandler(operation: {
              let (data, _) = try await URLSession.shared.data(from: url)
              return UIImage(data: data)
          }, onCancel: {
              // ... handle someithing in this return Void clousre
          })
      }
  }</code></pre>
</li>
</ul>
<h2 id="task-groups">Task Groups</h2>
<ul>
<li>여러 개의 <code>async</code> 리퀘스트를 보낸 뒤 리턴받은 모든 데이터를 한 번에 사용해야 하는 경우<pre><code class="language-swift">private func testWithForLoop(people: [Person]) async -&gt; [UIImage?] {
  var result = [UIImage?]()
  for person in people {
      do {
          try await result.append(person.image)
      } catch {
          // error handling
      }
  }
  return result
}</code></pre>
</li>
<li><code>For-Loop</code>를 통해 파라미터로 들어온 각 변수 별로 <code>await</code>를 통해 데이터가 리턴받을 때까지 서스펜션이 걸리기 때문에 병렬 처리가 아님</li>
<li>컨커런트한 태스크 수행이 필요하다면 앞서 언급된 <code>async let binding</code>이 필요<pre><code class="language-swift">private func testWithAsyncLetBinding(people: [Person]) async -&gt; [UIImage?] {
  var result = [UIImage?]()
  for person in people {
      async let image = person.image
      // ... cannot handle this image
  }
  return result
}</code></pre>
</li>
<li><code>async ley</code>으로 바인딩되는 변수의 개수가 동적으로 변할 경우 해당 인자에 대한 직접적인 접근이 어려워진다는 한계<pre><code class="language-swift">private func testWithTaskGroups(people: [Person]) async -&gt; [UIImage?] {
  await withTaskGroup(of: UIImage?.self, body: { group in
      for person in people {
          group.addTask {
              do {
                  let image = try await person.image
                  return image
              } catch {
                  // error handling
                  return nil
              }
          }
      }
      var result = [UIImage?]()
      for await image in group {
          result.append(image)
      }
      return result
  })
}</code></pre>
</li>
<li>태스크 자체를 그룹화하여 컨커런트하게 실행한다는 아이디어</li>
<li>자식 태스크를 가질 수 있는 여러 개의 태스크를 묶어서 실행</li>
<li><code>withTaskGroup</code> 내부에서 어떤 종류의 데이터 타입을 취급하는 넣어준 뒤, 바디에서 그룹으로 실행할 같은 종류의 태스크를 태스크 그룹에 넣기</li>
<li>태스크로 들어갈 때 클로저로 리턴하는 값이 <code>of</code> 뒤에 선언한 해당 데이터와 일치해야 함. 에러 핸들링 또한 <code>do catch</code>문을 동일하게 적용</li>
<li><code>for await</code>를 통해 해당 그룹 태스크가 컨커런트하게 리턴하는 <code>image</code>를 그때마다 받아와서 결과 <code>result</code>를 리턴 가능<pre><code class="language-swift">private func testWithTaskGroups(people: [Person]) async -&gt; [UIImage?] {
  await withTaskGroup(of: UIImage?.self, body: { group in
      for person in people {
          group.addTask(priority: person.isPrior ? .high : nil) {
              do {
                  let image = try await person.image
                  return image
              } catch {
                  // error handling
                  return nil
              }
          }
      }
      var result = [UIImage?]()
      for await image in group {
          result.append(image)
      }
      return result
  })
}
</code></pre>
</li>
</ul>
<pre><code>* 태스크 그룹에 태스크를 추가할 때 특정 변수에 따라 우선순위 또한 설정 가능
* 여러 개의 태스크를 동시적으로 수행할 때 우선순위가 높은 태스크를 먼저 실행

```swift
private func testWithTaskGropus(people: [Person]) async -&gt; [String: UIImage?] {
    await withTaskGroup(of: (String, UIImage?).self, body: { group in
        for person in people {
            group.addTask {
                do {
                    return try await (person.id, person.image)
                } catch {
                    return (person.id, nil)
                }
            }
        }
        return await group.reduce(into: [String: UIImage?]()) { $0[$1.0] = $1.1 }
    })
}</code></pre><ul>
<li>태스크의 결과가 동시적으로 리턴되기 때문에 결과 값을 최종 리턴하기 이전 정렬을 하는 방법이 유용함</li>
<li>키가 되는 값을 리턴 데이터 타입과 함께 <code>of</code>로 넘겨버리는 방법에 주목<h2 id="hierarchy">Hierarchy</h2>
</li>
<li>태스크 그룹이 존재하고, 해당 그룹에 추가된 자식 태스크가 동시적으로 실행</li>
<li>부모/자식 태스크가 없이 분리된 별도의 태스크를 생성 가능 → <code>Task.detached</code> 등을 통해 사용, 별도로 관리<h2 id="async-sequences">Async Sequences</h2>
</li>
<li>여러 개의 값을 리턴하는 비동기 작업을 실행해야 할 때 사용</li>
<li>일반적인 스위프트의 시퀀스 타입과 동일 → 시퀀스 내 값이 모두 <code>awaited asynchronously</code>하다는 것만 제외한다면</li>
<li>태스크 그룹을 사용할 때 <code>for await in ...</code>를 적용한 것이 그 예시</li>
<li>데이터 용량이 큰 csv 파일을 구조화해 사용해야 할 때<pre><code class="language-swift">private func fetchLineSerailly(with largeCSV: URL) async {
  Task {
      let (data, _) = try await URLSession.shared.data(for: URLRequest(url: largeCSV))
      let lines = String(data: data, encoding: .utf8)?.components(separatedBy: &quot;\n&quot;) ?? []
      for line in lines {
          // handle line...
      }
  }
}</code></pre>
</li>
<li>전체 파일을 인코딩한 뒤 결과를 <code>for loop</code>를 통해 핸들링<pre><code class="language-swift">private func fetchLineAsyncSequences(with largeCSV: URL) async {
  Task {
      let (bytes, _) = try await URLSession.shared.bytes(for: URLRequest(url: largeCSV))
      for try await line in bytes.lines {
          // handle line...
      }
  }
}</code></pre>
</li>
<li><code>bytes</code>라는 별도의 API를 통해 컨커런트하게 접근 가능</li>
</ul>
<pre><code class="language-swift">private func handleLocalFile(with localFile: URL) async {
    Task {
        let handle = try FileHandle(forReadingFrom: localFile)
        for try await line in handle.bytes.lines {
            // handle line by line
        }
    }
}</code></pre>
<ul>
<li>로컬 파일을 다룰 때에도 유용하게 사용할 수 있는 방법</li>
</ul>
<h2 id="actor">Actor</h2>
<ul>
<li><p>레이스 문제를 해결할 때 가장 간편한 해결 방법 중 한 가지</p>
</li>
<li><p>멀티 스레드 환경의 컨커런트한 데이터 접근 방법과 엮어 있음</p>
</li>
<li><p>해당 상태에 대한 자동화된 동기화 및 고립을 지원</p>
<pre><code class="language-swift">actor DataSource {
  let items = [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;]
  private var index = 0

  func next() {
      print(items[index])
      if items.indices.contains(index + 1) {
          index += 1
      } else {
          index = 0
      }
  }
}
</code></pre>
</li>
</ul>
<p>private func testActor() {
    let dataSource = DataSource()
    Task.detached { await dataSource.next() }
    Task.detached { await dataSource.next() }
    Task.detached { await dataSource.next() }
}</p>
<pre><code>* 컨커런트하게 `next()` 함수에 접근, 호출해야 할 경우 순차적 접근이 가능하도록 `actor` 클래스에서 내장 지원
```swift
@MainActor class DataSource {
    let items = [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;]
    private var index = 0

    func next() {
        print(items[index])
        if items.indices.contains(index + 1) {
            index += 1
        } else {
            index = 0
        }
    }
}

@MainActor private func testActor() {
    let dataSource = DataSource()
    Task.detached { await dataSource.next() }
    Task.detached { await dataSource.next() }
    Task.detached { await dataSource.next() }
}</code></pre><ul>
<li><p>액터의 실행 스레드가 메인 스레드에서 이루어진다는 게 필요할 때 <code>@MainActor</code> 프로토콜을 따름으로써 보장 가능</p>
<pre><code class="language-swift">actor DataSource {
  let items = [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;]
  private var index = 0

  func next() {
      print(items[index])
      if items.indices.contains(index + 1) {
          index += 1
      } else {
          index = 0
      }
  }

  nonisolated
  func check() {
      print(&quot;i&#39;m not isolated to this thread&quot;)
  }
}
</code></pre>
</li>
</ul>
<p>private func testActor() {
    let dataSource = DataSource()
    Task.detached {await dataSource.next()}
    dataSource.check()
}</p>
<pre><code>* 특정 함수, 특정 변수가 액터가 보장하는 스레드에 고립되어 있지 않아도 된다면 `nonisolated`를 통해 `await`로 기다리지 않아도 곧바로 호출 가능하도록 설정

## Plus
* 개발 환경 (iOS 13 이상) 체크
* 커스텀 `Continuation` 함수를 만들어 사용 가능
```swift
private func testContinuation() {
    withCheckedContinuation(&lt;#T##body: (CheckedContinuation&lt;T, Never&gt;) -&gt; Void##(CheckedContinuation&lt;T, Never&gt;) -&gt; Void#&gt;)
    withCheckedThrowingContinuation(&lt;#T##body: (CheckedContinuation&lt;T, Error&gt;) -&gt; Void##(CheckedContinuation&lt;T, Error&gt;) -&gt; Void#&gt;)
    withUnsafeContinuation(&lt;#T##fn: (UnsafeContinuation&lt;T, Never&gt;) -&gt; Void##(UnsafeContinuation&lt;T, Never&gt;) -&gt; Void#&gt;)
    withUnsafeThrowingContinuation(&lt;#T##fn: (UnsafeContinuation&lt;T, Error&gt;) -&gt; Void##(UnsafeContinuation&lt;T, Error&gt;) -&gt; Void#&gt;)
}</code></pre><pre><code class="language-swift">private func testAsyncWithCompletion(completion: @escaping((Result&lt;UIImage, Error&gt;) -&gt; ())) { }

private func testAsyncContinuationWithCompletion() async throws -&gt; UIImage {
    try await withCheckedThrowingContinuation({ continuation in
        testAsyncWithCompletion { result in
            continuation.resume(with: result)
        }
    })
}</code></pre>
<ul>
<li>컴플리션 핸들러 리턴 방식을 <code>async await</code>를 따르도록 리팩터링 가능</li>
</ul>
<pre><code class="language-swift">struct Item {
    let id = UUID()
}

enum ItemChange {
    case change(Item)
    case finished
}

func checkItems(change: @escaping((ItemChange) -&gt; ())) {
    // handle async task
}</code></pre>
<ul>
<li>이넘을 통해 특정 데이터의 변화를 전달, 해당 전달의 비동기 구문은 이스케이핑 클로저를 쓰고 있을 때<pre><code class="language-swift">func checkItems() -&gt; AsyncStream&lt;Item&gt; {
  AsyncStream&lt;Item&gt; { continuation in
      checkItems { change in
          switch change {
          case .change(let item): continuation.yield(item)
          case .finished: continuation.finish()
          }
      }
  }
}
</code></pre>
</li>
</ul>
<p>private func testCheckItems() async {
    Task {
        for await item in checkItems() {
            print(&quot;Current item: (item)&quot;)
        }
    }
}</p>
<pre><code>* `AsyncStream`을 통해 컴플리션을 감싸서 사용 가능

## SwiftUI + Async Await
* `.task` 모디파이어를 통해 iOS 15 부터 사용 가능
* `Task`를 통해 직접적으로 iOS 13부터 사용 가능했음

## Combine &amp; Async Await
* 컴바인의 퍼블리셔가 가지고 있는 `values` 프로퍼티를 통해 `AsyncSequence`와 동일한 작업 수행 가능
```swift
private func testCombineStyle(with numbers: AnyPublisher&lt;Int, Error&gt;) {
    numbers
        .map({$0 * 2})
        .prefix(3)
        .dropFirst()
        .sink { _ in
            // handle completion
        } receiveValue: { number in
            // handle number
        }
}</code></pre><ul>
<li>전형적인 컴바인 스타일<pre><code class="language-swift">private func testCombineAndAsync(with numbers: AnyPublisher&lt;Int, Error&gt;) async {
  let publisher = numbers
      .map({$0 * 2})
      .prefix(3)
      .dropFirst()
  Task {
      for try await number in publisher.values {
          // handle number
      }
      // handle completion
  }
}</code></pre>
</li>
<li>퍼블리셔의 <code>values</code>가 정확히 <code>AsyncSequences</code>와 유사하기 때문에 <code>sink</code> 단에서 실제로 내려오는 데이터를 <code>for (try) await</code>로 다룰 수 있음</li>
<li><code>AsyncSequence</code>를 위해 애플이 기본적으로 제공하는 오퍼레이터가 현재 컴바인 오퍼레이터를 대체할 수 있다는 게 유력함.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] UICollectionView: Prefetch]]></title>
            <link>https://velog.io/@j_aion/UIKit-UICollectionView-Prefetch</link>
            <guid>https://velog.io/@j_aion/UIKit-UICollectionView-Prefetch</guid>
            <pubDate>Sat, 31 Dec 2022 09:57:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=rm-gp2MxA1o&amp;pp=ugMICgJrbxABGAE%3D">Prefetching with TableViews (2022) – iOS
</a></p>
</blockquote>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=umFhHNpZuOg">Smooth TableView and CollectionView Infinite Scrolling Using Prefetch DataSource
</a></p>
</blockquote>
<h1 id="uicollectionview-prefetch">UICollectionView: Prefetch</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/63ad5fbf-ee67-4f55-9b07-4b77148998ad/image.png" alt=""></p>
<ul>
<li>컬렉션 뷰 <code>prefetch</code> 함수 사용</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li><code>prefetch</code> 델리게이트 함수를 통해 특정 인덱스 패스에 존재한느 모델이 사용할 네트워킹을 사전에 실행</li>
<li>데이터 캐시를 통한 UI 표시 효율</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource&lt;Section, PhotoModel&gt;!
    private var cellRegistration: UICollectionView.CellRegistration&lt;PhotoCollectionViewCell, PhotoModel&gt;!
    private lazy var listLayout: UICollectionViewLayout = {
        var listConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        listConfig.trailingSwipeActionsConfigurationProvider = {
            [weak self] indexPath -&gt; UISwipeActionsConfiguration? in
            guard let model = self?.dataSource.itemIdentifier(for: indexPath) else { return nil }
            return .init(actions: [.init(style: .destructive, title: &quot;Delete&quot;, handler: { [weak self] _, _, completion in
                self?.viewModel?.delete(model: model)
                completion(true)
            })])
        }
        return UICollectionViewCompositionalLayout.list(using: listConfig)
    }()</code></pre>
<ul>
<li><code>diffable</code> 데이터 소스를 사용하기 위해 미리 선언</li>
<li>스와이프 액션 설정, 셀 등록<pre><code class="language-swift">  private func setCollectionView() {
      collectionView = .init(frame: view.bounds, collectionViewLayout: listLayout)
      collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
      view.addSubview(collectionView)
      collectionView.delegate = self
      collectionView.prefetchDataSource = self
      collectionView.isPrefetchingEnabled = true
      cellRegistration = .init(handler: { cell, indexPath, model in
          cell.configure(with: model)
      })
      dataSource = .init(collectionView: collectionView, cellProvider: { [weak self] collectionView, indexPath, item in
          guard let self = self else { return nil }
          let cell = collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: item)
          self.viewModel?.prefetch(model: item)
          return cell
      })
  }
</code></pre>
</li>
</ul>
<pre><code>* `prefetchDataSource`를 통해 특정한 셀이 `display`되기 전에 사전 로드할 데이터를 캐치 가능
* `cellRegistration`, `dataSource`를 기존의 컬렉션 뷰 UI를 그리는 방법과 달리 컴플리셔 핸들러를 통해 설정
```swift
extension PhotosViewController: UICollectionViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        guard collectionView.frame.size.height &gt; 0 else { return }
        if offsetY + collectionView.frame.size.height &gt;= collectionView.contentSize.height - 200 {
            viewModel?.fetchImages()
        }
    }
}</code></pre><ul>
<li><code>infinite scrolling</code>을 통해 200 픽셀 정도로 현재 컬렉션 뷰 최하단 부를 스크롤할 경우 자동으로 현 시점의 쿼리한 데이터를 더 불러오도록 설정<h2 id="구현-화면">구현 화면</h2>
<img src="https://velog.velcdn.com/images/j_aion/post/2bb80a45-797b-423f-96bb-64fdbf092df0/image.gif" alt=""><blockquote>
<p>다운로드로 인해 상당히 메모리 부하가 심한데, <code>full</code>로 설정되어 있는 이미지 파일을 그대로 다운로드받기 때문. 리사이즈 또는 다운샘플링이 필요한 순간!</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] ChartStocksClone: Ticker Symbol Sheet UI]]></title>
            <link>https://velog.io/@j_aion/SwiftUI-ChartStocksClone-Ticker-Symbol-Sheet-UI</link>
            <guid>https://velog.io/@j_aion/SwiftUI-ChartStocksClone-Ticker-Symbol-Sheet-UI</guid>
            <pubDate>Fri, 30 Dec 2022 14:40:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=F5uOS-b003c&amp;list=PLuecTl5TrGwtQRtT6wxI0-l0fN-2V5QTJ&amp;index=3">Build Swift Charts Stocks App Part 3 - Ticker Symbol Sheet UI - SwiftUI iOS 16 App
</a></p>
</blockquote>
<h1 id="chartstocksclone-ticker-symbol-sheet-ui">ChartStocksClone: Ticker Symbol Sheet UI</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/ec10f8f4-6b53-4bb3-a7e7-ad3ee4e0df51/image.png" alt=""></p>
<ul>
<li>차트 선택 시 시트 뷰 구현</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li>시트 뷰 연결 로직 구현</li>
<li>디테일 뷰 UI 구현</li>
<li>API를 통한 특정 심볼 데이터 패치 및 UI 바인딩</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">struct StockTickerView: View {
    @StateObject var quoteViewModel: TickerQuoteViewModel
    @State private var selectedRange = ChartRange.oneDay
    @Environment(\.dismiss) private var dismiss
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            headerView.padding(.horizontal)
            Divider()
                .padding(.vertical, 8)
                .padding(.horizontal)
            scrollView
        }
        .padding(.top)
        .background(Color(uiColor: .systemBackground))
        .task {
            await quoteViewModel.fetchQuotes()
        }
    }
}</code></pre>
<ul>
<li>특정 리스트 셀을 탭했을 때 모달 프레젠테이션로 프레젠트될 시트 뷰</li>
<li><code>TickerQuoteViewModel</code>을 통해 UI로 표현할 데이터를 받아들임</li>
<li><code>task</code> 메소드를 통해 뷰 모델이 가지고 있는 데이터 패칭<pre><code class="language-swift">func fetchQuotes() async {
      phase = .fetching
      do {
          let response = try await stocksAPI.fetchQuotes(symbols: ticker.symbol)
          if let quote = response.first {
              phase = .success(quote)
          } else {
              phase = .empty
          }
      } catch {
          print(error.localizedDescription)
          phase = .failure(error)
      }
  }</code></pre>
</li>
<li><code>async</code> 함수이기 때문에 뷰 단의 <code>task</code> 내부에서 실행 가능</li>
<li><code>phase</code>를 데이터 패치 중, 성공/실패, 빈 화면 유무 등 이넘으로 관리 중이며, 비동기적 흐름에 의해 변경되기 때문에 현재 상황을 UI로 곧바로 표현할 수 있음<pre><code class="language-swift"></code></pre>
</li>
</ul>
<p>struct MainListView: View {
    @EnvironmentObject private var appViewModel: AppViewModel
    @StateObject var quotesViewModel = QuotesViewModel()
    @StateObject var searchViewModel = SearchViewModel()
    var body: some View {
        tickerListView
            .listStyle(.plain)
            .overlay { overlayView }
            .toolbar {
                titleToolbar
                attributionToolbar
            }
...
            .sheet(item: $appViewModel.selectedTicker) {
                StockTickerView(quoteViewModel: .init(ticker: $0, stocksAPI: quotesViewModel.stocksAPI))
                    .presentationDetents([.height(560)])
            }
...
    }
}</p>
<pre><code>* 선택한 데이터를 리스트로 보여주는 메인 뷰
* 시트 메소드를 통해 특정한 리스트를 클릭했을 때 전역으로 들고 있는 `appViewModel` 내부의 `selectedTicker` 퍼블리셔가 널 값이 아니라 값이 들어갈 때 자동으로 모달 프레젠트
* `presentationDetents`를 통해 iOS 최신 버전부터 도입된 하프 모달을 통해 높이 조정
```swift
@MainActor
struct SearchView: View {
    @EnvironmentObject private var appViewModel: AppViewModel
    @StateObject var quotesViewModel = QuotesViewModel()
    @ObservedObject var searchViewModel: SearchViewModel
    var body: some View {
        List(searchViewModel.tickers) { ticker in
            TickerListRowView(data: .init(symbol: ticker.symbol, name: ticker.shortname, price: quotesViewModel.priceForTicker(ticker), type: .search(isSaved: appViewModel.isAddedToMyTickers(ticker: ticker), onButtonDidTap: {
                Task { @MainActor in
                    appViewModel.toggleTicker(ticker)
                }
            })))
            .contentShape(Rectangle())
            .onTapGesture {
                Task { @MainActor in
                    appViewModel.selectedTicker = ticker
                }
            }
        }
        .background(Color(uiColor: .systemBackground))
        .listStyle(.plain)
        .refreshable {
            await quotesViewModel.fetchQuotes(tickers: searchViewModel.tickers)
        }
        .task(id: searchViewModel.tickers, {
            await quotesViewModel.fetchQuotes(tickers: searchViewModel.tickers)
        })
        .overlay {
            listSearchOverlay
        }
    }
}</code></pre><ul>
<li>서치 바의 텍스트 필드를 통해 검색할 경우 나타나는 결과 리스트 뷰</li>
<li>해당 리스트 뷰를 클릭했을 때에도 디테일 뷰 모달 프레젠테이션이 가능하도록 탭 제스처 구현</li>
<li>현재 <code>SearchView</code>는 메인 리스트 뷰에서 <code>overlay</code>를 통해 나타나고 있기 때문에 별도의 <code>sheet</code> 메소드를 사용할 필요가 없음</li>
<li>현 시점의 하위 뷰, 즉 오버레이를 하고 있는 메인 리스트 뷰에서 <code>appViewModel</code>이 들고 있는 <code>selectedTicker</code> 퍼블리셔 값을 계속해서 관찰하고 있기 때문에 해당 이벤트가 발생할 경우 모달 뷰가 프레젠트되기 때문<h2 id="구현-화면">구현 화면</h2>
<img src="https://velog.velcdn.com/images/j_aion/post/eec4b238-a577-4530-acf0-e2e5f1b1565a/image.gif" alt=""></li>
</ul>
<blockquote>
<p>이렇게 잘 구조화된 코드를 보고 있으면 대단하다는 생각부터 든다... 자연스럽게 더 잘 이해하고, &quot;내가&quot; 더 잘 할 수 있기를!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] GithubSearchClone]]></title>
            <link>https://velog.io/@j_aion/RxSwift-GithubSearchClone</link>
            <guid>https://velog.io/@j_aion/RxSwift-GithubSearchClone</guid>
            <pubDate>Fri, 30 Dec 2022 06:11:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://github.com/ReactorKit/ReactorKit">ReactorKit</a></p>
</blockquote>
<h1 id="githubsearchclone">GithubSearchClone</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/7de654e9-1fe7-42c7-8234-032078f646c4/image.png" alt=""></p>
<ul>
<li><code>ReactorKit</code> 깃허브 예시 파일 중 깃허브 서치 프로젝트 클론</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li><code>RxSwift</code>, <code>RxCocoa</code>, <code>RxDataSources</code>, <code>ReactorKit</code> 사용</li>
<li>깃허브 API를 통한 검색 쿼리 서비스 제공</li>
<li>검색 결과를 통한 테이블 뷰 <code>RxDataSources</code>로 구성</li>
<li>테이블 뷰 셀 선택 시 사파리 뷰 연결</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">final class SearchReactor: Reactor {
    enum Action {
        case updateQuery(query: String)
        case loadNextPage
    }
    enum Mutation {
        case setQuery(query: String)
        case setRepos(repos: [String], nextPage: Int?)
        case appendRepos(repos: [String], nextPage: Int?)
        case setLoadingNextPage(isLoading: Bool)
    }
    struct State {
        var query: String = &quot;&quot;
        var repos: [String] = []
        var nextPage: Int?
        var isLoadingNextPage: Bool = false
    }
...
}</code></pre>
<ul>
<li>리액터 프로토콜을 따르는 파이널 클래스 <code>SearchReactor</code>는 기좀 MVVM 스타일의 뷰 모델을 대리, 액션과 상태로 각 다룰 영역을 구분한 클래스</li>
<li><code>Action</code>을 통해 어떤 선택이 가능한지, <code>Mutation</code>을 통해 어떤 변화가 일어날지, <code>State</code>를 통해 어떤 종류의 변화가 적용이 되었는지 확인할 수 있음</li>
</ul>
<pre><code class="language-swift">    func mutate(action: Action) -&gt; Observable&lt;Mutation&gt; {
        switch action {
        case .updateQuery(query: let query):
            return Observable.concat([
                Observable.just(Mutation.setQuery(query: query)),
                search(query: query, page: 1)
                    .take(until: self.action.filter({ action in
                        switch action {
                        case .updateQuery(query: _): return true
                        default: return false
                        }
                    }))
                    .map({Mutation.setRepos(repos: $0, nextPage: $1)})
            ])
        case .loadNextPage:
            guard
                !currentState.isLoadingNextPage,
                let page = currentState.nextPage else { return .empty() }
            return Observable.concat([
                Observable.just(Mutation.setLoadingNextPage(isLoading: true)),
                search(query: currentState.query, page: page)
                    .take(until: self.action.filter({ action in
                        switch action {
                        case .updateQuery(query: _): return true
                        default: return false
                        }
                    }))
                    .map({Mutation.appendRepos(repos: $0, nextPage: $1)}),
                Observable.just(Mutation.setLoadingNextPage(isLoading: false))
            ])
        }
    }
</code></pre>
<ul>
<li>특정한 액션이 들어왔을 때 어떤 변화가 일어나는지 함수를 통해 표현하는 <code>mutate</code></li>
<li>액션의 종류에 따라 어떤 변화가 일어나는지 <code>Observable.concat</code>을 통해 <code>Observable</code>의 변화를 연속적으로 붙여서 리턴하는 게 관습적인 코드인 것 같음<pre><code class="language-swift">private func getURL(query: String, page: Int) -&gt; URL? {
      guard !query.isEmpty else { return nil }
      var components = URLComponents(string: baseURLString)
      let urlQueryItems: [URLQueryItem] = [
          .init(name: &quot;q&quot;, value: query.lowercased()),
          .init(name: &quot;page&quot;, value: page.description),
          .init(name: &quot;client_id&quot;, value: &quot;token [YOUR_GITHUB_ACCESS_TOKEN]&quot;)
      ]
      components?.queryItems = urlQueryItems
      return components?.url
  }
</code></pre>
</li>
</ul>
<p>private func search(query: String, page: Int) -&gt; Observable&lt;(repos: [String], nextPage: Int?)&gt; {
        guard let url = getURL(query: query, page: page) else { return .just((repos: [], nextPage: nil))}
        return URLSession.shared.rx.json(url: url)
            .map { json -&gt; ([String], Int?) in
                guard
                    let dict = json as? [String: Any],
                    let items = dict[&quot;items&quot;] as? [[String: Any]] else {
                    return ([], nil)
                }
                let repos = items.compactMap({ $0[&quot;full_name&quot;] as? String })
                let nextPage = repos.isEmpty ? nil : page + 1
                return (repos, nextPage)
            }
    }</p>
<pre><code>* 쿼리 검색 액션이 들어왔을 때 실행되는 검색 함수
* 깃허브 API를 사용하기 때문에 쿼리를 구성한 뒤 URL를 리턴, 해당 URL을 통해 `URLSession`을 사용한 검색 결과를 `Observable`로 감싸 리턴하는 게 검색 함수의 결과
```swift
    func reduce(state: State, mutation: Mutation) -&gt; State {
        var state = state
        switch mutation {
        case .setQuery(query: let query):
            state.query = query
        case .setRepos(repos: let repos, nextPage: let nextPage):
            state.repos = repos
            state.nextPage = nextPage
        case .appendRepos(repos: let repos, nextPage: let nextPage):
            state.repos.append(contentsOf: repos)
            state.nextPage = nextPage
        case .setLoadingNextPage(isLoading: let isLoading):
            state.isLoadingNextPage = isLoading
        }
        return state
    }
</code></pre><ul>
<li><p>현재 상태 및 변경 사항이 주어진다면 현재 상태가 어떻게 변화되어야 하는지 확인하는 함수</p>
</li>
<li><p>즉 <code>reduce</code>되는 결과값이 상태에 반영된다는 뜻</p>
</li>
<li><p>상태 구조체를 구성하는 쿼리, 레포지터리 배열, 페이지 정수, 로딩 중 여부 등 경우의 수에 따라 현재 상태 변경</p>
<pre><code class="language-swift">typealias ReactorView = ReactorKit.View
final class SearchViewController: UIViewController, ReactorView {
  private let searchController: UISearchController = {
      let controller = UISearchController(searchResultsController: nil)
      controller.searchBar.placeholder = &quot;Search Github Input...&quot;
      return controller
  }()
  private let tableView: UITableView = {
      let tableView = UITableView()
      tableView.register(UITableViewCell.self, forCellReuseIdentifier: &quot;tableViewCell&quot;)
      return tableView
  }()
  var disposeBag = DisposeBag()
  private let reactor = SearchReactor()

  override func viewDidLoad() {
      super.viewDidLoad()
      setUI()
      bind(reactor: reactor)
  }
...
}</code></pre>
</li>
<li><p><code>Reactor</code> 프레임워크의 <code>View</code> 프로토콜을 따르는 뷰 컨트롤러</p>
</li>
<li><p>뷰 컨트롤러, 셀 등은 모두 <code>View</code>로 간주하는 게 리액터 킷의 관점</p>
</li>
<li><p><code>bind(reacotr:)</code> 함수는 외부에서 리액터가 주입이 될 때 곧바로 실행이 되는 데, 현 시점에서는 뷰 컨트롤러 내부에서 <code>private</code>하게 리액터를 가지고 있고 로드가 될 때 자동으로 바인드 함수를 실행하는 방식으로 구현</p>
<pre><code class="language-swift">  func bind(reactor: SearchReactor) {
      searchController
          .searchBar
          .rx
          .text
          .orEmpty
          .throttle(.milliseconds(500), scheduler: MainScheduler.instance)
          .map({ SearchReactor.Action.updateQuery(query: $0)})
          .bind(to: reactor.action)
          .disposed(by: disposeBag)
      tableView
          .rx
          .contentOffset
          .filter { [weak self] offset in
              guard let self = self else { return false}
              guard self.tableView.frame.size.height &gt; 0 else { return false }
              return offset.y + self.tableView.frame.size.height &gt;= self.tableView.contentSize.height - 100
          }
          .map { _ in
              SearchReactor.Action.loadNextPage
          }
          .bind(to: reactor.action)
          .disposed(by: disposeBag)
      typealias SearchSection = SectionModel&lt;Int, String&gt;
      let dataSource: RxTableViewSectionedReloadDataSource&lt;SearchSection&gt; = .init { _, tableView, indexPath, item in
          let cell = tableView.dequeueReusableCell(withIdentifier: &quot;tableViewCell&quot;, for: indexPath)
          cell.textLabel?.text = item
          return cell
      }
      reactor
          .state
          .map({ $0.repos })
          .map({ [SearchSection(model: 0, items: $0)]})
          .asDriver(onErrorJustReturn: [])
          .drive(tableView.rx.items(dataSource: dataSource))
          .disposed(by: disposeBag)
      reactor
          .state
          .map({ $0.isLoadingNextPage })
          .distinctUntilChanged()
          .observe(on: MainScheduler.instance)
          .subscribe { [weak self] isLoading in
              guard let self = self else { return }
              self.tableView.tableFooterView = isLoading ? self.createSpinnerView() : nil
          }
          .disposed(by: disposeBag)
      tableView
          .rx
          .itemSelected
          .subscribe { [weak self, weak reactor] indexPath in
              guard let self = self else { return }
              self.view.endEditing(true)
              self.tableView.deselectRow(at: indexPath, animated: true)
              guard
                  let repo = reactor?.currentState.repos[indexPath.row],
                  let url = URL(string: &quot;https://www.github.com/\(repo)&quot;) else { return }
              let vc = SFSafariViewController(url: url)
              self.searchController.present(vc, animated: true)
          }
          .disposed(by: disposeBag)
  }
</code></pre>
</li>
</ul>
<pre><code>* `ReactorKit`과 별개로 `rx`를 통해 뷰를 그리는 파트
* 서치 바의 텍스트를 `rx`화하여 반응형으로 리액터의 쿼리를 업데이트하는 액션과 연동
* 테이블 뷰의 `contentOffset` 또한 반응형으로 감지, 특정 오프셋 이상이 넘길 때 (즉 스크롤을 마지막까지 당긴 뒤 계속해서 로드할 때) 다음 결과를 쿼리하라는 액션으로 바인딩
* 리액터의 각 상태값이 들고 있는 레포지터리 배열 데이터, 로딩 여부 등을 통해 테이블 뷰의 데이터 소스를 그리거나 푸터 뷰에 로딩 뷰를 잠시 보여줄 것인지 또한 구현
* 선택된 테이블 뷰 셀이 있다면 해당 셀로부터 사파리 웹뷰를 구성, 연결

## 구현 화면
![](https://velog.velcdn.com/images/j_aion/post/5e644821-dbec-46fd-a6fe-2f6ed76e550c/image.gif)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] AirPortClone: MapKit]]></title>
            <link>https://velog.io/@j_aion/RxSwift-AirPortClone-MapKit</link>
            <guid>https://velog.io/@j_aion/RxSwift-AirPortClone-MapKit</guid>
            <pubDate>Thu, 29 Dec 2022 07:10:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=LqCX85XPrJc&amp;list=PLRKlnZOyrBQdmks75bAtiZE-DkLXzp99D&amp;index=14">#12 Adding Pins to Map using MapKit - RxSwift MVVM Coordinator iOS App
</a></p>
</blockquote>
<h1 id="airportclone-mapkit">AirPortClone: MapKit</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/8ffc11e9-083e-4941-bdcb-503943c9c0e7/image.png" alt=""></p>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li>맵뷰 구현</li>
<li>맵뷰 어노테이션 뷰 구현</li>
<li>현재 위치 - 해당 위치 경로 구현</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">protocol AirportDetailsViewPresentable {
    typealias Input = ()
    typealias Output = (
        airportDetails: Driver&lt;AirportViewPresentable&gt;,
        mapDetails: Driver&lt;AirportMapViewPresentable&gt;
    )
    typealias Dependencies = (
        model: AirportModel,
        currentLocation: Observable&lt;(lat: Double, lon: Double)?&gt;
    )

    var input: AirportDetailsViewPresentable.Input { get }
    var output: AirportDetailsViewPresentable.Output { get }

    typealias ViewModelBuilder = (AirportDetailsViewPresentable.Input) -&gt; (AirportDetailsViewPresentable)
}
</code></pre>
<ul>
<li><p>맵뷰와 함께 해당 공항의 정보를 표현할 뷰에 해당할 뷰 모델 프로토콜</p>
</li>
<li><p>인풋을 통해 뷰 모델 자체를 리턴하는 뷰 모델 빌더 및 아웃풋을 구성하기 위한 디펜던시</p>
<pre><code class="language-swift">  private static func transform(input: AirportDetailsViewPresentable.Input, dependencies: AirportDetailsViewPresentable.Dependencies) -&gt; AirportDetailsViewPresentable.Output {
      let airportDetails: Driver&lt;AirportViewPresentable&gt; = dependencies
          .currentLocation
          .compactMap({ $0 })
          .map { [airportModel = dependencies.model] currentLocation in
              AirportViewModel(model: airportModel, currentLocation: currentLocation)
          }
          .asDriver(onErrorDriveWith: .empty())
      let mapDetails: Driver&lt;AirportMapViewPresentable&gt; = dependencies
          .currentLocation
          .compactMap({ $0 })
          .map { [airportModel = dependencies.model] currentLocation in
              guard
                  let lat = Double(airportModel.lat),
                  let lon = Double(airportModel.lon) else { throw URLError(.badURL) }

              return AirportMapViewModel(airportLocation: (lat: lat, lon: lon), airport: (name: airportModel.name, city: airportModel.city), currentLocation: currentLocation)
          }
          .asDriver(onErrorDriveWith: .empty())
      return (
          airportDetails: airportDetails,
          mapDetails: mapDetails
      )
  }
</code></pre>
</li>
</ul>
<pre><code>* 건네받은 디펜던시의 값을 통해 공항 정보 및 맵 뷰 정보를 업데이트 → `Driver`로 캐스팅해서 UI 업데이트에 적용
```swift
import Foundation

protocol AirportMapViewPresentable {
    var airport: (name: String, city: String) { get }
    var currentLocation: (lat: Double, lon: Double) { get }
    var airportLocation: (lat: Double, lon: Double) { get }

}</code></pre><ul>
<li>맵뷰를 그릴 때 사용할 별도의 프로토콜<pre><code class="language-swift">private var viewModel: AirportDetailsViewPresentable?
  var viewModelBuilder: AirportDetailsViewPresentable.ViewModelBuilder!</code></pre>
</li>
<li>뷰 컨트롤러에서 <code>private</code>으로 숨긴 뷰 모델과 달리 외부 접근 가능한 뷰 모델 빌더<pre><code class="language-swift">override func start() {
      let vc = AirportDetailsViewController()
      let locationService = LocationService.shared
      vc.viewModelBuilder = {
          AirportDetailsViewModel(input: $0, dependencies: (model: self.model, currentLocation: locationService.currentLocation))
      }
      router.present(vc, isAnimated: true, onDismiss: isCompleted)
  }</code></pre>
</li>
<li>디테일 뷰 컨트롤러를 <code>coordinator</code>에 띄우는 단계인 <code>start()</code> 함수에서 외부 접근 가능한 뷰 모델 빌더의 클로저를 현 시점에서 선언</li>
<li>뷰 모델을 만들 때 필요한 인풋 및 디펜던시가 존재하는 공간이 현재 <code>coordinator</code>이기 때문<pre><code class="language-swift">  private func bind() {
      viewModel?.output
          .airportDetails
          .map({ [weak self] viewModel in
              self?.nameLabel.text = viewModel.name
              self?.distanceLabel.text = viewModel.formattedDistance
              self?.countryLabel.text = viewModel.address
              self?.lengthLabel.text = viewModel.runwayLength
          })
          .drive()
          .disposed(by: disposeBag)
      viewModel?.output
          .mapDetails
          .map({ [weak self] viewModel in
              let currentPoint = CLLocationCoordinate2D(latitude: viewModel.currentLocation.lat, longitude: viewModel.currentLocation.lon)
              let airportPoint = CLLocationCoordinate2D(latitude: viewModel.airportLocation.lat, longitude: viewModel.airportLocation.lon)
              let currentPin = AirportPin(name: &quot;Current&quot;, coordinate: currentPoint)
              let airportPin = AirportPin(name: viewModel.airport.name, city: viewModel.airport.city, coordinate: airportPoint)
              self?.mapView.addAnnotations([currentPin, airportPin])
              print(&quot;MapView add annotations&quot;)
              let currentPlacemark = MKPlacemark(coordinate: currentPoint)
              let destinationPlacemark = MKPlacemark(coordinate: airportPoint)
              let directionRequest = MKDirections.Request()
              directionRequest.source = MKMapItem(placemark: currentPlacemark)
              directionRequest.destination = MKMapItem(placemark: destinationPlacemark)
              directionRequest.transportType = .automobile
              let directions = MKDirections(request: directionRequest)
              directions.calculate { response, error in
                  guard
                      let route = response?.routes.first,
                      error == nil else { return }
                  self?.mapView.addOverlay(route.polyline
                                           , level: .aboveRoads)
                  UIView.animate(withDuration: 1) {
                      let mapRect = route.polyline.boundingMapRect
                      let region = MKCoordinateRegion(mapRect)
                      self?.mapView.setRegion(region, animated: true)
                  }
              }
          })
          .drive()
          .disposed(by: disposeBag)
  }
</code></pre>
</li>
</ul>
<pre><code>* `Driver`에 의해 관찰 가능한 데이터가 들어올 때 바인딩되는 곳
* 스택 뷰에 해당 맵뷰의 정보를 라벨로 들여보내고, 커스텀 맵 어노테이션을 통해 해당 위치를 맵뷰에 표시, 현재 위치와의 경로 등을 표현

## 구현 화면
![](https://velog.velcdn.com/images/j_aion/post/f3b411b3-696e-454d-bb71-c8b95883ad1b/image.gif)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] AirPortClone: Modal Presentation]]></title>
            <link>https://velog.io/@j_aion/RxSwift-AirPortClone-Modal-Presentation</link>
            <guid>https://velog.io/@j_aion/RxSwift-AirPortClone-Modal-Presentation</guid>
            <pubDate>Wed, 28 Dec 2022 16:58:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=rOme38Pob4M&amp;list=PLRKlnZOyrBQdmks75bAtiZE-DkLXzp99D&amp;index=13">#11 Present and Dismiss a ViewController in Coordinator - RxSwift MVVM Coordinator iOS App
</a></p>
</blockquote>
<h1 id="airportclone-modal-presentation">AirPortClone: Modal Presentation</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/8735079f-3beb-4ae6-817f-2a3e89e4c286/image.png" alt=""></p>
<ul>
<li><code>coordinator</code> 패턴에서의 모달 전환 구현</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li><code>Routing</code> 프로토콜 내 모달 프레젠트 및 디스미스 함수 구현</li>
<li>클로저를 통한 디스미스 시 메모리 누수 방지</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">protocol RouterProtocol {
    func push(_ drawable: Drawable, isAnimated: Bool, onNavigationBack: NavigationBackClosure?)
    func pop(_ isAnimated: Bool)
    func popToRoot(_ isAnimated: Bool)
    func present(_ drawable: Drawable, isAnimated: Bool, onDismiss: NavigationBackClosure?)
}</code></pre>
<ul>
<li><p>라우팅을 담당하는 프로토콜 내 <code>present</code> 함수 추가</p>
<pre><code class="language-swift">func present(_ drawable: Drawable, isAnimated: Bool, onDismiss closure: NavigationBackClosure?) {
      guard let viewController = drawable.viewController else { return }
      if let closure = closure {
          closures.updateValue(closure, forKey: viewController.description)
      }
      navigationController.present(viewController, animated: isAnimated)
      viewController.presentationController?.delegate = self
  }</code></pre>
</li>
<li><p>해당 프로토콜을 따르는 라우터 클래스에서 해당 함수는 즉 모달로 프레젠트된 뷰 컨트롤러와 함께 파라미터로 건네받은 디스미스 클로저를 딕셔너리에 기록해두는 것, 즉 이전의 네비게이션 이동과 동일한 로직</p>
<pre><code class="language-swift">extension Router: UIAdaptivePresentationControllerDelegate {
  func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
      executeClosures(presentationController.presentedViewController)
  }
}</code></pre>
</li>
<li><p>프레젠테이션 컨트롤러 델리게이트를 따름으로써 <code>presentationControllerDidDismiss</code> 함수를 사용 가능</p>
</li>
<li><p>모달 뷰가 사라졌을 때 <code>executeClosures()</code>를 사용함으로써 딕셔너리에 기록해놓은 클로저가 해당 라우터 클래스에서 실행됨</p>
<pre><code class="language-swift">private func showAirportDetails(model: AirportModel) {
      let detailCoordinator = AirportDetailsCoordinator(router: router)
      add(coordinator: detailCoordinator)

      detailCoordinator.isCompleted = { [weak self, weak detailCoordinator] in
          guard let detailCoordinator = detailCoordinator else { return }
          self?.remove(coordinator: detailCoordinator)
      }
      detailCoordinator.start()
  }</code></pre>
</li>
<li><p>모달 전환이 이루어지는 곳은 테이블 뷰의 각 셀을 구현하는 <code>AirportsCoordinator</code>에서 클릭 이벤트가 발생하는 곳</p>
</li>
<li><p><code>detailCoordinator</code>라는 새로운 전환이 일어날 때 <code>isCompleted</code> - 즉 <code>onDismiss</code> 클로저에 파라미터로 들어갈 클로저를 선언</p>
</li>
<li><p><code>coordinator</code>에 들어간 메모리를 다시 제거해주는 장소</p>
<pre><code class="language-swift">override func start() {
      let vc = AirportDetailsViewController()
      router.present(vc, isAnimated: true, onDismiss: isCompleted)
  }</code></pre>
</li>
<li><p><code>AirportsCoordinator</code>에서 작동하는 <code>start()</code> 함수</p>
</li>
<li><p>실제 라우터 클래스의 <code>present</code> 함수가 실행되는 코드</p>
</li>
</ul>
<pre><code class="language-swift">private func setViewModel() {
        viewModel = viewModelBuilder((
            selectAirport: tableView.rx.modelSelected(AirportViewPresentable.self).asDriver(onErrorDriveWith: .empty()), ()
        ))
    }</code></pre>
<ul>
<li><code>AirportsViewController</code>에서 해당 <code>setViewModel</code> 함수가 실행되는 구문</li>
<li><code>tableView.rx.modelSelected</code>를 통해 반응형으로 현재 유저가 선택한 셀의 모델(<code>AirportViewPresentable</code>)을 뷰 모델 빌더에게 넘겨줄 수 있음</li>
</ul>
<pre><code class="language-swift">typealias Input = (
        selectAirport: Driver&lt;AirportViewPresentable&gt;, ()
    )</code></pre>
<ul>
<li>뷰 모델이 따르는 프레젠터블 프로토콜의 인풋</li>
</ul>
<pre><code class="language-swift">private typealias RoutingAction = (airportSelectRelay: PublishRelay&lt;AirportModel&gt;, ())
    private let routingAction = (airportSelectRelay: PublishRelay&lt;AirportModel&gt;(), ())
    typealias Routing = (airportSelect: Driver&lt;AirportModel&gt;, ())
    lazy var router: Routing = (airportSelect: routingAction.airportSelectRelay.asDriver(onErrorDriveWith: .empty()), ())</code></pre>
<ul>
<li>뷰 모델의 라우팅 액션 / 라우팅은 각각 이동할 뷰 컨트롤러가 가질 뷰 모델을 구성하는 역할</li>
<li><code>private</code>으로 감싸 놓은 <code>Relay</code>를 통해 <code>Driver</code>를 만들어주는 역할<pre><code class="language-swift">func process(dependencies: Dependencies) {
      input
          .selectAirport
          .map { [models = dependencies.models] viewModel in
              models.filter({ $0.code == viewModel.code}).first
          }
          .compactMap({ $0 })
          .map({ [routingAction] in
              routingAction.airportSelectRelay.accept($0)
          })
          .drive()
          .disposed(by: disposeBag)
  }</code></pre>
</li>
<li>입력된 <code>Set&lt;AirportModel&gt;</code> 가운데 입력된 모델과 코드가 일치하는 모델만 필터링, 라우팅 액션 <code>Relay</code>에 값을 들여보내는 곳<h2 id="구현-화면">구현 화면</h2>
<img src="https://velog.velcdn.com/images/j_aion/post/e12eeb49-bb47-4f72-9344-cbe5ede16d60/image.gif" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] AirPortClone: Handling Memory Leak]]></title>
            <link>https://velog.io/@j_aion/RxSwift-AirPortClone-Handling-Memory-Leak</link>
            <guid>https://velog.io/@j_aion/RxSwift-AirPortClone-Handling-Memory-Leak</guid>
            <pubDate>Wed, 28 Dec 2022 16:11:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=1Q2Vq6zhucE&amp;list=PLRKlnZOyrBQdmks75bAtiZE-DkLXzp99D&amp;index=12">#10 Handling Memory Leak on Back of ViewController - RxSwift MVVM Coordinator iOS App
</a></p>
</blockquote>
<h1 id="airportclone-handling-memory-leak">AirPortClone: Handling Memory Leak</h1>
<h2 id="구현-목표">구현 목표</h2>
<ul>
<li><code>Rx</code> 및 <code>Coordinator</code> 패턴 사용 시 발생 가능한 메모리 누수 방지</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li><code>Coordinator</code> 패턴 사용 시 발생하는 <code>child</code> 관리 문제 해결</li>
<li>커스텀 네비게이션 푸시/팝 구현</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">protocol Drawable {
    var viewController: UIViewController? { get }
}

extension UIViewController: Drawable {
    var viewController: UIViewController? { return self }
}</code></pre>
<ul>
<li>모든 뷰 컨트롤러가 <code>Drawable</code>이라는 커스텀 프로토콜을 따르도록 선언</li>
<li>해당 <code>viewController</code> 변수는 자기 자신을 리턴하는 옵셔널 변수</li>
<li>드로우, 즉 뒤로 되돌릴 수 있는(백) 종류의 뷰 컨트롤러라는 뜻<pre><code class="language-swift">typealias NavigationBackClosure = (() -&gt; ())
</code></pre>
</li>
</ul>
<p>protocol RouterProtocol {
    func push(_ drawable: Drawable, isAnimated: Bool, onNavigationBack: NavigationBackClosure?)
    func pop(_ isAnimated: Bool)
}</p>
<pre><code>* 네비게이션과 관련된 커스텀 프로토콜
* `Drawable`, 즉 뷰 컨트롤러 또한 해당 파라미터로 들어올 수 있으며 클로저 또한 함께 받기
* `onNavigationBack`에서는 네비게이션 백 버튼이 눌릴 때의 행동 또한 함께 규정 가능
```swift
final class Router: NSObject {
    private let navigationController: UINavigationController
    private var closures: [String: NavigationBackClosure] = [:]

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
        super.init()
        self.navigationController.delegate = self
    }
}</code></pre><ul>
<li>네비게이션 푸쉬/팝을 드라이브하는 주체</li>
<li>이니셜라이즈할 때 네비게이션 컨트롤러를 받기</li>
<li>클래스 내부 <code>private</code>으로 클로저를 딕셔너리로 가지고 있음</li>
<li>뷰 컨트롤러의 <code>description</code>을 통해 해다 클로저를 찾을 수 있음<pre><code class="language-swift">extension Router: RouterProtocol {
  func push(_ drawable: Drawable, isAnimated: Bool, onNavigationBack closure: NavigationBackClosure?) {
      guard let viewController = drawable.viewController else { return }
      if let closure = closure {
          closures.updateValue(closure, forKey: viewController.description)
      }
      navigationController.pushViewController(viewController, animated: isAnimated)
  }

</code></pre>
</li>
</ul>
<pre><code>func pop(_ isAnimated: Bool) {
    navigationController.popViewController(animated: isAnimated)
}

func executeClosures(_ viewController: UIViewController) {
    guard let closure = closures.removeValue(forKey: viewController.description) else { return }
    closure()
}</code></pre><p>}</p>
<pre><code>* 라우터 프로토콜을 따르는 해당 라우터 클래스는 푸쉬 함수에서 파라미터로 건네받은 네비게이션 컨트롤러에 뷰 컨트롤러를 딕셔너리에 기록
* 클로저를 실행하는 코드 역시 딕셔너리를 통해 사용
```swift
extension Router: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        guard let previousController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
            return
        }
        guard !navigationController.viewControllers.contains(previousController) else {
            return
        }
        executeClosures(previousController)
    }
}</code></pre><ul>
<li><p>이니셜라이즈 당시 입력받은 네비게이션 컨트롤러의 델리게이트 함수 중 <code>didShow</code>를 통해, 네비게이션이 나타났을 때 이전 뷰 컨트롤러를 찾아낼 수 있음</p>
</li>
<li><p>두 번째 <code>gaurd</code> 문은 더 이상 현재 네비게이션 컨트롤러가 해당 뷰 컨트롤러를 가지고 있지 않음을 다시 한 번 확인하는 코드. 즉 메모리에 남아 있지 않아야 하는 뷰 컨트롤러야만 한다는 뜻</p>
</li>
<li><p>이제 사라져야 마땅한 뷰 컨트롤러라면, 해당 뷰 컨트롤러를 받아들일 때 백 버튼 관련 클로저를 실행해야 한다는 뜻</p>
<pre><code class="language-swift">class BaseCoordinator: Coordinator {
  var childCoordinator: [Coordinator] = []
  var isCompleted: (()-&gt;())?

  func start() {
      fatalError(&quot;Children should be implemented in start func&quot;)
  }
}</code></pre>
</li>
<li><p>모든 <code>Coordinator</code>가 기본으로 따르는 <code>BaseCoordinator</code> 클래스</p>
</li>
<li><p><code>isCompleted</code>는 <code>onNavigationBack</code> 파라미터에서 구현될 커스텀 클로저, 현재는 널 값</p>
<pre><code class="language-swift">override func start() {
      let router = Router(navigationController: self.navigationController)
      let searchCoordinator = SearchCoordinator(router: router)
      self.add(coordinator: searchCoordinator)
      searchCoordinator.isCompleted = { [weak self, weak searchCoordinator] in
          guard let coordinator = searchCoordinator else { return }
          self?.remove(coordinator: coordinator)
      }
      searchCoordinator.start()
      window.rootViewController = navigationController
      window.makeKeyAndVisible()
  }</code></pre>
</li>
<li><p>가장 하단 부에서 뷰 컨트롤러 이동 등을 담당하는 <code>AppCoordinator</code> 클래스의 <code>start()</code> 함수</p>
</li>
<li><p><code>add</code>를 통해 이제 네비게이션 이동에 추가할 자식 <code>coordinator</code>에 값을 추가한다면, 반대로 백 버튼 등 네비게이션 팝을 했을 때 자식 <code>coordinator</code> 값 또한 제거해야 함</p>
</li>
<li><p><code>isCompleted</code> 클로저에서 해당 뷰 컨트롤러가 팝될 때 현재까지 기록된 <code>child</code>에서 해당 <code>coordinator</code>를 제거해주기로 함</p>
</li>
</ul>
<blockquote>
<p>네비게이션 푸시/팝 등 최하단 부에서 자동으로 관리해주던 영역까지 <code>coordinator</code>를 통해 관리해주어야 한다. (왜냐하면 <code>coordinator</code> 패턴을 따를 때 현 시점에서는 모든 이동 과정 역시 커스텀으로 구현한 <code>coordinator</code> 클래스에서 관리해주고 있기 때문이다. 무엇이 우선일까...?</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] AirPortClone: CoreLocation]]></title>
            <link>https://velog.io/@j_aion/RxSwift-AirPortClone-CoreLocation</link>
            <guid>https://velog.io/@j_aion/RxSwift-AirPortClone-CoreLocation</guid>
            <pubDate>Wed, 28 Dec 2022 15:20:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=4S5j9DqcfUw&amp;list=PLRKlnZOyrBQdmks75bAtiZE-DkLXzp99D&amp;index=11">#9 Distance Calculation using CoreLocation - RxSwift MVVM Coordinator iOS App
</a></p>
</blockquote>
<h1 id="airportclone-corelocation">AirPortClone: CoreLocation</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/8f8e24be-676e-47a7-b933-aeb4607559d6/image.png" alt=""></p>
<ul>
<li><code>CoreLocation</code>을 통한 현재 위치 및 거리 측정 로직 구현</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li>현재 위치와 특정 위치 간의 계산 자동화</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">final class LocationService: NSObject {
    static let shared = LocationService()
    private let manager = CLLocationManager()
    private let currentLocationRelay: BehaviorRelay&lt;(lat: Double, lon: Double)?&gt; = .init(value: nil)
    lazy var currentLocation: Observable&lt;(lat: Double, lon: Double)?&gt; = self.currentLocationRelay.asObservable().share(replay: 1, scope: .forever)

    private override init() {
        super.init()
        setLocationService()
    }

    private func setLocationService() {
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        manager.requestAlwaysAuthorization()

        if CLLocationManager.locationServicesEnabled() {
            manager.startUpdatingLocation()
        }
    }

    deinit {
        manager.stopUpdatingLocation()
    }
}

extension LocationService: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        let currentLocation = (lat: location.coordinate.latitude, lon: location.coordinate.longitude)
        currentLocationRelay.accept(currentLocation)
        manager.stopUpdatingLocation()
    }
}</code></pre>
<ul>
<li><p><code>currentLocationRelay</code>는 <code>BehaviorRelay</code> 타입으로 지역을 표현하는 튜플을 담아두는 릴레이</p>
</li>
<li><p><code>Observable</code>을 통해 해당 지역 튜플 데이터 관찰 가능</p>
</li>
<li><p><code>didUpdateLocations</code>를 통해 건네받은 현재 위치를 <code>currentLocationRelay</code>로 넘기기</p>
</li>
<li><p>싱글턴 패턴 구현</p>
<pre><code class="language-swift">override func start() {
      let vc = AirportsViewController()
      let locationService = LocationService.shared
      vc.viewModelBuilder = { [models] in
          let title = models.first?.city ?? &quot;&quot;
          return AirportsViewModel(input: $0, dependencies: (title: title, models: models, currentLocation: locationService.currentLocation))
      }
      self.navigationController.pushViewController(vc, animated: true)
  }</code></pre>
</li>
<li><p><code>AirportsCoordinator</code>에서 <code>start()</code>를 통해 다른 뷰 컨트롤러를 시동하는 구조</p>
</li>
<li><p><code>dependencies</code>를 통해 뷰 모델을 빌드하고 있는데, 현 시점의 <code>currentLocation</code>을 읽어오는 곳이 바로 현 시점.</p>
<pre><code class="language-swift">private static func getDistance(airportLocation: (lat: Double?, lon: Double?),
                           currentLocation: (lat: Double, lon: Double)) -&gt; Double? {
      guard
          let lat = airportLocation.lat,
          let lon = airportLocation.lon else { return nil }
      let current = CLLocation(latitude: currentLocation.lat, longitude: currentLocation.lon)
      let airport = CLLocation(latitude: lat, longitude: lon)
      return current.distance(from: airport)
  }</code></pre>
</li>
<li><p>API를 통해 읽어온 데이터와 현재 위치 값의 거리를 자동으로 계산해서 리턴하는 <code>static</code> 함수</p>
<pre><code class="language-swift">self.distance = AirportViewModel.getDistance(airportLocation: (lat: Double(model.lat), lon: Double(model.lon)), currentLocation: currentLocation)</code></pre>
</li>
<li><p>해당 함수를 통해 뷰 모델의 <code>distance</code> 값이 자동으로 계산</p>
<pre><code class="language-swift">extension AirportViewModel: Comparable {
  static func &lt; (lhs: AirportViewModel, rhs: AirportViewModel) -&gt; Bool {
      return lhs.distance ?? 0 &lt; rhs.distance ?? 0
  }

  static func == (lhs: AirportViewModel, rhs: AirportViewModel) -&gt; Bool {
      return lhs.code == rhs.code
  }
}</code></pre>
</li>
<li><p>현재 <code>Comparable</code> 프로토콜을 준수하는 뷰 모델은 <code>&lt;</code>, <code>==</code> 함수를 적용했기 때문에 이후 여러 개의 거리를 가진 뷰 모델이 테이블 뷰에 들어오기 전 <code>sorted</code>로 정렬 가능한 형태</p>
<h2 id="구현-화면">구현 화면</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/3b642333-a988-4712-8cee-75e9170d5e5d/image.gif" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] AirPortClone: UITableView & RxDataSources]]></title>
            <link>https://velog.io/@j_aion/RxSwift-AirPortClone-UITableView-RxDataSources-j4xzpijm</link>
            <guid>https://velog.io/@j_aion/RxSwift-AirPortClone-UITableView-RxDataSources-j4xzpijm</guid>
            <pubDate>Wed, 28 Dec 2022 07:48:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=RvOwlbWqjjs&amp;list=PLRKlnZOyrBQdmks75bAtiZE-DkLXzp99D&amp;index=10">#8 Airports UITableView Binding using RxDatasources - RxSwift MVVM Coordinator iOS App
</a></p>
</blockquote>
<h1 id="airportclone-uitableview--rxdatasources">AirPortClone: UITableView &amp; RxDataSources</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/932bfdb2-9dd0-4862-b4ba-6f589d6312aa/image.png" alt=""></p>
<ul>
<li><code>RxDataSources</code>를 통한 테이블 뷰 구현</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li>커스텀 테이블 뷰 구현</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">typealias AirportItemsSection = SectionModel&lt;Int, AirportViewPresentable&gt;

protocol AirportsViewPresentable {
    typealias Input = (
    )
    typealias Output = (
        title: Driver&lt;String&gt;,
        airports: Driver&lt;[AirportItemsSection]&gt;
    )
    typealias Dependencies = (
        title: String,
        models: Set&lt;AirportModel&gt;
    )
    typealias ViewModelBuilder = (AirportsViewPresentable.Input) -&gt; AirportsViewPresentable

    var input: AirportsViewPresentable.Input { get }
    var output: AirportsViewPresentable.Output { get }
}</code></pre>
<ul>
<li>뷰 모델 빌더는 인풋을 받아 현재 프로토콜 자체를 리턴</li>
<li>뷰 모델이 해당 <code>AirportsViewPresentable</code>을 따르기 때문에 사용 가능<pre><code class="language-swift">override func start() {
      let vc = AirportsViewController()
      vc.viewModelBuilder = { [models] in
          let title = models.first?.city ?? &quot;&quot;
          return AirportsViewModel(input: $0, dependencies: (title: title, models: models))
      }
      self.navigationController.pushViewController(vc, animated: true)
  }</code></pre>
</li>
<li>뷰 모델 빌더의 클로저를 현재 <code>AirportsCoordinator</code>에서 넣어줌</li>
<li>뷰 컨트롤러를 바인딩할 때 사용했던 데이터 모델 집합을 통해 뷰 모델을 구현<pre><code class="language-swift">private func configureDataSource() {
      dataSource = .init(configureCell: { _, tableView, indexPath, item in
          guard let cell = tableView.dequeueReusableCell(withIdentifier: AirportTableViewCell.identifier, for: indexPath) as? AirportTableViewCell else { fatalError() }
          cell.configure(with: item)
          return cell
      })
  }</code></pre>
</li>
<li><code>RxDataSources</code>가 제공하는 섹션 모델을 커스텀, 해당 섹션 모델을 통해 데이터 소스를 구성</li>
<li>데이터 소스를 표현하는 클로저 내부에서 실제 셀 UI 구성<pre><code class="language-swift">private func bind() {
      guard let dataSource = dataSource else { return }
      viewModel?
          .output
          .airports
          .drive(tableView.rx.items(dataSource: dataSource))
          .disposed(by: disposeBag)
      viewModel?
          .output
          .title
          .drive(rx.title)
          .disposed(by: disposeBag)
  }</code></pre>
</li>
<li><code>dataSource</code>를 통해 테이블 뷰 UI 표현</li>
<li><code>rx.title</code>은 뷰 컨트롤러 자체의 타이틀을 반응형으로 보여주는 듯<h2 id="구현-화면">구현 화면</h2>
<img src="https://velog.velcdn.com/images/j_aion/post/1a076c48-5232-4020-8bc4-5957a9d54088/image.gif" alt=""><blockquote>
<p><code>Coordinator</code> 내에서 뷰 컨트롤러와 뷰 모델을 빌드, 연결하는 과정에 점점 더 익숙해지는 것 같긴 한데...</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] AirPortClone: UITableViewCell & AirportViewModel]]></title>
            <link>https://velog.io/@j_aion/RxSwift-AirPortClone-UITableViewCell-AirportViewModel</link>
            <guid>https://velog.io/@j_aion/RxSwift-AirPortClone-UITableViewCell-AirportViewModel</guid>
            <pubDate>Wed, 28 Dec 2022 06:46:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=IYAfY_rUTew&amp;list=PLRKlnZOyrBQdmks75bAtiZE-DkLXzp99D&amp;index=9">#7 UITableViewCell Binding using AirportViewModel - RxSwift MVVM Coordinator iOS App
</a></p>
</blockquote>
<h1 id="airportclone-uitableviewcell--airportviewmodel">AirPortClone: UITableViewCell &amp; AirportViewModel</h1>
<h2 id="구현-목표">구현 목표</h2>
<ul>
<li>커스텀 테이블 뷰 셀 구현</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li>테이블 뷰 셀 구현</li>
<li>UI 바인딩 함수 구현</li>
<li>뷰 모델 구현</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">protocol AirportViewPresentable {
    var name: String { get }
    var code: String { get }
    var address: String { get }
    var distance: Double? { get }
    var formattedDistance: String { get }
    var runwayLength: String { get }
    var location: (lat: String, lon: String) { get }
}</code></pre>
<ul>
<li>뷰 모델이 따를 프로토콜<pre><code class="language-swift">extension AirportViewModel {
  init(model: AirportModel) {
      self.name = model.name
      self.code = model.code
      self.address = &quot;\(model.state ?? &quot;&quot;), \(model.country)&quot;
      self.runwayLength = &quot;Runway Length: \(model.runwayLength ?? &quot;NA&quot;)&quot;
      self.location = (lat: model.lat, lon: model.lon)
      self.distance = 0
  }
}</code></pre>
</li>
<li>데이터 모델을 파라미터로 받아 해당 프로토콜의 값을 이니셜라이즈하는 뷰 모델<pre><code class="language-swift">func configure(with model: AirportViewModel) {
      nameLabel.text = model.name
      distanceLabel.text = model.formattedDistance
      countryLabel.text = model.address
      lengthLabel.text = model.runwayLength
  }</code></pre>
</li>
<li>해당 뷰 모델을 통해 UI를 그리는 <code>configure</code> 함수<blockquote>
<p>강의의 주요 태스크는 <code>_view_presentable</code>이라는 프로토콜을 만든 뒤, 해당 프로토콜을 따르는 구조체(!)를 뷰 모델로 설정, 해당 뷰 모델을 통해 뷰 컨트롤러의 데이터를 담당하는 것인데, 과연 기존의 클래스를 직접 뷰 모델로 넣는 것과 무엇이 다른 것인지, 그저 프로토콜 지향 프로그래밍의 일종인 것인지 궁금하다. </p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Concurrency: Actor]]></title>
            <link>https://velog.io/@j_aion/UIKit-Concurrency-Actor</link>
            <guid>https://velog.io/@j_aion/UIKit-Concurrency-Actor</guid>
            <pubDate>Tue, 27 Dec 2022 10:12:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=6MK9mJ319bk">Concurrency using Actors | Swift 5.5 | Async/Await | Data Race
</a></p>
</blockquote>
<h1 id="concurrency-actor">Concurrency: Actor</h1>
<h2 id="race-problem">Race Problem</h2>
<ul>
<li>두 개 이상의 비동기 태스크가 여러 스레드에서 실행될 경우 <code>data inconsistency</code> 발생할 수 있음.</li>
<li>레이스 문제를 해결하기 위한 방법 중 스레드 세이프를 보장하는 방법이 존재</li>
</ul>
<pre><code class="language-swift">class Flight {
    let company = &quot;Luft Hansa&quot;
    var availableSeats = [&quot;1A&quot;, &quot;1B&quot;, &quot;1C&quot;]
    func getAvailableSeats() -&gt; [String] {
        availableSeats
    }
    func bookSeat() -&gt; String {
        return availableSeats.removeFirst()
    }
}</code></pre>
<ul>
<li><p>한정된 데이터가 존재하고 특정 함수를 통해 해당 데이터를 사용, 변경되는 경우</p>
<pre><code class="language-swift">private func testFlightProblem1() {
      let flight = Flight()
      let queue1 = DispatchQueue(label: &quot;queue1&quot;)
      let queue2 = DispatchQueue(label: &quot;queue2&quot;)

      queue1.async {
          let bookedSeat = flight.bookSeat()
          print(&quot;Booked seat is \(bookedSeat)&quot;)
      }
      queue2.async {
          let availableSeats = flight.getAvailableSeats()
          print(&quot;Available Seats \(availableSeats)&quot;)
      }
  }</code></pre>
</li>
<li><p>두 개 이상의 서로 다른 스레드에서 해당 함수를 사용해 동일한 데이터에 접근할 경우 레이스 문제 발생 가능</p>
<pre><code class="language-swift">Booked seat is 1A
Available Seats [&quot;1A&quot;, &quot;1B&quot;, &quot;1C&quot;]</code></pre>
</li>
<li><p>첫 번째 큐에서 작성한 태스크와 두 번째 큐에서 작성한 태스크가 동일한 데이터를 건드리고 있지만 비동기적으로 작동하기 때문에 태스크가 끝나고 나서 시작한 지 보장할 수 없음</p>
</li>
</ul>
<h2 id="dispatchqueue-barrier-flag">DispatchQueue: Barrier Flag</h2>
<ul>
<li>배리어 플래그를 통해 이후 태스크를 실행할 스레드의 실행을 &#39;블럭&#39;시킬 수 있음<pre><code class="language-swift">class Flight {
  let company = &quot;Luft Hansa&quot;
  var availableSeats = [&quot;1A&quot;, &quot;1B&quot;, &quot;1C&quot;]
  let barrierQueue: DispatchQueue = DispatchQueue(label: &quot;barrierQueue&quot;, attributes: .concurrent)
  func getAvailableSeats() -&gt; [String] {
      barrierQueue.sync(flags: .barrier) {
          return availableSeats
      }
  }
  func bookSeat() -&gt; String {
      barrierQueue.sync(flags: .barrier) {
          return availableSeats.removeFirst()
      }
  }
}</code></pre>
</li>
<li>컨커런트한 배리어 큐를 별도로 실행한 뒤, 레이스 문제가 발생할 수 있는 구문을 해당 큐의 배리어 플래그가 추가된 상태에서 실행<pre><code class="language-swift">Booked seat is 1A
Available Seats [&quot;1B&quot;, &quot;1C&quot;]</code></pre>
</li>
</ul>
<h2 id="actor">Actor</h2>
<ul>
<li>참조 타입, 프로토콜, 제네릭 등을 사용 가능하다는 점에서 클래스와 매우 유사. 상속 또한 사용 가능. 하지만 <code>data inconsistency</code>와 같은 동시성 문제를 신경쓸 필요가 없음</li>
<li>액터 내부 데이터: <code>let</code>, <code>var</code>과 같은 데이터의 가변성에도 영향을 받음 - 상수라면 문제 없이 참조 가능하지만, 값이 바뀔 수 있는 변수라면 별도의 블럭을 추가해야 함</li>
<li>저장 프로퍼티가 아니라 연산 프로퍼티라면 동일한 문제 발생 (<code>var</code>임)</li>
<li><code>nonisolated</code>라는 키워드를 통해 컴파일러에게 해당 데이터는 <code>var</code>임에도 불구하고 <code>let</code>과 같이 레이스 문제가 일어나지 않는다고 강제할 수 있음</li>
<li><code>async await</code>와 함께 액터 값을 조회/참조/변경 가능</li>
<li><code>Task</code> 블럭 내부 비동기 구문 작성</li>
</ul>
<pre><code class="language-swift">actor Flight {
    let company = &quot;Luft Hansa&quot;
    var availableSeats = [&quot;1A&quot;, &quot;1B&quot;, &quot;1C&quot;]
    func getAvailableSeats() -&gt; [String] {
        return availableSeats
    }
    func bookSeat() -&gt; String {
        return availableSeats.removeFirst()
    }
}
</code></pre>
<ul>
<li><p>액터로 선언한 데이터</p>
<pre><code class="language-swift">private func testFlightProblem2() {
      let flight = Flight()
      let queue1 = DispatchQueue(label: &quot;queue1&quot;)
      let queue2 = DispatchQueue(label: &quot;queue2&quot;)

      queue1.async {            
          Task {
              let bookedSeat = await flight.bookSeat()
              print(&quot;Booked seat is \(bookedSeat)&quot;)
          }
      }
      queue2.async {
          Task {
              let availableSeats = await flight.getAvailableSeats()
              print(&quot;Available Seats \(availableSeats)&quot;)
          }
      }
  }</code></pre>
</li>
<li><p><code>Task</code>, <code>async await</code>를 통해 비동기 구문 블럭 사용</p>
<pre><code class="language-swift">Available Seats [&quot;1A&quot;, &quot;1B&quot;, &quot;1C&quot;]
Booked seat is 1A</code></pre>
</li>
<li><p>위 상황에서는 <code>queue2</code>의 블럭이 먼저 실행되었음</p>
</li>
</ul>
<h2 id="main-actor">Main Actor</h2>
<pre><code class="language-swift">private func testFlightProblem3() {
        let flight = Flight()
        let queue1 = DispatchQueue(label: &quot;queue1&quot;)
        let queue2 = DispatchQueue(label: &quot;queue2&quot;)

        queue1.async {            
            Task { [weak self] in
                let bookedSeat = await flight.bookSeat()
                print(&quot;Booked seat is \(bookedSeat)&quot;)
                self?.updateLabel(seat: bookedSeat)
            }
        }
        queue2.async {
            Task {
                let availableSeats = await flight.getAvailableSeats()
                print(&quot;Available Seats \(availableSeats)&quot;)
            }
        }
    }</code></pre>
<ul>
<li><code>Task</code>의 비동기 구문은 현재 메인 스레드에서 실행되고 있지 않지만, 조회한 데이터를 UI로 갱신해야 하는 상황<pre><code class="language-swift">@MainActor
  private func updateLabel(seat: String) {
      queueOneLable.text = seat
  }</code></pre>
</li>
<li>백그라운드 스레드에서 실행된 함수라 할지라도 <code>@MainActor</code>을 따르는 함수이기 때문에 해당 태스크는 디스패치 메인 큐에서 실행, 즉 UIKit과 연결된 메인 스레드에서 실행되는 게 확실하게 보장</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Concurrency: QnA]]></title>
            <link>https://velog.io/@j_aion/UIKit-Concurrency-QnA</link>
            <guid>https://velog.io/@j_aion/UIKit-Concurrency-QnA</guid>
            <pubDate>Tue, 27 Dec 2022 09:31:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=lEMspH7Aq9U&amp;list=PLSbpzz0GJp5RTrjum9gWTqPhM4L3Kop0S&amp;index=6">Interview Questions on Concurrency, GCD, Operation Queue | Swift (Mastering Concurrency in iOS - 6)
</a></p>
</blockquote>
<h1 id="concurrency-qna">Concurrency: QnA</h1>
<h2 id="sync-vs-async">Sync vs Async</h2>
<ul>
<li>동기적 → 현재 스레드를 블럭: 특정 코드를 실행한다면 현재 스레드 실행 중 코드가 실행 완료될 때까지 블럭</li>
<li>비동기적 → 현재 스레드를 블럭하지 않음: 특정 코드를 실행한다면 별도로 실행 OK</li>
</ul>
<h2 id="serial-queue-vs-concurrent-queue">Serial Queue vs Concurrent Queue</h2>
<ul>
<li>시리얼: 한 번에 한 태스크</li>
<li>컨커런트: 한 번에 여러 개의 태스크 → 컨커런트 큐라 할지라도 FIFO라는 자료구조의 특성은 모두 보장</li>
</ul>
<h2 id="serial-vs-sync--concurrent-vs-asnyc">Serial vs Sync &amp; Concurrent vs Asnyc</h2>
<ul>
<li>시리얼/컨커런트 큐 → 현재 디스패치되고 있는 목적 큐에 영향을 미침. 어떻게 실행이 순차적으로 또는 동시적으로 진행될 것인지만</li>
<li>동기/비동기 → 해당 스레드로부터 디스패치되고 있는 현재 스레드에 영향을 미침. </li>
</ul>
<pre><code class="language-swift">    private func testProblem1() {
        let queue = DispatchQueue(label: &quot;printNumbers&quot;)
        var numbers: String = &quot;&quot;
        for i in 50...55 {
            numbers += &quot;\(i) &quot;
        }
        print(numbers)
        queue.async {
            var numbers: String = &quot;&quot;
            for i in 10...15 {
                numbers += &quot;\(i) &quot;
            }
            print(numbers)
        }
        queue.async {
            var numbers: String = &quot;&quot;
            for i in 0...5 {
                numbers += &quot;\(i) &quot;
            }
            print(numbers)
        }
        for i in 30...35 {
            numbers += &quot;\(i) &quot;
        }
        print(numbers)
    }</code></pre>
<ul>
<li>큐는 컨커런트하지 않으므로 디폴트로 시리얼 큐로 구현</li>
<li>두 개의 비동기 실행 블럭이 존재<pre><code class="language-swift">50 51 52 53 54 55 
30 31 32 33 34 35 
10 11 12 13 14 15 
0 1 2 3 4 5 </code></pre>
</li>
<li>비동기라 할지라도 시리얼 큐이기 때문에 10...15 프린트 블럭이 0...5보다 언제나 더 먼저 실행될 것은 확실하게 보장</li>
<li>30...35 프린트 블럭은 비동기 코드 블럭과 별개로 실행되기 때문에 언제 실행될 것인지 전후 상황을 예측 불가능</li>
</ul>
<pre><code class="language-swift">    private func testProblem2() {
        let queue = DispatchQueue(label: &quot;printNumbers&quot;, attributes: .concurrent)
        var numbers: String = &quot;&quot;
        for i in 50...55 {
            numbers += &quot;\(i) &quot;
        }
        print(numbers)
        queue.async {
            var numbers: String = &quot;&quot;
            for i in 10...15 {
                numbers += &quot;\(i) &quot;
            }
            print(numbers)
        }
        queue.async {
            var numbers: String = &quot;&quot;
            for i in 0...5 {
                numbers += &quot;\(i) &quot;
            }
            print(numbers)
        }
        numbers = &quot;&quot;
        for i in 30...35 {
            numbers += &quot;\(i) &quot;
        }
        print(numbers)
    }</code></pre>
<ul>
<li>컨커런트한 큐로 구현</li>
<li>시리얼 큐가 아니기 때문에 해당 큐 내부에서 실행된 두 개의 비동기 구문 순서는 보장 불가능<pre><code class="language-swift">50 51 52 53 54 55 
30 31 32 33 34 35 
0 1 2 3 4 5 
10 11 12 13 14 15</code></pre>
</li>
<li>일반 (메인 스레드)에서 실행된 50...55, 30...35 블럭은 순서대로 실행 예측 가능</li>
<li>비동기 구문의 두 가지 블럭은 30...35 구문 이전, 이후에 나올 지 예측 불가능</li>
<li>비동기 큐에 들어간 두 가지 태스크(10...15, 0...5)의 순서 또한 컨커런트한 큐인 까닭에 예측 불가능</li>
</ul>
<h2 id="qos---where-to-use">QoS - Where to use</h2>
<ul>
<li>단일한 파라미터에 의해 조정 가능한 퀄리티</li>
<li>유저 인터렉티브, 유저 이닛, 유틸리티, 백그라운드 등으로 크게 구별 가능</li>
<li>각각 애니메이션(UI 업데이트에 포함?), 즉각적 결과(문제없는 UX 플로우를 제공하는 데 필요한 데이터?), 오랫 동안 작동하는 태스크(유저가 해당 진행 정도를 알고 있는지?), 유저에게 보이지 않는 태스크(유저가 태스크를 알고 있는지?) 등 특징 존재</li>
</ul>
<h2 id="multiple-api-calls">Multiple API calls</h2>
<ul>
<li>여러 개의 API 호출을 시도한 뒤 해당 태스트들이 모두 종료되었을 때에만 다음 프로세스로 진행 가능한 방법</li>
<li>디스패치 그룹을 사용: enter / leave 함수 사용</li>
<li>notify 함수를 통해 캐치</li>
<li>컴바인 프레임워크의 zip을 통해서도 동일한 동작을 작동할 수 있지만, 해당 방법은 업스트림의 성공 값만을 다운스트림으로 전달하는 까닭에 API 호출 이후 태스크 실패 상황을 핸들링하는 데 어려움</li>
</ul>
<h2 id="race-condition">Race condition</h2>
<ul>
<li>디스패치 배리어 플래그</li>
<li>디스패치 세마포어<pre><code class="language-swift">private func addItems(item: ItemModel) {
      semaphore.wait()
      if walletBalance &gt;= item.price {
          PurchaseManager.shared.buyItem(item: item, balance: walletBalance) { [weak self] success in
              guard let self = self else { return }
              if success {
                  DispatchQueue.main.async {
                      self.walletBalance -= item.price
                      self.cartBalance += item.price
                      self.semaphore.signal()
                  }
              }
          }
      }
  }</code></pre>
</li>
<li>세마포어의 카운터 값을 커스텀 가능함으로써 현재 크리티컬 섹션에 들어갈 수 있는 스레드 개수를 조정할 수 있음. 디스패치 배리어는 세마포어보다 세밀한 조정을 하는 데 어려움</li>
</ul>
<h2 id="gcd-vs-operation-queue">GCD vs Operation Queue</h2>
<ul>
<li>GCD와 오퍼레이션 큐는 사실상 비교 가능한 대상이 아님</li>
<li>GCD는 로우 레벨의 API인 반면 오퍼레이션 큐는 탑 레벨의 추상 클래스</li>
<li>GCD는 상황이 복잡하지 않고 실행의 상태를 고려하지 않아도 될 때 주로 사용. 태스크에 대한 컨트롤이 그렇게 필요하지 않을 때.</li>
<li>오퍼레이션 큐는 오퍼레이션 간의 디펜던시를 조절하거나 클래스 상속을 통한 재사용 등 추후 컨트롤을 요할 때 사용.</li>
</ul>
<h2 id="cancel-the-task-in-gcd">Cancel the Task in GCD</h2>
<ul>
<li>디스패치 워크 아이템 → GCD를 사용하면서 해당 아이템을 취소 가능</li>
<li>하지만 워크 아이템 취소는 오퍼레이션을 취소하는 데 제공된 변수, 함수 등과는 별도로 다소 제한적</li>
</ul>
<h2 id="async-operation">Async Operation</h2>
<ul>
<li>오퍼레이션을 비동기적으로 실행하는 방법</li>
<li>오퍼레이션을 상속하는 새로운 클래스를 작성하기 → <code>start()</code>, <code>cancel()</code>, <code>isAsynchronous</code> 등 프로퍼티를 새롭게 작성한다면 해당 클래스는 비동기적으로 작동하는 오퍼레이션임</li>
</ul>
<pre><code class="language-swift">class AsyncOperation: Operation {
    enum State: String {
        case isReady
        case isExecuting
        case isFinished
    }

    var state: State = .isReady {
        willSet(newValue) {
            willChangeValue(forKey: state.rawValue)
            willChangeValue(forKey: newValue.rawValue)
        }
        didSet {
            didChangeValue(forKey: oldValue.rawValue)
            didChangeValue(forKey: state.rawValue)
        }
    }
    override var isAsynchronous: Bool { true }
    override var isExecuting: Bool { state == .isExecuting }
    override var isFinished: Bool {
        if isCancelled &amp;&amp; state != .isExecuting { return true }
        return state == .isFinished
    }

    override func start() {
        guard !isCancelled else {
            state = .isFinished
            return
        }
        state = .isExecuting
        main()
    }

    override func cancel() {
        state = .isFinished
    }
}
</code></pre>
<h2 id="dependency-between-tasks">Dependency between tasks</h2>
<ul>
<li>오퍼레이션 간의 디펜던시를 추가하는 방법</li>
<li>오퍼레이션 큐에 넣는 오퍼레이션 간의 실행 완료 → 실행 시작과 같은 순서를 확실하게 보장하는 방법</li>
</ul>
<h2 id="thread-safe-class">Thread safe class</h2>
<ul>
<li>디스패치 배리어, 세마포어 사용</li>
<li>크리티컬 섹션을 정의한 뒤 들어올 수 있는 스레드 컨트롤 가능</li>
<li>액터 사용</li>
<li>구조체는 값 타입이기 때문에 스레드 세이프한 반면, 참조 타입인 클래스는 스레드 세이프하지 않을 수 있다는 것 역시 체크</li>
</ul>
<h2 id="ui-update-in-background-thread">UI Update in background thread</h2>
<ul>
<li>백그라운드 스레드에서의 UI 업데이트의 가능 여부</li>
<li>UIKit는 메인 스레드와 연결</li>
<li>뷰 드로우 사이클과 연결된 메인 런 루프와 UI 리프레시 이벤트</li>
<li>그래픽 렌더링 또한 메인 스레드에서 해야 하는 이유 → 백그라운드 스레드에서 비동기적으로 실행될 경우 flickering이 발생할 수도 있기 때문</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Concurrency: Operations & Operation Queue]]></title>
            <link>https://velog.io/@j_aion/UIKit-Concurrency-Operations-Operation-Queue</link>
            <guid>https://velog.io/@j_aion/UIKit-Concurrency-Operations-Operation-Queue</guid>
            <pubDate>Mon, 26 Dec 2022 16:52:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=9PiEL1Sdv-k&amp;list=PLSbpzz0GJp5RTrjum9gWTqPhM4L3Kop0S&amp;index=5">Mastering Concurrency in iOS - Part 5 (Operations and Operation Queue)
</a></p>
</blockquote>
<h1 id="concurrency-operations--operation-queue">Concurrency: Operations &amp; Operation Queue</h1>
<h2 id="operation">Operation</h2>
<ul>
<li>GCD에 비해 실행 상태, 기능을 조정해야 할 때 사용</li>
<li>서로 다른 태스크 간의 의존성, 재사용되는 함수 블럭의 캡슐화 등을 고려해야 할 때</li>
<li>GCD의 탑 레벨에서 오퍼레이션은 일종의 최고로 추상화된 레이어</li>
<li>단일한 태스크와 연관된 코드 및 데이터를 표현하는 추상화된 단계</li>
<li>block, invocation 오퍼레이션이 존재하지만 스위프트에서 지원하는 오퍼레이션은 전자. 후자는 오브젝트 C에서만 지원</li>
<li>isReady</li>
<li>isExecuting</li>
<li>isCancelled</li>
<li>isFinished</li>
<li>하나의 인스턴스는 단 한 번만 실행 가능</li>
</ul>
<pre><code class="language-swift">private func testOperations1() {
        let operation: BlockOperation = .init {
            print(&quot;First Test&quot;)
            // as sync
            sleep(3)
        }
        operation.start()
    }</code></pre>
<ul>
<li>중간에 슬립을 주었기 때문에 블럭 오퍼레이션이 실행되고 3초 이후에 빠져나옴<pre><code class="language-swift">  private func testOperations2() {
      let operation: BlockOperation = .init()
      operation.addExecutionBlock {
          print(&quot;First block executed&quot;)
      }
      operation.addExecutionBlock {
          print(&quot;Second block executed&quot;)
      }
      operation.addExecutionBlock {
          print(&quot;Third block executed&quot;)
      }
      operation.start()
      /*
       About to begin operation
       First block executed
       Third block executed
       Second block executed
       Operation executed
       */
      // as async
  }
</code></pre>
</li>
</ul>
<pre><code>* 하나의 블럭 오퍼레이션에 익스큐션 블럭을 추가해서 시작
* 비동기적으로 실행되는 블럭
```swift
    private func testOperations3() {
        let operation: BlockOperation = .init()
        operation.completionBlock = {
            print(&quot;Execution completed&quot;)
        }

        operation.addExecutionBlock {
            print(&quot;First block executed&quot;)
        }
        operation.addExecutionBlock {
            print(&quot;Second block executed&quot;)
        }
        operation.addExecutionBlock {
            print(&quot;Third block executed&quot;)
        }
        DispatchQueue.global().async {
            operation.start()
            print(&quot;Did this run main thread: \(Thread.isMainThread)&quot;)
        }
        /*
         About to begin operation
         Operation executed
         First block executed
         Third block executed
         Second block executed
         Execution completed
         Did this run main thread: false
         */
        // even if in global async, should be serialized following the concept of operation execution blocks.
    }
</code></pre><ul>
<li>글로벌 큐에서 오퍼레이션이 비동기적으로 실행될 때, 블럭이 추가된 순서대로의 실행 순서를 보장하고 싶을 때에는 다른 방법을 모색해야 함</li>
</ul>
<pre><code class="language-swift">    private func testOperationQueues1() {
        let operationQueue: OperationQueue = .init()
        // operationQueue.maxConcurrentOperationCount = 1
        // with this property, serialization can be done
        let operation1: BlockOperation = .init()
        operation1.addExecutionBlock {
            print(&quot;Operation 1 being executed&quot;)
            for i in 1...10 {
                print(i)
            }
        }
        operation1.completionBlock = {
            print(&quot;Operation 1 executed&quot;)
            // completion block timing issue
        }

        let operation2: BlockOperation = .init()
        operation2.addExecutionBlock {
            print(&quot;Operation 2 being executed&quot;)
            for i in 11...20 {
                print(i)
            }
        }
        operation2.completionBlock = {
            print(&quot;Operation 2 executed&quot;)
        }

        operation2.addDependency(operation1)
        // operation2 -&gt; operation1. i.e. operation2 should wait for operation1 to be completed

        operationQueue.addOperation(operation1)
        operationQueue.addOperation(operation2)
        // by default -&gt; operation queue&#39;s task should be done concurrently
    }
</code></pre>
<ul>
<li><p>하나의 오퍼레이션 큐를 만든 뒤 해당 큐에 여러 개의 오퍼레이션을 넣는 것</p>
</li>
<li><p>디폴트 값으로는 큐에 들어간 오퍼레이션 블럭 실행은 또한 컨커런트하게 실행</p>
</li>
<li><p>오퍼레이션 1의 태스크를 2보다 &#39;먼저&#39; 실행해야 할 때에는 다음과 같은 방법 → 오퍼레이션 1을 먼저 작성했다면, <code>maxConcurrentOperationCount</code> 값을 설정함으로써 컨커런트하게 실행 가능한 오퍼레이션 개수를 제한해버리는 것. → 일반적으로는 오퍼레이션 간의 디펜던시를 통해 실행 순서를 보장</p>
<pre><code class="language-swift">private func printOneToTen() {
      DispatchQueue.global().async {
          for i in 1...10 {
              print(i)
          }
      }
  }

  private func printElevenToTwenty() {
      DispatchQueue.global().async {
          for i in 11...20 {
              print(i)
          }
      }
  }</code></pre>
</li>
<li><p>다음과 같은 프린트 함수를 각각 오퍼레이션 1, 2에서 실행한다고 가정해보자.</p>
<pre><code class="language-swift">private func testOperationQueues2() {
      let operationQueue: OperationQueue = .init()
      let operation1: BlockOperation = .init(block: printOneToTen)
      let operation2: BlockOperation = .init(block: printElevenToTwenty)
      operation2.addDependency(operation1)
      operationQueue.addOperation(operation1)
      operationQueue.addOperation(operation2)
  }</code></pre>
</li>
<li><p>디펜던시를 추가한다 할지라도 순차적 태스크 실행이 보장되지 않을 수 있음</p>
<blockquote>
<p>개인적으로 실행했을 때에는 언제나 디펜던시에 따라 태스크가 실행이 되었는데, 강의 영상에서는 프린트되는 숫자를 보면 컨커런트했다...</p>
</blockquote>
<pre><code class="language-swift">class AsyncOperation: Operation {
  enum State: String {
      case isReady
      case isExecuting
      case isFinished
  }

  var state: State = .isReady {
      willSet(newValue) {
          willChangeValue(forKey: state.rawValue)
          willChangeValue(forKey: newValue.rawValue)
      }
      didSet {
          didChangeValue(forKey: oldValue.rawValue)
          didChangeValue(forKey: state.rawValue)
      }
  }
  override var isAsynchronous: Bool { true }
  override var isExecuting: Bool { state == .isExecuting }
  override var isFinished: Bool {
      if isCancelled &amp;&amp; state != .isExecuting { return true }
      return state == .isFinished
  }

  override func start() {
      guard !isCancelled else {
          state = .isFinished
          return
      }
      state = .isExecuting
      main()
  }

  override func cancel() {
      state = .isFinished
  }
}
</code></pre>
</li>
</ul>
<pre><code>* 커스텀 오퍼레이션 클래스를 구현해 비동기 태스크를 수행
* KVO 패턴을 따라 `willSet`, `didSet`을 통해 해당 값의 변화를 관찰
* 언제나 비동기적인 클래스
* `start()` 함수를 오버라이드해서 현재 실행 중인 상태로 변경한 뒤 메인 함수를 실행
```swift
class PrintNumbersOperation: AsyncOperation {
    var range: Range&lt;Int&gt;
    init(range: Range&lt;Int&gt;) {
        self.range = range
    }

    override func main() {
        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }
            for i in self.range {
                print(i)
            }
            self.state = .isFinished
        }
    }
}
</code></pre><ul>
<li>위의 비동기 커스텀 오퍼레이션 클래스를 상속받는 프린트 오퍼레이션의 메인 함수는 글로벌 큐에서 이니셜라이즈 당시 파라미터로 입력받은 정수 구간을 출력한 뒤 종료되는 구조<pre><code class="language-swift">private func testOperationQueues3() {
      let operationQueue: OperationQueue = .init()
      let operation1: PrintNumbersOperation = .init(range: Range(0...25))
      let operation2: PrintNumbersOperation = .init(range: Range(26...50))
      operation2.addDependency(operation1)
      operationQueue.addOperation(operation1)
      operationQueue.addOperation(operation2)
      // serialized when async, using dependency
  }</code></pre>
</li>
<li>커스텀 비동기 오퍼레이션 클래스를 사용한 위의 오퍼레이션 큐는 언제나 디펜던시에 따라서 실행되는 게 보장</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Concurrency: Dispatch Barrier & Semaphore & Work Item Flags]]></title>
            <link>https://velog.io/@j_aion/UIKit-Concurrency-Dispatch-Barrier-Semaphore-Work-Item-Flags</link>
            <guid>https://velog.io/@j_aion/UIKit-Concurrency-Dispatch-Barrier-Semaphore-Work-Item-Flags</guid>
            <pubDate>Mon, 26 Dec 2022 15:56:36 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.youtube.com/watch?v=BWtQbgGWAdA&amp;list=PLSbpzz0GJp5RTrjum9gWTqPhM4L3Kop0S&amp;index=4">Mastering Concurrency in iOS - Part 4 (Dispatch Barrier, Semaphore, Work Item Flags)
</a></p>
<h1 id="concurrency-dispatch-barrier--semaphore--work-item-flags">Concurrency: Dispatch Barrier &amp; Semaphore &amp; Work Item Flags</h1>
<h1 id="dispatchworkitemflags">DispatchWorkItemFlags</h1>
<ul>
<li>워크 아이템을 모아놓은 집합</li>
<li>플래그: 여섯 개의 플래그를 통해 QoS 조정, 이중 배리어가 컨커런시와 관련성이 높음</li>
<li>barrier: 레이스 컨디션을 피하거나 태스크 타이밍을 컨트롤하고자 할 때 사용 가능한 플래그</li>
<li>컨커런트 큐에 특정한 워크 아이템이 들어갔을 때 일종의 배리어 역할을 하도록 하는 플래그</li>
</ul>
<pre><code class="language-swift">@objc private func didTapCombo() {
        for item in items {
            self.purchaseQueue.async(flags: .barrier) { [weak self] in
                guard let self = self else { return }
                self.addItems(item: item)
            }
        }
    }

    private func addItems(item: ItemModel) {
        if walletBalance &gt;= item.price {
            PurchaseManager.shared.buyItem(item: item, balance: walletBalance) { [weak self] success in
                guard let self = self else { return }
                if success {
                    DispatchQueue.main.async {
                        self.walletBalance -= item.price
                        self.cartBalance += item.price
                    }
                }
            }
        }
    }</code></pre>
<ul>
<li>레이스 컨디션을 해결하기 위해 커스텀 컨커런트 큐에 <code>barriers</code> 플래그를 설정한 채 비동기 구문을 작성하면, 내부 태스크를 순차적으로 보장할 수 있기 때문에 데이터 안전성을 보장할 수 있음</li>
</ul>
<h2 id="dispatch-semaphore">Dispatch Semaphore</h2>
<ul>
<li>디스패치 배리어를 통해 해결 가능한 레이스 문제를 세마포어로도 해결 가능</li>
</ul>
<h3 id="critical-section">Critical Section</h3>
<ul>
<li>공유 리소스에 접근하기 위한 구역</li>
<li>크리티컬 섹션에 멀티 스레드가 동시에 접근하게 되면 데이터 안전성에 해가 될 확률이 높음</li>
<li>배타적 접근을 통해 <code>data inconsistency</code>를 해결 가능 → 세마포어</li>
<li>디스패치 배리어 → 해당 태스크 실행 중 다른 태스크 실행을 막는 배타적 역할</li>
<li>세마포어 → 구현 방식에 따라 몇 개의 스레드를 실행할 것인지 모두 결정 가능. <code>counter</code> 값을 몇 개로 설정하느냐에 따라서 스레드 큐의 가용 개수를 설정 가능</li>
<li>크리티컬 섹션에 존재하는 스레드 개수에 따라서 <code>counter</code> 값이 변경, 해당 크리티컬 섹션으로 들어가기 전 단계에 <code>wait()</code>, <code>signal()</code> 메소드를 통해 스레드의 행동을 컨트롤</li>
</ul>
<pre><code class="language-swift">    private let semaphore = DispatchSemaphore(value: 1)
</code></pre>
<ul>
<li>커스텀 세마포어를 카운터 개수를 설정한 채로 구현<pre><code class="language-swift">@objc private func didTapCombo() {
      items.forEach { [weak self] item in
          self?.purchaseQueue.async {
              self?.addItems(item: item)
          }
      }
  }</code></pre>
</li>
<li>별도의 배리어 플래그를 설정하지 않은 채로 레이스 문제를 유발할 수 있는 구현 방식으로 여러 개의 비동기 함수를 동시 실행<pre><code class="language-swift">private func addItems(item: ItemModel) {
      semaphore.wait()
      if walletBalance &gt;= item.price {
          PurchaseManager.shared.buyItem(item: item, balance: walletBalance) { [weak self] success in
              guard let self = self else { return }
              if success {
                  DispatchQueue.main.async {
                      self.walletBalance -= item.price
                      self.cartBalance += item.price
                      self.semaphore.signal()
                  }
              }
          }
      }
  }</code></pre>
</li>
<li><code>wait()</code>를 통해 카운터 값에 맞춰 크리티컬 섹션에 들어갈 수 있는 스레드 개수를 한정, <code>signal()</code>을 통해 들어올 수 있음을 알려줌</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Concurrency: Dispatch Group & Dispatch Work Item]]></title>
            <link>https://velog.io/@j_aion/UIKit-Concurrency-Dispatch-Group-Dispatch-Work-Item</link>
            <guid>https://velog.io/@j_aion/UIKit-Concurrency-Dispatch-Group-Dispatch-Work-Item</guid>
            <pubDate>Mon, 26 Dec 2022 11:41:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=SGEWlYB6ZM0&amp;list=PLSbpzz0GJp5RTrjum9gWTqPhM4L3Kop0S&amp;index=3">Mastering Concurrency in iOS - Part 3 (Dispatch Group, Dispatch Work Item)
</a></p>
</blockquote>
<h1 id="concurrency-dispatch-group--dispatch-work-item">Concurrency: Dispatch Group &amp; Dispatch Work Item</h1>
<h2 id="dispatch-group">Dispatch Group</h2>
<ul>
<li>여러 개의 태스크를 그룹화 가능</li>
<li>여러 개의 태스크가 종료될 때까지 기다릴 수 있음</li>
<li>다른 태스크를 계속 진행할 수 있고, 그룹 내 태스크가 종료될 때 알림을 받을 수 있음</li>
<li>enter(), leave(), wait(), notify()</li>
</ul>
<pre><code class="language-swift">final class SplashViewController: UIViewController {
    private let spinnerView: UIActivityIndicatorView = {
        let view = UIActivityIndicatorView()
        view.startAnimating()
        return view
    }()
    private var launchDataDispatchGroup = DispatchGroup()
    private var cancellables = Set&lt;AnyCancellable&gt;()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        DispatchQueue.global().async { [weak self] in
            self?.getAppLaunchData()
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        spinnerView.style = .large
        spinnerView.center = view.center
    }

    private func setUI() {
        title = &quot;SplashView&quot;
        navigationItem.largeTitleDisplayMode = .always
        navigationController?.navigationBar.prefersLargeTitles = true
        view.backgroundColor = .systemBackground
        view.addSubview(spinnerView)
    }

    private func getAppLaunchData() {
        launchDataDispatchGroup.enter()
        NetworkManager
            .download(endPoint: .userPreferences)
            .sink { [weak self] completion in
                self?.launchDataDispatchGroup.leave()
                switch completion {
                case .failure(let error): print(error.localizedDescription)
                case .finished: break
                }
            } receiveValue: { _ in
                print(&quot;user preference data has received&quot;)
            }
            .store(in: &amp;cancellables)

        launchDataDispatchGroup.enter()
        NetworkManager
            .download(endPoint: .appConfig)
            .sink { [weak self] completion in
                self?.launchDataDispatchGroup.leave()
                switch completion {
                case .failure(let error): print(error.localizedDescription)
                case .finished: break
                }
            } receiveValue: { _ in
                print(&quot;app Configuration data has received&quot;)
            }
            .store(in: &amp;cancellables)

        let waitResult: DispatchTimeoutResult = launchDataDispatchGroup.wait(timeout: .now() + .seconds(5))
        DispatchQueue.main.async { [weak self] in
            switch waitResult {
            case .success:
                print(&quot;API call completed before timeout&quot;)
            case .timedOut:
                print(&quot;APIs timed out&quot;)
            }
            self?.spinnerView.stopAnimating()
            self?.navigateToSignVC()
        }
    }

    private func navigateToSignVC() {
        let vc = SignUpViewController()
        let navVC = UINavigationController(rootViewController: vc)
        let keyWindow = UIApplication.shared.keyWindow
        keyWindow?.rootViewController = navVC
    }
}</code></pre>
<ul>
<li><code>launchDataDispatchGroup</code>이라는 디스패치 그룹을 통해 태스크에 들어가고 나오는 과정을 컨트롤 가능</li>
<li><code>DispatchTimeoutResult</code>를 통해 체크하고자 하는 태스크에 걸리는 시간 내 종료 여부를 직접 확인 가능</li>
<li><code>notify</code> 프로퍼티를 사용한다면 시간과 관계 없이 해당 태스크가 모두 종료되었음을 알림받을 수 있음</li>
</ul>
<h2 id="dispatch-work-item">Dispatch Work Item</h2>
<ul>
<li>코드 블록의 캡슐화</li>
<li>디스패치 큐 및 디스패치 그룹 모두 디스패치될 수 있음</li>
<li>실행이 시작되지 않은 시점에도 해당 태스크를 취소할 수 있음</li>
<li>검색 쿼리와 같이 유저의 실시간 인터렉션이 태스크로 직결되는 상황에서 인터렉션이 종료될 때까지 불필요한 태스크가 여러 번 실행될 수 있음 → 최종 결과에 상응하는 태스크를 제외한, 이전에 발생했던 태스크들이 존재한다면 해당 태스크를 중도 취소/사전 취소 가능하다는 뜻</li>
<li>cancel: 실행 이전에 프로퍼티 값이 참이라면 실행되지 않음. 실행 도중 워크 아이템이 취소된다면 <code>cancel</code>은 참을 리턴하지만 실행은 중단되지 않을 것. </li>
</ul>
<pre><code class="language-swift">static func checkUserNameAvailable(userName: String) -&gt; AnyPublisher&lt;Bool, Error&gt; {
        return Future { result in
            DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
                let answer = userName == &quot;Pikachu&quot; ? true : false
                result(.success(answer))
            }
        }
        .eraseToAnyPublisher()
    }</code></pre>
<ul>
<li>간단한 실험을 위해 특정 쿼리 값을 넣었을 때에만 참을 리턴하고, 나머지는 거짓을 리턴하는 함수를 만들자</li>
<li>비동기 데이터를 처리하기 위해, 그리고 네트워킹을 모킹하기 위해 <code>Future</code> 내에서 <code>asyncAfter</code>를 써서 1초 뒤에 해당 값을 리턴한다.</li>
</ul>
<pre><code class="language-swift">    @objc private func textFieldDidEdit() {
        nameAvailabilityWorkItem?.cancel()
        errorLabel.isHidden = true
        let userName = nameTextField.text ?? &quot;&quot;

        let workItem: DispatchWorkItem = DispatchWorkItem {
            NetworkManager.checkUserNameAvailable(userName: userName)
                .receive(on: DispatchQueue.main)
                .sink { _ in
                } receiveValue: { [weak self] isAvailable in
                    self?.errorLabel.isHidden = isAvailable
                }
                .store(in: &amp;self.cancellables)

        }
        nameAvailabilityWorkItem = workItem
        DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1), execute: workItem)
    }
</code></pre>
<ul>
<li>특정 텍스트 필드(이름)에 텍스트를 입력할 때마다 API를 통해 텍스트 필드에 입력된 API 쿼리를 실행하는 게 아니라, 타이핑이 끝난 지 1초가 지나서야 비로소 API 쿼리를 실행(디스패치 글로벌 큐에서 <code>asyncAfter</code>를 통해 해당 워크 아이템을 실행하는 타이밍을 1초 후로 조정하는 게 핵심)</li>
<li>지난 시점 생성된 워크 아이템은 <code>cancel</code>을 통해 아직 실행되지 않았다면 취소 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/j_aion/post/7b28593c-50a3-4907-87bf-dd390063cbe8/image.gif" alt=""></p>
<blockquote>
<p>위와 같은 쿼리문의 타이밍 조절은 퍼블리셔를 다루는 여러 가지 기법 중 <code>throttle</code>, <code>debounce</code>를 통해 더욱 더 쉽게 조절할 수 있다! 하지만 디스패치 워크 아이템 또한 하나의 방법이 될 수 있다는 것 역시 체크해 놓자</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Concurrency: Dispatch Queue & QoS & Attributes]]></title>
            <link>https://velog.io/@j_aion/UIKit-Concurrency-Dispatch-Queue-QoS-Attributes</link>
            <guid>https://velog.io/@j_aion/UIKit-Concurrency-Dispatch-Queue-QoS-Attributes</guid>
            <pubDate>Mon, 26 Dec 2022 09:36:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=yH0RBTdNi3U&amp;list=PLSbpzz0GJp5RTrjum9gWTqPhM4L3Kop0S&amp;index=2">Mastering Concurrency in iOS - Part 2 (Dispatch Queues, Quality of Service, Attributes)
</a></p>
</blockquote>
<h1 id="concurrency-dispatch-queue--qos--attributes">Concurrency: Dispatch Queue &amp; QoS &amp; Attributes</h1>
<h2 id="dispatch-queue">Dispatch Queue</h2>
<h3 id="main-queue">Main Queue</h3>
<ul>
<li>시스템이 생성한 메인 큐</li>
<li>순차적으로 태스크를 할당, <code>메인 스레드</code>를 사용</li>
<li>다른 큐에서의 메인 스레드 사용은 지양됨 → UIKit이 메인 스레드에 묶여 있는 까닭에 모든 UI 관련 동작은 메인 큐에서 진행되어야 함</li>
</ul>
<h3 id="global-cuncurrent-queues">Global Cuncurrent Queues</h3>
<ul>
<li>시스템이 생성한 글로벌 큐</li>
<li>동시적으로 태스크를 할당</li>
<li><code>메인 스레드</code>가 아닌 다른 스레드를 사용</li>
<li>QoS를 통해 우선순위 결정</li>
</ul>
<pre><code class="language-swift">DispatchQueue.main.async {
    let threadCondition = Thread.isMainThread ? &quot;Execution in Main Thread&quot; : &quot;Execution in Global Thread&quot;
    print(&quot;Main Queue: \(threadCondition)&quot;)
}

DispatchQueue.global(qos: .background).async {
    let threadCondition = Thread.isMainThread ? &quot;Execution in Main Thread&quot; : &quot;Execution in Global Thread&quot;
    print(&quot;Global Queue: \(threadCondition)&quot;)
}</code></pre>
<ul>
<li>글로벌 큐에서의 모든 <code>qos</code>를 적용한다 할지라도 메인 스레드는 오로지 메인 큐에서만 사용 가능</li>
</ul>
<h3 id="application-box">Application Box</h3>
<ul>
<li>NSRunloop → UIKit → 이벤트 대기 → AppDelegate ... → GCD Queue → DB Query...</li>
<li>유저 인터렉션 등 UI 관련 이벤트는 메인 스레드 주관이지만, 해당 데이터를 얻기 위한 DB 조회 등 모든 사건을 메인 스레드에서 처리할 경우 불필요한 리소스 낭비</li>
<li>글로벌 큐에서 제공하는 멀티 스레딩을 통해 리소스의 효율적 사용 가능</li>
<li>싱글 코어 HW: HW의 코어가 하나라면, 코어 당 한 타임 퀀덤에 할당할 수 있는 큐는 하나라는 뜻. 즉 어떤 큐를 실행해야 할지 결정할 필요가 있음</li>
<li>글로벌 큐에서 실행되는 여러 가지 종류의 태스크는 종류에 따라 우선순위를 설정 가능하기 때문에 어떻게 리소스를 사용할 것인지 결정 가능</li>
</ul>
<h2 id="qos">QoS</h2>
<ul>
<li>User Interactive: 애니메이션(메인 큐에서의 UI 핸들링과 별도로 애플이 도입한 유저 인터렉티브 Qos는 현재 논쟁적 이슈) - UI 업데이트와 관련 있는지 체크</li>
<li>User Initiated: 직접적인 결과(테이블뷰 스크롤을 통해 다음 데이터를 즉각적으로 얻어와야 하는 시점 등) - 부드러운 UX를 제공하는 데 있어 필요한 데이터인지 체크</li>
<li>Utility: 오랫 동안 유지되는 태스크 - 유저가 해당 진행도를 알고 있는지 체크</li>
<li>Background: 유저에게 보이지 않는 태스크 - 유저가 해당 태스크를 알고 있는지 체크</li>
<li>유저 인터렉티브 - 유저 이닛 - 유틸리티 - 백그라운드 순서대로 우선순위 보장</li>
<li>Default: 유저 이닛과 유틸리티 사이</li>
<li>Unspecified: QoS 정보가 없을 때 </li>
</ul>
<pre><code class="language-swift">DispatchQueue.global(qos: .background).async {
    for i in 100...200 {
        print(i)
    }
}

DispatchQueue.global(qos: .userInteractive).async {
    for i in 0...99 {
        print(i)
    }
}</code></pre>
<ul>
<li>QoS가 서로 다른 글로벌 큐 간의 실행 순서는 보장할 수 없지만 종료 시점은 예측 가능 → 백그라운드 글로벌 큐의 태스크가 유저 인터렉티브 큐에서 실행한 태스크보다 늦게 종료됨은 보장할 수 있음</li>
</ul>
<h2 id="attributes">Attributes</h2>
<ul>
<li>concurrent: 커스텀 큐를 설정할 때 동시성 여부를 체크 가능</li>
<li>initiallyInactive: 시작 시점에는 액티브하지 않은 상태로 설정 가능</li>
<li>target queue: 커스텀 큐가 실제로 화면 뒤에서 사용하는 큐. 특정 디스패치 큐의 우선순위는 그 큐의 타겟 큐로부터 상속받음. 타겟 큐를 특정하지 않는다면 디폴트로 &#39;디폴트 우선순위 글로벌 큐&#39;가 타겟 큐가 됨</li>
</ul>
<pre><code class="language-swift">let a = DispatchQueue(label: &quot;A&quot;)
let b = DispatchQueue(label: &quot;B&quot;, attributes: [.concurrent, .initiallyInactive])
b.setTarget(queue: a)

b.async {
    print(&quot;Testing Thread Activation&quot;)
}

b.activate()

a.async {
    for i in 0...5 {
        print(i)
    }
}

a.async {
    for i in 6...10 {
        print(i)
    }
}

b.async {
    for i in 11...15 {
        print(i)
    }
}

b.async {
    for i in 16...20 {
        print(i)
    }
}
</code></pre>
<ul>
<li><code>.initiallyInactive</code>를 어트리뷰트로 설정하지 않고 이후 <code>b</code> 스레드에 타겟을 설정한다면 크래쉬 → 액티브한 상태의 스레드의 타겟 큐는 변경 불가능하기 때문에 비활성화 상태에서 타겟 큐를 설정, 이후 해당 스레드를 활성화해야 함</li>
</ul>
<h3 id="auto-release-frequency">Auto Release Frequency</h3>
<ul>
<li>inherit: 타겟 큐로부터 상속</li>
<li>workItem: 개별의 오토 릴리즈 풀</li>
<li>never: 개별 오토 릴리즈 풀을 설정하지 않음</li>
</ul>
<h3 id="serial-queue-async">Serial Queue Async</h3>
<pre><code class="language-swift">var value: Int = 20
let serialQueue = DispatchQueue(label: &quot;com.queue.Serial&quot;)

func doAsyncTaskInSerialQueue() {
    for i in 1...3 {
        serialQueue.async {
            if Thread.isMainThread {
                print(&quot;task running in main thread&quot;)
            } else {
                print(&quot;task running in global thread&quot;)
            }
            guard
                let imageURL = URL(string: &quot;https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg&quot;),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print(&quot;\(i) finished downloading&quot;)
        }
    }
}

doAsyncTaskInSerialQueue()

serialQueue.async {
    for i in 0...3 {
        value = i
        print(&quot;\(value) in next block&quot;)
    }
}

print(&quot;last line in playground&quot;)

/*
 task running in global thread
 last line in playground
 1 finished downloading
 task running in global thread
 2 finished downloading
 task running in global thread
 3 finished downloading
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 */
</code></pre>
<ul>
<li>디스패치 큐는 기본적으로 순차적으로 잡을 실행하는 시리얼 큐</li>
<li>시리얼 큐로 들어가는 태스크를 비동기적으로 실행한다 할지라도 들어간 &#39;순서&#39;대로 시리얼 큐가 실행</li>
</ul>
<h3 id="serial-queue-sync">Serial Queue Sync</h3>
<pre><code class="language-swift">var value: Int = 20
let serialQueue = DispatchQueue(label: &quot;com.queue.Serial&quot;)

func doSyncTaskInSerialQueue() {
    for i in 1...3 {
        serialQueue.sync {
            if Thread.isMainThread {
                print(&quot;task running in main thread&quot;)
            } else {
                print(&quot;task running in global thread&quot;)
            }
            guard
                let imageURL = URL(string: &quot;https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg&quot;),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print(&quot;\(i) finished downloading&quot;)
        }
    }
}

doSyncTaskInSerialQueue()

serialQueue.sync {
    for i in 0...3 {
        value = i
        print(&quot;\(value) in next block&quot;)
    }
}

print(&quot;last line in playground&quot;)

/*
 task running in main thread
 1 finished downloading
 task running in main thread
 2 finished downloading
 task running in main thread
 3 finished downloading
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 last line in playground
 */</code></pre>
<ul>
<li>동기 상황의 스레드는 메인 스레드. 메인 스레드는 메인 큐에서밖에 실행할 수 없으므로 현 시점은 메인 큐</li>
<li>동기 <code>sync</code> 블럭을 통해 실행한 디스패치 큐로 인해 메인 큐에서 실행되는 태스크는 현재 블락된 상황 → 즉 메인 스레드는 유휴 상태(idle)이므로 시스템은 이러한 메인 스레드를 활용하고자 함 → <code>sync</code> 블럭 내부에서 사용되는 스레드가 글로벌이 아니라 메인 스레드가 되는 까닭</li>
</ul>
<h3 id="concurrent-queue-async">Concurrent Queue Async</h3>
<pre><code class="language-swift">var value: Int = 20
let concurrentQueue = DispatchQueue(label: &quot;com.queue.Concurrent&quot;, attributes: .concurrent)

func doAsyncTaskInConcurrentQueue() {
    for i in 1...3 {
        concurrentQueue.async {
            if Thread.isMainThread {
                print(&quot;task running in main thread&quot;)
            } else {
                print(&quot;task running in global thread&quot;)
            }
            guard
                let imageURL = URL(string: &quot;https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg&quot;),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print(&quot;\(i) finished downloading&quot;)
        }
    }
}

doAsyncTaskInConcurrentQueue()

concurrentQueue.async {
    for i in 0...3 {
        value = i
        print(&quot;\(i) in next block&quot;)
    }
}

print(&quot;last line in playground&quot;)

/*
 task running in global thread
 last line in playground
 task running in global thread
 task running in global thread
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 3 finished downloading
 1 finished downloading
 2 finished downloading
 */
</code></pre>
<ul>
<li>컨커런트 큐에서 비동기적으로 실행되는 태스크는 순서를 보장하지 않음</li>
</ul>
<h3 id="concurrent-queue-sync">Concurrent Queue Sync</h3>
<pre><code class="language-swift">var value: Int = 20
let concurrentQueue = DispatchQueue(label: &quot;com.queue.Concurrent&quot;, attributes: .concurrent)

func doSyncTaskInConcurrentQueue() {
    for i in 1...3 {
        concurrentQueue.sync {
            if Thread.isMainThread {
                print(&quot;task running in main thread&quot;)
            } else {
                print(&quot;task running in global thread&quot;)
            }
            guard
                let imageURL = URL(string: &quot;https://www.nintenderos.com/wp-content/uploads/2020/04/anime-pokemon-pikachu.jpg&quot;),
                let _ = try? Data(contentsOf: imageURL) else { return }
            print(&quot;\(i) finished downloading&quot;)
        }
    }
}

doSyncTaskInConcurrentQueue()

concurrentQueue.sync {
    for i in 0...3 {
        value = i
        print(&quot;\(i) in next block&quot;)
    }
}

print(&quot;last line in playground&quot;)

/*
 task running in main thread
 1 finished downloading
 task running in main thread
 2 finished downloading
 task running in main thread
 3 finished downloading
 0 in next block
 1 in next block
 2 in next block
 3 in next block
 last line in playground
 */</code></pre>
<ul>
<li>동기적으로 실행되는 스레드는 메인 스레드에서 실행</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] Infinite ScrollView]]></title>
            <link>https://velog.io/@j_aion/UIKit-Infinite-ScrollView</link>
            <guid>https://velog.io/@j_aion/UIKit-Infinite-ScrollView</guid>
            <pubDate>Sun, 25 Dec 2022 17:01:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://levelup.gitconnected.com/how-to-create-infinite-scroll-in-uitableview-b021732922df">How to Create Infinite Scroll in UITableView </a>)</p>
</blockquote>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=TxH35Iqw89A">Swift: Infinite Scroll &amp; Pagination Tableview (Xcode 11, iOS) - 2020
</a></p>
</blockquote>
<h1 id="infinite-scrollview">Infinite ScrollView</h1>
<h2 id="구현-목표">구현 목표</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/27816445-fd7c-443b-983e-61d9837e248b/image.png" alt=""></p>
<ul>
<li>기존 스크롤 뷰에 등록된 데이터 이상을 스크롤할 경우 서버 데이터 요청 및 테이블 뷰 UI 리로드</li>
</ul>
<h2 id="구현-태스크">구현 태스크</h2>
<ul>
<li>테이블 뷰 UI 구현</li>
<li>깃허브 유저 데이터 API 구현</li>
<li>서버 데이터 요청을 판별하는 로직 구현</li>
<li>서버 데이터 시 스피너 뷰 추가 및 데이터 패치 완료 시 스피너 삭제 로직 구현</li>
</ul>
<h2 id="핵심-코드">핵심 코드</h2>
<pre><code class="language-swift">final class InfiniteScrollViewModel {
    let users: CurrentValueSubject&lt;[UserModel], Never&gt; = .init([])
    let isPaging: CurrentValueSubject&lt;Bool, Never&gt; = .init(false)
    private var currentLastId: Int? = nil
    private let userService = GithubAPIService.shared

    init() {
        fetchUsers(perPage: 30)
    }

    public func fetchUsers(perPage: Int = 10) {
        guard !isPaging.value else { return }
        var userSubscription: AnyCancellable?
        isPaging.send(true)
        userSubscription = userService
            .fetchUsers(perPage: perPage, sinceId: currentLastId)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: NetworkService.handleCompletion(completion:), receiveValue: { [weak self] receivedUsers in
                guard let self = self else { return }
                var currentUsers = self.users.value
                currentUsers.append(contentsOf: receivedUsers)
                self.users.send(currentUsers)
                self.currentLastId = currentUsers.last?.id
                self.isPaging.send(false)
                userSubscription?.cancel()
            })
    }
}</code></pre>
<ul>
<li>스크롤 뷰 컨트롤러의 데이터와 관련된 뷰 모델</li>
<li>현재 페이지네이션(즉 서버 데이터 요청)이 이루어지고 있는지 <code>isPaging</code>이라는 퍼블리셔를 통해 관리 → 단순한 불리언 변수가 아니라 퍼블리셔로 한 까닭은 해당 값 변화에 따라 뷰 컨트롤러에서 유의미한 UI 변경 이벤트가 발생하기 때문</li>
<li><code>fetchUsers</code> 함수는 곧 서버에 데이터를 요청하는 함수. 뷰 모델이 이니셜라이즈되는 순간에만 30개의 데이터를 요청하고, 페이지네이션, 즉 유저가 스크롤을 최하단 부로 내려 새로운 데이터를 서버에 요청할 때에는 기본적으로 10개의 데이터만을 요청</li>
<li>컴플리션 핸들러가 아니라 컴바인을 통한 <code>one-shot</code> 퍼블리셔를 통해 API 데이터 요청 함수를 사용</li>
<li><code>guard</code>를 통해 현 시점에 페이지네이션, 즉 서버 데이터 요청이 진행 중이라면 중복된 데이터를 요청하지 못하도록 제어</li>
<li><code>currentLastId</code>를 통해 다음에 불러 올 유저 아이디를 점진적으로 추가</li>
<li>API는 싱글턴 패턴으로 구현<pre><code class="language-swift">private func bind() {
      viewModel
          .users
          .sink { [weak self] _ in
              self?.tableView.reloadData()
          }
          .store(in: &amp;cancellables)
      viewModel
          .isPaging
          .receive(on: DispatchQueue.main)
          .sink { [weak self] isPaging in
              self?.tableView.tableFooterView = isPaging ? self?.createSpinnerFooter() : nil
          }
          .store(in: &amp;cancellables)
  }</code></pre>
</li>
<li>뷰 컨트롤러가 뷰 모델의 퍼블리셔를 구독하는 함수</li>
<li>뷰 모델의 데이터가 새롭게 변경될 때마다 테이블 뷰를 리로드함으로써 갱신 자동화 → UI를 그리는 리소스 이슈가 있으므로 데이터 소스를 일반에서 <code>Diffable</code>로 변경하거나 추가된 데이터만큼만 <code>reloadItems</code> 등을 통해 로드할 수 있음</li>
<li>뷰 모델의 <code>isPaging</code>은 유저에 의한 페이지네이션이 발생할 때에만 값이 참이 되고, 데이터를 서버에서 패치해왔다면 다시 거짓이 되는 퍼블리셔</li>
<li>해당 퍼블리셔에 따라 로딩 중이라는 의미를 유저에게 전달해야 함 → 스피너 뷰를 테이블 뷰 최하단 뷰, 즉 푸터 뷰에 추가함으로써 전달 가능</li>
<li>데이터 패치가 마무리되었다면 다시 하단 부를 없앰으로써 (<code>nil</code> 화) 알림<pre><code class="language-swift">func scrollViewDidScroll(_ scrollView: UIScrollView) {
      let position = scrollView.contentOffset.y
      let footerPadding: CGFloat = 100
      if position &gt; (tableView.contentSize.height - footerPadding - scrollView.frame.size.height) {
          viewModel.fetchUsers()
      }
  }</code></pre>
</li>
<li>스크롤 뷰를 상속하는 테이블 뷰(또는 컬렉션 뷰)의 특성 상 델리게이트 함수로 어느 정도의 스크롤이 진행되었는지 감지 가능</li>
<li><code>contentOffset</code>을 통해 어느 방향으로 얼마큼 스크롤되었는지 확인 가능</li>
<li>유저가 스크롤한 정도가 현재 존재하는 테이블 뷰의 컨텐츠 사이즈에서 푸터 뷰를 제외한 높이보다 크다면, 즉 마지막 컨텐츠를 넘어서 계속해서 스크롤을 하고 있다는 뜻 → 뷰 모델의 퍼블릭 함수인 패치 함수를 사용</li>
</ul>
<h2 id="소스-코드">소스 코드</h2>
<pre><code class="language-swift">import Foundation
import Combine

class NetworkService {
    enum NetworkingError: LocalizedError {
        case badURLResponse(url: URL?)
        case unknown
        var errorDescription: String? {
            switch self {
            case .badURLResponse(url: let url): return &quot;[🔥] Bad Response from URL: \(url?.absoluteString ?? &quot;&quot;)&quot;
            case .unknown: return &quot;[⚠️] Unknown error occured&quot;
            }
        }
    }

    static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL?) throws -&gt; Data {
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode &gt;= 200 &amp;&amp; response.statusCode &lt; 300 else
        { throw NetworkingError.badURLResponse(url: url) }
        return output.data
    }

    static func download(with url: URL) -&gt; AnyPublisher&lt;Data, Error&gt; {
        return URLSession
            .shared
            .dataTaskPublisher(for: url)
            .tryMap({try handleURLResponse(output: $0, url: url)})
            .retry(3)
            .eraseToAnyPublisher()
    }
    static func download(with urlRequest: URLRequest) -&gt; AnyPublisher&lt;Data, Error&gt; {
        return URLSession
            .shared
            .dataTaskPublisher(for: urlRequest)
            .tryMap({try handleURLResponse(output: $0, url: urlRequest.url)})
            .retry(3)
            .eraseToAnyPublisher()
    }
    static func handleCompletion(completion: Subscribers.Completion&lt;Error&gt;) {
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished: break
        }
    }
}</code></pre>
<ul>
<li>네트워크를 사용하는 함수가 많기 때문에 해당 함수의 모듈화</li>
<li><code>static</code>을 통해 곧바로 사용 가능<pre><code class="language-swift">import Foundation
import Combine
</code></pre>
</li>
</ul>
<p>final class GithubAPIService {
    static let shared = GithubAPIService()
    private let baseURLString = &quot;<a href="https://api.github.com/users&quot;">https://api.github.com/users&quot;</a>
    private let token = &quot;token [YOUR_GITHUB_TOKEN]&quot;</p>
<pre><code>private init() {}

private func getURL(perPage: Int, sinceId: Int?) -&gt; URLRequest? {
    var urlComponents = URLComponents(string: baseURLString)
    let urlQueryItems: [URLQueryItem] = [
        .init(name: &quot;per_page&quot;, value: &quot;\(perPage)&quot;),
        .init(name: &quot;since&quot;, value: sinceId?.description ?? &quot;&quot;)
    ]
    urlComponents?.queryItems = urlQueryItems

    guard let url = urlComponents?.url else { return nil }
    var urlRequest = URLRequest(url: url)
    urlRequest.httpMethod = &quot;GET&quot;
    urlRequest.setValue(token, forHTTPHeaderField: &quot;Authorization&quot;)
    return urlRequest
}

public func fetchUsers(perPage: Int = 30, sinceId: Int? = nil) -&gt; AnyPublisher&lt;[UserModel], Error&gt; {
    guard let url = getURL(perPage: perPage, sinceId: sinceId) else {
        return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
    }
    return NetworkService
        .download(with: url)
        .decode(type: [UserModel].self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}</code></pre><p>}</p>
<pre><code>* 토큰을 통해 인증하지 않는다면 API 이용 가용량이 적기 때문에 헤더에 토큰 값을 실어 인증
* 싱글턴 패턴으로 구현
```swift

import Foundation

struct UserModel: Codable, Identifiable {
    let id: Int
    let name: String
    let avatarURL: String

    enum CodingKeys: String, CodingKey {
        case id
        case name = &quot;login&quot;
        case avatarURL = &quot;avatar_url&quot;
    }
}</code></pre><ul>
<li>아이디, 사진, 이름만을 간략하게 나타내기 위한 <code>Codable</code>을 따르는 구조체<pre><code class="language-swift">import Combine
import Foundation
</code></pre>
</li>
</ul>
<p>final class InfiniteScrollViewModel {
    let users: CurrentValueSubject&lt;[UserModel], Never&gt; = .init([])
    let isPaging: CurrentValueSubject&lt;Bool, Never&gt; = .init(false)
    private var currentLastId: Int? = nil
    private let userService = GithubAPIService.shared</p>
<pre><code>init() {
    fetchUsers(perPage: 30)
}

public func fetchUsers(perPage: Int = 10) {
    guard !isPaging.value else { return }
    var userSubscription: AnyCancellable?
    isPaging.send(true)
    userSubscription = userService
        .fetchUsers(perPage: perPage, sinceId: currentLastId)
        .receive(on: DispatchQueue.main)
        .sink(receiveCompletion: NetworkService.handleCompletion(completion:), receiveValue: { [weak self] receivedUsers in
            guard let self = self else { return }
            var currentUsers = self.users.value
            currentUsers.append(contentsOf: receivedUsers)
            self.users.send(currentUsers)
            self.currentLastId = currentUsers.last?.id
            self.isPaging.send(false)
            userSubscription?.cancel()
        })
}</code></pre><p>}</p>
<pre><code>* 사실 진정한 의미에서의 MVVM 스타일대로라면 (적어도 내가 알고 있는 선상에서는) 인풋/아웃풋을 나누는 게 보다 효율적이겠지만, 구현 속도의 문제상 그렇게 하지는 않았다.
* 인풋을 보자면 유저의 페이지네이션 유무 / 아웃풋을 보자면 페이지네이션 진행 중의 유무 등으로 구분할 수 있을 것이다.
```swift
final class InfiniteScrollViewController: UIViewController {
    private let viewModel = InfiniteScrollViewModel()
    private var cancellables = Set&lt;AnyCancellable&gt;()
    private lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(InfiniteTableViewCell.self, forCellReuseIdentifier: InfiniteTableViewCell.identifier)
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        bind()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
    }

    private func setUI() {
        view.backgroundColor = .systemBackground
        title = &quot;Infinite ScrollView&quot;
        navigationItem.largeTitleDisplayMode = .always
        navigationController?.navigationBar.prefersLargeTitles = true
        view.addSubview(tableView)
    }

    private func bind() {
        viewModel
            .users
            .sink { [weak self] _ in
                self?.tableView.reloadData()
            }
            .store(in: &amp;cancellables)
        viewModel
            .isPaging
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isPaging in
                self?.tableView.tableFooterView = isPaging ? self?.createSpinnerFooter() : nil
            }
            .store(in: &amp;cancellables)
    }
}</code></pre><ul>
<li>간단하게 테이블 뷰를 추가한 뷰 컨트롤러<pre><code class="language-swift">extension InfiniteScrollViewController: UITableViewDelegate {
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
      let position = scrollView.contentOffset.y
      let footerPadding: CGFloat = 100
      if position &gt; (tableView.contentSize.height - footerPadding - scrollView.frame.size.height) {
          viewModel.fetchUsers()
      }
  }
}</code></pre>
</li>
<li>스크롤 뷰 델리게이트 함수는 페이지네이션의 핵심 파트</li>
</ul>
<pre><code class="language-swift">extension InfiniteScrollViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {
        return viewModel.users.value.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: InfiniteTableViewCell.identifier, for: indexPath) as? InfiniteTableViewCell else { fatalError() }
        let model = viewModel.users.value[indexPath.row]
        cell.configure(with: model)
        return cell
    }

    private func createSpinnerFooter() -&gt; UIView {
        let footerPadding: CGFloat = 100
        let footer = UIView(frame: .init(x: 0, y: 0, width: view.frame.size.width, height: footerPadding))
        let spinner = UIActivityIndicatorView()
        footer.addSubview(spinner)
        spinner.center = footer.center
        spinner.startAnimating()
        return footer
    }
}</code></pre>
<ul>
<li><p>셀을 그리는 데이터 소스 함수</p>
</li>
<li><p>페이지네이션이 진행 중일 때 그 순간의 스피너 뷰를 푸터 뷰로 만들어야 하기 때문에 함수화</p>
</li>
<li><p><code>Diffable</code>로 변경 가능</p>
<pre><code class="language-swift">final class InfiniteTableViewCell: UITableViewCell {
  static let identifier = &quot;InfiniteTableViewCell&quot;
  private let avatarImageView: UIImageView = {
      let imageView = UIImageView()
      imageView.translatesAutoresizingMaskIntoConstraints = false
      imageView.layer.masksToBounds = true
      imageView.image = UIImage(systemName: &quot;person.circle&quot;)
      return imageView
  }()
  private let nameLabel: UILabel = {
      let label = UILabel()
      label.text = &quot;Name&quot;
      label.translatesAutoresizingMaskIntoConstraints = false
      label.textColor = .label
      label.textAlignment = .center
      label.numberOfLines = 0
      return label
  }()
  private let idLabel: UILabel = {
      let label = UILabel()
      label.text = &quot;100&quot;
      label.translatesAutoresizingMaskIntoConstraints = false
      label.textColor = .systemGray
      label.textAlignment = .center
      return label
  }()
  private var imageSubscription: AnyCancellable?

  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
      super.init(style: style, reuseIdentifier: reuseIdentifier)
      setUI()
  }

  override func layoutSubviews() {
      super.layoutSubviews()
      applyConstraints()
  }

  override func prepareForReuse() {
      super.prepareForReuse()
      nameLabel.text = nil
      idLabel.text = nil
      avatarImageView.image = nil
      avatarImageView.image = UIImage(systemName: &quot;person.circle&quot;)
      imageSubscription?.cancel()
  }

  required init?(coder: NSCoder) {
      fatalError(&quot;init(coder:) has not been implemented&quot;)
  }

  private func setUI() {
      contentView.addSubview(avatarImageView)
      contentView.addSubview(nameLabel)
      contentView.addSubview(idLabel)
  }

  private func applyConstraints() {
      let nameLabelConstraints = [
          nameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 10),
          nameLabel.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 10),
          nameLabel.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant: -10),
          nameLabel.trailingAnchor.constraint(equalTo: idLabel.leadingAnchor, constant: -10)
      ]
      NSLayoutConstraint.activate(nameLabelConstraints)

      let avatarImageViewConstraints = [
          avatarImageView.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 10),
          avatarImageView.heightAnchor.constraint(equalToConstant: 30),
          avatarImageView.widthAnchor.constraint(equalToConstant: 30),
          avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
      ]
      avatarImageView.layer.cornerRadius = avatarImageView.frame.size.width / 2
      NSLayoutConstraint.activate(avatarImageViewConstraints)

      let idLabelConstraints = [
          idLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
          idLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 10),
          idLabel.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 10),
          idLabel.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant: -10)
      ]
      NSLayoutConstraint.activate(idLabelConstraints)
  }

  func configure(with model: UserModel) {
      idLabel.text = model.id.description
      nameLabel.text = model.name

      guard let url = URL(string: model.avatarURL) else { return }

      imageSubscription = NetworkService
          .download(with: url)
          .compactMap({UIImage(data: $0)})
          .receive(on: DispatchQueue.main)
          .sink(receiveCompletion: NetworkService.handleCompletion(completion:), receiveValue: { [weak self] image in
              self?.avatarImageView.image = image
              self?.imageSubscription?.cancel()
          })
  }
}</code></pre>
</li>
<li><p>퍼블릭 함수로 열려 있는 <code>configure</code>을 통해 받아들인 데이터로 셀 UI를 그리기</p>
</li>
<li><p><code>contentView</code>에 맞춰 오토 레이아웃을 했기 때문에 해당 아이디 값이 늘어난다면 그에 맞춰 셀이 늘어나는 다이나믹 구조</p>
</li>
<li><p>별개로 아바타 이미지 뷰의 크기는 고정</p>
</li>
<li><p><code>AnyCancellables</code>을 전역 변수로 가지고 있기 때문에 <code>prepareForReuse</code>에서 불필요한 데이터 다운로드 태스크를 중도 취소 가능</p>
<h2 id="구현-화면">구현 화면</h2>
<p><img src="https://velog.velcdn.com/images/j_aion/post/d5e6e50c-0ee5-434d-bf95-96f052b4b702/image.gif" alt=""></p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>