<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>KimSwift</title>
    <link>https://kimswift.tistory.com/</link>
    <description>기록해서 나중에 보는 iOS 블로그</description>
    <language>ko</language>
    <pubDate>Thu, 20 Aug 2026 10:21:21 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kim.drw</managingEditor>
    <image>
      <title>KimSwift</title>
      <url>https://tistory1.daumcdn.net/tistory/6745044/attach/8c6797b1d699437b96515a60f59f269f</url>
      <link>https://kimswift.tistory.com</link>
    </image>
    <item>
      <title>사이드 프로젝트의 현실적인 접근법, 2-2-2 방법론</title>
      <link>https://kimswift.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 제가 Bunny 앱을 개발할 때 실제로 적용했던 사이드 프로젝트 개발 방법론 중 하나인 &lt;b&gt;2-2-2 방법론&lt;/b&gt;에 대해 소개해드리겠습니다. 이 방법은 앱 아이디어를 빠르게 검증하고, 실제로 서비스까지 연결하는 데 매우 유용했던 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;426&quot; data-start=&quot;255&quot; data-ke-size=&quot;size16&quot;&gt;이 방법은 &lt;b&gt;Jordi Bruin&lt;/b&gt;이라는 개발자에 의해 처음 소개되었으며, 관련 영상은 유튜브에서도 확인하실 수 있습니다. 이 개발자는 2년간 20개 이상의 앱을 출시하면서 얻은 노하우를 바탕으로 이 방법론을 정립하였고, 실제로 &lt;b&gt;Apple Design Awards&lt;/b&gt; 후보에도 오른 바 있습니다.&lt;/p&gt;
&lt;hr data-end=&quot;431&quot; data-start=&quot;428&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;457&quot; data-start=&quot;433&quot; data-ke-size=&quot;size23&quot;&gt;아이디어는 넘치지만 실행은 어렵습니다&lt;/h3&gt;
&lt;p data-end=&quot;630&quot; data-start=&quot;459&quot; data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트를 해보신 분들이라면 공감하실 것입니다. 머릿속에는 항상 번뜩이는 아이디어가 넘쳐나고, 노트 앱이나 메모장에는 구현해보고 싶은 기능이나 서비스들이 빼곡하게 적혀 있습니다. 저 또한 마찬가지였습니다.&lt;br /&gt;아이디어는 많았지만, 막상 실행하려고 하면 어느새 진입 장벽 앞에서 멈추곤 하였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;712&quot; data-start=&quot;632&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;658&quot; data-start=&quot;632&quot;&gt;&amp;ldquo;이건 완성까지 몇 달은 걸릴 것 같은데&amp;hellip;&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;679&quot; data-start=&quot;659&quot;&gt;&amp;ldquo;혼자 다 하긴 너무 벅차겠다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;712&quot; data-start=&quot;680&quot;&gt;&amp;ldquo;지금 당장 시작해도 이게 과연 사람들에게 필요할까?&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;824&quot; data-start=&quot;714&quot; data-ke-size=&quot;size16&quot;&gt;이런 고민을 하다 보면 결국 아이디어는 실행되지 못한 채 그대로 쌓이기만 합니다.&lt;br /&gt;그러다 우연히 2-2-2 방법론을 접하게 되었고, 그때부터 제 사이드 프로젝트 진행 방식이 완전히 바뀌었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;829&quot; data-start=&quot;826&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;847&quot; data-start=&quot;831&quot; data-ke-size=&quot;size23&quot;&gt;2-2-2 방법론이란?&lt;/h3&gt;
&lt;p data-end=&quot;936&quot; data-start=&quot;849&quot; data-ke-size=&quot;size16&quot;&gt;이 방법론의 핵심은 숫자 그대로입니다.&lt;br /&gt;&lt;b&gt;2시간 &amp;rarr; 2일 &amp;rarr; 2주&lt;/b&gt;, 총 3단계에 걸쳐 아이디어를 빠르게 검증하고 실제로 출시까지 도달하는 것입니다.&lt;/p&gt;
&lt;hr data-end=&quot;941&quot; data-start=&quot;938&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;967&quot; data-start=&quot;943&quot; data-ke-size=&quot;size20&quot;&gt;1. 2시간 안에 프로토타입 만들기&lt;/h4&gt;
&lt;p data-end=&quot;1068&quot; data-start=&quot;969&quot; data-ke-size=&quot;size16&quot;&gt;아이디어가 떠올랐다면 바로 2시간을 타이머로 설정하고, 그 시간 안에 기본적인 형태의 프로토타입을 만들어봅니다.&lt;br /&gt;이때 중요한 것은 &lt;b&gt;완성도&lt;/b&gt;가 아니라 &lt;b&gt;가능성&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1135&quot; data-start=&quot;1070&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1087&quot; data-start=&quot;1070&quot;&gt;UI는 거칠어도 괜찮습니다.&lt;/li&gt;
&lt;li data-end=&quot;1113&quot; data-start=&quot;1088&quot;&gt;버튼이 제대로 작동하지 않아도 괜찮습니다.&lt;/li&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1114&quot;&gt;기능이 한두 개뿐이어도 괜찮습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1212&quot; data-start=&quot;1137&quot; data-ke-size=&quot;size16&quot;&gt;중요한 것은 이 아이디어를 직접 구현할 수 있는 수준인지, 그리고 만들면서 &lt;b&gt;흥미를 유지할 수 있는지&lt;/b&gt;를 빠르게 확인하는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;1310&quot; data-start=&quot;1214&quot; data-ke-size=&quot;size16&quot;&gt;시간 안에 아무것도 만들 수 없다면, 이 아이디어는 현재 제 역량에 맞지 않거나 너무 복잡한 아이디어일 수 있습니다.&lt;br /&gt;이 경우 과감하게 넘어가는 것도 하나의 전략입니다.&lt;/p&gt;
&lt;hr data-end=&quot;1315&quot; data-start=&quot;1312&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;1351&quot; data-start=&quot;1317&quot; data-ke-size=&quot;size20&quot;&gt;2. 2일 안에 친구들과 공유할 수 있는 버전 만들기&lt;/h4&gt;
&lt;p data-end=&quot;1453&quot; data-start=&quot;1353&quot; data-ke-size=&quot;size16&quot;&gt;프로토타입이 완성되었다면, 다음은 이틀 동안 집중해서 친구들에게 보여줄 수 있는 형태로 발전시킵니다.&lt;br /&gt;최소한의 기능이라도 직접 조작하고 체험해볼 수 있는 수준이 되면 좋습니다.&lt;/p&gt;
&lt;p data-end=&quot;1453&quot; data-start=&quot;1353&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1565&quot; data-start=&quot;1455&quot; data-ke-size=&quot;size16&quot;&gt;저 같은 경우에는 Bunny 앱의 초기 버전을 지인 몇 명에게 공유하였고, &amp;ldquo;사진 넘기는 기능이 직관적이지 않다&amp;rdquo;, &amp;ldquo;메뉴 바 위치가 눈에 잘 띄지 않는다&amp;rdquo;와 같은 피드백을 받을 수 있었습니다. 이 과정에서 얻는 &lt;b&gt;외부 피드백&lt;/b&gt;은 매우 중요합니다.&lt;br /&gt;우리는 내 앱을 누구보다 잘 안다고 생각하지만, 사용자의 시선은 전혀 다를 수 있습니다.&lt;/p&gt;
&lt;hr data-end=&quot;1655&quot; data-start=&quot;1652&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;1689&quot; data-start=&quot;1657&quot; data-ke-size=&quot;size20&quot;&gt;3. 2주 안에 출시할 수 있는 최종 버전 만들기&lt;/h4&gt;
&lt;p data-end=&quot;1783&quot; data-start=&quot;1691&quot; data-ke-size=&quot;size16&quot;&gt;마지막 단계는 2주 안에 &lt;b&gt;출시 가능한 수준&lt;/b&gt;까지 끌어올리는 것입니다. 이때 중요한 것은 모든 기능을 넣는 것이 아니라, &lt;b&gt;핵심 기능만 남기는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;1914&quot; data-start=&quot;1785&quot; data-ke-size=&quot;size16&quot;&gt;실제로 Bunny 앱도 처음 기획 단계에서는 다양한 아이디어가 있었지만, 2주 안에 출시하기 위해 가장 필수적인 요소만 남기고 나머지는 과감히 제외하였습니다. 이렇게 앱을 빠르게 출시해보면 생각보다 많은 것을 배울 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1961&quot; data-start=&quot;1916&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1937&quot; data-start=&quot;1916&quot;&gt;사용자들이 어떤 피드백을 주는지&lt;/li&gt;
&lt;li data-end=&quot;1961&quot; data-start=&quot;1938&quot;&gt;어떤 기능을 가장 유용하게 느끼는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1995&quot; data-start=&quot;1963&quot; data-ke-size=&quot;size16&quot;&gt;이 모든 데이터는 &lt;b&gt;다음 개선을 위한 기반&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;hr data-end=&quot;2000&quot; data-start=&quot;1997&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2021&quot; data-start=&quot;2002&quot; data-ke-size=&quot;size23&quot;&gt;왜 이 방법이 효과적일까요?&lt;/h3&gt;
&lt;p data-end=&quot;2174&quot; data-start=&quot;2023&quot; data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트는 &lt;b&gt;동기 부여&lt;/b&gt;가 가장 큰 문제입니다. 열정적으로 시작했다가 시간이 흐르면서 점점 시들해지기 쉽습니다.&lt;br /&gt;하지만 2-2-2 방법은 &lt;b&gt;짧은 사이클&lt;/b&gt;로 빠르게 실험과 검증을 반복하기 때문에, &lt;b&gt;작은 성공의 경험&lt;/b&gt;을 계속해서 쌓을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2397&quot; data-start=&quot;2176&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2221&quot; data-start=&quot;2176&quot;&gt;실행 전에 수많은 고민을 할 필요 없이, 바로 만들어보고 판단할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2263&quot; data-start=&quot;2222&quot;&gt;실현 가능성이 낮은 아이디어에 몇 주씩 쓰는 일을 방지할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2309&quot; data-start=&quot;2264&quot;&gt;짧은 시간 내에 출시해야 하므로, 무엇이 중요한지 자연스럽게 걸러지게 됩니다.&lt;/li&gt;
&lt;li data-end=&quot;2357&quot; data-start=&quot;2310&quot;&gt;사용자 반응을 바로 받아보고, 실제로 필요한 기능이 무엇인지 확인할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2397&quot; data-start=&quot;2358&quot;&gt;작은 성공이 쌓이면서 개발자로서의 자존감과 동기 부여가 이어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2402&quot; data-start=&quot;2399&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2413&quot; data-start=&quot;2404&quot; data-ke-size=&quot;size23&quot;&gt;마무리하며&lt;/h3&gt;
&lt;p data-end=&quot;2538&quot; data-start=&quot;2415&quot; data-ke-size=&quot;size16&quot;&gt;저는 Bunny 앱을 만들면서 이 방법을 처음 시도해보았고, 그 경험이 매우 만족스러웠습니다. 완벽한 앱을 만들겠다는 부담감에서 벗어나, &lt;b&gt;일단 시작하고 피드백을 통해 다듬는 방식&lt;/b&gt;이 제 성향에 더 잘 맞았습니다.&lt;/p&gt;
&lt;p data-end=&quot;2628&quot; data-start=&quot;2540&quot; data-ke-size=&quot;size16&quot;&gt;만약 지금 여러분의 머릿속에 멋진 아이디어가 있는데 실행하지 못하고 있다면, &lt;b&gt;이번 주말 2시간을 투자해서 첫 프로토타입을 만들어보시길 추천드립니다.&lt;/b&gt;&lt;/p&gt;</description>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/37</guid>
      <comments>https://kimswift.tistory.com/37#entry37comment</comments>
      <pubDate>Sat, 29 Mar 2025 00:35:39 +0900</pubDate>
    </item>
    <item>
      <title>Swift: 연결리스트 (Linked List)</title>
      <link>https://kimswift.tistory.com/36</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;오늘 다룰 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 리스트는 데이터들의 모음인 기본적인 자료구조입니다. 연속적인 메모리 블록을 할당하는 배열과 달리, 연결 리스트는 참조(포인터)를 통해 연결된 개별 노드로 구성됩니다. 스위프트에서 이들이 어떻게 작동하고 구현되는지 살펴보겠습니다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결 리스트(Linked List) 기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연결 리스트&lt;/b&gt;는 각 노드가 데이터와 다음 노드에 대한 참조(포인터)를 포함하는 자료구조입니다. 배열과 달리 메모리 상에서 연속적으로 위치하지 않고, 각 노드는 독립적으로 존재하며 서로 연결됩니다.&lt;br /&gt;연결 리스트는 크게 두 가지 유형이 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단일 연결 리스트(Singly Linked List)&lt;/b&gt;: 각 노드가 다음 노드에 대한 참조만 가집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이중 연결 리스트(Doubly Linked List)&lt;/b&gt;: 각 노드가 이전 노드와 다음 노드에 대한 참조를 모두 가집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트의 시작점을 추적하기 위해 &lt;b&gt;head&lt;/b&gt; 포인터를 사용하고, 종종 리스트의 끝을 가리키는 &lt;b&gt;tail&lt;/b&gt; 포인터도 함께 사용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결 리스트의 성능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 리스트는 대부분의 연산이 &lt;b&gt;O(n)&lt;/b&gt; 시간이 소요되어 일반적으로 배열보다 느립니다. 하지만 메모리를 복사할 필요 없이 포인터만 변경하면 되므로 더 유연합니다.&lt;br /&gt;특정 노드에 접근하려면 head에서 시작하여 next 포인터를 따라가야 하기 때문에 연산 시간이 O(n)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;그러나 한 번 노드에 대한 참조를 얻으면 삽입 및 삭제와 같은 작업은 매우 빠릅니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이런 특성 때문에 연결 리스트를 다룰 때는 가능하면 리스트의 앞부분에 새 항목을 삽입하는 것이 좋습니다. 이는 &lt;b&gt;O(1)&lt;/b&gt; 연산입니다.&lt;br /&gt;*tail 포인터를 유지한다면 뒤에 삽입하는 것도 마찬가지로 빠릅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Swift로 구현한 간단한 연결 리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 노드를 정의하는 클래스를 만들어 보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;public class LinkedListNode&amp;lt;T&amp;gt; {
  var value: T
  var next: LinkedListNode?
  weak var previous: LinkedListNode?

  public init(value: T) {
    self.value = value
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이는 제네릭 타입으로, T는 노드에 저장하고 싶은 어떤 종류의 데이터도 될 수 있습니다.&lt;br /&gt;이제 LinkedList 클래스를 구현해 보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;public class LinkedList&amp;lt;T&amp;gt; {
  public typealias Node = LinkedListNode&amp;lt;T&amp;gt;

  private var head: Node?

  public var isEmpty: Bool {
    return head == nil
  }

  public var first: Node? {
    return head
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;리스트가 비어있는지 확인하는 isEmpty 프로퍼티와 첫 번째 노드를 반환하는 first 프로퍼티를 구현했습니다. last 프로퍼티는 head에서 시작하여 다음 노드가 nil일 때까지 리스트를 따라가서 마지막 노드를 찾습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리스트에 노드 추가하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 끝에 새 노드를 추가하는 append 메서드를 구현해 보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;  public func insert(_ node: Node, atIndex index: Int) {
   let newNode = node
   if index == 0 {
     newNode.next = head                      
     head?.previous = newNode
     head = newNode
   } else {
     let prev = self.node(atIndex: index-1)
     let next = prev.next

     newNode.previous = prev
     newNode.next = prev.next
     prev.next = newNode
     next?.previous = newNode
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 메서드는 새 Node 객체를 생성한 다음, last 프로퍼티를 사용해 마지막 노드를 찾습니다. 마지막 노드가 없다면 리스트가 비어있는 것이므로 head가 새 노드를 가리키도록 합니다. 마지막 노드가 있다면 next와 previous 포인터를 연결하여 새 노드를 연결합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;노드 삭제하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드를 삭제하는 remove 메서드를 구현해 보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;  public func remove(node: Node) -&amp;gt; T {
    let prev = node.previous
    let next = node.next

    if let prev = prev {
      prev.next = next
    } else {
      head = next
    }
    next?.previous = prev

    node.previous = nil
    node.next = nil
    return node.value
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 메서드는 노드를 리스트에서 제거할 때, 이전 노드와 다음 노드 사이의 연결을 재구성합니다. 제거하려는 노드가 첫 번째 노드라면 head 포인터를 업데이트해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리스트 뒤집기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 리스트를 뒤집는 알고리즘은 다음과 같습니다&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;  public func reverse() {
    var node = head
    tail = node // If you had a tail pointer
    while let currentNode = node {
      node = currentNode.next
      swap(&amp;amp;currentNode.next, &amp;amp;currentNode.previous)
      head = currentNode
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 메서드는 전체 리스트를 순회하면서 각 노드의 next와 previous 포인터를 교환합니다. 또한 head 포인터를 가장 마지막 요소로 이동시킵니다.&lt;/p&gt;
&lt;h1&gt;고급 기능: map과 filter&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열처럼 연결 리스트에도 map과 filter 함수를 구현할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;  public func map&amp;lt;U&amp;gt;(transform: T -&amp;gt; U) -&amp;gt; LinkedList&amp;lt;U&amp;gt; {
    let result = LinkedList&amp;lt;U&amp;gt;()
    var node = head
    while node != nil {
      result.append(transform(node!.value))
      node = node!.next
    }
    return result
  }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;  public func filter(predicate: T -&amp;gt; Bool) -&amp;gt; LinkedList&amp;lt;T&amp;gt; {
    let result = LinkedList&amp;lt;T&amp;gt;()
    var node = head
    while node != nil {
      if predicate(node!.value) {
        result.append(node!.value)
      }
      node = node!.next
    }
    return result
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이러한 함수를 사용하면 연결 리스트의 값을 변환하거나 필터링할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 연결 리스트를 사용할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 리스트가 유용한 대표적인 예는 큐를 구현할 때입니다. 배열로 큐를 구현하면 앞에서 요소를 제거할 때 다른 모든 요소를 메모리에서 전부 이동시켜야 하므로 느립니다. 하지만 연결 리스트를 사용하면 head가 두 번째 요소를 가리키도록 바꾸기만 하면 되므로 훨씬 빠릅니다.&lt;/p&gt;</description>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/36</guid>
      <comments>https://kimswift.tistory.com/36#entry36comment</comments>
      <pubDate>Sat, 15 Mar 2025 15:09:04 +0900</pubDate>
    </item>
    <item>
      <title>Swift: Queue 구현하기</title>
      <link>https://kimswift.tistory.com/35</link>
      <description>&lt;div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;큐(Queue) 기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;큐&lt;/b&gt;는 새로운 항목을 리스트의 뒤쪽에 추가하고, 기존 항목을 리스트의 앞쪽에서 제거할 수 있는 자료구조입니다. 다시 말해, 가장 먼저 추가한 항목이 가장 먼저 제거됩니다. 흔히 편의점에서 들을 수 있는 선입선출(FIFO, First In First Out) 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 큐가 왜 필요할까요? 많은 알고리즘에서 객체를 임시 리스트에 추가했다가 나중에 이를 꺼내는 경우가 있습니다. 이때 추가와 제거의 순서가 중요한 경우 큐를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐와 비슷한 자료구조로 스택(Stack)이 있는데, 스택은 후입선출(LIFO, Last In First Out) 방식으로 동작합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;큐의 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 숫자를 큐에 추가하는 경우를 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740904973622&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;queue.enqueue(10)

queue.enqueue(3)

queue.enqueue(57)&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;위 queue는 이제 [10,3,57] 입니다. 이제 해당 큐의 앞에서 하나의 항목을 꺼내보겠습니다.&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1740905078476&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;queue.dequeue()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10이 가장 먼저 삽입된 숫자였기 때문에 위 코드는 10을 반환합니다. 이후 남은 항목들이 한 칸씩 앞 당겨져서 이제 큐는 [3 , 57 ]이 됩니다. 다음으로 deque는 3을, 그다음은 57을 반환합니다. 큐가 비어 있을 경우, nil을 반환하거나 구현 방식에 따라 오류 메시지를 출력할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 추가 및 제거 순서가 중요하지 않다면, 큐 대신 스택을 사용할 수 있습니다. 스택은 더 간단하고 빠르게 동작합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Swift로 구현한 간단한 큐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Swift에서 큐를 간단히 구현한 예제입니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1740905227940&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public struct Queue&amp;lt;T&amp;gt; {
  fileprivate var array = [T]()

  public var isEmpty: Bool {
    return array.isEmpty
  }
  
  public var count: Int {
    return array.count
  }

  public mutating func enqueue(_ element: T) {
    array.append(element)
  }
  
  public mutating func dequeue() -&amp;gt; T? {
    if isEmpty {
      return nil
    } else {
      return array.removeFirst()
    }
  }
  
  public var front: T? {
    return array.first
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구현은 간단하고 잘 작동하지만, 최적화는 되어 있지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;enqueue&lt;/b&gt;: 배열의 끝에 항목을 추가하는 작업은 항상 &lt;b&gt;O(1)&lt;/b&gt;, 즉 일정한 시간이 걸립니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dequeue&lt;/b&gt;: 배열의 앞에서 항목을 제거하는 작업은 &lt;b&gt;O(n)&lt;/b&gt;, 즉 배열 크기에 비례하는 시간이 걸립니다. 이는 비효율적이기 때문에 최적화가 필요합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;enqueue&lt;/b&gt;(항목 추가)은&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;왜 O(1), 즉 상수 시간 연산이 걸릴까요? 그 이유는 &lt;b&gt;Swift의 배열은 항상 끝부분에 여분의 공간을 보유하고 있기 때문입니다.&lt;/b&gt; 코드를 예로 들어보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740905450038&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;emsp;var queue = Queue&amp;lt;String&amp;gt;()
&amp;emsp;queue.enqueue(&quot;Ada&quot;)
&amp;emsp;queue.enqueue(&quot;Steve&quot;)
&amp;emsp;queue.enqueue(&quot;Tim&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 Queue는 실제로는 아래와 같은 모습을 띄게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ &quot;Ada&quot;, &quot;Steve&quot;, &quot;Tim&quot;, xxx, xxx, xxx ]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 xxx는 아직 값을 채워지지 않고 미리 빼놓은 메모리 공간을 의미합니다. 새로운 항목을 추가하면 다음 빈 공간(즉, xxx 자리)을 채우게 됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ &quot;Ada&quot;, &quot;Steve&quot;, &quot;Tim&quot;, &quot;Grace&quot;, xxx, xxx ]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 이 과정은 메모리의 한 위치에서 다른 위치로 데이터를 복사하는 것으로, 상수 시간 작업입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 배열의 끝에 있는 빈 공간은 정해진 개수만 존재합니다. 마지막 xxx가 사용되고 새로운 항목을 추가해야 할 때는, 배열이 더 넓은 메모리 공간을 확보하기 위해 크기를 재조정해야 합니다. 크기를 재조정하는 과정에서는 새로운 메모리를 할당하고 기존 데이터를 모두 새로운 배열로 복사하게 되는데, 이는 O(n) 시간 복잡도를 가지며 상대적으로 느립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 이 과정은 가끔씩만 발생하므로, 배열의 끝에 항목을 추가하는 평균 시간은 여전히 O(1)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, dequeue(항목 제거)의 경우는 다릅니다. dequeue는 배열의 시작 부분에서 항목을 제거하는 연산인데, 이때 배열에 남아 있는 모든 요소들을 메모리 상에서 한 칸씩 앞으로 이동시켜야 하므로 항상 O(n) 연산이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 첫 번째 항목 &quot;Ada&quot;를 dequeue 하면 한 칸씩 당겨서 &quot;Ada&quot; 자리에 &quot;Steve&quot;가, &quot;Steve&quot; 자리에 &quot;Tim&quot;이, 그리고 &quot;Tim&quot; 자리에 &quot;Grace&quot;가 복사됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;before: [ &quot;Ada&quot;, &quot;Steve&quot;, &quot;Tim&quot;, &quot;Grace&quot;, xxx, xxx ]&lt;br /&gt;after: [ &quot;Steve&quot;, &quot;Tim&quot;, &quot;Grace&quot;, xxx, xxx, xxx ]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 메모리의 요소들을 모두 이동시키는 작업은 배열의 크기가 n이라고 했을 때 항상 O(n) 시간이 소요됩니다. 따라서 저희는 선형적 시간 복잡도가 걸리는 dequeue는 고쳐야 합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;효율적인 큐 구현&lt;/h2&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dequeue를 효율적으로 바꾸기 위해서 배열의 앞쪽에도 미리 공간을 확보해 놓을 수 있습니다. Swift에서는 이런 방식의 배열(queue)을 지원하지 않기 때문에 직접 구현해줘야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하자면, 배열의 앞쪽에서 항목을 하나 뺄 때마다 항목들을 전부 옮기는 게 아니라, 맨 앞의 값이 있는 자리를 비웠다고 표시하는 것입니다. 위 배열 예시에서 &quot;Ada&quot;를 Dequeue 하면 아래와 같이 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[&amp;nbsp;xxx,&amp;nbsp;&quot;Steve&quot;,&amp;nbsp;&quot;Tim&quot;,&amp;nbsp;&quot;Grace&quot;,&amp;nbsp;xxx,&amp;nbsp;xxx&amp;nbsp;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 통해 구현해 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740906121657&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public struct Queue&amp;lt;T&amp;gt; {
  fileprivate var array = [T?]()
  fileprivate var head = 0
  
  public var isEmpty: Bool {
    return count == 0
  }

  public var count: Int {
    return array.count - head
  }
  
  public mutating func enqueue(_ element: T) {
    array.append(element)
  }
  
  public mutating func dequeue() -&amp;gt; T? {
    guard head &amp;lt; array.count, let element = array[head] else { return nil }

    array[head] = nil
    head += 1

    let percentage = Double(head)/Double(array.count)
    if array.count &amp;gt; 50 &amp;amp;&amp;amp; percentage &amp;gt; 0.25 {
      array.removeFirst(head)
      head = 0
    }
    
    return element
  }
  
  public var front: T? {
    if isEmpty {
      return nil
    } else {
      return array[head]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 부분은 거의 유사하고 dequeue() 부분이 다릅니다. 큐에서 dequeue 할 때 array[head]의 값을 nil로 만들어 값을 뺀 것과 같은 효과를 만들어 냅니다. 이 방법은 Queue의 dequeue를 할 때마다 queue의 앞부분에 nil이라는 값들이 쌓여서 배열이 점점 커지게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해서 배열의 앞부분의 nil 값의 개수를 계산해 약 25%의 배열이 사용되고 있지 않다면 (nil로 채워져 있다면), 이 사용하지 않는 값들을 지워냅니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740906418616&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    let percentage = Double(head)/Double(array.count)
    if array.count &amp;gt; 50 &amp;amp;&amp;amp; percentage &amp;gt; 0.25 {
      array.removeFirst(head)
      head = 0
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/35</guid>
      <comments>https://kimswift.tistory.com/35#entry35comment</comments>
      <pubDate>Sun, 2 Mar 2025 18:08:38 +0900</pubDate>
    </item>
    <item>
      <title>내가 생각할 때 영어 공부에서 가장 중요한 것</title>
      <link>https://kimswift.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저의 올해 목표 중 하나는 영어 공부입니다. 사실 개발 공부를 시작한 이후로 영어 공부를 조금 소홀히 해왔는데요. 그래서 이번 기회에 제가 영어 학습에서 가장 중요하다고 생각하는 &amp;ldquo;시간&amp;rdquo;에 대해 정리하고 마음을 다잡아 보려고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영어를 잘한다는 것의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어를 배우는 목적은 사람마다 다양합니다. 해외 취업, 미드 감상, 영어로 된 정보 습득 등 다양한 이유가 있죠. 제가 영어를 배우는 이유는 단순히 영어를 좋아하고 &lt;b&gt;잘하고 싶기 때문입니다&lt;/b&gt;. 그렇다면, 영어를 잘한다는 것은 구체적으로 무엇을 의미할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유럽언어공통기준(CEFR)에서는 언어 능력을 A1에서 C2까지 총 6단계로 구분합니다. 이 중 C2은 원어민에 가까운 수준을 의미하고, C1 또는 B2는 &lt;b&gt;일상 및 관심없는 분야까지 듣고 말하고 읽고 쓰는 데 어려움이 없는 수준으로 정의됩니다&lt;/b&gt;. 제가 목표로 하고 이 글에서 말하는 영어를 잘한다는 것은 이 C1~B2 수준을 의미합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영어 학습에서 가장 중요한 요소는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 C1~B2 수준에 도달하기 위해 가장 중요한 요소는 무엇일까요? 많은 사람들이 &amp;ldquo;매일 원어민과 대화하는 것이 중요하다&amp;rdquo;, &amp;ldquo;좋아하는 영화를 통째로 외워라&amp;rdquo;, &amp;ldquo;외국인과 연애를 해보라&amp;rdquo;와 같은 다양한 방법론을 강조합니다. 하지만, 제가 생각하기에 이런 방법들보다 앞서 고려해야 하는 가장 중요한 요소는 바로 &lt;b&gt;시간&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 좋은 방법을 사용하더라도, 절대적인 시간이 부족하면 언어를 잘할 수 없습니다. 언어 학습은 결국 얼마나 많은 시간을 투자했느냐에 달려 있습니다. 그렇다면 한국인이 영어를 잘하게 되는 데 얼마만큼의 시간이 걸릴까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한국인으로서 영어를 잘하게 되는 데 걸리는 시간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국인이 영어를 배우는 과정에서 가장 큰 장벽 중 하나는 영어와 한국어의 언어적, 문화적 차이입니다. 미국 국무부 외교관 양성 기관인 FSI(Foreign Service Institute)는 한국어를 학습 난이도가 가장 높은 &amp;ldquo;&lt;b&gt;카테고리 5(Super-hard language)&lt;/b&gt;&amp;rdquo;언어로 분류하고 있습니다. 이 기관에 따르면, 미국인이 한국어를 B2~C1 수준으로 배우는 데 약 &lt;b&gt;2200시간&lt;/b&gt;이 소요된다고 합니다. 반대로 한국인이 영어를 배우는 데도 비슷한 시간이 걸리거나 조금 더 적게 걸릴 수 있겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2200시간이라는 숫자가 감이 오지 않을 수 있는데요, 이를 구체적으로 살펴보면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쉬는 날 없이 하루에 2시간씩 공부한다고 가정하면, 약 &lt;b&gt;3년&lt;/b&gt;이 걸립니다.&lt;/li&gt;
&lt;li&gt;주말에는 쉬면서 공부한다면, 약 &lt;b&gt;4년 반&lt;/b&gt;이 걸립니다.&lt;/li&gt;
&lt;li&gt;만약 하루에 1시간씩 공부한다면, 그 시간은 &lt;b&gt;6 ~ 9년&lt;/b&gt;으로 늘어납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 주위를 한 번 둘러보면 이 시간이 얼마나 낙관적인지를 알 수 있는데요. 주변에 교환학생이나 워홀로 해외에서 6개월, 1년 정도 살다 온 친구들을 보면 어떤가요? 예를 들어, 하루에 8시간 동안 영어를 듣고 말한다고 하면 1년에 약 &lt;b&gt;3650시간을&lt;/b&gt; 투자하게 됩니다. 그런데도 앞서 말한 B2, C1 수준에 도달하지 못하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;다시 말해, 한국인이 영어를 배우는 것은 여러 요소에 따라서 2,000 ~ 3,000 시간을 넘을 수도 있는 정~~~말 긴 여정이라는 것입니다.&lt;/u&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 시간에 집중해야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 영어 학습에서 시간이 가장 중요하다고 생각하는 이유는 두 가지입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시간을 기준으로 올바른 학습 방향을 설정할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람들이 영어 성취가 더딜 때 학습 방법에 문제가 있다고 생각하고 새로운 방법을 찾곤 합니다. 하지만 대부분의 경우 문제는 &quot;방법&quot;이 아니라,&amp;nbsp;&lt;b&gt;공부 시간이 턱없이 부족하다는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2200시간이라는 목표를 기준으로 하루 2시간을 공부하면 약 3년이 걸리지만, 하루 4시간을 공부하면 약 1.5년으로 단축됩니다. 따라서,&amp;nbsp;&lt;b&gt;하루에 얼마나 공부 시간을 확보할 수 있느냐&lt;/b&gt;를 항상 고민해야합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현실적인 목표를 세움으로써 좌절하지 않고 꾸준히 나아갈 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 단기간에 유창해질 수 있다는 광고나 몇 주 만에 영어로 대화할 수 있다는 비현실적인 목표를 믿었던 사람들이 결국 자신의 실패를 자책하며 포기하는 모습을 많이 봐왔습니다. 현실적인 목표를 세우고 장기적으로 학습 여정을 계획하는 것이 중간에 지치지 않고 꾸준히 학습을 이어갈 수 있는 방법입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앞으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어를 배우는 과정은&amp;nbsp;&lt;b&gt;매우 긴 여정&lt;/b&gt;입니다. 이 과정에서 스스로의 실력이 늘고 있는지에 대한 의구심이 들거나, 주변 사람들과 비교하면서 조급함을 느끼는 순간들이 분명 있을 것입니다. 그럴 때마다 영어 학습은 본래 시간이 오래 걸리는 과정이라는 점을 스스로에게 상기시키고, 정직하게 꾸준히 공부를 지속하고자 합니다.&lt;/p&gt;</description>
      <category>일상 &amp;amp; 생각</category>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/34</guid>
      <comments>https://kimswift.tistory.com/34#entry34comment</comments>
      <pubDate>Sun, 16 Feb 2025 19:05:57 +0900</pubDate>
    </item>
    <item>
      <title>Swift: Hash Table 정리</title>
      <link>https://kimswift.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 해시테이블에 대해서 한 번 정리해 보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시테이블 개념정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블을 사용하면 키로 값을 저장하고 불러올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift에서는 Dictionary 타입에서 해시 테이블을 찾아볼 수 있는데요. Dictionary 타입은 내부적으로 해시 테이블로 구현되어 있기에, Dictionary의 key 값이 Hashable 프로토콜을 채택해야 하는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시테이블 작동 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블은 사실 간단한 배열입니다. 해시 테이블에 특정 키 값에 값을 넣는다면, 해시 테이블은 크 키 값을 사용해서 배열의 인덱스를 구해 그곳에 값을 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;hashTable[&quot;firstName&quot;] = &quot;Steve&quot;

    The hashTable array:
    +--------------+
    | 0:           |
    +--------------+
    | 1:           |
    +--------------+
    | 2:           |
    +--------------+
    | 3: firstName |---&amp;gt; Steve
    +--------------+
    | 4:           |
    +--------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서 &amp;ldquo;firstName&amp;rdquo;이라는 키 값은 인덱스 3과 연결된 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 String 타입의 키 값을 어떻게 배열의 인덱스로 바꾸는 걸까요? 이 작업은 Hashing을 통해 이루어 지기에 이 자료구조를 해시 테이블이라고 부릅니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;hashTable[&quot;firstName&quot;] = &quot;Steve&quot;

// &quot;firstName&quot;.hashValue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 해시 테이블은 &amp;ldquo;firstName&amp;rdquo;의 hashValue 프라퍼티를 이용합니다. 이때 해시 값은 -4799450059917011053와 같이 엄청나게 크거나 작은 값이 나오기 때문에 배열의 인덱스로 사용하기 적절하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 해시 값을 배열의 크기로 나누어 나머지 값을 사용하게 됩니다. 위 예시의 경우 배열의 크기가 5이니 abs(-4799450059917011053) % 5 = 3으로 배열의 인덱스 값이 3이 된 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블을 사용하기 때문에 Swift의 Dictionary의 값에서 없을 삽입하고 찾고 지우는 작업이 모두 O(1) 상수 시간 대 작업으로 굉장히 효율적입니다. 또한 해시 값과 그 값을 나눠 계산한 인덱스 값을 예측하기 어렵기 때문에 Dictionary에서는 요소들의 순서가 보장되지 않는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;충돌 방지하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위 방법은 한 가지 문제가 있는데요. 해시 값을 배열의 크기로 나눈 나머지를 인덱스로 사용하면 두 개 이상의 키 값이 하나의 인덱스로 매핑되는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 충돌을 방지하는 방법 중 하나는 Chaining(체이닝)입니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;    buckets:
    +-----+
    |  0  |
    +-----+     +----------------------------+
    |  1  |---&amp;gt; | hobbies: Programming Swift |
    +-----+     +----------------------------+
    |  2  |
    +-----+     +------------------+     +----------------+
    |  3  |---&amp;gt; | firstName: Steve |---&amp;gt; | lastName: Jobs |
    +-----+     +------------------+     +----------------+
    |  4  |
    +-----+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체이닝을 이용한다는 것은 하나의 배열 요소에 여러 개의 키/밸류 페어가 담기는 것을 의미합니다. 이때 일반적으로 배열의 요소는 버킷이라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고 불리고 그 버킷에 담긴 리스트들을 체인이라고 부릅니다. 위 예시에서는 5개의 버킷이 있고 3번째 버킷에 2개의 체인이 있는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서 &amp;ldquo;lastName&amp;rdquo; 키에 저장된 값을 아래와 같이 찾게 되면&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let x = hashTable[&quot;lastName&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &amp;ldquo;lastName&amp;rdquo;을 가지고 배열의 인덱스 값 3을 계산해 냅니다. 그리고 3번째 버킷에는 체인이 있으니 이 체인을 하나씩 돌면서 키 값이 일치하는 체인을 찾아낸 뒤 그 값을 반환하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 체인은 짧게 유지하는 게 해시 테이블에서 값을 찾는 속도를 빠르게 유지하는 데 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift로는 아래와 구조체를 통해 구현할 수 있습니다. 앞서 알아본 것처럼 키 값은 해싱을 통해 배열의 인덱스 값으로 계산하기 때문에 제너릭 타입인 Key는 Hashable을 채택해야만 합니다.&amp;nbsp;그리고 체인 부분에 해당하는 키/밸류 페어는 Element, 이 Element를 담게 되는 버킷은 Bucket 타입으로 표현되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value, updateValue, removeValue메서드를 구현해 해시 테이블에 값을 저장하거나 삭제하고, 또 값을 불러오게 됩니다. 이때 앞서 말했듯 Bucket에는 체인(배열)이 담기기 때문에 해당 Key에 맞는 값을 찾기 위해 for 문을 통해 하나씩 체인을 돌며 키 값을 비교하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct HashTable&amp;lt;Key: Hashable, Value&amp;gt; {
    private typealias Element = (key: Key, value: Value)
    private typealias Bucket = [Element]
    private var buckets: [Bucket]

    private(set) public var count = 0

    public var isEmpty: Bool {
        return count == 0
    }

    public init(capacity: Int) {
        assert(capacity &amp;gt; 0)
        buckets = Array&amp;lt;Bucket&amp;gt;(repeatElement([], count: capacity ))
    }


    // 인덱스 찾기 
    private func index(forKey key: Key) -&amp;gt; Int {
        return abs(key.hashValue % buckets.count)
    }

    // 특정 값 찾기
    public func value(forKey key: Key) -&amp;gt; Value? {
        let index = self.index(forKey: key)
        for element in buckets[index] {
            if element.key == key {
                return element.value
            }
        }
        return nil // key not in hash table
    }

    // 새로운 값 삽입 or 특정 값 업데이트
    public mutating func updateValue(_ value: Value, forkey key: Key) -&amp;gt; Value? {
        let index = self.index(forKey: key)

        // 양동이 안에 키값이 존재하면 업데이트
        for (i, element) in buckets[index].enumerated() {
            if element.key == key {
                let oldValue = element.value
                buckets[index][i].value = value
                return oldValue
            }
        }

        // 양동이 안에 값이 없으면 체인에 넣기
        buckets[index].append((key: key, value: value))
        count += 1
        return nil
    }

    // 값 삭제하기
    public mutating func removeValue(forkey key: Key) -&amp;gt; Value? {
        let index = self.index(forKey: key)

        for (i, element) in buckets[index].enumerated() {
            if element.key == key {
                buckets[index].remove(at: i)
                count -= 1
                return element.value
            }
        }

        return nil // key not in the hash table.
    }

    public subscript(key: Key) -&amp;gt; Value? {
        get {
            return value(forKey: key)
        }
        set {
            if let value = newValue {
                updateValue(value, forkey: key)
            } else {
                removeValue(forkey: key)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>기술 &amp;amp; 언어</category>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/33</guid>
      <comments>https://kimswift.tistory.com/33#entry33comment</comments>
      <pubDate>Sun, 19 Jan 2025 22:05:31 +0900</pubDate>
    </item>
    <item>
      <title>Swift - SwiftUI로 이미지 크롭 뷰 구현하기</title>
      <link>https://kimswift.tistory.com/32</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 다룰 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 오늘은 제가 메뉴바 갤러리 앱 &lt;a href=&quot;https://apps.apple.com/kr/app/bunny-%EB%A9%94%EB%89%B4%EB%B0%94-%EA%B0%A4%EB%9F%AC%EB%A6%AC/id6737684816?mt=12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bunny&lt;/a&gt;를 개발할 때 바닐라 SwiftUI로 ImageCropView를 구현한 경험을 공유해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고려할 기능들&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 기록 2024-12-02 오전 1.03.47.gif&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y2LH3/btsK4cns8y2/TzaxjRbaN69jyOrEkNlN4K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y2LH3/btsK4cns8y2/TzaxjRbaN69jyOrEkNlN4K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y2LH3/btsK4cns8y2/TzaxjRbaN69jyOrEkNlN4K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/y2LH3/btsK4cns8y2/TzaxjRbaN69jyOrEkNlN4K/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;350&quot; height=&quot;366&quot; data-filename=&quot;화면 기록 2024-12-02 오전 1.03.47.gif&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;!-- {&quot;width&quot;:320} --&gt;&lt;br /&gt;위처럼 이미지를 수정하고 크롭 할 수 있는 뷰에 들어갈 기능을 나열해 보면 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 이미지 수정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 수정&lt;/li&gt;
&lt;li&gt;크기 수정&lt;/li&gt;
&lt;li&gt;회전&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자 이미지 수정 시 타겟 크기에서 벗어나지 않도록 제한
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최소 크기 설정&lt;/li&gt;
&lt;li&gt;위치 수정(드래그) 시 타겟 틀 밖으로 벗어나지 않도록 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;뷰에 맞춰 최초 이미지 스케일 설정&lt;/li&gt;
&lt;li&gt;View를 Image로 변환하는 로직 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI 뷰 계층은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UnderlyingImageView ( 하단 이미지 )&lt;/li&gt;
&lt;li&gt;CutHole ( 크롭 후 결과를 보여주는 프리뷰 틀 )&lt;/li&gt;
&lt;li&gt;Controls ( 회전 버튼, 완료 버튼 )&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UnderlyingImageView은 사용자가 직접 상호작용 하면서 이미지를 수정하게 되는 뷰입니다. CutHole을 통해 크롭 된 이미지를 예상하면서 수정을 마친 뒤 Controls 뷰의 완료 버튼을 클릭하면 CutHole &amp;lsquo;틀&amp;rsquo;에 담긴 뷰가 이미지로 변환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;이번 포스팅에서는 UnderlyingImageView, Controls을 통해 이미지 수정 기능과 뷰를 이미지로 변환하는 방법에 대해서 중점적으로 다뤄보겠습니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 &lt;a href=&quot;https://gist.github.com/hjsupernova/2a912453cf5a1810ba329863b67ded8c&quot;&gt;SwiftUI CropImageView&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지 수정 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gesture을 통한 Scale, Rotate, Offset 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 확대하거나 축소하고, 회전, 이동하는 것은 SwiftUI의 Gesture을 통해 구현할 수 있습니다. 각각의 수정은 MagnificationGesture, RotationGesture, DragGesture을 통해 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 Gesture를 뷰에서 인식하기 위해서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Gesture를 생성 및 설정을 한 뒤,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. gesture() 모디파이어를 통해 뷰에 더해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 거의 동일하기 때문에 DragGesture만을 예시로 들어보겠습니다. 저는 Gesture를 한눈에 보기 편하도록 interactionView뷰를 생성해 사용했습니다. ImageView에 바로 .gesture() 모디파이어를 붙여 해당 뷰를 드래그할 수 도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct UnderlyingImageView: View {
    // MARK: - Gestures
    // 1
    @Binding var offset: CGSize
    @State private var tempOffset: CGSize = .zero

    var interactionView: some View {
        Color.white.opacity(0.0001)
            .gesture(dragGesture)
            .gesture(magnificationGesture)
            .gesture(rotationGesture)
    }

    // 2 
    var dragGesture: some Gesture {
        DragGesture()
            .onChanged { value in
                tempOffset = value.translation
            }
            .onEnded { value in
                offset = offset + tempOffset
                tempOffset = .zero
            }
    }
    ...

    // 3 
    var body: some View {
        imageView
            .rotationEffect(rotation + tempRotation)
            .scaleEffect(scale * tempScale)
            .offset(offset + tempOffset)
            .overlay(interactionView)&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 드래그한 위치의 영구적인 값을 보관하기 위한 &lt;b&gt;offset&lt;/b&gt;과 드래그를 할 때 실시간으로 값을 더할 &lt;b&gt;tempOffset&lt;/b&gt; 변수를 선언해 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DragGesture()&lt;/b&gt;를 생성하고 &lt;b&gt;.onChanged()&lt;/b&gt; 메서드를 통해 사용자가 드래그하는 동안의 값을 받아와 tempOffset을 업데이트해 줍니다. 드래그가 끝나면 offset에 tempOffset에 담긴 드래그 이동 값을 더해주고 tempOffset을 초기화해 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;imageView&lt;/b&gt;에 &lt;b&gt;interactionView&lt;/b&gt;를 오버레이 해서 드래그 값을 받아옵니다. 그 후 .offset() 모디파이어를 더해 드래그가 진행되는 도중에는 tempOffset 값을 통해 뷰가 움직이게 되고 드래그가 멈추면 offset에 저장된 값이 해당 위치에 뷰를 놓게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 Gesture들도 동일하게 구현하고 rotationEffect, scaleEffect를 통해 뷰와 연결해 줄 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지 수정 시 타겟 틀(CutHole)을 벗어나지 않도록 제한&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 예시 영상에서 처럼 사용자가 이미지를 수정할 때 타겟 틀에서 벗어나지 못하도록 하는 것이 목표입니다. (보는 것만큼 간단하지는 않으니 주의하세요!   )&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 width, height 계산하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 이후 작업들에서 사용될 이미지의 width와 height를 적절히 계산해야 합니다. 크롭 할 이미지는 외부에서 받아오지만 사용자가 이미지를 회전할 수 있다는 것을 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 그림에서 처럼 이미지를 오른쪽으로 90도 회전하면 기존의 넓이가 높이가 되고, 기존의 높이가 넓이가 됩니다. 다시 말해, 회전 값 Rotation이 0 아니고, 90으로 나누어지고 180으로 나누어지지 않는다면 (아래의 오른쪽 같은 모습이라면 ) 이미지의 넓이와 높이를 반대로 사용해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bItdxi/btsK4cuezCp/cnHD7A4uBpGmo9Dp9k96K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bItdxi/btsK4cuezCp/cnHD7A4uBpGmo9Dp9k96K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bItdxi/btsK4cuezCp/cnHD7A4uBpGmo9Dp9k96K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbItdxi%2FbtsK4cuezCp%2FcnHD7A4uBpGmo9Dp9k96K0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1348&quot; height=&quot;649&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;img src=&quot;image%202.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로는 아래와 같이 구현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct UnderlyingImageView: View {
    var image: NSImage
    var viewSize: CGSize
    var targetSize: CGSize

    var isRoatedOddMultiplesOf90Deg: Bool {
        rotation != .zero
        &amp;amp;&amp;amp; rotation.degrees.truncatingRemainder(dividingBy: 90) == 0
        &amp;amp;&amp;amp; rotation.degrees.truncatingRemainder(dividingBy: 180) != 0
    }

    var imageWidth: CGFloat {
        isRoatedOddMultiplesOf90Deg ? image.size.height : image.size.width
    }

    var imageHeight: CGFloat {
        isRoatedOddMultiplesOf90Deg ? image.size.width : image.size.height
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;드래그 시 X축 Y축 경계(Bound) 계산하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 이미지를 드래그해서 CutHole 틀을 벗어나지 못하도록 드래그할 수 있는 최대범위를 정해야 합니다. (틀 크기 - 이미지 크기) / 2 계산을 통해 한쪽 범위를 구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 예시로 들어보겠습니다. 이미지의 크기가 6x3이고 틀이 3x3이라고 했을 때, X축 경계의 한쪽은 (3-6) /2 = -1.5가 됩니다. 아래 그림에서 주황색 이미지를 오른쪽으로 이동시킨다고 생각해 보시면 이해가 편하실 겁니다. 따라서 X축의 이동 반경은 -1.5 ~ 1.5입니다. Y축도 똑같은 방법으로 계산할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;877&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bACwdV/btsK4xY7377/PkwpHwfHKb4ANOUdGhLvXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bACwdV/btsK4xY7377/PkwpHwfHKb4ANOUdGhLvXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bACwdV/btsK4xY7377/PkwpHwfHKb4ANOUdGhLvXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbACwdV%2FbtsK4xY7377%2FPkwpHwfHKb4ANOUdGhLvXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1253&quot; height=&quot;877&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;877&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;img src=&quot;image%203.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로는 아래와 같이 구현할 수 있습니다. 이때 사용자가 이미지를 확대하는 경우도 있으니 해당 scale에 맞춰서 경계 값을 적절히 계산해줘야 합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;    func xOffsetBounds(at scale: CGFloat) -&amp;gt; ClosedRange&amp;lt;CGFloat&amp;gt; {
        let width = imageWidth * scale
        let range = (targetSize.width - width) / 2
        return range &amp;gt; 0 ? -range ... range : range ... -range
    }

    func yOffsetBounds(at scale: CGFloat) -&amp;gt; ClosedRange&amp;lt;CGFloat&amp;gt; {
        let height = imageHeight * scale
        let range = (targetSize.height - height) / 2
        return range &amp;gt; 0 ? -range ... range : range ... -range
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타겟 틀보다 이미지가 작아지지 않도록 제한 ( 최소 Scale )&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 이미지의 축소할 때 이미지가 타켓 틀 보다 작아지지 않도록 해야 합니다. 다시 말해, 이미지가 틀을 꽉 채우도록 하는 것이 목표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 틀이 4x4일 때 사용자가 이미지의 스케일을 줄여(이미지를 축소해서) 이미지의 크기가 2x2 되는 것을 방지하도록 최소 스케일 값을 계산해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 타겟 틀을 꽉 채우도록 하기 위해서는, 이미지의 넓이나 높이가 틀의 크기에 맞추기 위해 필요한 스케일 값 중 최댓값을 사용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;    var minimumScale: CGFloat {
        let widthScale = targetSize.width / imageWidth
        let heightScale = targetSize.height / imageHeight
        return max(widthScale, heightScale)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타겟 틀에 맞게 offset과 scale 조정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 앞에서 계산한 값을 바탕으로, 이미지의 스케일이 너무 작거나 이미지가 타겟 틀 밖으로 벗어난 경우, 틀에 맞게 조정해 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;offset&lt;/b&gt;&lt;br /&gt;앞의 예시에서 처럼 드래그 경계(Bound)가 ~1.5 ~ 1.5이라고 가정해 보겠습니다. 이때 저희는 이 범위를 벗어난 경우에 가까운 경계 값으로 변경해야 합니다. 예를 들어 -2 만큼 갔으면 -1.5로, 4 만큼 갔으면 1.5로 값을 변경해야 합니다. (clamped extension을 통해 가독성 향상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Scale&lt;/b&gt;&lt;br /&gt;현재 스케일이 최소 스케일 값보다 작은 경우에 스케일을 최소 값으로 변경해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 &lt;b&gt;adjustToFulfillTargetFrame&amp;nbsp;&lt;/b&gt;함수를 통해 코드로 구현할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;    func adjustToFulfillTargetFrame() {
        // 이미지가 CutHole보다 작은 경우만 문제가 된다. 그 경우를 핸들하는 중.
        let clampedScale = max(minimumScale, scale)
        var clampedOffset = offset

        clampedOffset.width = clampedOffset.width.clamped(to: xOffsetBounds(at: clampedScale))
        clampedOffset.height = clampedOffset.height.clamped(to: yOffsetBounds(at: clampedScale))

        if clampedScale != scale || clampedOffset != offset {
            withAnimation(.interactiveSpring()) {
                scale = clampedScale
                offset = clampedOffset
            }
        }
    }


extension Comparable {
    func clamped(to limits: ClosedRange&amp;lt;Self&amp;gt;) -&amp;gt; Self {
        return min(max(self, limits.lowerBound), limits.upperBound)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.onChange 메서드를 통해 이미지 수정 적절한지 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scale, offset, rotation값이 변경될 때마다 &lt;b&gt;adjustToFulfillTargetFrame&lt;/b&gt;함수를 호출해서 이미지 수정이 적절한 지 검증하고, 알맞은 형태와 위치로 원상 복귀할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;    var body: some View {
        imageView
            .rotationEffect(rotation + tempRotation)
            .scaleEffect(scale * tempScale)
            .offset(offset + tempOffset)
            .overlay(interactionView)
            .onChange(of: scale) { _ in
                adjustToFulfillTargetFrame()
            }
            .onChange(of: offset) { _ in
                adjustToFulfillTargetFrame()
            }
            .onChange(of: rotation) { _ in
                adjustToFulfillTargetFrame()
            }
            .frame(width: viewSize.width, height: viewSize.height)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;View에 맞게 이미지의 초기 스케일 값 계산하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 뷰의 크기보다 크거나 작은 경우에 View의 범위 내에서 이미지가 최대한 잘 보이도록 초기 스케일 값을 계산하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우엔 두 스케일 중 최댓값을 사용할 경우 넓이나 높이가 잘릴 수 있기 때문에 최솟값으로 설정해 줍니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;    func setInitialScale(basedOn viewSize: CGSize) {
        guard viewSize != .zero else { return }
        let widthScale = viewSize.width / imageWidth
        let heightScale = viewSize.height / imageHeight

        scale = min(widthScale, heightScale)
    }


...
        imageView
            .onAppear {
                setInitialScale(basedOn: viewSize)
            }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;View를 Image로 변환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI View는 &lt;b&gt;ImageRenderer&lt;/b&gt; 클래스를 통해 이미지 데이터로 변환할 수 있습니다. 이때 &lt;b&gt;scale&lt;/b&gt; 프라퍼티 값이 기본이 1이기 때문에 값을 변경하지 않으면 2x, 3x 모니터에서 이미지가 흐리게 보일 수 있습니다. 따라서 Environment의 &lt;b&gt;displayScale&lt;/b&gt; 값을 가져와 scale프라퍼티 값을 업데이트해 줍니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct CropImageVIew: View {
    @State private var croppedImage: NSImage?
    @Environment(\.displayScale) var displayScale
    .
    .
    .


    var body: some View { 
        ZStack { 
            UnderlyingImageView() 
            CutHoleView()
            Controls { 
            	do { 
                    croppedImage = try crop()
                } catch { 
                    // failed to crop
                }
            }
        }
    }

    @MainActor
    func crop() throws -&amp;gt; NSImage {
        let snapshotView = UnderlyingImageView(
            scale: $scale,
            rotation: $rotation,
            offset: $offest,
            image: image,
            viewSize: viewSize,
            targetSize: targetSize
        )
        .frame(width: targetSize.width, height: targetSize.height)

        let renderer = ImageRenderer(content: snapshotView)
        renderer.scale = displayScale

        if let image = renderer.nsImage {
            return image
        } else {
            throw CropError.failToRender// Eror handling needed
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다! 궁금한 점 혹은 오류를 발견하셨으면 주저 말고 댓글 남겨주세요 ( __ )&lt;/p&gt;</description>
      <category>기술 &amp;amp; 언어</category>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/32</guid>
      <comments>https://kimswift.tistory.com/32#entry32comment</comments>
      <pubDate>Mon, 2 Dec 2024 14:42:27 +0900</pubDate>
    </item>
    <item>
      <title>Swift: 앱 평가 요청하기</title>
      <link>https://kimswift.tistory.com/31</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱스토어 리뷰, 평가의 중요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 사람들은 뉴스나 Youtube를 볼 때 댓글부터 확인하는 경향이 있다고 합니다. 이는 타인의 의견을 중요하게 생각하는 문화가 커졌음을 보여준다고 생각합니다. 앱스토어에서도 마찬가지로 리뷰와 평가는 다운로드 전환율을 높이는 핵심 요소입니다. 저 또한 리뷰가 적거나 부정적인 앱은 다운로드를 망설이곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해, 긍정적이고 신뢰할 수 있는 평가가 많을수록 앱의 성공가능성이 높아집니다. 오늘은 이와 관련된 두 가지 내용을 다뤄보겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱에서 평가를 요청하는 방법&lt;/li&gt;
&lt;li&gt;긍정적인 평가를 유도할 수 있는 요청 시점 (개인 앱 사례) 앱 평가 요청하기&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;앱 평가 요청하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift에서는 앱 평가 요청을 매우 간단하게 구현할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI에서는 &lt;b&gt;Environment&lt;/b&gt;의 함수를 사용해 한 줄로 요청할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;@Environment(\.requestReview) private var requestReview

... 
private func presentReivew() { 
    requestReview() 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI를 사용하지 않는 경우에는 &lt;b&gt;SKStoreReviewController&lt;/b&gt;의 &lt;b&gt;requestReview(in:)&lt;/b&gt; 메서드를 사용합니다. 이때 기기에서 현재 활성화된 Scene을 전달해야 합니다.&lt;br /&gt;( for iPadOS, iOS )&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;SKStoreReviewController.requestReviewInCurrentScene()

extension SKStoreReviewController {
    public static func requestReviewInCurrentScene() {
        if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
            DispatchQueue.main.async {
                requestReview(in: scene)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MacOS에서는 아래와 같이 SKStoreReviewController의 &lt;b&gt;requestReview()&lt;/b&gt; 메서드를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;SKStoreReviewController.requestReview()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clVQZA/btsKLo9X7ZI/jxYwyeuGrEdPlgAk8PrEfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clVQZA/btsKLo9X7ZI/jxYwyeuGrEdPlgAk8PrEfK/img.png&quot; data-alt=&quot;평가 요청 알림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clVQZA/btsKLo9X7ZI/jxYwyeuGrEdPlgAk8PrEfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclVQZA%2FbtsKLo9X7ZI%2FjxYwyeuGrEdPlgAk8PrEfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;321&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;평가 요청 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;img src=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202024-11-16%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%205.14.15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가 요청 함수를 호출해도 평가 알림이 항상 표시되지는 않습니다. 시스템은 다음 조건에서만 평가 요청 알림을 표시합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 아직 리뷰, 평가를 하지 않은 경우&lt;/li&gt;
&lt;li&gt;연간 최대 3회까지만 요청 가능&lt;/li&gt;
&lt;li&gt;사용자가 리뷰를 남긴 경우엔, 이전 리뷰 후 1년이 지나고 새로운 버전이 출시된 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적절한 앱 평가 요청 시기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플의 &lt;a href=&quot;https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews&quot;&gt;Ratings and reviews HIG 문서&lt;/a&gt;의 공식 가이드라인은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;작업 중인 사용자에게 요청하지 않기&lt;/li&gt;
&lt;li&gt;짧은 기간 내 반복 요청하지 않기&lt;/li&gt;
&lt;li&gt;시스템의 기본 요청 컴포넌트를 사용하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 &lt;a href=&quot;https://developer.apple.com/documentation/storekit/requesting_app_store_reviews&quot;&gt;다른 문서&lt;/a&gt;에서는 앱 시작 직후의 리뷰 요청이나, 버튼 클릭을 통한 직접적인 리뷰 요청을 피하라고 권장합니다. ( 알림이 표시되지 않을 수 있기 때문)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 가이드라인을 바탕으로 &lt;b&gt;긍정적인 리뷰를 유도&lt;/b&gt;하기 위해서 제가 만든 &lt;a href=&quot;https://apps.apple.com/kr/app/bunny-%EB%A9%94%EB%89%B4%EB%B0%94-%EA%B0%A4%EB%9F%AC%EB%A6%AC/id6737684816?mt=12&quot;&gt;&amp;lrm;Bunny - 메뉴바 갤러리&lt;/a&gt;앱에서는 아래 조건을 충족할 때 리뷰를 요청합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱 설치 후 최소 5일 경과: 앱의 필요성을 인지한 사용자&lt;/li&gt;
&lt;li&gt;지난 요청한 앱 버전과 다른 버전인 경우: 의미 있는 업데이트 후 요청&lt;/li&gt;
&lt;li&gt;최소 실행 횟수 기준 충족: 실제 앱을 사용해 본 사용자&lt;/li&gt;
&lt;li&gt;중요 기능(앱 이미지 추가) 사용 후: 앱 사용법을 이해한 시점&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 다음과 같습니다.&lt;br /&gt;우선 조건을 나타내는 &lt;b&gt;shouldAskForRating&lt;/b&gt; 변수를 생성했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;timeSinceFirstLaunch: 앱 설치 후 5일 경과&lt;/li&gt;
&lt;li&gt;appversion: 지난번 리뷰 요청한 앱의 버전인 경우&lt;/li&gt;
&lt;li&gt;accessCount: 갤러리 실행(토글) 7회 이상&lt;/li&gt;
&lt;li&gt;ratingEventCount: 이미지 추가 2회 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731748872869&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    var shouldAskForRating: Bool {
        guard !isDebuggingEnabled else { return true }
        guard let firstLaunchDate = Defaults[.firstOpenDate] else { return false }
        let timeSinceFirstLaunch = Date().timeIntervalSince(firstLaunchDate)
        let timeUntilRate: TimeInterval = 60 * 60 * 24 * 5

        return Defaults[.accessCount] &amp;gt;= 7
        &amp;amp;&amp;amp; Defaults[.ratingEventsCount] &amp;gt;= 2
        &amp;amp;&amp;amp; timeSinceFirstLaunch &amp;gt;= timeUntilRate
        &amp;amp;&amp;amp; Defaults[.lastVersionPromptedForReview] != applicationVersionProvider()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건을 충족하는 경우에만 &lt;b&gt;askForRating()&lt;/b&gt; 함수를 통해 리뷰 요청을 합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;    func askForRatingIfNeeded() {
        guard shouldAskForRating else { return }
        askForRating()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;class RatingService {
    var isDebuggingEnabled: Bool {
        #if DEBUG
            return true
        #else
            return false
        #endif
    }

    var shouldAskForRating: Bool {
        guard !isDebuggingEnabled else { return true }
        guard let firstLaunchDate = Defaults[.firstOpenDate] else { return false }
        let timeSinceFirstLaunch = Date().timeIntervalSince(firstLaunchDate)
        let timeUntilRate: TimeInterval = 60 * 60 * 24 * 5

        return Defaults[.accessCount] &amp;gt;= 7
        &amp;amp;&amp;amp; Defaults[.ratingEventsCount] &amp;gt;= 2
        &amp;amp;&amp;amp; timeSinceFirstLaunch &amp;gt;= timeUntilRate
        &amp;amp;&amp;amp; Defaults[.lastVersionPromptedForReview] != applicationVersionProvider()
    }

    func askForRatingIfNeeded() {
        guard shouldAskForRating else { return }
        askForRating()
    }

    func askForRating() {
        Defaults[.lastVersionPromptedForReview] = applicationVersionProvider()

        SKStoreReviewController.requestReview()
    }

    func applicationVersionProvider() -&amp;gt; String {
        return Bundle.main.appVersion
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 함수를 이미지 추가를 처리하는 곳에서 호출하면 아래와 같이 리뷰 요청 알림이 나타납니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTOs0C/btsKMO7ypXR/LbbpFm209Y8J0OFbbmnMyK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTOs0C/btsKMO7ypXR/LbbpFm209Y8J0OFbbmnMyK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTOs0C/btsKMO7ypXR/LbbpFm209Y8J0OFbbmnMyK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cTOs0C/btsKMO7ypXR/LbbpFm209Y8J0OFbbmnMyK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;365&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;애플의 가이드라인 '작업 중인 사용자에게 요청하지 않기'을 따르기 위해서 아래와 같이 2초 딜레이를 주고, 사용자가 작업하지 않는 것이 확실할 때 리뷰를 요청할 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731749862987&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// Presents the rating and review request view after a two-second delay.
private func presentReview() {
    Task {
        // Delay for two seconds to avoid interrupting the person using the app.
        try await Task.sleep(for: .seconds(2))
        await requestReview()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Bunny앱의 경우에는 이러한 딜레이가 필요하지 않다고 판단했습니다. Bunny앱은 이미지 추가와 수정 기능을 모두 제공하고 있지만, 이미지 수정 부가적인 기능으로 모든 사용자가 반드시 활용하지는 않을 것으로 보았습니다. 따라서 이미지를 추가 직후 리뷰를 요청해도 사용자의 작업 흐름을 방해하지 않는다고 생각했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 사용 횟수 7회, 핵심 기능 2회 등과 같은 조건을 통해, 이미 앱의 주요 기능에 대한 이해가 있는 유저들에게만 리뷰 요청이 전달됩니다. 따라서, 이미지 크기 조절이 필요한 경우에도 해당 기능을 이미 숙지한 사용자들이라 부정적인 리뷰로 이어질 가능성은 낮다고 판단했습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 앱을 만드는 것만큼이나 그 가치를 사용자에게 전달하는 것이 중요해졌습니다. 적절한 시점의 리뷰 요청은 앱의 가치를 알리는 중요한 수단이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 요청 기능을 배포한 지 3일이 지났고, 2일 후면 유저들에게 첫 리뷰 요청이 시작됩니다. 경과를 해당 글에 업데이트하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2024.11.16 평가 1개..  )&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2298&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tWpyU/btsKKvPyni8/PB08CCd56d3aaZfKYgAckk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tWpyU/btsKKvPyni8/PB08CCd56d3aaZfKYgAckk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tWpyU/btsKKvPyni8/PB08CCd56d3aaZfKYgAckk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtWpyU%2FbtsKKvPyni8%2FPB08CCd56d3aaZfKYgAckk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2298&quot; height=&quot;484&quot; data-origin-width=&quot;2298&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2024.12.2 평가4개 리뷰1개 !!!!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 리뷰가 너무 적어서 조건을 바꿀 생각도 해봤는데 현재까지는 이론처럼 좋은 평가와 이론만 남겨서 기분이 좋네요 ㅎ...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2336&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb9cBL/btsK4KKLsPb/MANaFqyrkgkZgEHhumGHI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb9cBL/btsK4KKLsPb/MANaFqyrkgkZgEHhumGHI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb9cBL/btsK4KKLsPb/MANaFqyrkgkZgEHhumGHI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb9cBL%2FbtsK4KKLsPb%2FMANaFqyrkgkZgEHhumGHI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2336&quot; height=&quot;986&quot; data-origin-width=&quot;2336&quot; data-origin-height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;img src=&quot;image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ratings&amp;nbsp;and&amp;nbsp;reviews&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/storekit/requesting_app_store_reviews&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Requesting&amp;nbsp;App&amp;nbsp;Store&amp;nbsp;reviews&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.avanderlee.com/swift/skstorereviewcontroller-app-ratings/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Increase&amp;nbsp;App&amp;nbsp;Ratings&amp;nbsp;by&amp;nbsp;using&amp;nbsp;SKStoreReviewController&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기술 &amp;amp; 언어</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/31</guid>
      <comments>https://kimswift.tistory.com/31#entry31comment</comments>
      <pubDate>Sat, 16 Nov 2024 19:20:56 +0900</pubDate>
    </item>
    <item>
      <title>Swift: App, Scene, Window, View 알아보기</title>
      <link>https://kimswift.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 SwiftUI의 앱을 구성하고 있는 App, Scene, Window, View의 정의와 관계에 대해 알아보겠습니다.&lt;br /&gt;또한, Scene의 종류에 대해 알아보고 macOS에서 메뉴 바 TodoList 앱을 만들면서 Scene을 활용하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;App, Scene, Window, View&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI의 앱은 크게 App, Scene, View로 이루어져 있습니다. 아래 그림과 같이 &lt;b&gt;View들이 모여서 Scene이 되고 Scene들이 모여 App을 구성합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;1322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O1Faq/btsKf6bHhYg/S81CWEEzsVG3hKf6ZLn5tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O1Faq/btsKf6bHhYg/S81CWEEzsVG3hKf6ZLn5tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O1Faq/btsKf6bHhYg/S81CWEEzsVG3hKf6ZLn5tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO1Faq%2FbtsKf6bHhYg%2FS81CWEEzsVG3hKf6ZLn5tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1944&quot; height=&quot;1322&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;1322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;View&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 유저가 앱을 봤을 때 보이는 모든 것이 View라고 할 수 있습니다. UI의 이미지, 텍스트, 컨테이너 뷰 등 모든 것이 뷰입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Window&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Window는 기기의 스크린 위에 나타나는 물리적인 창을 의미합니다. Window는 뷰의 컨테이너 역할을 합니다. iOS에서는 전체 스크린을 꽉 채우는 하나의 Window가 있고 Window내의 뷰가 변화하면서 UI를 표시합니다. 반면, macOS에서는 여러 개의 Window를 통해 앱의 UI를 표시하는 것이 일반적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Scene&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scene은 스크린의 특정 구역을 의미합니다. 일반적으로 iOS, tvOS, watchOS에서는 하나의 Scene이 전체 스크린을 차지합니다. 반면, iPadOS, macOS에서는 여러 개의 Scene이 기기 스크린 위에 나타날 수 있습니다. 이 Scene은 Window의 컨테이너 역할을 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;App&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 Xcode에서 새로운 프로젝트를 만들 때 SwiftUI 프레임워크를 UI 프레임워크로 SwiftUI를 선택하면 App 파일이 생성됩니다.&lt;br /&gt;App 프로토콜을 채택하는 구조체가 있고 이 구조체는 앱의 &lt;b&gt;시작 지점(Entry point) 역할을 하게 됩니다&lt;/b&gt;. App 구조체는 하나 이상의 Scene으로 구성됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Mutliple Window 구현하기 (Scene)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI에서는 Scene을 활용해 여러 개의 Window를 지원하는 앱을 만들 수 있습니다. 아래는 일반적으로 사용하는 5개의 Scene을 정리한 표입니다.&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;유형&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;지원 플랫폼&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;WindowGroup&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모든 플랫폼에서 데이터 기반 앱을 구축.&lt;/td&gt;
&lt;td&gt;모든 Apple 플랫폼&lt;/td&gt;
&lt;td&gt;멀티 윈도우 프레젠테이션을 지원하며, 여러 창이 필요한 앱에 유용 (예: macOS, iPadOS).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DocumentGroup&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;문서 기반 앱을 구축.&lt;/td&gt;
&lt;td&gt;iOS, macOS&lt;/td&gt;
&lt;td&gt;문서를 관리하며, 텍스트 에디터나 파일 기반 앱에 유용.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Settings&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;앱 내 설정 인터페이스 정의.&lt;/td&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;앱 설정을 나타내는 인터페이스 제공.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Window&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;단일, 고유한 창을 나타냄.&lt;/td&gt;
&lt;td&gt;모든 Apple 플랫폼&lt;/td&gt;
&lt;td&gt;글로벌 앱 상태에 적합하며, 단일 창만 필요할 때 유용. 멀티 윈도우 동작을 허용하지 않음.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MenuBarExtra&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;시스템 메뉴 바에 지속적인 앱을 컨트롤할 수 있는 아이콘(레이블)을 나타냄.&lt;/td&gt;
&lt;td&gt;macOS (전용)&lt;/td&gt;
&lt;td&gt;앱이 실행 중이면 항상 사용 가능하며, 앱이 스크린에 있지 않아도 사용 가능.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개 이상의 Scene을 동시에 (보조 Scene으로) 사용할 수도 있습니다. 아래와 같이 App의 body에 Scene을 이어 작성하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 처음 실행될 때 처음 작성된 Scene이 화면에 나타나게 되기 때문에 &lt;b&gt;Scene을 작성하는 순서가 중요합니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct MenuBarTodoDemoApp: App {
    var body: some Scene {
        MenuBarExtra(&quot;App&quot;, systemImage: &quot;checklist&quot;) {
            ContentView()
        }
        .menuBarExtraStyle(.window)

        Window(&quot;About&quot;, id: &quot;about&quot;) {
            AboutView()
        }
        .windowResizability(.contentSize)

        WindowGroup(id: &quot;detail&quot;, for: TodoItem.self) { $item in
            if let item {
                DetailView(item: .constant(item))
            }
        }
        .windowResizability(.contentSize)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서는 MenuBarExtra가 제일 상단에 위치하기 때문에 앱 실행 시 어떤 Window도 나타나지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 하나의 Scene에서 다른 Scene으로 즉, 하나의 Window에서 다른 Window를 어떻게 띄울 수 있을까요?&lt;br /&gt;SwiftUI에서는 openWindow()라는 SwiftUI Environment내의 호출 가능한 타입을 지원합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;@Environment(\.openWindow) private var openWindow 
...
openWindow(id: &quot;about&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Menubar Todo App 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 Scene을 활용해 macOS의 Menubar에서 접근할 수 있는 간단한 Todo 리스트 앱을 만든다고 가정해 보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Menubar 클릭 시 Todo리스트 메인 Window(View) 표시&lt;/li&gt;
&lt;li&gt;Todo 항목 클릭 시 DetailView 표시&lt;/li&gt;
&lt;li&gt;Info 버튼 클릭 시 AboutView 표시&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pcqIh/btsKheGoZGn/HJIlUnCDVOGxxHXtzIbkY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pcqIh/btsKheGoZGn/HJIlUnCDVOGxxHXtzIbkY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pcqIh/btsKheGoZGn/HJIlUnCDVOGxxHXtzIbkY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpcqIh%2FbtsKheGoZGn%2FHJIlUnCDVOGxxHXtzIbkY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;464&quot; height=&quot;269&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App은 아래와 같이 MenubarExtra, WindowGroup, Window 3개의 Scene으로 구성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct MenuBarTodoDemoApp: App {
    @StateObject private var todoListStore = TodoListStore()

    var body: some Scene {
        MenuBarExtra(&quot;App&quot;, systemImage: &quot;checklist&quot;) {
            ContentView(todoListStore: todoListStore)
        }
        .menuBarExtraStyle(.window)
        
        WindowGroup(id: &quot;detail&quot;, for: TodoItem.ID.self) { $itemId in
            DetailView(itemId: itemId ?? UUID(), todoListStore: todoListStore)
        }
        .windowResizability(.contentSize)

        Window(&quot;About&quot;, id: &quot;about&quot;) {
            AboutView()
        }
        .windowResizability(.contentSize)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Menubar를 탭할 시 일반적인 Menu가 아닌 Window를 표시하기 위해 .menuBarExtraStyle(.window) 모디파이어를 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image%205.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rx3mJ/btsKiptrEZc/02Uv0EPUCeC5HU2dOInOV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rx3mJ/btsKiptrEZc/02Uv0EPUCeC5HU2dOInOV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rx3mJ/btsKiptrEZc/02Uv0EPUCeC5HU2dOInOV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frx3mJ%2FbtsKiptrEZc%2F02Uv0EPUCeC5HU2dOInOV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;253&quot; height=&quot;398&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;!-- {&quot;width&quot;:193} --&gt;&lt;br /&gt;AboutView의 경우 여러 개의 Window로 표시하는 것은 불필요하기에 Window Scene을 사용합니다. 이때 &lt;b&gt;id&lt;/b&gt;는 나중에 해당 Window를 다른 Window에서 열 때 사용됩니다. AboutView()의 경우 크기 조절하는 것도 불필요하기에 .windowResizability(.contentSize)을 사용해 해당 Window의 크기 조절을 막습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image%206.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCHbKC/btsKg0u2TTf/o1ww5NH8u9A0LK0Aa7IM60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCHbKC/btsKg0u2TTf/o1ww5NH8u9A0LK0Aa7IM60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCHbKC/btsKg0u2TTf/o1ww5NH8u9A0LK0Aa7IM60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCHbKC%2FbtsKg0u2TTf%2Fo1ww5NH8u9A0LK0Aa7IM60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;287&quot; height=&quot;436&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;930&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;!-- {&quot;width&quot;:187} --&gt;DetailView도 마찬가지로 여러 개의 Window로 표시하는 것은 불필요하지만, WindowGroup에서만 data를 주입해서 Window를 열 수 있기 때문에 WindowGroup을 사용합니다. 이때 ID, String과 같은 가벼운 타입을 사용하기를 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Window를 열기 위해선 SwiftUI View내부에서 openWindow(:id) 함수를 호출하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;@Environment(\.openWindow) var openWindow
...
Button {
    openWindow(id: &quot;about&quot;)
} label: {
    Image(systemName: &quot;info.circle.fill&quot;)
        .foregroundColor(.blue)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WindowGroup을 통해 Window를 열기 위해서는 id와 value를 넣어줍니다. 이때 value에 들어가는 타입은 앞서 WindowGroup에서 정의한 타입과 일치해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;@Environment(\.openWindow) var openWindow
...
ForEach($todoListStore.todoItems) { $item in
    HStack {
        Button {
            openWindow(id: &quot;detail&quot;, value: item.id)
        } label: {
            HStack {
                Text(item.text)
                    .foregroundColor(.primary)
                    .strikethrough(item.isCompleted)
                Spacer()
                Image(systemName: &quot;chevron.right&quot;)
                    .font(.caption)
                    .foregroundColor(.gray)
            }
        }
        .buttonStyle(PlainButtonStyle())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료:&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2020/10037/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;App&amp;nbsp;essentials&amp;nbsp;in&amp;nbsp;SwiftUI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2022/10061?time=341&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bring&amp;nbsp;multiple&amp;nbsp;windows&amp;nbsp;to&amp;nbsp;your&amp;nbsp;SwiftUI&amp;nbsp;app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기술 &amp;amp; 언어</category>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/30</guid>
      <comments>https://kimswift.tistory.com/30#entry30comment</comments>
      <pubDate>Thu, 24 Oct 2024 12:59:27 +0900</pubDate>
    </item>
    <item>
      <title>글또 10기 시작! 다짐 글</title>
      <link>https://kimswift.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 글또 9기에 이어 글또 10기에 다시 참여하게 되었습니다. 10기 활동을 잘 이어나가기 위해서 이번 기수에서 이루고자 하는 목표와 그 목표를 위한 액션 아이템을 정리해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또에 다시 지원하게 된 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 10기에 다시 지원하게 된 이유는 크게 두 가지입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;지속적인 동기부여&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예치금 제도로 인한 적당한 압박감 형성&lt;/li&gt;
&lt;li&gt;다른 참여자들의 열정적인 활동에서 받는 자극&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;글쓰기를 통한 학습 효과 극대화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글쓰기를 통한 자연스러운 복습&lt;/li&gt;
&lt;li&gt;학습 내용 견고화&lt;/li&gt;
&lt;li&gt;신뢰할 수 있는 개인 자료 축적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 9기 기간 이후에도 글을 계속 써오긴 했지만 활동 기간만큼 자주 쓰지는 못했습니다. 확실히 예치금 제도가 글을 계속 쓰는데 도움이 되었던 것 같습니다. 그리고 다른 분들이 매주 열심히 글을 작성하는 모습을 보면 자연스럽게 동기부여가 되었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9기 활동에서 느낀 것 중 하나는 글쓰기가 학습에 많은 도움이 된다는 것입니다. 저는 복습이 학습에서 정말 중요하다고 생각합니다. 글을 쓰는 과정에서 자연스럽게 학습한 내용이 복습되고, 조금 더 논리적으로 기억에 저장되는 것을 느낄 수 있었습니다. 또한, 개발을 하면서 자주 나오는 개념을 위주로 직접 정리하다 보니, 믿을 수 있고 바로 찾아볼 수 있는 자료가 생긴다는 점이 개발하는 데 있어서 큰 도움이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, 글또 활동을 통해 꾸준한 글쓰기 습관을 유지하고 이를 통해 학습 효과를 높이고자 10기에 참여하게 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또 10기, 목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 10기 활동에서 저의 목표는 &lt;b&gt;학습한 내용을 내가 나중에 봐도 쉽고 빠르게 이해할 수 있는 글을 작성하는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 쓰는 데에는 여러 가지 목적이 있겠지만 지금 저에겐 글쓰기는 학습을 위한 보조 도구 역할이 큽니다. 따라서, 저에게 좋은 글은 &amp;lsquo;&lt;b&gt;내가 이해하기 쉬운 글&lt;/b&gt;&amp;rsquo;이고 어떻게 하면 나중에 다시 봐도 쉽고 빠르게 이해할 수 있는 글을 작성할 수 있을지를 고민하고 실천하는 것이 이번 글또 10기 목표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 필수는 아니지만 한번 도전해보고 싶은 것은 소모임 활동입니다. 요즘 쉴 때 낭비되는 시간이 너무 많다고 생각이 들어서 글또에서 진행되는 소모임을 하나 참가해 보려고 생각합니다. 너무 많은 시간을 들이긴 어려울 것 같아 최근 파스타 만들면서 관심 생긴 야매요리또, 혹은 그림그려또, 월간 메이커스 중 하나를 참여해 보려고 생각 중입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;달성하기 위한 액션 아이템&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 새로운 글 포맷, 형식 활용해 보기 (도표 활용, 아웃라이너 활용, 목차)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 매월 초 월별 글감 미리 정하기 (기본기 3개, 프로젝트 1개 )&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 쉽게 이해할 수 있는 데 큰 역할을 하는 것이 이미지와 잘 정리된 도표라고 생각합니다. 지금까지는 시간이 많이 걸린다는 이유로 이미지나 도표 같은 건 거의 사용 못했던 것 같습니다. 하지만, 대부분이 글로만 이루어진 경우엔 나중에 다시 보거나 할 때 이해하기가 어려운 경우가 많았습니다. 따라서, 최대한 이미지와 같은 여러 장치를 활용해서 한눈에 핵심 내용을 파악할 수 있도록 하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 액션 아이템은 몇 달 전부터 해오던 것인데, 매월 초 글감을 3개 정도는 미리 정해놓고 1개는 그때그때 생각나는 것을 작성하면 주제에 대한 고민을 확연히 줄일 수 있었습니다. 이 습관은 이번 10기에서도 계속 유지해보려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>일상 &amp;amp; 생각</category>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/29</guid>
      <comments>https://kimswift.tistory.com/29#entry29comment</comments>
      <pubDate>Tue, 1 Oct 2024 03:52:57 +0900</pubDate>
    </item>
    <item>
      <title>Swift: 동시성 프로그래밍</title>
      <link>https://kimswift.tistory.com/28</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 다룰 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 동시성 프로그래밍 개념과 Swift에서 GCD를 활용한 동시성 프로그래밍에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동시성 vs. 병렬성 ( Concurrency vs. Parallelism )&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성은 여러 가지 작업을 동시(같은 시점)에 &quot;&lt;b&gt;다루는 개념&lt;/b&gt;&amp;rdquo;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림 예시) 싱글 코어 CPU, 3개의 작업 (Task, T1, T2, T3 )&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cx81D/btsJnGpz9Up/px124zpSPPS9kzcZaw1GA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cx81D/btsJnGpz9Up/px124zpSPPS9kzcZaw1GA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cx81D/btsJnGpz9Up/px124zpSPPS9kzcZaw1GA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCx81D%2FbtsJnGpz9Up%2Fpx124zpSPPS9kzcZaw1GA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1123&quot; height=&quot;445&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 그림을 보면 CPU는 모든 시점에서 &lt;b&gt;하나의 작업&lt;/b&gt;만을 수행하는 것을 확인할 수 있습니다. (T1 - T2 - T1 - T3&amp;hellip; 이런 식으로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 CPU 코어에서 작업을 바꾸는 것을 &lt;b&gt;콘텍스트 스위칭&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 여러 개의 작업을 번갈아 가면서 수행하는 것이지만, 사람의 눈에는 이 스위칭이 너무 빨라서 마치 CPU가 세 개의 작업을 동시에 하는 것처럼 보이게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;병렬성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬성은 여러 가지를 동시에 &lt;b&gt;&amp;ldquo;하는 개념&amp;rdquo;&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림 예시) 트리플 코어, 3개의 작업&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;1194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AJrpR/btsJlW1SqVZ/IHt4I2t14EXRvu39KlWbfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AJrpR/btsJlW1SqVZ/IHt4I2t14EXRvu39KlWbfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AJrpR/btsJlW1SqVZ/IHt4I2t14EXRvu39KlWbfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAJrpR%2FbtsJlW1SqVZ%2FIHt4I2t14EXRvu39KlWbfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1123&quot; height=&quot;1194&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 트리플 코어의 예시에서는 3개의 작업이 같은 시점에 같이 수행됩니다. 이는 싱글코어의 경우와는 다르게 &lt;b&gt;실제로&lt;/b&gt; 같은 시점에 3개의 작업이 동시에 진행되는 것을 의미합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;병렬적 동시성 (Parallel Concurrency)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬적 동시성은 하나 이상의 작업을 하나 이상의 CPU 코어에서 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림 예시) 더블 코어, 3가지 태스크&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOlP4n/btsJm6h81lJ/GCauwQ1oSxYvZCCkMd5GYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOlP4n/btsJm6h81lJ/GCauwQ1oSxYvZCCkMd5GYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOlP4n/btsJm6h81lJ/GCauwQ1oSxYvZCCkMd5GYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOlP4n%2FbtsJm6h81lJ%2FGCauwQ1oSxYvZCCkMd5GYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1123&quot; height=&quot;834&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;834&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;img src=&quot;oduQB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 코어에서는 작업 T1과 T3가 &lt;b&gt;동시적&lt;/b&gt;으로 진행되고 있고, 두 번째 코어에서는 작업 T2가 &lt;b&gt;병렬적&lt;/b&gt;으로 되고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 형태를 엄밀히 말하면 병렬적 동시성이라고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성은 여러 가지 작업을 한 번에 &lt;b&gt;다루는 개념&lt;/b&gt;, 병렬성은 여러 가지 작업을 한 번에 &lt;b&gt;하는 개념&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, 병렬성은 동시성의 하위 개념. 만약 특정 작업이 나눠질 수 없어 싱글 코어 환경에서도 동시적으로 처리되지 못하면, 당연히 병렬적으로도 불가능함.&lt;/li&gt;
&lt;li&gt;병렬성은 반드시 하드웨어가 뒷받침되어야 함. 싱글 코어 환경에서는 동시적 작업을 병렬적으로 처리할 수 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;실질적으론 iOS의 동시성은 병렬적 동시성을 의미하는 경우가 많을 것으로 생각함..&lt;/i&gt;&lt;br /&gt;&lt;a href=&quot;https://stackoverflow.com/a/76954105/11448352&quot;&gt;What is the difference between concurrency and parallelism?&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue vs. Threads&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift 언어 내에서의 동시성을 알아보기 전 용어 정리를 간단히 해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Task: 작업, 실행하고 싶은 코드 블록&lt;/li&gt;
&lt;li&gt;Queue: 작업의 목록 ( aka. 투두 리스트 ), 선입 선출&lt;/li&gt;
&lt;li&gt;Thread: 작업을 처리하는 주체, 작업은 Queue에 의해 배정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 작업의 흐름은 Queue -&amp;gt; Thread입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift에서는 GCD(Grand Central Dispatch)를 통해 Queue - Thread 간 상호작용을 추상화했습니다.&lt;br /&gt;&lt;b&gt;GCD 덕분에 개발자는 어떤 큐의 어떤 작업을 어떤 스레드에서 처리할지 고민 안 해도 됩니다.&lt;/b&gt;&amp;nbsp;이는 OS에서 가장 효율적인 방법으로 처리됩니다.&lt;br /&gt;&lt;a href=&quot;https://medium.com/macoclock/grand-central-dispatch-threads-vs-queues-sync-vs-async-serial-vs-concurrent-eb4f0075a21d&quot;&gt;Grand Central Dispatch: Threads vs Queues, Sync vs Async, Serial vs Concurrent&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://stackoverflow.com/questions/21202789/use-of-the-terms-queues-multicore-and-threads-in-grand-central-dispatch&quot;&gt;Use of the terms &quot;queues&quot;, &quot;multicore&quot;, and &quot;threads&quot; in Grand Central Dispatch&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GCD (Grand Central Dispatch)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GCD 용어 및 개념 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCD는 동시적 작업을 다루기 위한 저수준 API입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거창해 보이지만 GCD를 통한 동시적 작업은 아래와 같은 간단한 과정으로 이루어집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Queue(DispatchQueue)에 작업(Work Items)을 할당.&lt;/li&gt;
&lt;li&gt;GCD에서 어떤 스레드에서 어떤 작업(Work Items)을 처리할지 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatchQueue: Swift에서 사용하는 Queue 클래스 이름 ( 즉, 작업 목록 )&lt;br /&gt;Work Item: Queue가 처리할 작업 ( Queue 생성 혹은 생성 후 Queue에 할당되는 코드 블록 ).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatchQueue의 특징은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업을 비동기적, 혹은 동기적으로 보낼 수 있다.&lt;/li&gt;
&lt;li&gt;작업을 메인 스레드, 백그라운드 스레드로 보낼 수 있다.&lt;/li&gt;
&lt;li&gt;선입 선출 (FIFO) 특징을 가지고 있다.&lt;/li&gt;
&lt;li&gt;직렬 혹은 동시적인 특징을 가질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatchQueue 생성 방법은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let queue = DispatchQueue(label: &quot;com.hjsupernova.myQueue&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이블은 디버깅할 때 사용되고 중복되서는 안됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;동기 vs. 비동기 예시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했듯 Queue에 작업을 비동기, 동기로 보낼지 선택할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기 예시&lt;br /&gt;&lt;b&gt;.sync&lt;/b&gt; 메서드를 통해 현재 스레드에서 커스텀 큐로 작업을 동기적으로 보냈습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func simpleQueues() {
    let queue = DispatchQueue(label: &quot;com.hjsupernova.myQueue&quot;)

    queue.sync {
        for i in 0..&amp;lt;10 {
            print(&quot; &quot;, i)
        }
    }

    for i in 100..&amp;lt;110 {
        print(&quot;Ⓜ️&quot;, i)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 스레드 ( 메인스레드 )에서 큐에 동기적으로 작업을 보냈기 때문에 보낸 작업이 끝날 때까지 메인 스레드는 블락됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;  1
  2
  3
  4
  5
  6
  7
  8
  9
Ⓜ️ 100
Ⓜ️ 101
Ⓜ️ 102
Ⓜ️ 103
Ⓜ️ 104
Ⓜ️ 105
Ⓜ️ 106
Ⓜ️ 107
Ⓜ️ 108
Ⓜ️ 109&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 예시&lt;br /&gt;&lt;b&gt;.async&lt;/b&gt; 메서드를 통해 현재 스레드(메인)에서 커스텀 큐에 비동기로 작업을 보냈습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;queue.async {
    for i in 0..&amp;lt;10 {
        print(&quot; &quot;, i)
    }
}

for i in 100..&amp;lt;110 {
    print(&quot;Ⓜ️&quot;, i)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기로 작업을 보냈기 때문에 메인 큐가 다른 작업이 백그라운드에서 일어날 동안 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;  0
Ⓜ️ 100
Ⓜ️ 101
Ⓜ️ 102
Ⓜ️ 103
Ⓜ️ 104
  1
Ⓜ️ 105
Ⓜ️ 106
  2
Ⓜ️ 107
  3
Ⓜ️ 108
  4
Ⓜ️ 109
  5
  6
  7
  8
  9&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Quality Of Service (QoS), 큐의 우선순위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue의 우선순위를 부여해 어떤 큐의 작업이 먼저 끝나야 하는지를 정할 수 있습니다.&lt;br /&gt;이는 Quality of Service라고 불리고, 기본값은 Default입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 6가지 우선순위는 다음과 같습니다.&lt;br /&gt;userInteractive 가장 높은 우선순위 -&amp;gt; unspecified 가장 낮은 우선순위&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;userInteractive&lt;/li&gt;
&lt;li&gt;userInitiated&lt;/li&gt;
&lt;li&gt;default&lt;/li&gt;
&lt;li&gt;utility&lt;/li&gt;
&lt;li&gt;background&lt;/li&gt;
&lt;li&gt;unspecified&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메인스레드는 항상 최우선으로 처리됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우선순위가 같은 경우&lt;/b&gt;&lt;br /&gt;두 개의 큐의 qos 인자값을 userInitiated로 같도록 설정했습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func queuesWithQoS() {
    let queue1 = DispatchQueue(label: &quot;com.hjsupernova.queue1&quot;, qos: .userInitiated)
    let queue2 = DispatchQueue(label: &quot;com.hjsupernova.queue2&quot;, qos: .userInitiated)

    queue1.async {
        for i in 0..&amp;lt;10 {
            print(&quot; &quot;, i)
        }
    }

    queue2.async {
        for i in 100..&amp;lt;110 {
            print(&quot; &quot;, i)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 큐의 우선순위가 같음으로 병렬적으로 작업이 진행됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  0
  100
  101
  1
  102
  2
  3
  103
  4
  104
  105
  5
  6
  7
  8
  106
  9
  107
  108
  109&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우선순위가 차이 나는 경우&lt;/b&gt;&lt;br /&gt;두 번째 큐의 우선순위를 낮은 우선순위인 .utility로 변경했습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let queue1 = DispatchQueue(label: &quot;com.hjsupernova.queue1&quot;, qos: .userInitiated)
let queue2 = DispatchQueue(label: &quot;com.hjsupernova.queue2&quot;, qos: .utility)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 작업이 우선순위가 높음으로 더 많은 자원이 배정되어 우선적으로 처리됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  0
  100
  1
  2
  3
  4
  5
  6
  7
  8
  9
  101
  102
  103
  104
  105
  106
  107
  108
  109&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메인스레드&lt;/b&gt;&lt;br /&gt;메인스레드의 작업인 For문을 M이모티콘으로 표시해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func queuesWithQoS() {
    let queue1 = DispatchQueue(label: &quot;com.hjsupernova.queue1&quot;, qos: .userInitiated)
    let queue2 = DispatchQueue(label: &quot;com.hjsupernova.queue2&quot;, qos: .utility)

    queue1.async {
        for i in 0..&amp;lt;10 {
            print(&quot; &quot;, i)
        }
    }

    queue2.async {
        for i in 100..&amp;lt;110 {
            print(&quot; &quot;, i)
        }
    }

    for i in 1000..&amp;lt;1010 {
        print(&quot;Ⓜ️&quot;, i)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 메인 스레드 작업이 가장 먼저 처리되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Ⓜ️ 1000
Ⓜ️ 1001
Ⓜ️ 1002
  0
Ⓜ️ 1003
Ⓜ️ 1004
  1
Ⓜ️ 1005
  2
Ⓜ️ 1006
Ⓜ️ 1007
  3
  4
Ⓜ️ 1008
  5
  6
  100
Ⓜ️ 1009
  101
  102
  7
  103
  8
  104
  9
  105
  106
  107
  108
  109&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시 큐 (Concurrent Queue)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 모든 예시의 Queue는 직렬(Serial)적입니다. &lt;b&gt;(Custom Queue는 기본적으로 직렬입니다)&lt;/b&gt;&lt;br /&gt;즉, 하나의 큐에 두 개 이상의 작업을 넣으면 작업들이 동시에 처리되는 것이 아니라 들어온 순서대로 차례차례 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬 큐 예시&lt;br /&gt;직렬 큐 &lt;b&gt;anotherQueue&lt;/b&gt;에 세 가지 작업을 차례대로 넣어보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func concurrentQueues() {
    let anotherQueue = DispatchQueue(label: &quot;com.hj.anotherQueue&quot;, qos: .utility)
    anotherQueue.async {
        for i in 0..&amp;lt;10 {
            print(&quot; &quot;, i)
        }
    }
    anotherQueue.async {
        for i in 100..&amp;lt;110 {
            print(&quot; &quot;, i)
        }
    }
    anotherQueue.async {
        for i in 1000..&amp;lt;1010 {
            print(&quot;⚫️&quot;, i)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상했듯이 큐에 들어간 작업은 차례대로 한 작업이 끝나면 다음 작업이 진행되는 식으로 순서대로 진행됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  0
  1
  2
  3
  4
  5
  6
  7
  8
  9
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
⚫️ 1000
⚫️ 1001
⚫️ 1002
⚫️ 1003
⚫️ 1004
⚫️ 1005
⚫️ 1006
⚫️ 1007
⚫️ 1008
⚫️ 1009&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동시 큐 예시&lt;/b&gt;&lt;br /&gt;동시 큐를 생성하기 위해선 &lt;b&gt;attributes&lt;/b&gt;의 인자값에 &lt;b&gt;.concurrent&lt;/b&gt;를 넣으면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let anotherQueue = DispatchQueue(label: &quot;com.hj.anotherQueue&quot;, qos: .utility, attributes: .concurrent)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 작업들이 병렬적으로 진행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  0
  100
  1
⚫️ 1000
⚫️ 1001
⚫️ 1002
⚫️ 1003
  2
  3
  4
  5
⚫️ 1004
⚫️ 1005
⚫️ 1006
⚫️ 1007
⚫️ 1008
  101
  102
  103
  104
  105
  6
  7
  8
  9
  106
  107
⚫️ 1009
  108
  109&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Global Queue, Main Queue&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 Custom Queue를 만들어 사용할 필요 없이 시스템에서 제공하는 Global Queue를 사용해도 됩니다. Global Queue는 기본적으로 동시적 성격을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 &lt;b&gt;DispatchQueue.global()&lt;/b&gt;을 통해 Global Queue를 생성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let globalQueue = DispatchQueue.global(qos: .userInitiated)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 스레드의 큐인 Main Queue에는 다음과 같이 접근할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;DispatchQueue.main.async { 

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DispatchWorkItem 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatchWorkItem는 Queue에 넣을 작업을 대체할 수 있는 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 workItem 객체를 생성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let workItem = DispatchWorkItem { 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WorkItem은 &lt;b&gt;perform() 메서드를&lt;/b&gt; 통해 실행할 수 있습니다. 이 메서드는 현재 스레드(메인)에서 작업을 바로 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;workItem.perform()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;notify() 메서드를&lt;/b&gt; 사용해 해당 workItem이 끝나면 다른 queue에 알림을 보내 새로운 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;workItem.notify(queue: DispatchQueue.main) {
    print(&quot;value = &quot;, value)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;globalQueue.async(execute: workItem)&lt;/b&gt;와 같이 코드 블록 대신 생성한 &lt;b&gt;DispatchWorkItem&lt;/b&gt;을 보낼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func workItems() {
    let globalQueue = DispatchQueue.global(qos: .userInitiated)
    var value = 10

    let workItem = DispatchWorkItem {
        value += 5
    }

    globalQueue.async(execute: workItem)

    workItem.notify(queue: DispatchQueue.main) {
        print(&quot;value = &quot;, value)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.appcoda.com/grand-central-dispatch/&quot;&gt;Grand Central Dispatch (GCD) and Dispatch Queues in Swift&lt;/a&gt;&lt;/p&gt;</description>
      <category>기술 &amp;amp; 언어</category>
      <author>kim.drw</author>
      <guid isPermaLink="true">https://kimswift.tistory.com/28</guid>
      <comments>https://kimswift.tistory.com/28#entry28comment</comments>
      <pubDate>Sat, 31 Aug 2024 18:28:14 +0900</pubDate>
    </item>
  </channel>
</rss>