<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mingyu's velog</title>
        <link>https://velog.io/</link>
        <description>iOS &amp; macOS &amp; visionOS 개발하는 사람</description>
        <lastBuildDate>Mon, 23 Dec 2024 08:52:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. mingyu's velog. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gyu_u0108" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Swift 의 sleep() 함수 정복하기 - Swift Concurrency]]></title>
            <link>https://velog.io/@gyu_u0108/Swift-%EC%9D%98-sleep-%EA%B7%B9%ED%95%9C%EC%9C%BC%EB%A1%9C-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-Swift-Concurrency</link>
            <guid>https://velog.io/@gyu_u0108/Swift-%EC%9D%98-sleep-%EA%B7%B9%ED%95%9C%EC%9C%BC%EB%A1%9C-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-Swift-Concurrency</guid>
            <pubDate>Mon, 23 Dec 2024 08:52:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>sleep()</strong> 에 대해 알아보자!</p>
</blockquote>
<p>안녕하세요! </p>
<p>이번 글에서는 Swift 에서의 <strong>sleep()</strong> 함수들에 대해 알아보고, Swift Concurrency 의 <strong>Task.sleep()</strong> 를 어떻게 하면 더 잘 사용할 수 있는지 같이 고민해보도록 하겠습니다!</p>
<h2 id="blocking-방식의-sleep">Blocking 방식의 sleep()</h2>
<h3 id="1-sleep-함수">1. sleep() 함수</h3>
<p>sleep() 함수는 C 언어를 배우면서 한 번쯤 만나게 되는 근본 함수 중 하나입니다. 이 함수는 현재 스레드의 작업을 딜레이 시키는 간단한 기능을 가지고 있습니다.</p>
<pre><code class="language-swift">print(&quot;시작&quot;)
sleep(10)
print(&quot;10초 지남&quot;)</code></pre>
<p>Swift에서도 이 C 언어 기반의 sleep() 함수를 사용할 수 있습니다.</p>
<p>Swift는 macOS 및 iOS 같은 Darwin 기반의 OS 위에서 실행되며, 이들 시스템은 POSIX(Portable Operating System Interface) 표준을 따르기 때문이죠.</p>
<p><img src="https://velog.velcdn.com/images/gyu_u0108/post/6913eed2-b838-40bd-8cfe-bd5be972a5e4/image.png" alt=""></p>
<p>POSIX는 C 언어 기반의 함수 라이브러리(unistd.h 등)를 제공하며, 여기서 Swift의 sleep() 함수도 도입된 것입니다. </p>
<p>따라서 Swift에서의 sleep()은 C 언어의 sleep()을 그대로 활용하며, 동일한 방식으로 초 단위의 대기를 처리합니다.</p>
<h3 id="2-usleep-함수">2. usleep() 함수</h3>
<p>usleep() 함수는 sleep() 함수의 자매품입니다.</p>
<p>차이점은 초 단위의 대기를 처리하는 sleep() 함수와 달리 usleep() 함수는 마이크로초 단위의 대기를 처리할 수 있어 더 세밀한 시간 조작이 가능하다는 것입니다.</p>
<pre><code class="language-swift">print(&quot;시작&quot;)
usleep(10000000)
print(&quot;10초 지남&quot;)</code></pre>
<p>usleep() 함수도 sleep() 함수와 함께 unistd.h 에 포함되어 있습니다.</p>
<h3 id="3-threadsleep-함수">3. Thread.sleep() 함수</h3>
<p>sleep() 함수처럼 호출한 현재 스레드를 차단합니다.</p>
<p>이전의 C 언어 기반의 sleep() 함수와 다르게 Foundation 프레임워크에 포함되어 있으며 조금 더 Swift 스러운 설계로 구현된 함수입니다.</p>
<p><img src="https://velog.velcdn.com/images/gyu_u0108/post/8e6ee4c3-1112-4d36-acde-3a709c106fb5/image.png" alt=""></p>
<p>구현부를 살펴보면 두 종류의 방식으로 호출할 수 있습니다.</p>
<h4 id="sleepuntil-data-date">sleep(until: data: Date)</h4>
<pre><code class="language-swift">print(&quot;시작&quot;)
Thread.sleep(until: .now + 10)
print(&quot;10초 지남&quot;)</code></pre>
<h4 id="sleepfortimeinterval-ti-timeinterval">sleep(forTimeInterval ti: TimeInterval)</h4>
<pre><code class="language-swift">print(&quot;시작&quot;)
Thread.sleep(forTimeInterval: 10)
print(&quot;10초 지남&quot;)</code></pre>
<p>여기까지 스레드를 Blocking 시켜 작업에 딜레이를 주는 sleep() 함수에 대해 알아보았습니다.</p>
<p>하지만 스레드를 Blocking 시키는 방식의 sleep() 에는 크나큰 단점이 있습니다. 바로 비동기 프로그래밍에는 적합하지 않다는 것입니다.</p>
<h2 id="non-blocking-방식의-sleep">Non-Blocking 방식의 sleep()</h2>
<h3 id="1-asyncafter-함수">1. asyncAfter() 함수</h3>
<p>asyncAfter() 함수는 GCD(Grand Central Dispatch) 에 포함된 함수입니다.</p>
<pre><code class="language-swift">// Dispatch &gt; DispatchQueue
@preconcurrency public func asyncAfter(deadline: DispatchTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @Sendable @convention(block) () -&gt; Void</code></pre>
<p>asyncAfter() 함수는 sleep 라는 이름을 가지고 있지는 않지만, 이 글의 메인 주제인 Swift Concurrency 의 Task.sleep() 가 도입되기 전까지 비동기적인 대기를 구현하기 위한 거의 유일한 방법이였습니다.</p>
<p>호출 방법도 간단한데, deadline 에 작업이 종료될 시점을 작성하고 work 의 클로저에 실행할 코드를 작성 후 호출하면 됩니다.</p>
<pre><code class="language-swift">print(&quot;시작&quot;)
DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
    print(&quot;10초 지남&quot;)
}</code></pre>
<p>asyncAfter() 의 장점인 Non-Blocking 방식의 비동기적 대기에 대해 조금 더 알아보겠습니다.</p>
<p>asyncAfter() 의 작동은 sleep() 처럼 단순 스레드의 딜레이(대기) 가 아닌 작업의 suspend(중지) 입니다.</p>
<p>(작업 실행을 예약 하고 예약 시작 전까진 잠깐 suspend(중지), 단순히 예약이기 때문에 그 동안 다른 작업을 할 수 있게 됨)</p>
<p><img src="https://velog.velcdn.com/images/gyu_u0108/post/e3d53f8a-e571-4337-bf98-9dc257d383c3/image.svg" alt=""></p>
<p>하지만 Blocking 방식의 함수들은 스레드 자체를 딜레이 시키는 것이기 때문에 해당 스레드에서 일어나는 모든 작업이 멈추게 됩니다.</p>
<p>실제 동작을 간단히 두 가지의 sleep() 함수를 호출해서 비교하는 예제 코드로 살펴보며 더 자세히 알아보도록 하겠습니다!</p>
<p>먼저 Blocking 방식의 함수였던 sleep() 를 호출해보겠습니다.</p>
<pre><code class="language-swift">print(&quot;시작&quot;)
sleep()
print(&quot;10초 지남&quot;)
print(&quot;다른 작업 시작&quot;)</code></pre>
<blockquote>
<p>시작 
(10초 대기 후)
10초 지남
다른 작업 시작</p>
</blockquote>
<p>스레드를 Blocking 해서 대기를 했기 때문에 10초가 대기되는 동안 다른 작업도 함께 10초 동안 suspend 하게 됩니다.</p>
<p>이번에는 Blocking 방식의 함수였던 sleep() 를 호출해보겠습니다.</p>
<pre><code class="language-swift">print(&quot;시작&quot;)
DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
    print(&quot;10초 지남&quot;)
}
print(&quot;다른 작업 시작&quot;)</code></pre>
<blockquote>
<p>시작 
다른 작업 시작
(10초 대기 후)
10초 지남</p>
</blockquote>
<p>스레드가 Non-Blocking 상태였기 때문에 10초간 suspend 하는 중 다른 작업을 실행하게 됩니다. 그래서 다른 작업이 비동기적으로 잘 실행되었고 10초 후 &quot;10초 지남&quot; 도 잘 출력되게 되었습니다.</p>
<p>하지만 asyncAfter() 는 콜백 방식이라는 명확한 한계 때문에 클로저가 쌓이면서 가독성이 안좋아지는 &quot;콜백 지옥&quot;이라는 문제점을 야기하였고, 또 중간에 취소할 수 없다는 단점을 가지고 있었습니다.</p>
<h3 id="2-tasksleep-함수">2. Task.sleep() 함수</h3>
<p>그래서!! Swift Concurrency 에서는 asyncAfter() 의 단점을 완벽히 개선하여, 같은 sleep 기능을 제공하지만 훨씬 더 Swift 적인 Task.sleep() 가 나오게 되었습니다!</p>
<pre><code class="language-swift">// _Concurrency &gt; Task
public static func sleep(nanoseconds duration: UInt64) async throws</code></pre>
<p>Task.sleep() 는 호출시 호출한 Task 를 suspend 시킵니다.</p>
<p>이는 스레드를 차단하는 것이 아니고 Task 를 잠깐 일시 정지하는 것이기 때문에 최적화된 Non-Blocking 방식을 제공하며, 심지어 Task 기 때문에 작업 도중 언제든 취소할 수 있습니다.</p>
<p>Task.sleep() 를 사용하는 경우, 반드시 Task 컨텍스트 내부에서 사용해야하며 async 함수이기 때문에 await 와 함께 사용해야합니다.</p>
<pre><code class="language-swift">print(&quot;시작&quot;)
Task {
    try? await Task.sleep(nanoseconds: 10 * 1_000_000_000)
    print(&quot;10초 지남&quot;)
}</code></pre>
<p>(이거이거 Task.sleep() 맛 봐야겟지?)</p>
<h3 id="3-정리">3. 정리</h3>
<table>
<thead>
<tr>
<th align="left">함수</th>
<th align="center">Blocking 여부</th>
<th align="center">시간 단위</th>
<th align="center">비동기 지원</th>
<th align="center">취소 가능성</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>Thread.sleep()</code></td>
<td align="center">Blocking</td>
<td align="center">초/밀리초</td>
<td align="center">❌</td>
<td align="center">❌</td>
</tr>
<tr>
<td align="left"><code>usleep()</code></td>
<td align="center">Blocking</td>
<td align="center">마이크로초</td>
<td align="center">❌</td>
<td align="center">❌</td>
</tr>
<tr>
<td align="left"><code>sleep()</code></td>
<td align="center">Blocking</td>
<td align="center">초</td>
<td align="center">❌</td>
<td align="center">❌</td>
</tr>
<tr>
<td align="left"><code>DispatchQueue.asyncAfter()</code></td>
<td align="center">Non-Blocking</td>
<td align="center">초</td>
<td align="center">✅</td>
<td align="center">❌</td>
</tr>
<tr>
<td align="left"><code>Task.sleep()</code></td>
<td align="center">Non-Blocking</td>
<td align="center">나노초</td>
<td align="center">✅</td>
<td align="center">✅</td>
</tr>
</tbody></table>
<p>사실 딜레이를 구현하는 방법이 더 있긴 하지만 약간 사파 느낌이라 다루지 않았습니다ㅎ (ex. Timer, Combine)</p>
<h2 id="마무리">마무리</h2>
<p>Swift Concurrency 2편이였지만 Swift Concurrency 스러운 내용을 크게 담지 못하여서 조금 아쉬웠습니다. 그래도 이 글을 통해 sleep() 함수에 대해 흥미를 가지는 분이 생긴다면 전 그것만으로도 만족할 것 같습니다ㅎㅎ</p>
<p>긴 글 읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@MainActor 제대로 사용하기 - Swift Concurrency]]></title>
            <link>https://velog.io/@gyu_u0108/MainActor-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Swift-Concurrency-1</link>
            <guid>https://velog.io/@gyu_u0108/MainActor-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Swift-Concurrency-1</guid>
            <pubDate>Fri, 20 Dec 2024 08:49:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Swift Concurrency 의 <strong>@MainActor</strong> 에 대해 알아보자!</p>
</blockquote>
<p>안녕하세요! </p>
<p>이번 글에서는 Swift 를 사용해 UI 개발을 할 때 정말 자주 사용되는 Swift Concurrency 문법인 <strong>@MainActor</strong> 에 대해 알아보고, 어떻게 하면 제대로 사용할 수 있는지 같이 살펴보도록 하겠습니다!</p>
<h2 id="mainactor">MainActor</h2>
<h3 id="1-mainactor-란">1. MainActor 란?</h3>
<p>MainActor 는  <a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0316-global-actors.md">SE-0316: Global Actors</a> 에서 도입된 <code>Global Actor</code> 의 한 종류이며, 특정 코드가 메인 스레드에서 실행되도록 보장해주는 역할을 합니다.</p>
<pre><code class="language-swift">@globalActor final public actor MainActor : GlobalActor { }</code></pre>
<p>(MainActor 구현부)</p>
<p>여기서 중요하게 봐야할 부분은 바로 &quot;특정 코드가 메인 스레드에서 실행되는 것을 보장한다&quot; 입니다.</p>
<h3 id="2-메인-스레드란">2. 메인 스레드란?</h3>
<p>iOS 앱은 앱이 실행될 때 기본적으로 하나의 메인 스레드에서 시작됩니다. 이때 메인 스레드는 UI 업데이트와 사용자 이벤트 처리를 담당합니다.</p>
<p>여러분들이 Xcode 디버거에서 자주 보셨던 <code>Thread 1</code>로 표시되는 스레드가 바로 메인 스레드입니다! 메인 스레드에서 실행 중인 코드나 작업은 Xcode 디버그 세션의 <code>Thread 1</code>에서 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/gyu_u0108/post/9c6609b6-9ed7-49a8-9ec5-1d0e13ca2b86/image.png" alt=""></p>
<p>(예를 들어, 앱의 UI 가 업데이트 되거나 버튼을 누르는 등의 이벤트를 처리하는 동안 디버거에서 <code>Thread 1</code>에 해당 작업이 표시됩니다.)</p>
<h3 id="3-만약-다른-스레드에서-ui-를-업데이트-한다면">3. 만약 다른 스레드에서 UI 를 업데이트 한다면?</h3>
<p>만약 메인 스레드가 아닌 다른 스레드에서 UI 를 업데이트 하게 되면 어떤 문제가 발생할까요? 바로 <strong>런타임 경고</strong>가 발생합니다. (보라보라한 경고)</p>
<pre><code class="language-swift">Text(viewModel.labelText)
    .onAppear {
        viewModel.updateLabelText()
    }

class ViewModel: ObservableObject {
    @Published var labelText: String = &quot;&quot;

    func updateLabelText() {
        Task {
            let data = await fetchData()
            labelText = data.text
        }
    }
}</code></pre>
<p>(일부 코드 생략)</p>
<p>SwiftUI 를 통해 대표적인 예시를 하나 들어보자면, 주로 개발을 하며 어떠한 비동기적인 작업을 처리하기 위해서는 Task 블럭 내부에서 비동기적인 작업을 진행하고 결과를 @Published 변수에 업데이트합니다.</p>
<p>하지만 Task 블럭의 내부에서 실행되는 코드는 Swift 런타임에 의해 관리되며, 작업의 성격에 따라 적절한 스레드에서 실행됩니다. 즉, 메인 스레드가 아닌 다른 스레드에서 실행될 수도 있다는 것입니다.</p>
<p>해당 코드를 사용한 앱을 실행하게 되면 다른 스레드에서 labelText 의 업데이트가 진행되기 때문에 아래와 같이 <strong>런타임 경고</strong>가 뜨게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/gyu_u0108/post/47fcc921-f204-4b44-b67c-c9212b233943/image.png" alt=""></p>
<blockquote>
<p>Publishing changes from background threads is not allowed; make sure to publish values from the main thread.</p>
</blockquote>
<p>(실제로 위와 같은 보라색 경고가 뜨면 화면에선 큰 문제가 발생하지 않는 것처럼 보이지만, Data Race 가 일어나거나 UI 업데이트가 멈추는 경우도 발생할 수 있고, 크러쉬가 나기도 합니다.)</p>
<h2 id="mainactor-1">@MainActor</h2>
<h3 id="1-mainactor-를-사용하는-방법">1. @MainActor 를 사용하는 방법</h3>
<p>간단히 클래스, 구조체, 변수, 함수 등에 <code>@MainActor</code> 를 명시적으로 적용하기만 하면, 해당 코드가 항상 메인 스레드에서 실행되게 됩니다.</p>
<pre><code class="language-swift">@MainActor var url: URL?

@MainActor func updateLabelText() { }</code></pre>
<p>주로 <code>ObservableObject</code> 혹은 <code>@Observable</code> 을 사용하는 클래스는 UI 업데이트와 직접적인 연관성이 크기 때문에, <code>@MainActor</code> 를 명시하는 것이 좋습니다.</p>
<pre><code class="language-swift">@MainActor
class ViewModel: ObservableObject { }
// 혹은
@Observable @MainActor
class ViewModel { }</code></pre>
<p>단순히 클래스에 <code>@MainActor</code> 를 명시하는 것만으로도 해당 클래스의 모든 메서드나 프로퍼티들이 메인 스레드에서의 실행을 보장하게 되기 때문이죠.</p>
<p>여기까지 보면 CGD 보다 훨씬 직관적이고 쉽다..! 라는 생각이 듭니다.
하지만 방심하면 큰일나게 됩니다. 언제나 예외는 있거든요ㅎㅎㅎ</p>
<h3 id="2-mainactor-를-쓰면-안되는-경우">2. @MainActor 를 쓰면 안되는 경우?</h3>
<p>예시 코드였던 updateLabelText() 에 <code>@MainActor</code> 를 적용해보겠습니다.</p>
<pre><code class="language-swift">@MainActor func updateLabelText() {
    Task {
        let data = await fetchData()
        labelText = data.text
    }
}</code></pre>
<p>실제로 호출해보면 정상적으로 메인 스레드에서 실행되게 됩니다!</p>
<p>하지만... 만약 여기서 호출한 fetchData() 가 네트워크 요청, 파일 읽기, 또는 CPU 집약적인 작업이라면, 앱의 성능에 심각한 영향을 미칠 수도 있습니다.</p>
<p>(메인 스레드가 이 작업을 처리하느라 다른 UI 작업을 처리하지 못하게 되기 때문이죠.)</p>
<p>그런 경우 메인 스레드에서 실행되어야 하는 작업과 아닌 작업을 분리시켜야 합니다.</p>
<h3 id="2-1-nonisolated">2-1. nonisolated</h3>
<p>만약 <code>@MainActor class</code> 내부에 있는 특정 프로퍼티나 메서드를 메인 스레드에서 실행시키고 싶지 않다면 <code>nonisolated</code> 를 명시하면 해당 프로퍼티나 메서드는 MainActor 의 영향을 받지 않고 실행하게 됩니다.</p>
<pre><code class="language-swift">// @MainActor 가 명시된 코드블럭 내부에 있는 메서드입니다.
nonisolated func updateLabelText() {
    Task {
        let data = await fetchData()
        labelText = data.text
    }
}</code></pre>
<p>(여기서 왜 하필 키워드가 isolated(분리, 격리) 가 아닌 <strong>non</strong>isolated 인가요? 라는 의문이 들었습니다. 전 오히려 MainActor 의 영역에서 분리하는 느낌이였기 때문에 isolated 가 더 어올린다고 생각했었어요.)</p>
<p>찾아보니 사실 &quot;Actor 에 포함된 모든 것들은 이미 격리된 상태이다&quot; 라는 것이 Swift 의 기본 동작이였다고 합니다. 그래서 <code>nonisolated</code> 는 &quot;격리된 상태에서 벗어난&quot; 특별한 예외 케이스를 명시하는 것이라고 합니다.
(<a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md">SE: 0313-actor-isolation-control</a>)</p>
<p>하지만 단순히 해당 메서드 전체를 MainActor 에서 부터 격리 해버린다면 fetchData() 뿐만 아니라 UI 업데이트 코드까지 백그라운드 스레드에서 동작해버리게 됩니다.</p>
<h3 id="2-2-mainactorrun">2-2. MainActor.run()</h3>
<pre><code class="language-swift">extension MainActor {
    /// Execute the given body closure on the main actor.
    public static func run&lt;T&gt;(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -&gt; T) async rethrows -&gt; T where T : Sendable
}</code></pre>
<p>(구현부)</p>
<p>단순히 일부 코드만 메인 스레드에서 실행시키기 위해서는 <code>MainActor.run()</code> 을 호출하면 됩니다. </p>
<p>이 경우 해당 코드가 어떤 스레드에서 실행되고 있었든 <code>MainActor.run()</code> 의 body 에 있는 코드는 무조건 메인 스레드에서 실행되게 됩니다.</p>
<pre><code class="language-swift">func updateLabelText() async {
    // 다른 스레드에서 fetchData() 호출 후 결과 대기
    let data = await fetchData()

    // 메인 스레드에서 UI 업데이트
    await MainActor.run {
        labelText = data.text
    }
}</code></pre>
<p>혹은 <code>MainActor.run()</code> 이 리턴값을 가지게 할 수도 있습니다.</p>
<pre><code class="language-swift">func printName() async {
    let name = await MainActor.run {
        return &quot;mingyu lee&quot;
    }
    print(name)
}</code></pre>
<p>여기서 비슷한 역할을 하는 GCD 의 DispatchQueue.main.async() 가 떠오르게 됩니다. DispatchQueue.main.async() 와 <code>MainActor.run()</code> 는 작동 방식에서의 차이가 있는데, 여기서 <code>MainActor.run()</code> 의 정점이 나타나게 됩니다.</p>
<p>먼저 DispatchQueue.main.async() 를 통해 메인 스레드에 작업을 요청하면, 메인 스레드의 작업 큐에 올라가게 됩니다. 그 후 다음 Run Loop 가 오면 실행되게 됩니다. (그래서 어떠한 상황이든 작업 지연이 발생하게 됩니다.)</p>
<p>반면, <code>MainActor.run()</code> 의 코드가 이미 메인 스레드에서 실행 중이라면 작업을 바로 실행하게 됩니다. (메인 스레드에서 실행 중이 아니여도 즉시 메인 스레드로 전환하여 실행합니다)</p>
<p>이는 아까 찾아본 Actor 가 <strong>액터 격리(isolation)</strong> 를 기반으로 동작하기 때문에 가능한 최적화라고 합니다.</p>
<h3 id="2-2-task--mainactorrun">2-2. Task + MainActor.run()</h3>
<p>만약 결과를 기다리지(await) 않고 MainActor 에서 실행되게 하기 위해서는 간단히 Task 를 따로 만들어 주면 됩니다.</p>
<pre><code class="language-swift">func printName() {
    Task {
        await MainActor.run {
            print(&quot;mingyu&quot;)
        }
    }
    print(&quot;lee&quot;) // print(&quot;mingyu&quot;) 를 기다리지 않고 출력됩니다. 
}
// lee
// mingyu</code></pre>
<p>혹은 Task 의 Closure 에 { @MainActor in } 을 사용해서 표현할 수도 있습니다.</p>
<pre><code class="language-swift">func printName() {
    Task { @MainActor in
        print(&quot;mingyu&quot;)
    }
    print(&quot;lee&quot;) 
}
// lee
// mingyu</code></pre>
<p>(해당 방법은 MainActor 에서 실행 중이더라도 작업이 Swift 런타임에 의해 관리되며, 실행 시점이 약간 지연될 수 있다는 점에서 <code>MainActor.run()</code> 과 차이가 있습니다.)</p>
<p>하지만 해당 방법은 동기식 컨텍스트에서 자주 쓰이게 됩니다. 이 말은 await 를 명시하지 않고도 <code>MainActor.run()</code> 을 호출할 수 있다는 것이기 때문에 저도 개발하면서 대부분 해당 방법을 사용합니다!</p>
<h2 id="마무리">마무리</h2>
<p>학교 다닐 때, Swift Concurrency 에 대한 강연을 열어서 발표했을 정도로 개인적으로 Swift 에서 가장 좋아하는 기술 스택인 Swift Concurrency 에 대한 글을 적을 수 있어서 좋았습니다ㅎㅎ </p>
<p>긴 글 읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVVM 패턴에서 SwiftData 구현하기]]></title>
            <link>https://velog.io/@gyu_u0108/SwiftUI-MVVM-%EC%97%90%EC%84%9C-SwiftData-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@gyu_u0108/SwiftUI-MVVM-%EC%97%90%EC%84%9C-SwiftData-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 25 Oct 2024 06:34:36 GMT</pubDate>
            <description><![CDATA[<p>(유명한 SwiftUI + ViewModel 밈) </p>
<blockquote>
<p><strong>SwiftUI 의 View 에서 SwiftData 의 로직을 분리시켜보자!</strong></p>
</blockquote>
<h2 id="배경">배경</h2>
<p>visionOS 앱을 개발하며 나름 세상에 공개된 지 얼마 되지 않은 따끈따끈한 프레임워크인 <code>SwiftData</code>를 적용해보기로 하였습니다. 하지만 제가 개발하고 있었던 visionOS 앱은 무려 <code>SwiftUI + MVVM</code>로 개발 중인 앱이었습니다.</p>
<p>위 썸네일처럼, 몇몇 SwiftUI 개발자분들께서는 <code>SwiftUI + MVVM</code>은 족쇄다! 지양해야 한다! 라고 하셨습니다. </p>
<p>하지만 저는 <code>Combine</code> 덕분에 <code>SwiftUI + MVVM</code> 코드에 대해 별 불편함 없이 만족하며 잘 살아왔습니다. (보드야 뭐 잘 달리기만 하면 그만이니까요ㅎ)</p>
<p><code>SwiftUI + MVVM</code>이었던 이번 프로젝트에서도 저는 어느 때와 같이 View와 State 를 가진 프로퍼티가 완벽히 분리된 그런 <strong>MVVM스러운 코드</strong>를 짜고 싶었습니다. 하지만 결국 찾아오고야 말았습니다.</p>
<pre><code class="language-swift">import SwiftUI
import SwiftData

/// Model
struct Data: Identifiable {
    var id: UUID
    var text: String
}

/// View
struct ExampleView: View {
    @Query(sort: \.text, order: .forward) var data: [Data]
    @Environment (\.modelContext) var context

    var body: some View {
        List(data) {
            Text($0.text)
        }
    }
}</code></pre>
<blockquote>
<p>바로 <code>SwiftData</code>의 <strong><code>@Query macro</code></strong>로 만든 프로퍼티를 View에서 분리하는 과정에서 평소처럼 easy하게 ViewModel로 보낼 수 없다는 것이였습니다.</p>
</blockquote>
<p>그리곤 생각했죠. &quot;아? 어!? 이건 아니지...&quot; MVVM스러운 코드를 원했던 저는 <strong>너무 불편했습니다.</strong> 바로 해결하러 가보도록 하겠습니다.</p>
<h2 id="해결">해결</h2>
<h3 id="modelcontext">ModelContext</h3>
<p>먼저 SwiftData + MVVM 구현하기 위해서는 <code>SwiftData</code>의 <code>modelContext</code>가 가지는 역할을 알아야 합니다. 간단하게  <code>modelContext</code>는 데이터모델의 CRUD(추가, 읽기, 업데이트, 삭제) 기능을 수행해주는 역할을 합니다.</p>
<pre><code class="language-swift">@Environment (\.modelContext) var context
context.fetch()
context.delete()
context.insert()
context.save()</code></pre>
<p>&quot;엇? 그러면 이 <code>modelContext</code>를 View 레이어에서 분리한다면 <code>@Query</code>를 사용하지 않고도 <code>ViewModel</code>에서의 데이터 CRUD 처리가 가능하지 않을까요?&quot;</p>
<p>여기서 이 의문을 해결하기 위한 키포인트는 이 context 를 담고 있는 Container 입니다.</p>
<h3 id="modelcontainer">ModelContainer</h3>
<p>사실 <code>@Environment</code> 프로퍼티 래퍼를 통해 <code>modelContext</code> 를 가져오기 위해 먼저 <code>ModelContainer</code> 를 생성하고 이 <code>ModelContainer</code> 를 Scene 의 환경으로 설정하는 과정을 가져야 합니다.</p>
<pre><code class="language-swift">/// @main App
WindowGroup {
    ContentView()
}
.modelContainer(ModelContainer)</code></pre>
<p>해당 코드에 등장한 <code>ModelContainer</code>객체는 앱의 스키마와 모델 스토리지를 관리하는 객체로서 context 를 제공해주는 역할을 합니다. </p>
<p>이 말은 즉슨, 반대로 이 <code>ModelContainer</code> 를 통해 <code>modelContext</code> 에 접근하여 context 의 기능을 사용할 수 있다는 것 입니다.</p>
<pre><code class="language-swift">/// ModelContainer 구현부
public class ModelContainer : Equatable, @unchecked Sendable {
    // 생략
    @MainActor public var mainContext: ModelContext { get }
}</code></pre>
<p>실제로 <code>ModelContainer</code>의 구현부를 보면 get only 인 <code>ModelContext</code> 가 프로퍼티의 형태로 저 써주세요..!! 하는 느낌으로 존재하고 있는 것을 확인할 수 있었습니다.</p>
<h3 id="viewmodel--swiftdata">ViewModel + SwiftData</h3>
<p>이제 View 레이어와 이 SwiftData 로직을 분리하기 위한 코드를 구현해보도록 하겠습니다. 가장 먼저 해야할 부분은 <code>ViewModel</code> 에서 <code>ModelContext</code> 에 접근할 수 있도록 하는 객체를 만드는 것 입니다.</p>
<pre><code class="language-swift">final class DataService {
    private let modelContainer: ModelContainer
    private let modelContext: ModelContext

    @MainActor static let shared = DataService()

    @MainActor private init() {
        self.modelContainer = try! ModelContainer(
            for: DataModel.self,
            configurations: ModelConfiguration(isStoredInMemoryOnly: false)
        )
        self.modelContext = modelContainer.mainContext
    }
}</code></pre>
<p><code>DataService</code>라는 이름의 해당 class 는 딱 하나의 <code>ModelContainer</code> 객체를 가질 수 있게 싱글톤으로 구현되어 있습니다. 또 유심히 봐야할 부분은 <code>@MainActor</code> 어노테이션입니다. </p>
<p><code>modelContainer</code> 의 <code>mainContext</code> 는 <code>@MainActor</code> 어노테이션을 통해 정의되어 있습니다. 그래서 <code>mainContext</code> 에 접근하는 요소에도 함께 <code>@MainActor</code> 를 작성해주셔야 합니다!</p>
<pre><code class="language-swift">final class DataService {
    // 생략
    func fetchDatas() -&gt; [DataModel] {
        do {
            return try modelContext.fetch(FetchDescriptor&lt;DataModel&gt;())
        } catch {
            fatalError(&quot;error fetchDatas\n\(error.localizedDescription)&quot;)
        }
    }

    func addData(_ data: DataModel) {
        modelContext.insert(workspace)
        do {
            try modelContext.save()
        } catch {
            fatalError(error.localizedDescription)
        }
    }
}</code></pre>
<p>그리고는 <code>DataService</code>내에서 <code>modelContext</code>에 접근해 원하시는 기능들을 구현해주시면 됩니다. (사실 애플에 따르면 <code>modelContext.save()</code>는 ModelContext에 autosave 기능이 존재하기 때문에 꼭 명시적으로 호출하지 않아도 된다고 합니다!)</p>
<p>이제 이 객체를 활용한 ViewModel을 구현해보도록 하겠습니다.</p>
<pre><code class="language-swift">@Observable
final class ViewModel {
    var datas: [dataModel] = []

    private let dataService: DataService

    init(dataService: DataService) {
        self.dataService = dataService
    }

    func addData() {
        let data = dataModel(data: &quot;my data&quot;)
        dataService.addData(data)
    }
}</code></pre>
<p>해당 <code>ViewModel</code>에는 <code>DataService</code>의 객체를 통해 데이터를 삽입하는 기능이 구현되어 있습니다. 하지만 여기서 사소하지만 약간 아쉬운 문제가 발생하게 됩니다.</p>
<p>바로 <code>@Query</code> macro만의 내부 DB와 항상 동기화된 상태, 즉 Observable한 상태를 포기하게 된 것이고 <strong>SwiftData 의 장점을 활용하지 못하게</strong> 되었다는 것입니다...</p>
<p>(당연하지만 <code>@Query</code> macro의 또 다른 장점인 filter도 활용하지 못한다는 것을 글을 쓰면서 알게 되었습니다...)</p>
<pre><code class="language-swift">@Observable
final class ViewModel {
    /// 생략 
    init(dataService: DataService) {
        self.dataService = dataService
        datas = dataSource.fetchDatas() // fetch
    }

    func addData() {
        let data = dataMoel(data: &quot;my data&quot;)
        dataService.addData(data)
        datas = dataSource.fetchDatas() // fetch
    }
}</code></pre>
<p>그래서 어쩔 수 없이 <code>fetch()</code> 를 직접 명시적으로 호출하는 방식으로 datas 의 업데이트를 구현하였습니다. (Update 혹은 Delete 로직이 들어간 함수가 있다면 동일하게 fetch 를 마무리로 종료시켜야 합니다.)</p>
<p>사실 MVVM 에서 SwiftData 를 사용하는 시도 자체가 애플에서 원하는 방향성과 멀어졌다고 생각하기에 아쉽지만 넘어가도록 하겠습니다. <del>(애플이 정말 원하는게 뭘까..)</del></p>
<h3 id="view">View</h3>
<p>이제 마지막으로 구현했던 ViewModel 을 View 에 추가해주도록 하겠습니다.</p>
<pre><code class="language-swift">/// 이전 코드
struct ExampleView: View {
    @Query(sort: \.text, order: .forward) var data: [Data]
    @Environment (\.modelContext) var context

    var body: some View {
        List(data) {
            Text($0.text)
        }
    }
}
/// SwiftData + MVVM 코드
struct ExampleView: View {
    var viewModel: ViewModel = ViewModel(dataSource: .shared)

    var body: some View {
        VStack {
            List(viewModel.datas) {
                Text(&quot;\($0.data)&quot;)
            }
        }
    }
}</code></pre>
<p>View 와 State 가 분리된 그런 코드가 완성되었습니다!</p>
<h2 id="결론">결론</h2>
<p>해당 문제를 해결하기 위해 <a href="https://developer.apple.com/videos/play/wwdc2023/10187">애플</a>에서 제공하는 코드 스니펫들을 여럿 읽어보았습니다. 하지만 MVVM과 결합하여 사용하는 코드 스니펫은 눈을 씻고 찾아봐도 없었습니다. (당연하지...)</p>
<p>사실 이번 <code>MVVM + SwiftData</code>는 큰 고민 없이 해결한 문제였긴 하지만, 애플이 원하는 SwiftUI + MVVM 의 방향성에 대해 약간의 의문을 가지게 되는 계기가 되었습니다.</p>
<p>저만의 뇌피셜이긴 하지만 애플이 <code>@Observable</code> macro를 만들어준 이유는 &quot;야, 니들 ViewModel 더 편하게 만들어서 써<del>&quot;인 줄 알았는데, 또 나름 비슷한 시기에 출시한 SwiftData에서는 &quot;ViewModel 쓰지 마</del> 그럼 더 편하게 해줄게&quot;인 것 같습니다...</p>
<p>아직 SwiftData를 적용할 프로젝트가 많진 않을 것 같지만, 저랑 똑같은 문제를 겪은 분들이 이 글을 보고 도움을 받으시면 좋겠다는 생각이 들어 해결 프로세스를 공유하는 글을 써보았습니다.</p>
<blockquote>
<p>여기까지 읽어주시느라 정말 수고 하셨습니다. 감사합니다!</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>