<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sustainable-git.log</title>
        <link>https://velog.io/</link>
        <description>https://github.com/sustainable-git</description>
        <lastBuildDate>Wed, 26 Feb 2025 14:18:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sustainable-git.log</title>
            <url>https://velog.velcdn.com/images/sustainable-git/profile/f95153ca-8547-4dc2-aa5f-63429dde945d/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sustainable-git.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sustainable-git" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[접근성을 고려한 iOS 앱 개발 (4) - VoiceOver 직접 설정하기]]></title>
            <link>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-iOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-4-VoiceOver-%EC%A7%81%EC%A0%91-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-iOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-4-VoiceOver-%EC%A7%81%EC%A0%91-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Feb 2025 14:18:03 GMT</pubDate>
            <description><![CDATA[<h2 id="코드로-설정해보자">코드로 설정해보자</h2>
<p>Apple은 다양한 UI 요소에 VoiceOver 기능을 기본적으로 내장해 두었습니다.
하지만, 개발자가 접근성을 고려하지 않거나 커스텀 UI를 사용하게 되면 VoiceOver 기능이 정상적으로 동작하지 않을 수 있습니다.
이런 경우 VoiceOver 기능을 직접 설정하는 방법을 알아보겠습니다.</p>
<br>

<h2 id="voiceover-활성화-비활성화">VoiceOver 활성화, 비활성화</h2>
<pre><code class="language-swift">// UIKit
let view = UIView()
view.isAccessibilityElement = true

// SwiftUI
Text(&quot;텍스트&quot;)
  .accessibilityHidden(true)</code></pre>
<p>앱을 개발하다 보면 VoiceOver로 읽어야 하는 경우와 읽지 않아야 하는 경우가 생길 수 있습니다.
이 때는 <code>isAccessibilityElement</code> 또는 <code>accessibilityHidden(_:)</code> 을 사용하여 조정할 수 있습니다.</p>
<br>

<h2 id="label">Label</h2>
<pre><code class="language-swift">// UIKit
let image = UIImage(systemName: &quot;xmark&quot;)
image?.accessibilityLabel = &quot;x 표시&quot;

// SwiftUI
Image(systemName: &quot;xmark&quot;)
    .accessibilityLabel(Text(&quot;x 표시&quot;))</code></pre>
<p>VoiceOver로 읽는 내용을 변경하려면 <code>accessibilityLabel</code> 값을 설정하면 됩니다.
SwiftUI에서 <code>accessibilityLabel(_:)</code> modifier에 <code>Text(_:)</code> 를 사용하면 다국어 처리가 되므로 이를 참고하셔서 개발하시면 좋습니다.</p>
<br>

<h2 id="hint-value">Hint, Value</h2>
<pre><code class="language-swift">// UIKit
let button = UIButton()
button.accessibilityHint = &quot;이중탭하여 열기&quot;
button.accessibilityValue = &quot;접혀있음&quot;

// SwiftUI
Button { } label: {
    Text(&quot;펼치기&quot;)
}
.accessibilityHint(Text(&quot;이중탭하여 열기&quot;))
.accessibilityValue(Text(&quot;접혀있음&quot;))
</code></pre>
<p><code>AccessibilityLabel</code> 에 추가 설명을 제공하려면 <code>AccessibilityHint</code> 또는 <code>AccessibilityValue</code> 를 사용하면 됩니다.
단, 아래의 지침에 맞도록 간략하게 설명하여야 합니다.</p>
<img width=715 src="https://velog.velcdn.com/images/sustainable-git/post/e66a49a0-9347-4012-a8c8-5422602ca3a6/image.png">

<br>

<h2 id="traits">Traits</h2>
<pre><code class="language-swift">// UIKit
let button = UIButton()
button.accessibilityTraits.remove(.button)
button.accessibilityTraits.formUnion([.adjustable, .notEnabled])

// SwiftUI
Button { } label: {
  Text(&quot;재생 이미지&quot;)
}
.accessibilityRemoveTraits(.isButton)
.accessibilityAddTraits([.playsSound, .isImage])</code></pre>
<p>Traits를 설정하면 VoiceOver 사용자가 해당 요소를 더 쉽게 인식할 수 있습니다.
예를 들어, 커스텀 UI로 Image에 TapGesture를 추가한 경우가 있다고 하면
<code>accessibilityAddTraits(.isButton)</code> modifier를 사용하여 사용자가 이미지를 버튼으로 인식하도록 변경할 수 있습니다.</p>
<br>

<h2 id="결론">결론</h2>
<p>VoiceOver는 iOS 앱의 접근성을 높이는 중요한 기능이며, 개발자는 UIKit과 SwiftUI에서 다양한 설정을 통해 이를 최적화할 수 있습니다.
기본적으로 iOS의 모든 UI 요소는 VoiceOver를 지원하지만,
<code>accessibilityLabel</code>, <code>accessibilityHint</code>, <code>accessibilityTraits</code> 등 추가적인 접근성 기능을 활용하면 사용자 경험을 더욱 개선할 수 있습니다.</p>
<p>특히, 커스텀 UI를 사용할 경우 VoiceOver가 개발자의 의도와 다르게 동작할 가능성이 높기 때문에, 적절한 접근성 속성을 설정하여 보완하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[접근성을 고려한 iOS 앱 개발 (3) - VoiceOver 사용하기]]></title>
            <link>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-iOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-3-VoiceOver-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-iOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-3-VoiceOver-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 27 Jan 2025 03:12:01 GMT</pubDate>
            <description><![CDATA[<h2 id="voiceover">VoiceOver</h2>
<ul>
<li>VoiceOver는 <strong>화면 읽기(Screen Reader)</strong> 기능입니다.</li>
<li>시각 장애인이나 저시력 사용자가 iOS 기기를 효율적으로 사용할 수 있도록 설계된 접근성 도구입니다.</li>
<li>화면의 텍트스와 UI 요소를 음성으로 읽어주며, 사용자는 터치와 제스처를 이용해 앱을 사용합니다.</li>
</ul>
<hr>
<h3 id="주요-기능">주요 기능</h3>
<ol>
<li>화면 읽기</li>
</ol>
<ul>
<li>화면의 텍스트, 버튼, 메뉴 등 UI 요소를 음성으로 읽어줍니다.</li>
<li>사용자의 터치 지점이나 탐색 대상의 콘텐츠를 설명합니다.</li>
</ul>
<ol start="2">
<li>탐색 및 제스처 지원</li>
</ol>
<ul>
<li>일반적인 터치 조작 대신 VoiceOver 전용 제스처로 앱과 시스템을 탐색할 수 있습니다.<ul>
<li>한 번 탭: 선택된 항목 설명</li>
<li>두 번 탭: 선택된 항목 실행</li>
<li>flick: 다음 항목으로 이동</li>
<li>세 손가락 스와이프: 스크롤</li>
</ul>
</li>
</ul>
<ol start="3">
<li>터치 탐색</li>
</ol>
<ul>
<li>손가락 아래에 있는 항목을 음성으로 알려줍니다.</li>
</ul>
<ol start="4">
<li>로터</li>
</ol>
<ul>
<li>두 손가락을 시계 방향 또는 반시계 방향으로 돌리는 동작으로 고급 제어 기능을 사용할 수 있습니다.</li>
</ul>
<ol start="5">
<li>다국어 및 다중 음성 지원</li>
</ol>
<ul>
<li>다양한 언어와 음성을 텍스트 음성 변환(TTS)로 지원합니다.</li>
</ul>
<ol start="6">
<li>피드백 및 힌트 제공</li>
</ol>
<ul>
<li>UI 요소와 상호작용할 때 피드백을 제공하고, 특정 요소의 사용법에 대한 힌트를 안내합니다.</li>
</ul>
<br>

<h2 id="활성화-방법">활성화 방법</h2>
<img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/a3b2f96f-0005-4526-9823-99edd952d817/image.gif">

<ul>
<li>설정 &gt; 손쉬운 사용 &gt; VoiceOver &gt; 활성화</li>
</ul>
<br>

<h3 id="편리하게-사용하는-방법">편리하게 사용하는 방법</h3>
<table>
<thead>
<tr>
<th align="center">단축키 사용</th>
<th align="center">자막 패널</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/68d45b49-893c-4417-9c0f-0b631b07b805/image.gif"></td>
<td align="center"><img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/6c7fe61d-c777-4116-a71c-1ff60d26ffd3/image.gif"></td>
</tr>
</tbody></table>
<ul>
<li>제가 VoiceOver를 사용할 때 반드시 사용하는 두 가지 기능입니다.</li>
<li>단축키 사용<ul>
<li>iPhone의 전원 버튼을 세 번 클릭하여 VoiceOver를 켜거나 끌 수 있습니다.</li>
<li>어떤 상태에서도 VoiceOver를 활성화/비활성화 할 수 있기 때문에 앱을 테스트할 때 이 기능을 활용하면 상당히 편리합니다.</li>
<li>설정 방법: 설정 &gt; 손쉬운 사용 &gt; 손쉬운 사용 단축키 &gt; VoiceOver 선택</li>
</ul>
</li>
<li>자막 패널<ul>
<li>VoiceOver 사용 시 하단에 읽어주는 항목을 자막으로 표시해 줍니다.</li>
<li>소리를 듣기 어려운 환경(예: 회사)에서 VoiceOver를 테스트 할 때 자막으로 읽어주는 항목을 확인할 수 있어 상당히 유용합니다.</li>
<li>설정 방법: 설정 &gt; 손쉬운 사용 &gt; VoiceOver &gt; 자막 패널 선택</li>
</ul>
</li>
</ul>
<br>

<h2 id="사용-방법">사용 방법</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/b5dfcd2c-c640-4600-b7dc-1699359d4fa4/image.gif" alt=""></p>
<ul>
<li>VoiceOver 실행하기(단축키 사용을 추천드립니다)</li>
<li>터치 탐색<ul>
<li>손가락 아래에 있는 항목으로 이동하고, 내용을 읽어줍니다.</li>
</ul>
</li>
<li>좌우로 flick<ul>
<li>flick이란 손가락으로 짧고 빠르게 화면을 스와이프하는 동작을 의미합니다.</li>
<li>다음 또는 이전 항목으로 이동하고, 내용을 읽어줍니다.</li>
</ul>
</li>
<li>두 손가락 터치<ul>
<li>VoiceOver가 말하는 것을 도중에 멈추도록 합니다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/4ae6dabb-8f1d-4752-a108-15ad99717a5d/image.gif" alt=""></p>
<ul>
<li>이중 탭<ul>
<li>항목 실행</li>
</ul>
</li>
<li>하단에서 짧게 드래그<ul>
<li>홈으로 이동</li>
</ul>
</li>
<li>하단에서 길게 드래그<ul>
<li>앱 전환기</li>
</ul>
</li>
<li>상단에서 짧게 드래그<ul>
<li>제어 센터</li>
</ul>
</li>
<li>상단에서 길게 드래그<ul>
<li>알림 센터</li>
</ul>
</li>
<li>두 손가락 문지르기<ul>
<li>취소, 뒤로 가기</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/4bfb48e4-22b7-4256-8cda-1665d6239eae/image.gif" alt=""></p>
<ul>
<li>세 손가락 flick<ul>
<li>드래그</li>
</ul>
</li>
<li>중앙을 중심으로 두 손가락 시계/반시계 방향으로 돌리기<ul>
<li>Rotor 실행</li>
<li>고급 기능 사용</li>
</ul>
</li>
</ul>
<br>

<h2 id="결론">결론</h2>
<p>VoiceOver는 iOS 기기를 사용하는 시각 장애인 및 저시력 사용자에게 강력한 접근성 기능을 제공합니다.
화면 읽기, 터치 탐색, flick 제스처와 같은 다양한 기능은 사용자들에게 앱과 시스템을 쉽게 탐색하고 사용할 수 있는 도구를 제공합니다.
또한, 단축키와 자막 패널 같은 편리한 기능을 통해 사용성과 테스트 환경이 크게 향상됩니다.</p>
<p>VoiceOver는 단순한 접근성 기능을 넘어, 모든 사용자가 더 나은 디지털 경험을 할 수 있도록 돕는 중요한 도구입니다.
이를 활용하면 누구나 쉽게 iOS의 기능을 이해하고 사용할 수 있으며,
앱 개발자와 디자이너는 VoiceOver를 통해 제품의 접근성을 높이고 더 많은 사용자에게 다가갈 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[접근성을 고려한 iOS 앱 개발 (2) - Dynamic Layout for Dynamic Type]]></title>
            <link>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-iOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-2-Dynamic-Layout-for-Dynamic-Type</link>
            <guid>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-iOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-2-Dynamic-Layout-for-Dynamic-Type</guid>
            <pubDate>Thu, 09 Jan 2025 14:54:29 GMT</pubDate>
            <description><![CDATA[<h2 id="객체-크기-변화로-인한-레이아웃-조정">객체 크기 변화로 인한 레이아웃 조정</h2>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/0b9bcff3-7463-4464-9bd8-cedcf83f5f60/image.gif">

<p>가끔 Dynamic Type을 적용할 경우 레이아웃이 깨지는 경우가 발생할 수 있습니다.
글자의 크기가 너무 커져서 글자가 잘리거나, 객체들이 서로의 영역을 침범하는 현상이 생기기도 하죠.
오늘은 Dynamic Type으로 인해 객체의 크기가 커질 때 레이아웃을 어떻게 변경할 수 있으며, 어떤 형태로 변형해야 하는지에 대해 설명드리겠습니다.</p>
<br>

<h2 id="텍스트의-개행-수-제한">텍스트의 개행 수 제한</h2>
<p>위의 사례에서 확인할 수 있듯, SwiftUI와 UIKit의 기본 개행 제한은 다릅니다.
SwiftUI의 Text는 기본적으로 무제한으로 개행이 가능하지만, UIKit의 UILabel은 기본적으로 한 줄로 제한됩니다.
이때 각각 <code>lineLimit(_:)</code>와 <code>numberOfLines</code>를 설정하면 개행 수를 변경할 수 있습니다.</p>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center">Text</th>
<th align="center">UILabel</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Framework</td>
<td align="center">SwiftUI</td>
<td align="center">UIKit</td>
</tr>
<tr>
<td align="center">방식</td>
<td align="center">view modifier</td>
<td align="center">property</td>
</tr>
<tr>
<td align="center">문법</td>
<td align="center"><a href="https://developer.apple.com/documentation/swiftui/environmentvalues/linelimit">lineLimit</a></td>
<td align="center"><a href="https://developer.apple.com/documentation/uikit/uilabel/numberoflines">numberOfLines</a></td>
</tr>
<tr>
<td align="center">default</td>
<td align="center">nil (무제한)</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center">무제한으로 만드는 법</td>
<td align="center">nil 대입</td>
<td align="center">0 대입</td>
</tr>
</tbody></table>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/6dec134d-00ad-4052-97f2-7c54257022f3/image.gif">

<br>

<h2 id="개행-방식의-변경">개행 방식의 변경</h2>
<p>텍스트의 줄 바꿈 수가 제한된 경우, 표시할 수 없는 글자는 ...으로 표시됩니다.
이때 어떤 글자를 우선적으로 보여줄지 선택할 수 있습니다.
SwiftUI에서는 <code>truncationMode(_:)</code>, UIKit에서는 <code>lineBreakMode</code>를 설정하여 이를 조정할 수 있습니다.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/7a5cd079-f1f2-4b50-bbd6-cf4d9836c2aa/image.gif">

<br>

<h2 id="이미지-크기도-조정">이미지 크기도 조정</h2>
<p>SwiftUI에서는 Dynamic Type에 따라 이미지 크기가 자동으로 변경됩니다.
하지만 UIKit에서는 별도로 처리하여 Dynamic Type에 맞춰 이미지 크기를 조정할 수 있습니다.</p>
<pre><code class="language-swift">let configuration = UIImage.SymbolConfiguration(textStyle: .body)
let image = UIImage(systemName: &quot;star&quot;, withConfiguration: configuration)</code></pre>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/ac11d565-e498-483a-b513-495bcb05c33b/image.gif">

<br>

<h2 id="가로-세로-레이아웃-변경">가로 세로 레이아웃 변경</h2>
<p>글자와 UI 요소가 함께 있을 때, 글자 크기가 변화함에 따라 둘 중 하나가 전부 표시되지 않을 수 있습니다.
이런 경우, Dynamic Type의 크기에 따라 레이아웃 형태를 변경하여 표현할 수 있습니다.
대표적인 방법으로는 Dynamic Type이 특정 크기 이상일 때 UIStackView의 axis를 변경하는 방법이 있습니다.</p>
<pre><code class="language-swift">        stackView.axis = traitCollection.preferredContentSizeCategory &lt; .accessibilityMedium ? .horizontal : .vertical</code></pre>
<p>SwiftUI에서는 if 구문으로 분기하거나 AnyLayout을 사용하여 이를 구현할 수 있습니다.</p>
<pre><code class="language-swift">    var layout: AnyLayout {
        sizeCategory &lt; .accessibilityMedium ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())
    }</code></pre>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/c36e3b20-2042-4ef2-af09-e046f779dbd7/image.gif">

<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: .zero) {
            Color.clear.overlay {
                    SwiftUIView()
                }
            Divider()
            Color.clear.overlay {
                    ViewControllerRepresentable()
                }
        }
    }
}

struct SwiftUIView: View {
    @Environment(\.sizeCategory) var sizeCategory

    var body: some View {
        layout {
            Text(&quot;테스트용 텍스트&quot;)
            Image(systemName: &quot;star&quot;)
        }
        .padding()
        .border(.black)
    }

    var layout: AnyLayout {
        sizeCategory &lt; .accessibilityMedium ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())
    }
}

final class ViewController: UIViewController {
    private var containerView: UIView = .init()
    private var stackView: UIStackView = .init()
    private var label: UILabel = .init()
    private var switchButton: UISwitch = .init()

    override func viewDidLoad() {
        view.backgroundColor = .systemGray4
        view.addSubview(containerView)
        containerView.addSubview(stackView)
        containerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.backgroundColor = .white
        containerView.layer.cornerRadius = 20
        containerView.layer.borderWidth = 1
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.addArrangedSubview(label)
        stackView.addArrangedSubview(switchButton)
        stackView.alignment = .center
        label.text = &quot;테스트용 텍스트&quot;
        label.font = UIFont.preferredFont(forTextStyle: .body)
        label.adjustsFontForContentSizeCategory = true
        label.numberOfLines = 0
        label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        switchButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
        NSLayoutConstraint.activate([
            containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            containerView.widthAnchor.constraint(equalToConstant: 350),
            stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 30),
            stackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 15),
            stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -30),
            stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -15)
        ])
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        stackView.axis = traitCollection.preferredContentSizeCategory &lt; .accessibilityMedium ? .horizontal : .vertical
    }
}

struct ViewControllerRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -&gt; some UIViewController { ViewController()}

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
}

#Preview {
    ContentView()

}</code></pre>
<br>

<h2 id="또-다른-좋은-방법">또 다른 좋은 방법</h2>
<p>Dynamic Type의 크기가 커져도 콘텐츠를 최대한 모두 보이게 하는 방법이 있습니다.
그 방법은 가능한 많은 화면을 ScrollView로 감싸는 것입니다.
이렇게 하면 글자와 이미지의 크기가 커져도 사용자는 스크롤을 통해 전체 내용을 모두 확인할 수 있습니다.
하지만 이 방법은 상황에 따라 불가능할 수 있으며, 중첩된 스크롤로 인해 UX가 나빠질 수 있음을 고려해야 합니다.</p>
<br>

<h2 id="결론">결론</h2>
<p>Dynamic Type을 지원하는 앱에서 텍스트 크기 변화에 따른 레이아웃 조정은 중요한 부분입니다.
<code>lineLimit</code>, <code>numberOfLines</code>, <code>truncationMode(_:)</code> 등을 활용해 텍스트의 개행을 제어하고,
레이아웃을 가로와 세로로 동적으로 변경하는 방법을 고려할 수 있습니다.
또한, 스크롤을 사용해 콘텐츠를 모두 표시하는 방법도 유용하지만, 중첩된 스크롤로 인한 UX 문제를 주의해야 합니다.</p>
<p>Dynamic Type을 적절히 활용하면 다양한 화면 크기와 글자 크기에 맞춰 유연한 디자인을 제공하고, 접근성을 고려한 사용자 경험을 향상시킬 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[접근성을 고려한 iOS 앱 개발 (1) - Dynamic Type]]></title>
            <link>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1-%EB%86%92%EC%9D%80-iOS-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-Dynamic-Type-1</link>
            <guid>https://velog.io/@sustainable-git/%EC%A0%91%EA%B7%BC%EC%84%B1-%EB%86%92%EC%9D%80-iOS-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-Dynamic-Type-1</guid>
            <pubDate>Thu, 26 Dec 2024 08:01:23 GMT</pubDate>
            <description><![CDATA[<h2 id="당신의-제품에-닿고-싶어요">당신의 제품에 닿고 싶어요</h2>
<p>우리가 앱과 모바일 서비스를 만들고 제공하는 이유는 <strong>많은 사람에게 도움</strong>이 되기 위해서입니다.
이를 위해 구형 스마트폰, 낮은 iOS 버전에서도 앱과 기능이 정상적으로 동작하도록 로직을 파편화하거나, 심지어 생산성이 높은 개발 도구를 포기하기도 합니다.
그러나 가끔, 우리는 세계 인구의 15%에 해당하는 엄청난 규모의 사람들이 필요로 하는 기능을 신경 쓰지 않고 개발하는 경우가 있습니다.</p>
<p><strong>전 세계 인구의 15%</strong>는 장애를 가지고 있습니다.
이 사람들은 우리의 제품을 사용하고 싶지만, 영구적이거나 일시적, 또는 환경적인 문제로 인해 일반적인 방법으로 앱을 사용하는 데 어려움을 겪고 있습니다.
이들이 앱을 쉽게 이용할 수 있도록 하기 위해 우리는 <strong>&quot;접근성&quot;</strong>을 보장해야 합니다.</p>
<p>이번 글부터는 UIKit 또는 SwiftUI에서 접근성을 지원하는 방법에 대해 소개할 예정입니다.
오늘은 그 중에서도 글자의 크기를 변경하는 Dynamic Type에 대해 알아보겠습니다.</p>
<hr>
<h2 id="dynamic-type-설정-방법">Dynamic Type 설정 방법</h2>
<img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/7b862bf6-15c8-41a4-817c-97854ac7b5f5/image.gif">

<p><strong>Dynamic Type</strong>은 사용자가 설정한 텍스트 크기에 따라 앱의 텍스트 크기가 자동으로 조정되는 접근성 기능입니다.
이 기능을 활용하기 위해서는 <strong>설정 &gt; 손쉬운 사용 &gt; 디스플레이 및 텍스트 크기 &gt; 더 큰 텍스트</strong> 로 이동하여야 합니다.</p>
<br>

<h2 id="dynamic-type을-앱에-적용하는-방법">Dynamic Type을 앱에 적용하는 방법</h2>
<p>SwiftUI의 Text는 기본적으로 Dynamic Type이 적용되어 있습니다.
하지만 UIKit의 경우, 개발 당시 접근성에 대한 설계를 하지 않았어서 이 값을 수동으로 적용해주어야 합니다.
따라서 UIKit의 경우 <code>UIFont.preferredFont(fotTextStyle:)</code> 구문과 <code>adjustsFontForContentSizeCategory</code>를 설정해 주어야 합니다.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/593033c2-a11c-449d-8683-4ceca4769e7d/image.gif">

<br>

<h2 id="custom-font에-dynamic-type을-적용하는-방법">Custom Font에 Dynamic Type을 적용하는 방법</h2>
<p>앱 개발을 하다 보면 앱에 맞는 특정 폰트를 사용할 때가 있습니다.
이런 경우에는 <code>UIFont.preferredFont(forTextStyle:)</code> 구문을 활용해 Dynamic Type을 적용할 수 있습니다.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/bafcaf2f-89e5-43b3-a584-bf7780b95584/image.gif">

<pre><code class="language-swift">/// SwiftUI의 경우
extension View {
    func customFont(named fontName: String, size: UIFont.TextStyle) -&gt; some View {
        self.font(.custom(fontName, size: UIFont.preferredFont(forTextStyle: size).pointSize))
    }
}

/// UIKit의 경우
        label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont(name: &quot;NanumGothicLight&quot;, size: UIFont.preferredFont(forTextStyle: .body).pointSize)!)
        label.adjustsFontForContentSizeCategory = true</code></pre>
<table>
<thead>
<tr>
<th align="center">TextStyle</th>
<th align="center">기본 크기</th>
</tr>
</thead>
<tbody><tr>
<td align="center">.caption2</td>
<td align="center">11</td>
</tr>
<tr>
<td align="center">.caption1</td>
<td align="center">12</td>
</tr>
<tr>
<td align="center">.footnote</td>
<td align="center">13</td>
</tr>
<tr>
<td align="center">.subheadline</td>
<td align="center">15</td>
</tr>
<tr>
<td align="center">.callout</td>
<td align="center">16</td>
</tr>
<tr>
<td align="center">.body</td>
<td align="center">17</td>
</tr>
<tr>
<td align="center">.headline</td>
<td align="center">17</td>
</tr>
<tr>
<td align="center">.title3</td>
<td align="center">20</td>
</tr>
<tr>
<td align="center">.title2</td>
<td align="center">22</td>
</tr>
<tr>
<td align="center">.title1</td>
<td align="center">28</td>
</tr>
<tr>
<td align="center">.largeTitle</td>
<td align="center">34</td>
</tr>
</tbody></table>
<br>

<h2 id="dynamic-type을-제한하는-방법">Dynamic Type을 제한하는 방법</h2>
<p>Dynamic Type의 경우 텍스트의 크기가 커지기 때문에 Layout이 망가질 가능성이 높습니다.
이 때문에 Dynamic Type의 크기를 제한하는 방법도 있습니다.</p>
<img width=700 src="https://velog.velcdn.com/images/sustainable-git/post/560b4fb9-05cf-4d71-91fc-7a93959ea7f5/image.png">

<p>iOS15 이상의 SwiftUI에서는 <code>.dynamicTypeSize(_:)</code>를 이용해 크기를 제한하면 됩니다.
UIKit에서는 <code>traitCollectionDidChange(_:)</code> 에서 수동으로 처리할 수 있습니다.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/767dfb7a-75fc-4988-8b10-90b8e11e545c/image.gif">

<pre><code class="language-swift">/// 아래로 갈 수록 크기가 큼
xSmall
small
medium
large
xLarge
xxLarge
xxxLarge
accessibility1
accessibility2
accessibility3
accessibility4
accessibility5</code></pre>
<pre><code class="language-swift">struct ContentView: View {
    var body: some View {
        Text(&quot;Dynamic Type Test&quot;)
        Text(&quot;SwiftUI의 Text입니다.&quot;)
            .dynamicTypeSize(.xSmall ... .xxxLarge)
        LimitedDynamicTypeLabelRepresentable()
            .frame(height:50)
        Spacer()
    }
}

struct LimitedDynamicTypeLabelRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -&gt; some UIView {
        let label = LimitedDynamicTypeLabel()
        label.text = &quot;UIKit의 Label입니다.&quot;
        return label
    }

    func updateUIView(_ uiView: UIViewType, context: Context) { }
}

final class LimitedDynamicTypeLabel: UILabel {
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        let maximumCategory = UITraitCollection(preferredContentSizeCategory: .extraExtraExtraLarge)
        let currentCategory = self.traitCollection.preferredContentSizeCategory
        if currentCategory &gt; maximumCategory.preferredContentSizeCategory {
            self.adjustsFontForContentSizeCategory = false
            self.font = UIFont.preferredFont(forTextStyle: .body, compatibleWith: maximumCategory)
        } else {
            self.adjustsFontForContentSizeCategory = true
            self.font = UIFont.preferredFont(forTextStyle: .body)
        }
    }
}</code></pre>
<br>

<h2 id="결론">결론</h2>
<p>앱의 접근성은 단순히 기능을 넘어, 더 많은 사용자들이 제품을 원활하게 이용할 수 있도록 돕는 중요한 요소입니다. Dynamic Type을 활용하여 텍스트 크기를 자동으로 조정하면, 사용자가 원하는 크기로 쉽게 텍스트를 조정할 수 있어, 시각적 어려움을 겪는 사용자에게 큰 도움이 됩니다.
UIKit과 SwiftUI에서 Dynamic Type을 구현하는 방법을 익히고, 사용자 맞춤형 폰트를 적용하는 방법을 통해, 우리는 더 많은 사람들에게 도움이 되는 제품을 제공할 수 있습니다.
모든 사용자가 차별 없이 앱을 사용할 수 있도록, 접근성을 고려한 개발은 선택이 아닌 필수임을 잊지 말아야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI의 애니메이션이 서로 간섭할 때 해결 방법]]></title>
            <link>https://velog.io/@sustainable-git/SwiftUI%EC%9D%98-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%EC%9D%B4-%EC%84%9C%EB%A1%9C-%EA%B0%84%EC%84%AD%ED%95%A0-%EB%95%8C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@sustainable-git/SwiftUI%EC%9D%98-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%EC%9D%B4-%EC%84%9C%EB%A1%9C-%EA%B0%84%EC%84%AD%ED%95%A0-%EB%95%8C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 09 Dec 2024 11:33:28 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>SwiftUI는 복잡한 애니메이션을 선언형으로 간단히 구현할 수 있는 강력한 프레임워크입니다.
상태 변화만 잘 정의하고 변경하면, 몇 줄의 코드로도 부드럽고 자연스러운 애니메이션을 쉽게 만들 수 있습니다.</p>
<p>그러나 SwiftUI에서 여러 애니메이션이 동시에 실행될 경우, 간섭으로 인해 문제가 발생할 수 있습니다.
특히, 서로 다른 애니메이션이 하나의 애니메이션을 조기에 종료시키는 현상이 나타날 수 있습니다.</p>
<br>

<h2 id="애니메이션-도중-다른-애니메이션을-실행하면-발생하는-문제">애니메이션 도중 다른 애니메이션을 실행하면 발생하는 문제</h2>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/74aabbce-a032-43a0-a80b-cdff01c18771/image.gif">

<p>왼쪽 끝에서 오른쪽 끝으로 1초마다 왕복하며 색을 변경하는 사각형이 있습니다.
아래 버튼을 눌러 텍스트를 등장시키면, 사각형의 왕복 동작이 멈추는 것을 확인할 수 있습니다.
이때 색상 변화는 정상적으로 계속 진행됩니다.</p>
<p>텍스트 코드의 <code>withAnimation</code>을 제거하면 애니메이션이 정상적으로 작동합니다.
이것은 하나의 애니메이션이 다른 애니메이션의 동작에 영향을 주어 덮어 씌워진다는 것을 의미합니다.</p>
<pre><code class="language-swift">struct ContentView: View {
    @State private var isAnimated: Bool = false
    @State private var isShow: Bool = false

    var body: some View {
        ZStack {
            VStack(spacing: 50) {
                Rectangle()
                    .fill(isAnimated ? Color.red : Color.blue)
                    .frame(width: 50, height: 50)
                    .offset(x: isAnimated ? 100 : -100)
                if isShow {
                    Text(&quot;애니메이션 사라짐&quot;)
                }
            }

            VStack {
                Spacer()
                Button(&quot;텍스트 등장&quot;) {
                    withAnimation {
                        isShow.toggle()
                    }
                }
            }
        }
        .onAppear {
            withAnimation(.linear(duration: 1).repeatForever()){
                isAnimated.toggle()
            }
        }
    }
}</code></pre>
<br>

<h2 id="애니메이션-view를-모듈로-분리해도-문제는-해결되지-않는다">애니메이션 View를 모듈로 분리해도 문제는 해결되지 않는다.</h2>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/ae62b491-012a-41df-a9d9-df8619a1bcd2/image.gif">

<p>사각형의 애니메이션 View를 별도의 모듈로 분리하면 문제가 해결될까요?
아쉽게도 그렇지 않습니다.</p>
<p>이 문제는 모듈화 여부와는 무관하며, 애니메이션 컨텍스트의 동작 방식이 문제입니다.
오히려 모듈화된 상태에서는 문제의 원인을 파악하기 어려울 수 있습니다.</p>
<pre><code class="language-swift">struct ContentView: View {
    @State private var isShow: Bool = false

    var body: some View {
        ZStack {
            VStack(spacing: 50) {
                AnimationView()
                if isShow {
                    Text(&quot;애니메이션 여전히 사라짐&quot;)
                }
            }
            VStack {
                Spacer()
                Button(&quot;텍스트 등장&quot;) {
                    withAnimation {
                        isShow.toggle()
                    }
                }
            }
        }

    }
}

struct AnimationView: View {
    @State private var isAnimated: Bool = false

    var body: some View {
        Rectangle()
            .fill(isAnimated ? Color.red : Color.blue)
            .frame(width: 50, height: 50)
            .offset(x: isAnimated ? 100 : -100)
            .onAppear {
                withAnimation(.linear(duration: 1).repeatForever()){
                    isAnimated.toggle()
                }
            }
    }
}</code></pre>
<br>

<h2 id="명시적으로-animation을-제거하면-문제를-해결할-수-있다">명시적으로 animation을 제거하면 문제를 해결할 수 있다</h2>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/3bbe52f8-9ad1-4cbd-af14-2bcf3bc1d883/image.gif">

<p>문제를 해결하려면, 다른 애니메이션에 영향을 받지 않도록 명시적으로 설정해야 합니다.
<code>animation(_:, value:)</code> 함수의 첫 번째 인자에 nil을 지정하고, 영향을 받지 않기를 원하는 property의 값을 value에 지정하면 해당 상태 변화가 애니메이션되지 않도록 설정할 수 있습니다.</p>
<pre><code class="language-swift">struct ContentView: View {
    @State private var isAnimated: Bool = false
    @State private var isShow: Bool = false

    var body: some View {
        ZStack {
            VStack(spacing: 50) {
                Rectangle()
                    .fill(isAnimated ? Color.red : Color.blue)
                    .frame(width: 50, height: 50)
                    .offset(x: isAnimated ? 100 : -100)
                    .animation(nil, value: isShow)
                if isShow {
                    Text(&quot;애니메이션 정상동작&quot;)
                }
            }

            VStack {
                Spacer()
                Button(&quot;텍스트 등장&quot;) {
                    withAnimation {
                        isShow.toggle()
                    }
                }
            }
        }
        .onAppear {
            withAnimation(.linear(duration: 1).repeatForever()){
                isAnimated.toggle()
            }
        }
    }
}</code></pre>
<br>

<h2 id="swiftui-애니메이션은-컨텍스트-충돌을-조심해야-한다">SwiftUI 애니메이션은 컨텍스트 충돌을 조심해야 한다</h2>
<p>이 사례를 통해 SwiftUI 애니메이션이 동일한 컨텍스트에서 상태 변경이 겹칠 경우 문제가 발생할 수 있음을 확인했습니다.
예를 들어, 색상 변경 애니메이션은 정상적으로 유지되었지만, 위치 변경 애니메이션은 텍스트 애니메이션에 의해 종료되었습니다.</p>
<p>따라서 SwiftUI에서 애니메이션을 사용할 때는 컨텍스트가 겹치지 않도록 조절해야 하며, 명시적으로 애니메이션을 분리하거나 특정 상태 변화가 애니메이션에 영향을 주지 않도록 처리해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 17의 @Observable 매크로: SwiftUI 성능 최적화와 상태 관리 간소화]]></title>
            <link>https://velog.io/@sustainable-git/Observable</link>
            <guid>https://velog.io/@sustainable-git/Observable</guid>
            <pubDate>Sun, 24 Nov 2024 10:32:14 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>SwiftUI에서는 Dynamic property의 상태를 변경하여 View를 업데이트합니다.
이를 최적화하기 위해 각 Dynamic property를 별도의 View로 모듈화하거나
<code>@StateObject</code> 또는 <code>@ObservedObject</code>를 사용해 ViewModel을 파편화하여 불필요한 업데이트를 최소화하려는 노력이 필요했습니다.</p>
<p>그러나 두 가지 이상의 여러 View를 동시에 업데이트해야 하는 상황이 많아지면서, ViewModel 파편화에는 한계가 드러나기 시작합니다.
특히 <code>@Published</code>를 기준으로 ViewModel을 지나치게 분리하면 코드의 복잡성이 급격히 증가하고, 가독성이 심각하게 저해될 수 있습니다.
반면, ViewModel을 분리하지 않고 하나로 통합하면 Main Thread에서 불필요한 계산 작업이 늘어나 프레임 드랍과 같은 성능 문제가 발생할 수 있습니다.</p>
<p>이러한 문제를 해결하기 위해, iOS 17에서 <code>@Observable</code> 매크로가 도입되었습니다.
이를 통해 ViewModel을 분리하지 않고도 필요한 부분만 효율적으로 업데이트할 수 있어, 코드 가독성을 유지하면서도 성능 최적화를 달성할 수 있습니다.</p>
<p>이 글에서는 <code>@Observable</code> 매크로의 동작 원리와 활용 방법, 그리고 이를 사용해야 하는 이유에 대해 알아보겠습니다.</p>
<br>

<h2 id="observable이란">@Observable이란?</h2>
<img width=650 src="https://velog.velcdn.com/images/sustainable-git/post/1f4d4292-af19-48a2-970a-fe2c1b4d05b8/image.png">

<p><a href="https://developer.apple.com/documentation/Observation">https://developer.apple.com/documentation/Observation</a></p>
<p><code>@Observable</code>은 내부적으로 컴파일러가 상태 변화를 자동으로 추적하고, SwiftUI View에서 이를 반영할 수 있는 코드를 생성해 줍니다.
이를 통해 상태 관리를 위한 추가적인 설정 없이도 데이터와 UI 간의 동기화를 간단하게 구현할 수 있습니다.
또한, <code>@Published</code>와 같은 반복적인 Boilerplate 코드를 작성하지 않아도 된다는 장점이 있습니다.</p>
<p>ViewModel에 <code>@Observable</code>을 사용하면 <code>@StateObject</code>를 사용하는 경우보다 코드가 훨씬 간결해지고, 관리가 용이해집니다.
복잡한 상태 관리 로직을 간소화하고, SwiftUI의 선언적 프로그래밍 스타일에 더욱 부합합니다.</p>
<table>
<thead>
<tr>
<th align="center">@StateObject</th>
<th align="center">@Observable</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img width=500 src="https://velog.velcdn.com/images/sustainable-git/post/a56e605c-a327-45d7-b663-08670522b66b/image.png"></td>
<td align="center"><img width=500 src="https://velog.velcdn.com/images/sustainable-git/post/c1030032-a471-400c-82e8-ddd9aeb92b17/image.png"></td>
</tr>
</tbody></table>
<br>

<h2 id="observable을-사용해야-하는-이유">Observable을 사용해야 하는 이유</h2>
<p><code>@Observable</code>은 <code>@StateObject</code>와 <code>@ObservedObject</code>를 대체할 수 있는 상위 호환 기술입니다.
이를 사용하면 기존의 <code>@StateObject</code>와 <code>@ObservedObject</code>가 필요한 모든 상황에서 대체될 수 있으며, 성능 측면에서도 아래와 같은 이점을 제공합니다</p>
<p>예를 들어, ObservableObject를 채택하는 객체의 경우, 내부 <code>@Published</code> 값이 변경되면 값 변경과 직접 관련이 없는 View라도 해당 객체를 참조하고 있는 모든 View의 body가 다시 호출됩니다.
이는 불필요한 View 업데이트를 초래하며, 성능 저하의 원인이 됩니다.</p>
<p><a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B02">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기2</a></p>
<pre><code class="language-swift">import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0

    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;count = \(viewModel.count)&quot;) {
                viewModel.upCount()
            }
            AnotherView(viewModel: viewModel)
        }
    }
}

struct AnotherView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;나는 값이 안변하는데&quot;)
    }
}</code></pre>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/3509524f-9891-4c25-86ff-4cc400a83196/image.gif">

<p>만약 ViewModel을 <code>@Observable</code>로 변경하면, 상태가 변경된 property와 관련이 없는 View의 body는 호출되지 않습니다.
즉, 불필요한 View 렌더링을 방지하여 성능을 최적화할 수 있습니다.</p>
<pre><code class="language-swift">import SwiftUI

@Observable final class ViewModel {
    private(set) var count: Int = 0

    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    private var viewModel: ViewModel = .init()

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;count = \(viewModel.count)&quot;) {
                viewModel.upCount()
            }
            AnotherView(viewModel: viewModel)
        }
    }
}

struct AnotherView: View {
    var viewModel: ViewModel

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;나는 값이 안변하는데&quot;)
    }
}</code></pre>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/1e72ab16-0a0b-4afb-bd3b-3cfb7b1ffb0e/image.gif">

<p>상태가 변경되는 property와 무관한 경우, body가 호출되지 않는다는 점은 ViewModel 내에 여러 속성을 추가해도 불필요한 렌더링이 발생하지 않는다는 의미입니다.
이로 인해 ViewModel을 <code>@Published</code> 마다 분리하지 않아도 되고, 코드의 가독성도 높아지며 유지보수도 더 쉬워지는 장점이 있습니다.</p>
<br>

<h2 id="observable의-추가-이점">Observable의 추가 이점</h2>
<h3 id="1-model이-observable인-경우-viewmodel은-observable이-아니어도-됩니다">1. Model이 Observable인 경우 ViewModel은 Observable이 아니어도 됩니다.</h3>
<pre><code class="language-swift">final class ViewModel {
    let model = Model()

    func upCount() {
        model.upNumber()
    }
}

@Observable final class Model {
    private(set) var number: Int = 0

    func upNumber() {
        number += 1
    }
}</code></pre>
<h3 id="2-model이-observable인-경우-viewmodel을-struct로-참조할-수-있습니다">2. Model이 Observable인 경우 ViewModel을 struct로 참조할 수 있습니다.</h3>
<pre><code class="language-swift">struct ViewModel {
    let model = Model()

    func upCount() {
        model.upNumber()
    }
}

@Observable final class Model {
    private(set) var number: Int = 0

    func upNumber() {
        number += 1
    }
}</code></pre>
<h3 id="3-viewmodel을-let으로-참조할-수-있습니다">3. ViewModel을 let으로 참조할 수 있습니다.</h3>
<pre><code class="language-swift">struct ViewModel {
    let model = Model()

    func upCount() {
        model.upNumber()
    }
}

@Observable final class Model {
    private(set) var number: Int = 0

    func upNumber() {
        number += 1
    }
}

struct ContentView: View {
    let viewModel = ViewModel()

    var body: some View {
        Button(&quot;count = \(viewModel.model.number)&quot;) {
            viewModel.upCount()
        }
    }
}</code></pre>
<br>

<h2 id="class가-아닌-객체에도-적용이-가능한가요">class가 아닌 객체에도 적용이 가능한가요?</h2>
<p>struct는 값을 복사하여 전달하기 때문에, 상태 추적을 위해 참조가 필요한 @Observable의 특성과 맞지 않습니다.</p>
<p>actor는 데이터를 안전하게 처리하기 위해 비동기 환경에서 동작합니다.
하지만 @Observable은 상태 변화에 동기적으로 반응하며, View를 즉시 업데이트합니다.
이 때문에 actor는 @Observable에 적합하지 않습니다.</p>
<p>따라서, @Observable은 class에서만 사용 가능합니다.</p>
<br>

<h2 id="결론">결론</h2>
<p><code>@Observable</code> 매크로는 iOS 17에서 도입된 강력한 상태 관리 도구로, SwiftUI의 View 업데이트를 보다 효율적으로 처리할 수 있게 해줍니다.
이를 통해 기존의 <code>@StateObject</code>나 <code>@ObservedObject</code>를 대체하며, ViewModel을 복잡하게 파편화하지 않고도 불필요한 렌더링을 줄여 성능을 최적화할 수 있습니다.
또한, <code>@Observable</code>을 사용하면 코드의 가독성도 높아지고 유지보수가 용이해지며, 상태 변화에 따른 View 업데이트를 자동으로 관리할 수 있습니다.</p>
<p>그러나 <code>@Observable</code>은 class에서만 사용할 수 있기 때문에, 값 타입인 struct나 비동기 환경에서 동작하는 actor와는 호환되지 않는다는 점을 염두에 두어야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI가 View를 비교하고 렌더링하는 방식]]></title>
            <link>https://velog.io/@sustainable-git/SwiftUI%EA%B0%80-View%EB%A5%BC-%EB%B9%84%EA%B5%90%ED%95%98%EA%B3%A0-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@sustainable-git/SwiftUI%EA%B0%80-View%EB%A5%BC-%EB%B9%84%EA%B5%90%ED%95%98%EA%B3%A0-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Sun, 17 Nov 2024 11:50:20 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>SwiftUI는 새로운 View와 기존 View를 비교하고 변경된 부분만을 업데이트하여 성능을 최적화합니다.
그렇다면, SwiftUI는 어떻게 비교하고 렌더링 여부를 결정할까요?
이 글에서는 SwiftUI가 뷰를 어떻게 비교하고, 변경된 부분만 업데이트하는지에 대해 자세히 살펴보겠습니다.</p>
<br>

<h2 id="swiftui가-view를-렌더링할-때의-과정">SwiftUI가 View를 렌더링할 때의 과정</h2>
<p>SwiftUI는 View의 Dependency가 변경되면 <strong>새로운 View 값을 생성(init)</strong> 합니다.
이때 &quot;새로 생성된 View&quot;는 렌더링을 위해 만들어진 것이 아닙니다.
만약 새로 생성된 View가 기존 View와 동일하다면, SwiftUI는 새로 만든 View를 버리고 아무 작업도 하지 않습니다.</p>
<blockquote>
<p>Dependency 변경으로 init이 호출되지만, 새로 생성된 View가 이전과 동일하면 body가 호출되지 않습니다.</p>
</blockquote>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/3f60196d-939e-44ec-909e-d9303abf5da6/image.gif">

<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var number = 0

    var body: some View {
        VStack {
            Button(&quot;눌러 = \(number)&quot;) {
                number += 1
            }
            ChildView()
        }
    }
}

fileprivate struct ChildView: View {
    init() {
        print(&quot;init occured&quot;)
    }

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;호출되지 않는 편안함&quot;)
    }
}</code></pre>
<p>새로 생성된 View가 <strong>이전과 동일하지 않다면 Body를 호출</strong>합니다.
그러나, body가 호출되었다고 해서 항상 화면이 렌더링되는 것은 아닙니다.
SwiftUI는 body가 동일한지 diffing을 통해 비교하고, 같으면 View Tree가 변경되지 않도록 렌더링을 생략합니다.</p>
<blockquote>
<p>ViewModel 값이 업데이트되면 body를 호출합니다. 하지만, body가 이전과 동일하면 다시 렌더링하지 않습니다.</p>
</blockquote>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/4e4d1bfb-7f2c-4765-acbb-35bf486f1940/image.gif">

<table>
<thead>
<tr>
<th align="center">렌더링 한 경우</th>
<th align="center">렌더링 하지 않은 경우</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/sustainable-git/post/5c5617b0-377b-4b00-9d04-84f7412d8f19/image.png"></td>
<td align="center"><img src="https://velog.velcdn.com/images/sustainable-git/post/44771b63-2ada-4750-9aec-631a9e78ddc1/image.png"></td>
</tr>
</tbody></table>
<p><code>let _ = Self._printChange()</code> 를 통해 body가 호출되었음을 확인할 수 있습니다.
하지만, Instrument로 확인해 보면 렌더링이 일어나지 않아 CPU 점유율이 렌더링이 일어났을 때와 크게 차이가 남을 알 수 있습니다.</p>
<br>

<h2 id="swiftui가-view를-비교하는-방법">SwiftUI가 View를 비교하는 방법</h2>
<p>SwiftUI는 아래의 세 가지 방법으로 View가 이전과 같은지 비교한다고 합니다.</p>
<ol>
<li>memcmp (빠름)</li>
<li>equality (중간)</li>
<li>reflection (느림)</li>
</ol>
<h3 id="memcmp">memcmp</h3>
<p>SwiftUI의 View 비교에서 memcmp는 <strong>값 타입</strong>에 대해 <code>Equatable</code> 대신 바이트 단위 비교가 가능한 경우에 사용합니다.
특히 POD 객체와 같이 property가 단순하고 추가적인 메모리 할당이나 상태 변화가 없는 구조체인 경우 memcmp를 이용해 비교합니다.</p>
<blockquote>
<p><strong>POD(Plain Old Data)</strong> 는 단순히 데이터를 저장하고 추가적인 복사, 이동, 소멸과 같은 특별한 동작을 가지지 않는 경우를 말합니다.
<a href="https://github.com/swiftlang/swift/blob/main/docs/ABIStabilityManifesto.md#type-properties">swiftlang.github</a></p>
</blockquote>
<p>Swift는 다음 값 타임을 POD 타입으로 분류합니다.</p>
<ul>
<li><code>Int</code>, <code>Float</code>, <code>Range&lt;POD&gt;</code>, <code>ClosedRange&lt;POD&gt;</code></li>
</ul>
<p>다음 값 타입을 non-POD 타입으로 분류합니다.</p>
<ul>
<li><code>String</code>, <code>Character</code>, <code>Array</code>, <code>Dictionary</code>, <code>Set</code>, <code>Error</code>, <code>Result</code>, <code>@State</code>, <code>@Binding</code> ...</li>
</ul>
<p>객체의 경우<code>_isPOD(_:)</code> 함수를 이용해 특정 타입이 POD 타입인지 확인할 수 있습니다.</p>
<h3 id="equality">equality</h3>
<p>View가 Equatable을 채택한 경우 equality 방식으로 비교합니다.
<code>Text(_:)</code>와 같이 간단한 View는 SwiftUI에서 자체적으로 <code>Equatable</code>을 채택하고 있습니다.
자세한 내용은 아래 글을 참고하세요.
<a href="https://velog.io/@sustainable-git/EquatableView%EC%99%80-Equatable%EB%A1%9C-SwiftUI-View%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0">https://velog.io/@sustainable-git/EquatableView와-Equatable로-SwiftUI-View의-렌더링-성능-개선하기</a></p>
<h3 id="reflection">reflection</h3>
<p><code>@State</code>, <code>@Binding</code> 과 같은 Dynamic property가 변경되면 body를 호출하고 diffing을 합니다.
SwiftUI는 이 때 reflection을 이용한 비교를 수행합니다.
reflection이 사용되는 경우 새로 생성된 View 트리와 기존 Tree를 비교하기 위해 View의 하위 Tree를 순회하며 차이를 분석합니다.
두 Tree가 동일하다면, SwiftUI는 기존 View를 재사용하고 렌더링을 생략합니다.</p>
<h3 id="정리">정리</h3>
<table>
<thead>
<tr>
<th align="center">비교 방식</th>
<th align="center">상황</th>
<th align="center">단계</th>
<th align="center">결과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">mcmcmp</td>
<td align="center">상태 변경이 없는 경우</td>
<td align="center">init 만으로 비교</td>
<td align="center">메모리 비교를 통해 변경 사항을 확인하고, 동일한 경우 body 호출을 생략할 수 있음</td>
</tr>
<tr>
<td align="center">equality</td>
<td align="center">View가 <code>Equatable</code>을 채택한 경우</td>
<td align="center">== 함수를 사용해 비교</td>
<td align="center">값이 동일하면 불필요한 렌더링을 생략하고 기존 View를 사용</td>
</tr>
<tr>
<td align="center">reflection</td>
<td align="center">동적 상태, 속성이 변경된 경우</td>
<td align="center">body가 호출되어 비교</td>
<td align="center">새로운 View Tree와 기존 View Tree를 비교하고 필요 시 렌더링</td>
</tr>
</tbody></table>
<br>

<h2 id="결론">결론</h2>
<p>SwiftUI는 렌더링 최적화를 위해 세 가지 방식(memcmp, equality, reflection)을 사용하여 View를 비교합니다.
이 중 <strong>POD 객체</strong>는 빠르게 비교할 수 있는 memcmp 방식을 사용하며, <strong>Equatable</strong>을 채택한 타입은 값 비교로 최적화를 이룹니다.
동적 속성(@State, @Binding)이 변경되면 reflection 방식으로 비교하고, 필요 시 렌더링을 생략하는 구조로 동작합니다.</p>
<p>성능 최적화를 위해 POD 객체를 사용하는 것이 유리하며, 그렇지 않다면 <strong>Equatable</strong>을 채택하여 뷰의 불필요한 렌더링을 최소화할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EquatableView와 Equatable로 SwiftUI View의 렌더링 성능 개선하기]]></title>
            <link>https://velog.io/@sustainable-git/EquatableView%EC%99%80-Equatable%EB%A1%9C-SwiftUI-View%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sustainable-git/EquatableView%EC%99%80-Equatable%EB%A1%9C-SwiftUI-View%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 09 Nov 2024 11:36:16 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>SwiftUI의 View는 상태 변수(State)의 값이 변할 때마다 View를 다시 업데이트하는 특징이 있습니다.
그러나, 값이 변하더라도 View를 다시 그릴 필요가 없는 경우에도 이러한 특성은 불필요한 업데이트를 발생시켜 성능에 영향을 줄 수 있습니다.
이런 경우 EquatableView를 사용하면 SwiftUI가 값의 변화를 비교하여 불필요한 업데이트를 방지할 수 있습니다.</p>
<br>

<h2 id="equatableview란">EquatableView란?</h2>
<img width=660 src="https://velog.velcdn.com/images/sustainable-git/post/0ae7c3f2-0870-4196-8839-1b18401dbc31/image.png">

<p>View가 Equatable을 채택하면, SwiftUI가 해당 View의 업데이트를 요청할 때 현재 값과 이전 값이 동일한지 비교하여, 값이 같다면 View를 다시 그리지 않습니다.
사용법은 아래와 같습니다.</p>
<pre><code class="language-swift">EquatableView(content: ChildView(number: number)
// 또는 
ChildView(number: Number)
    .equatable()</code></pre>
<p>또는 View가 Equatable을 채택하면 <code>Equatable(content:)</code> 또는 <code>.equatable()</code> 없이도 동일한 방식으로 작동합니다.
Equatable 프로토콜을 View가 채택한 경우, 별도로 EquatableView로 감싸지 않더라도 값 변화 감지를 통해 불필요한 렌더링을 방지할 수 있습니다.</p>
<pre><code class="language-swift">struct NumberParityView: View, Equatable {
    ...
}</code></pre>
<br>

<h2 id="예시--init은-매번-일어나지만-body는-상황에-따라-다시-그리지-않는-경우">예시 : init은 매번 일어나지만 body는 상황에 따라 다시 그리지 않는 경우</h2>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/860f7cee-eb17-42bc-a573-3ee9de65cea1/image.gif">

<p>버튼을 누르면 number 값을 임의로 변경하는 부모 View가 있습니다.
자식 View는 number의 값이 짝수인지 홀수인지에 따라 다르게 그려지도록 구성되었습니다.
버튼을 눌러 number의 값이 바뀌면 자식 View는 매번 init이 호출됩니다.
그러나 body는 홀짝이 바뀌는 경우에만 호출되므로, 이를 통해 불필요한 업데이트를 줄이고 최적화할 수 있습니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var number = 0

    var body: some View {
        VStack {
            NumberParityView(number: number)
                .padding()
                .border(number % 2 == 0 ? .red : .blue)

            Button(&quot;Random Number Generator&quot;) {
                randomNumberButtonTouched()
            }
            Text(&quot;number = \(number)&quot;)
        }
    }

    private func randomNumberButtonTouched() {
        number = Int.random(in: 1...100)
    }
}

struct NumberParityView: View, Equatable {
    @State private var flag = false
    let number: Int

    init(number: Int) {
        self.number = number
        print(&quot;init&quot;)
    }

    var body: some View {
        let _ = Self._printChanges()
        Text(number % 2 == 0 ?  &quot;EVEN&quot; : &quot;ODD&quot;)
    }

    static func == (lhs: Self, rhs: Self) -&gt; Bool {
        lhs.number % 2 == rhs.number % 2
    }
}</code></pre>
<br>

<h2 id="잠깐-flag는-뭔가요">잠깐, flag는 뭔가요?</h2>
<p>EquatableView를 사용할 때 유의할 점이 있습니다.
위 예제에서 View 내에 <code>@State</code> 값이 없으면 SwiftUI는 해당 View를 매번 새로운 상태로 간주하고 View를 업데이트할 때마다 body를 새로 호출하게 됩니다.
실제로 <code>@State private var flag</code>가 없으면 body가 매번 렌더링됨을 확인할 수 있습니다.
단, Equatable을 준수하는 모델을 사용하는 경우에는 정상적으로 동작합니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var numberModel = NumberModel(number: 0)

    var body: some View {
        VStack {
            NumberParityView(numberModel: numberModel)
                .padding()
                .border(numberModel.number % 2 == 0 ? .red : .blue)

            Button(&quot;Random Number Generator&quot;) {
                randomNumberButtonTouched()
            }
            Text(&quot;number = \(numberModel.number)&quot;)
        }
    }

    private func randomNumberButtonTouched() {
        numberModel = NumberModel(number: Int.random(in: 0...1))
    }
}

struct NumberParityView: View, Equatable {
    let numberModel: NumberModel

    init(numberModel: NumberModel) {
        self.numberModel = numberModel
        print(&quot;init&quot;)
    }

    var body: some View {
        let _ = Self._printChanges()
        Text(numberModel.number % 2 == 0 ?  &quot;EVEN&quot; : &quot;ODD&quot;)
    }

    static func == (lhs: Self, rhs: Self) -&gt; Bool {
        lhs.numberModel == rhs.numberModel
    }
}

struct NumberModel: Equatable {
    let number: Int
}</code></pre>
<h3 id="외부-값이-단순한-값일-경우">외부 값이 단순한 값일 경우</h3>
<p>내부에 <code>@State</code> 가 없다면 View를 Value type으로 간주합니다.
이 경우 View 내부의 상태를 SwiftUI가 추적하지 않기 때문에 Equatable을 채택했다 하더라도 매번 View가 렌더링하게 됩니다.</p>
<h3 id="외부-값이-equatable을-준수하는-데이터-모델일-경우">외부 값이 Equatable을 준수하는 데이터 모델일 경우</h3>
<p>데이터 모델이 Equatable을 준수하기 때문에 <code>@State</code> 없이도 모델이 동일한지 비교하고, 동일하면 다시 렌더링하지 않습니다.</p>
<br>

<h2 id="결론">결론</h2>
<p>SwiftUI의 EquatableView는 렌더링 성능을 최적화하는 데 유용하게 사용할 수 있습니다.
특히 값이 자주 변하지 않거나, 특정 조건에서만 View가 변경될 필요가 있을 때 성능을 크게 개선할 수 있습니다.
그러나 모든 경우에 적합한 것은 아니며, 자주 변하는 데이터의 경우 오히려 비교 오버헤드로 인해 성능 저하가 발생할 수 있습니다.
또한 비교가 복잡한 데이터 타입에서는 Equatable을 구현하는 비용이 다시 렌더링하는 비용보다 클 수 있어 주의가 필요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[효율적인 iOS 동영상 재생을 위한 AVPlayerLayer 활용법]]></title>
            <link>https://velog.io/@sustainable-git/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-iOS-%EB%8F%99%EC%98%81%EC%83%81-%EC%9E%AC%EC%83%9D%EC%9D%84-%EC%9C%84%ED%95%9C-AVPlayerLayer-%ED%99%9C%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@sustainable-git/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-iOS-%EB%8F%99%EC%98%81%EC%83%81-%EC%9E%AC%EC%83%9D%EC%9D%84-%EC%9C%84%ED%95%9C-AVPlayerLayer-%ED%99%9C%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sun, 03 Nov 2024 11:49:53 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>iOS 앱에서 동영상 재생 화면을 구현할 때, AVPlayerLayer를 어떻게 사용하는지가 사용자 경험에 큰 영향을 미칩니다.
AVPlayerLayer는 Core Animation을 통해 비디오를 직접 렌더링하는데,
이를 어떻게 활용하느냐에 따라 화면 전환과 애니메이션 성능이 달라질 수 있습니다.
이 글에서는 AVPlayerLayer를 이용해 동영상을 표시하는 두 가지 방법을 소개하고, 각각의 장단점을 비교하겠습니다.</p>
<br>

<h2 id="시작하기-전에--avplayerlayer를-사용하는-이유">시작하기 전에 : AVPlayerLayer를 사용하는 이유</h2>
<p>AVPlayer는 비디오 파일을 로드하고 재생하는데 필요한 기능을 제공합니다.
하지만, 실제로 비디오를 화면에 렌더링하기 위해서는 AVPlayerLayer가 필요합니다.</p>
<p>AVPlayerLayer는 Core Animation을 사용해 비디오를 렌더링합니다.
또한, 비율(aspect ratio)과 정렬(videoGravity) 설정을 지원하여 UIView의 영역에 맞게 조정할 수 있습니다.
비디오를 그릴 때 사용할 Core Image 필터나 애니메이션 효과를 적용할 수 있어 추가적인 시각 효과도 제공합니다.</p>
<br>

<h2 id="방법-1--avplayerlayer를-addsublayer로-추가하는-방법">방법 1 : AVPlayerLayer를 addSubLayer로 추가하는 방법</h2>
<p>AVPlayerLayer는 CALayer를 상속합니다.
따라서 AVPlayerLayer를 UIViewController의 view에 addSubLayer하여 미디어를 재생할 layer를 추가할 수 있습니다.
그런데, CALayer는 Autolayout을 지원하지 않기 때문에 layout의 변경이 필요한 경우 수동으로 frame 값을 조정해 주어야 합니다.
그래서 <code>viewDidLayoutSubviews()</code> 함수에서 AVplayerLayer의 frame을 수동으로 변경시켜 주어야 합니다.</p>
<img width=400 src="https://velog.velcdn.com/images/sustainable-git/post/5a77dd6c-c28a-43a1-8fc2-16d8ec06e11d/image.gif">

<pre><code class="language-swift">import UIKit
import AVFoundation

final class ContentViewController: UIViewController {
    private var player: AVPlayer?
    private var playerLayer: AVPlayerLayer?

    override func viewDidLoad() {
        setupPlayer()
    }

    func setupPlayer() {
        guard let url = URL(string: &quot;http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4&quot;) else { return }
        player = AVPlayer(url: consume url)
        playerLayer = AVPlayerLayer(player: player)
        playerLayer?.frame = view.bounds
        playerLayer?.videoGravity = .resizeAspect

        if let playerLayer {
            view.layer.addSublayer(playerLayer)
        }
        player?.play()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        playerLayer?.frame = view.bounds
    }
}</code></pre>
<br>

<h2 id="방법-2--uiview의-layer를-avplayerlayer로-치환해서-사용하는-방법">방법 2 : UIView의 layer를 AVPlayerLayer로 치환해서 사용하는 방법</h2>
<p>UIView의 기본 CALayer는 더 전문화된 기능을 사용하기 위해 다른 타입의 layer로 치환할 수 있습니다.
예를 들면 CAShapeLayer, CAGradientLayer, CAEmitterLayer 등을 사용하여 더 화려한 View를 그릴 수 있죠.
그리고 AVPlayerLayer도 CALayer를 상속하는 만큼 동일하게 치환할 수 있습니다.
이렇게 치환하게 되면 AVPlayerLayer가 UIView에 적용된 AutoLayout을 그대로 따라가게 됩니다.
layout이 바뀔 때마다 수동으로 frame을 조정할 필요가 없기 때문에 가독성이 높고 유지 보수가 편리한 코드가 만들어 집니다.</p>
<img width=400 src= "https://velog.velcdn.com/images/sustainable-git/post/dd515c52-7d05-4f2d-be03-a316dabf1f41/image.gif">

<pre><code class="language-swift">import UIKit
import AVFoundation

final class AVPlayerView: UIView {
    override class var layerClass: AnyClass { AVPlayerLayer.self }
    private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
    private(set) var player: AVPlayer? {
        get { playerLayer.player }
        set { playerLayer.player = newValue }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        playerLayer.videoGravity = .resizeAspect
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        playerLayer.videoGravity = .resizeAspect
    }

    func loadVideo(from url: URL) {
        player = AVPlayer(url: url)
        player?.play()
    }
}

final class ContentViewController: UIViewController {
    private var avPlayerView: AVPlayerView?

    override func viewDidLoad() {
        super.viewDidLoad()
        setUpPlayer()
    }

    func setUpPlayer() {
        let avPlayerView = AVPlayerView()
        avPlayerView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(avPlayerView)

        NSLayoutConstraint.activate([
            avPlayerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            avPlayerView.topAnchor.constraint(equalTo: view.topAnchor),
            avPlayerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            avPlayerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])

        if let url = URL(string: &quot;http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4&quot;) {
            avPlayerView.loadVideo(from: consume url)
        }

        self.avPlayerView = avPlayerView
    }
}</code></pre>
<br>

<h2 id="두-방법중에-선택해야-할-방법은">두 방법중에 선택해야 할 방법은?</h2>
<p>두 가지 방법 중 더 권장되는 방식은 두 번째 방법입니다.
첫 번째 방법은 <code>viewDidLayoutSubviews()</code> 에서 frame을 수동으로 조정해야 하므로 성능에 부담이 될 수 있습니다.
layout 계산을 수동으로 처리해야 하며, 반복적으로 호출되고, 최적화가 되지 않습니다.
하지만 NSLayoutConstraint를 활용해 AutoLayout을 적용한 경우는 위의 경우보다 더 좋은 성능을 제공합니다.
또한 코드가 간결해지고 유지 보수가 간편해집니다.</p>
<p>그리고, 위에 첨부한 영상을 자세히 보면 두 영상의 애니메이션이 다름을 확인할 수 있습니다.
첫 번째 영상의 애니메이션은 무언가가 밀어 넣는듯한 느낌이 있지만, 두 번째 영상은 자연스럽게 전환이 됩니다.
AutoLayout을 활용했을 때 훨씬 부드럽고 자연스러운 애니메이션 효과가 나타남을 확인할 수 있습니다.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/9d606a52-109e-41cd-a5e8-e9b8bd679d7e/image.png">

<br>

<h2 id="추가--동영상-플레이어를-개발할-때-uikit을-사용하는-이유">추가 : 동영상 플레이어를 개발할 때 UIKit을 사용하는 이유</h2>
<p>우리는 UIKit에서 AVPlayerLayer, 또는 SwiftUI에서 VideoPlayer를 사용할 수 있습니다.
그러나 단순한 View를 구성하는 경우가 아니라면 성능과 커스터마이징의 용이함으로 인해 AVPlayerLayer를 사용하는게 유리합니다.</p>
<h3 id="1-ui-업데이트-및-상태-관리">1. UI 업데이트 및 상태 관리</h3>
<ul>
<li>VideoPlayer(SwiftUI) : 상태 변화가 발생하면 View를 다시 그리고, 이에 따라 <strong>추가적인 오버헤드</strong>가 발생할 수 있습니다.</li>
<li>AVPlayerLayer(UIKit) : UIKit의 단일 레이어이기 때문에 오버헤드가 거의 없으며, 필요한 프레임만을 업데이트하기 때문에 <strong>메모리와 CPU 사용이 더 가볍고 효율적</strong>입니다.</li>
</ul>
<h3 id="2-초기-진입로딩-속도">2. 초기 진입(로딩) 속도</h3>
<ul>
<li>VideoPlayer(SwiftUI) : SwiftUI가 전체 View 계층을 Run time에 관리하므로 초기 로드시 <strong>UI 생성 시간</strong>이 더 걸릴 수 있습니다.</li>
<li>AVPlayerLayer(UIKit) : 단일 AVPlayerLayer가 직접 레이어에 추가되므로, 초기 로드 <strong>속도가 약간</strong> 더 빠를 수 있습니다.</li>
</ul>
<h3 id="3-애니메이션-및-화면-전환">3. 애니메이션 및 화면 전환</h3>
<ul>
<li>VideoPlayer(SwiftUI) : 아직 SwiftUI의 애니메니션과 화면 전환은 UIKit보다 최적화가 부족합니다. 빠른 화면 전환과 애니메이션에서 <strong>프레임 드랍이 발생</strong>할 수 있습니다.</li>
<li>AVPlayerLayer(UIKit) : UIKit의 AVPlayerLayer가 화면 전환과 애니메이션에 더 <strong>최적화</strong>되어 있습니다.</li>
</ul>
<h3 id="4-커스터마이징">4. 커스터마이징</h3>
<ul>
<li>VideoPlayer(SwiftUI) : 재생, 일시 정지, 되감기 등 기능 추가에 <strong>별도의 상태 관리 및 뷰 작업</strong>이 필요하므로 성능에 좋지 않을 수 있습니다.</li>
<li>AVPlayerLayer(UIKit) : sublayer로 자유롭게 커스터마이징이 가능하고, <strong>성능에 영향이 없게 인터페이스를 구현</strong>할 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ViewModel init()이 반복 호출되는 이유: SwiftUI의 @StateObject 올바르게 사용하기]]></title>
            <link>https://velog.io/@sustainable-git/ViewModel-init%EC%9D%B4-%EB%B0%98%EB%B3%B5-%ED%98%B8%EC%B6%9C%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-SwiftUI%EC%9D%98-StateObject-%EC%98%AC%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sustainable-git/ViewModel-init%EC%9D%B4-%EB%B0%98%EB%B3%B5-%ED%98%B8%EC%B6%9C%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-SwiftUI%EC%9D%98-StateObject-%EC%98%AC%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 27 Oct 2024 08:50:29 GMT</pubDate>
            <description><![CDATA[<h2 id="viewmodel-init이-지속적으로-호출되는-현상">ViewModel init이 지속적으로 호출되는 현상</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/7c0268e9-d5e3-418b-b10c-abbadf6bc15f/image.gif" alt=""></p>
<p>부모 View에 카운터, 자식 View에도 카운터가 있는 View를 만들어 보았습니다.
부모 View는 <code>@State</code>로 number 값을 변경하여 View를 업데이트합니다.
자식 View는 <code>@StateObject</code>로 ViewModel을 가지고 있고, viewModel의 number 값을 변경하여 View를 업데이트합니다.</p>
<p>그런데, 부모 View의 number값이 변경될 때마다 자식 View의 ViewModel이 매번 <code>init()</code>이 됩니다.
그 이유가 무엇일까요?</p>
<pre><code class="language-swift">import SwiftUI

class ViewModel: ObservableObject {
    @Published private(set) var number: Int = 0

    init() {
        print(&quot;viewModel initialized&quot;)
    }

    func upCount() {
        number += 1
    }
}

struct ContentView: View {
    @State private var count: Int = 0

    var body: some View {
        VStack {
            ChildView()
            Button(&quot;부모 뷰 카운터 = \(count)&quot;) {
                count += 1
            }
        }
        .padding()
        .border(.red)
    }
}

struct ChildView: View {
    @StateObject var viewModel: ViewModel

    init() {
        let viewModel = ViewModel()
        _viewModel = StateObject(wrappedValue: viewModel)
    }

    var body: some View {
        VStack {
            Button(&quot;자식 뷰 카운터 = \(viewModel.number)&quot;) {
                viewModel.upCount()
            }
        }
        .padding()
        .border(.blue)
    }
}</code></pre>
<br>

<h2 id="문제의-원인은-stateobject를-생성-위치에-있다">문제의 원인은 @StateObject를 생성 위치에 있다</h2>
<pre><code class="language-swift">struct ChildView: View {
    @StateObject var viewModel: ViewModel

    init() {
        let viewModel = ViewModel()
        _viewModel = StateObject(wrappedValue: viewModel)
    }

    var body: some View {
        VStack {
            Button(&quot;자식 뷰 카운터 = \(viewModel.number)&quot;) {
                viewModel.upCount()
            }
        }
        .padding()
        .border(.blue)
    }
}</code></pre>
<p>위 상황에서 <code>@StateObject</code> 인 ViewModel을 생성할 때 자식 View의 init()에서 생성하고 있습니다.
일반적으로 <code>@StateObject var viewModel = ViewModel()</code> 처럼 선언하지만,
ViewModel 내부에 특정 값을 초기화해야 할 경우에는 View의 <code>init()</code>에서 ViewModel을 생성하게 됩니다.
하지만, 여기서 <code>@StateObject</code>의 초기화 과정을 명확하게 이해하지 못하면 문제가 발생할 수 있습니다.</p>
<br>

<h2 id="stateobject의-init에는-autoclosure가-있다">@StateObject의 init()에는 @autoclosure가 있다.</h2>
<pre><code class="language-swift">@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@frozen @propertyWrapper public struct StateObject&lt;ObjectType&gt; : DynamicProperty where ObjectType : ObservableObject {
    @inlinable public init(wrappedValue thunk: @autoclosure @escaping () -&gt; ObjectType)
    ...
}</code></pre>
<p>위 코드는 SwiftUI에서 <code>@StateObject</code>가 구현된 부분입니다.
<code>init()</code> 함수에는 <code>@autoclosure</code>가 적용되어 있습니다.
이 <code>@autoclosure</code>는 wrappedValue에 값을 할당하며, View의 lifecycle 동안 오직 한 번만 호출됩니다.</p>
<pre><code class="language-swift">    init() {
        let viewModel = ViewModel()
        _viewModel = StateObject(wrappedValue: viewModel)
    }</code></pre>
<p>하지만, View의 <code>init()</code> 함수는 자주 호출될 수 있습니다.
때문에 <code>_viewModel = StateObject(wrappedValue: viewModel)</code> 구문은 한 번만 호출되지만,
<code>let viewModel = ViewModel()</code> 구문은 부모 View의 <code>@State</code> 값 변화에 따라 지속적으로 실행됩니다.</p>
<br>

<h2 id="해결-방법">해결 방법</h2>
<p>이 문제를 해결하는 방법은 두 가지가 있습니다.</p>
<ol>
<li><code>@StateObject(wrappedValue:)</code>값을 _viewModel에 직접 대입하는 방법</li>
</ol>
<pre><code class="language-swift">    init() {
        _viewModel = StateObject(wrappedValue: ViewModel())
    }</code></pre>
<ol start="2">
<li><code>@StateObject(wrappedValue:)</code> 로 ViewModel을 만든 후 _viewModel에 대입하는 방법</li>
</ol>
<pre><code class="language-swift">    init() {
        let viewModel = StateObject(wrappedValue: ViewModel())
        _viewModel = viewModel
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/e18bb4e2-1efd-4514-82fe-f449c4b5a36d/image.gif" alt=""></p>
<pre><code class="language-swift">import SwiftUI

class ViewModel: ObservableObject {
    @Published private(set) var number: Int = 0

    init() {
        print(&quot;viewModel initialized&quot;)
    }

    func upCount() {
        number += 1
    }
}

struct ContentView: View {
    @State private var count: Int = 0

    var body: some View {
        VStack {
            ChildView()
            Button(&quot;부모 뷰 카운터 = \(count)&quot;) {
                count += 1
            }
        }
        .padding()
        .border(.red)
    }
}

struct ChildView: View {
    @StateObject var viewModel: ViewModel

    init() {
        _viewModel = StateObject(wrappedValue: ViewModel())
    }

    var body: some View {
        VStack {
            Button(&quot;자식 뷰 카운터 = \(viewModel.number)&quot;) {
                viewModel.upCount()
            }
        }
        .padding()
        .border(.blue)
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[렌더링 개선으로 CPU 사용률 2.8% 절감한 이야기 (SwiftUI 렌더링 관점에서 본 @StateObject와 @ObservedObject를 혼동하면 안되는 이유)]]></title>
            <link>https://velog.io/@sustainable-git/%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B0%9C%EC%84%A0%EC%9C%BC%EB%A1%9C-CPU-%EC%82%AC%EC%9A%A9%EB%A5%A0-2.8-%EC%A0%88%EA%B0%90%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-SwiftUI-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C-%EB%B3%B8-StateObject%EC%99%80-ObservedObject%EB%A5%BC-%ED%98%BC%EB%8F%99%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@sustainable-git/%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B0%9C%EC%84%A0%EC%9C%BC%EB%A1%9C-CPU-%EC%82%AC%EC%9A%A9%EB%A5%A0-2.8-%EC%A0%88%EA%B0%90%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-SwiftUI-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C-%EB%B3%B8-StateObject%EC%99%80-ObservedObject%EB%A5%BC-%ED%98%BC%EB%8F%99%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Wed, 23 Oct 2024 10:10:44 GMT</pubDate>
            <description><![CDATA[<h2 id="미리보는-결과">미리보는 결과</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/9b111e58-dc52-462c-b56b-3fad0fde0438/image.png" alt="">
(좌측이 개선 이전, 우측이 개선 이후입니다. 개선 이후에는 해당 함수가 호출되지 않아 공백으로 남았습니다.)</p>
<p><strong>문제:</strong>
당시 View에서 터치, 드래그, 또는 내부 이벤트가 발생할 때마다 updateUIViewController 함수가 매번 호출되어, UIViewControllerRepresentable에서 불필요한 업데이트가 발생했습니다.
이 문제는 주입된 ViewModel이 <code>@ObservedObject</code>가 아닌 <code>@StateObject</code>로 잘못된 property wrapper를 사용했기 때문이었습니다.</p>
<p><strong>해결:</strong>
Property wrapper를 <code>@ObservedObject</code>로 다시 변경함으로써, 불필요한 리렌더링을 방지하고, 이벤트 발생 시마다 updateUIViewController 함수가 호출되지 않도록 했습니다.</p>
<p><strong>성능 개선:</strong>
이를 통해 의미 없이 소모되던 <strong>메인 스레드 점유 시간 104ms</strong>와 <strong>CPU 사용률 2.8%</strong>를 절감할 수 있었습니다.
이 수치는 특히 애니메이션이나 반복 작업이 많은 환경에서 성능 향상에 큰 영향을 미쳤습니다.</p>
<br>

<h2 id="서론">서론</h2>
<ul>
<li><a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기</a></li>
<li><a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B02">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기2</a></li>
<li><a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B03-%EB%8B%A4%EB%A5%B8-View%EC%9D%98-State-%EA%B0%92-%EB%B3%80%ED%99%94%EB%A1%9C-%EC%9D%B8%ED%95%B4-View%EA%B0%80-%EC%B4%88%EA%B8%B0%ED%99%94%EB%90%98%EB%8A%94-%ED%98%84%EC%83%81">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기3-다른-View의-State-값-변화로-인해-View가-초기화되는-현상</a></li>
</ul>
<p><code>@StateObject</code>와 <code>@ObservedObject</code>는 SwiftUI에서 <code>ObservableObject</code>를 사용하는 방법입니다.
두 방법 모두 <code>@Published</code>로 선언된 프로퍼티의 값이 변경되면 View를 랜더링한다는 공통점이 있습니다.</p>
<p>하지만<code>@StateObject</code>는 View가 해당 객체를 <strong>소유</strong>할 때, <code>@ObservedObject</code>는 외부에서 <strong>전달받을 때</strong> 사용한다는 차이점이 있습니다.
이 차이를 무시하면, View가 다시 그려질 때 <code>@ObservedObject</code>가 다시 생성되면서 값이 초기화되는 대참사가 일어나곤 합니다.</p>
<p>그렇다면, 모든 경우에 <code>@StateObject</code>를 사용하면? 당연히 안되겠죠?
오늘은 렌더링 관점에서 <code>@StateObject</code>와 <code>@ObservedObject</code>를 혼동해서는 안 되는 이유를 살펴보겠습니다.</p>
<br>

<h2 id="자식-view에서-stateobject로-viewmodel을-받으면-안-되는-이유">자식 View에서 @StateObject로 ViewModel을 받으면 안 되는 이유</h2>
<p>테스트를 위해, 아무 역할도 하지 않는 ViewModel을 부모 View에 <code>@StateObject</code>로 선언합니다.
그다음, 이를 <code>@ObservedObject</code>로 자식 View에 전달해 의존성 주입을 합니다.</p>
<p>이후 부모 View에 있는 버튼을 눌러 number 값을 증가시키면 어떻게 될까요?
이 경우 부모 View만 업데이트되고, ViewModel이 역할을 하지 않기 때문에 자식 View에서는 렌더링이 발생하지 않습니다.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/1a430abd-078d-40f2-af09-f164b2007d76/image.gif">

<p>그렇다면, 만약 자식 View가 ViewModel을 <code>@StateObject</code>로 참조하면 어떻게 될까요?
이때는 부모 View에서 버튼을 누르면 자식 View도 다시 렌더링됩니다.</p>
<p>문제는, ViewModel이 부모 View의 <code>@State</code> 값과 전혀 관련이 없는데도 자식 View가 다시 그려진다는 점입니다.
전혀 관계 없는 상태의 변화 때문에 불필요한 렌더링이 발생하는 것이죠.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/d5f6a12c-403d-4b95-b6fb-d6ff6547dbe5/image.gif">

<pre><code class="language-swift">import SwiftUI

fileprivate final class ViewModel: ObservableObject {}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    @State private var number: Int = 0

    var body: some View {
        VStack {
            ChildView(viewModel: viewModel)
            Button(&quot;number = \(number)&quot;) {
                number += 1
            }
        }
    }
}

fileprivate struct ChildView: View {
    @StateObject var viewModel: ViewModel

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;넘버는 나랑 무관해서 렌더링이 일어나면 안 됨&quot;)
    }
}</code></pre>
<br>

<h2 id="viewmodel을-state로-두면-어떤-일이-일어날까">ViewModel을 @State로 두면 어떤 일이 일어날까?</h2>
<p>이번에는 ViewModel을 <code>@State</code>로 선언하면 어떤 결과가 나올지 살펴보겠습니다.
우선, 부모 View에서 ViewModel의 property wrapper를 <code>@State</code>로 두고, number 변수를 ViewModel 안에 넣어 <code>@Published</code>로 선언해 보겠습니다.</p>
<p>이렇게 하면, 자식 View는 ViewModel을 <code>@ObservedObject</code>로 참조하므로, number 값이 바뀔 때마다 자식 View가 다시 그려지게 됩니다.
하지만, 부모 View는 이 변화를 감지하지 못합니다.</p>
<p>그 이유는, 부모 View에서 ViewModel을 <code>@State</code>로 선언했기 때문입니다.
<code>@State</code>는 값 타입(Value Type)의 변화를 추적하여 View를 다시 그리지만, ViewModel은 참조 타입(Reference Type)이므로, 내부 값의 변화만으로는 부모 View에 변경 사항이 전달되지 않습니다.
결국 부모 View는 number 값의 변화를 알지 못해 업데이트가 발생하지 않게 되는 것이죠.</p>
<p>이로 인해 부모 View에서는 렌더링이 일어나지 않아 숫자가 변하지 않는 이상한 상황이 발생하게 됩니다.</p>
<img width=800 src="https://velog.velcdn.com/images/sustainable-git/post/10305700-0d83-4940-a732-871ad2ff335e/image.gif">

<pre><code class="language-swift">import SwiftUI

fileprivate final class ViewModel: ObservableObject {
    @Published private(set) var number: Int = 0

    func upCount() {
        number += 1
    }
}

struct ContentView: View {
    @State private var viewModel = ViewModel()

    var body: some View {
        VStack {
            ChildView(viewModel: viewModel)
            Button(&quot;number = \(viewModel.number)&quot;) {
                viewModel.upCount()
            }
        }
    }
}

fileprivate struct ChildView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;ViewModel이 Publish 하니까 다시 그려 짐&quot;)
    }
}</code></pre>
<br>

<h2 id="결론">결론</h2>
<p>ViewModel은 해당 Life cycle을 관리할 View에서 <code>@StateObject</code>로 선언하고, 자식 View에 주입할 때는 반드시 <code>@ObservedObject</code>로 사용해야 합니다.
전역적으로 관리가 필요한 객체는 <code>@ObservedObject</code> 대신 <code>@EnvironmentObject</code>로 처리해도 됩니다.</p>
<p>여기서 <code>@EnvironmentObject</code>를 무조건 사용하는 것이 더 편리할 것 같다는 생각이 들 수 있습니다.
하지만, <code>@EnvironmentObject</code>는 주입이 제대로 이루어지지 않을 경우 크래시가 발생할 수 있습니다.
특히 ViewModel을 다양한 View에서 사용하는 경우, 이 문제가 더 자주 발생할 수 있다는 점을 꼭 유념해야 합니다.</p>
<p>ps. ViewModel을 <code>@State</code>로 두어 부모 View의 렌더링은 없애고 자식 View만 렌더링 되게 하면 좋지 않을까 라는 생각이 든다면 그냥 let으로 두면 되니 <code>@State</code>로 두지 마세요...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS에서 Xcode로 다국어 지원하기(4)]]></title>
            <link>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B04</link>
            <guid>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B04</guid>
            <pubDate>Thu, 17 Oct 2024 14:44:18 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p><a href="https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B02">https://velog.io/@sustainable-git/iOS에서-Xcode로-다국어-지원하기2</a>
<a href="https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B03">https://velog.io/@sustainable-git/iOS에서-Xcode로-다국어-지원하기3</a>
이전 글에서 String catalog를 활용해 다국어를 처리하는 방법을 소개하였습니다.
오늘은 이전 글에 이어서 String Catalog를 활용해서 겪을수 있는, 제가 실제로 겪었던 이슈를 소개해보겠습니다.</p>
<br>

<h2 id="lld만-덩그러니">%lld만 덩그러니</h2>
<img width=383 src="https://velog.velcdn.com/images/sustainable-git/post/b118db5e-03f8-4acd-a96c-473cc09c4adb/image.png">

<p>String Catalog를 쓰다 보면 이렇게 덩그렇게 %lld만 있는 상황이 벌어집니다.
이런 이유는 String Catalog가 <code>Text(_:)</code> View에 넣어놓은 모든 String을 LocalizedStringResource로 판단하기 때문입니다.
그래서 이렇게 번역이 되지 않아야 할 문자까지 번역으로 넣어놓습니다.
이를 해결하려면 <code>Text(_:)</code>가 아닌 <code>Text(verbatim:)</code>을 사용해야 합니다.</p>
<img width=822 src="https://velog.velcdn.com/images/sustainable-git/post/3c0e922c-385a-441e-9d23-ca30f64ae175/image.png">

<p>verbatim은 직역하면 <strong>&quot;말 그대로&quot;</strong> 라는 뜻입니다.
이렇게 하면 String catalog가 자동으로 String을 가져오지 않아 번역이 되지 않는 <code>Text</code> View를 만들 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/a4f68b42-3f43-4efa-937a-dbe72e12382e/image.gif" alt=""></p>
<br>

<h2 id="stringformat_-값을-번역하는-법">String(format:,_:) 값을 번역하는 법</h2>
<pre><code class="language-swift">    if hours &gt; 0 {
        return String(format: &quot;%02d시 %02d분 %02d초&quot;, hours, minutes, seconds)
    } else {
        return String(format: &quot;%02d분 %02d초&quot;, minutes, seconds)
    }</code></pre>
<p>개발을 하다 보면 시 분 초를 두 자리씩 보여줘야 하는 경우를 마주할 수 있습니다.
하지만, <code>String(format:,_:)</code> 함수는 string catalog에서 자동으로 등록해주지 않습니다.
그렇기에 이를 자동으로 처리할 수 있는 <code>String(localized:)</code> 함수를 사용하는게 좋습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/ae7b1267-0585-4bac-bf8d-beb29ec4390e/image.png" alt=""></p>
<br>

<h2 id="동일한-key가-여러개일-때동음이의">동일한 Key가 여러개일 때(동음이의)</h2>
<img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/9c9747cc-3153-40a0-817e-60b977341ed7/image.png">

<p>String catalog에서 Key는 동일한 값이 존재할 수 없고, 반드시 Hashable해야 합니다.
따라서 위의 예시의 <code>방망이</code> 와 <code>박쥐</code>의 사례처럼 각각의 Key에 값을 다르게 지정해 주어야 합니다.
저의 경우 개발 가독성 향상을 위해 영어 Key를 한국어 Key로 변경하는 작업을 했기 때문에 <code>배</code>의 사례와 같은 경우가 발생하였습니다.
Key를 한국어로 바꾸어야하는 상황이었는데, 6개 언어(한,간체,번체,일,영,태국)로 번역되어야 하는 것이 큰 문제였습니다.
각 언어마다 3500자, 6개 언어인 21000자가 String Catalog로 Migration되어 있습니다.</p>
<p>다행인 점은, 기존의 영어 Key도 Hashable해야하기 때문에<del>(사실은 겹치는게 있어서 고생했지만)</del>
Filter에 기존의 영어 Key를 넣어 주면 쉽게 찾을 수 있었습니다.
String catalog에서 새로운 Key를 만들고(자동으로 만들어진 Key가 있으면 사용하면 됩니다)
만약 <code>배</code>의 사례처럼 중복되는 것이 있으면 <code>배1</code> 처럼 Key를 만들고 Korean 번역을 <code>배</code>로 해주면 됩니다.
단, 해당 Key에 각 언어마다 번역된 값을 일일이 넣어줘야 합니다. </p>
<br>

<h2 id="merge-conflict">Merge Conflict</h2>
<img width=600 src="https://velog.velcdn.com/images/sustainable-git/post/bad8e83f-c915-4e60-bade-3223486931e8/image.png">

<p>String catalogs는 merge conflict에 매우 매우 취약한 형태입니다.
매 빌드마다 String catalogs가 조금씩 바뀔 수 있는데, 크지 않은 변경 또는 변경이 없는데에도 conflict가 발생할 수 있습니다.
저장되는 형태의 특성상 컴퓨터가 변경된 위치를 알아차리기 힘들기 때문에 두 명 이상의 수정에 엄청난 수의 conflict가 발생하곤 합니다.
게다가 소스코드와 달리 디버깅이 매우 어려운 형태로 발생하며, 조금의 문제라도 있으면 String catalog가 열리지 않습니다.
또한, merge conflict의 패턴이 일관적이지 않아 간단한 스크립트로도 처리하기 어렵습니다.
때문에 String catalog를 사용할 때에는 merge conflict에 유의하며 개발하는 것을 추천드립니다.</p>
<p>혹시 tuist처럼 변환해주는 프로그램을 만드신다면 공유 부탁드립니다...</p>
<br>

<h2 id="개행이-있는-문자가-번역이-안되는-현상">개행이 있는 문자가 번역이 안되는 현상</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/b80f6850-de4e-490f-b4ad-c85fb0ad6e3f/image.png" alt=""></p>
<p>우선, String catalog에서 Key가 아닌 value에서 개행을 처리하려면 <code>\n</code>이 아닌 <code>control + enter</code>를 사용하는 것에 유의해야 합니다.
그러나, 이런 처리를 제대로 하였음에도 번역이 안되는 경우가 있습니다.
그럴 때에는 String catalog를 source code로 열어 보아야 합니다.</p>
<img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/0563ef16-f236-4495-a97a-cd3ab6166d83/image.png">

<p>개행이 <code>\\n</code>으로 되어 있거나 <code>\r</code>이 들어가 있는 경우가 있습니다.
이는 특히 기존의 legacy strings 파일을 Migration한 경우 더 많이 발생할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/ca9c5d96-1eaf-4f4c-93f7-ca8b56c7df84/image.png" alt=""></p>
<br>

<h2 id="국가마다-숫자를-표기하는-방법이-다릅니다">국가마다 숫자를 표기하는 방법이 다릅니다</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/a3598a7e-c49b-4871-b9b8-e2b7200dc916/image.png" alt=""></p>
<p>국가마다 천의 자리를 표기하는 방법이 다릅니다.
인도의 경우 특정 지역에서는 불규칙적으로 띄워쓰기를 하는 경우도 있습니다.
여기서 주의해야할 점은 바로 숫자를 <code>\(Int)</code>의 형태로 써야한다는 것입니다.</p>
<p>많은 개발자들이 숫자를 <strong>보여준다</strong>에 집중하기 때문에 이를 <code>String</code>의 형태로 전달하곤 합니다.
하지만, 그렇게 되면 국가마다 다른 숫자 단위 표기를 제대로 지원하기 어렵습니다.
때문에 정수 값을 다룰 때에는 <code>\(Int)</code>의 형태로 쓰기 바랍니다.</p>
<br>

<h2 id="언어-제거하기">언어 제거하기</h2>
<p>String Catalog는 각 하나의 Key를 제거하는 방법은 존재하지만, 하나의 언어를 통째로 지우기는 불가능합니다.
프로젝트 언어에서 특정 언어를 제거하고 빌드해도 사라지지 않습니다. <del>(이걸 지우려고 한 세월 날렸습니다)</del>
결국 제가 선택한 방법은 python 스크립트를 이용해 string catalog 파일에서 특정 언어를 지우고, 다시 갈아 끼우는 것이었습니다.</p>
<pre><code class="language-py">import json

def remove_localizations(input_filepath, output_filepath, languages_to_remove):

    with open(input_filepath, &#39;r&#39;, encoding=&#39;utf-8&#39;) as file:      # Load the JSON data from the file
        data = json.load(file)

    for string_key in list(data[&quot;strings&quot;].keys()):                 # Remove specified languages from the &#39;strings&#39; section
        string_data = data[&quot;strings&quot;][string_key]
        if &quot;localizations&quot; in string_data:                          # Check if &#39;localizations&#39; exists before trying to access it
            for lang in languages_to_remove:
                if lang in string_data[&quot;localizations&quot;]:
                    del string_data[&quot;localizations&quot;][lang]

    with open(output_filepath, &#39;w&#39;, encoding=&#39;utf-8&#39;) as file:     # Write the updated JSON data back to a file
        json.dump(data, file, indent=2, ensure_ascii=False)


input_filepath      = &#39;local_in.txt&#39;        # Name of file with all languages       (ie: &#39;local_in.txt&#39; file in the same directory, so use the relative path)
output_filepath     = &#39;local_out.txt&#39;       # Name of file with the the new changes (ie: &#39;local_out.txt&#39; the original or you can overwrite the file if you want)
languages_to_remove = [&#39;en&#39;, &#39;ja&#39;, &#39;th-TH&#39;, &#39;zh-Hans&#39;, &#39;zh-Hant&#39;]          # Languages to remove                   (in my case, I only wanted to keep the root (&#39;en&#39;) language and &#39;jp&#39;)

# Call the function to remove the languages
remove_localizations(input_filepath, output_filepath, languages_to_remove)
</code></pre>
<br>

<h2 id="한-문장에-여러-개의-색상과-폰트가-필요한-경우">한 문장에 여러 개의 색상과 폰트가 필요한 경우</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/1da9375a-99e8-4cc5-b7a7-20d9c59d597e/image.png" alt=""></p>
<p>여기 제가 개발한 방법을 소개하지만, 여러 가지 방법으로 개발할 수 있습니다.
언어마다 각 속성이 어디서 어떻게 동작하는지가 바뀔 수 있습니다.
그 전에 속성이 동작 해야하는지, 하지 않아야 하는지도 국가마다 다를 수 있습니다.</p>
<ul>
<li>예) 중국어는 빨갛게, 한국어는 볼드로 해주세요. 근데 영어는 해당 표현이 없으니 표현을 빼고 해주세요.</li>
</ul>
<p>그럼에도 불구하고 가능한 최선의 경우라고 생각하는 코드를 만들었으니 도움이 되길 바랍니다.
html과 유사한 느낌으로 개발해 보았습니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack(spacing: 5) {
            LocalizedAttributedText(&quot;사과는 6000원 입니다.&quot;, attributes: [&quot;&lt;color&gt;&quot; : .init([.foregroundColor : UIColor.red])])
        }
    }
}

struct LocalizedAttributedText: View {
    let attributedString: AttributedString

    init(_ localizedStringResource: LocalizedStringResource, attributes: [String: AttributeContainer]) {
        var attributedString = AttributedString(String(localized: consume localizedStringResource))
        attributes.forEach { (key, value) in
            if let regex = try? Regex(#&quot;&lt;(?&lt;tagName&gt;[a-zA-Z0-9_]+)&gt;&quot;#),
               let match = key.firstMatch(of: consume regex),
               let tagName = (consume match).output[&quot;tagName&quot;]?.value,
               let startRange = attributedString.range(of: &quot;&lt;\(tagName)&gt;&quot;),
               let endRange = attributedString.range(of: &quot;&lt;/\(tagName)&gt;&quot;) {
                attributedString[startRange.upperBound..&lt;endRange.lowerBound].setAttributes(value)
                attributedString.removeSubrange(consume endRange)
                attributedString.removeSubrange(consume startRange)
            }
        }
        self.attributedString = consume attributedString
    }

    var body: some View {
        Text(attributedString)
    }
}

#Preview {
    ContentView()
}</code></pre>
<br>

<h2 id="마무리">마무리</h2>
<p>Xcode에서 String catalog를 활용해 다국어 처리를 하는 방법과 발생할 수 있는 다양한 이슈, 해결방법을 알아보았습니다.
위 내용이 SwiftUI 위주로 구성되어 있지만, UIKit에서도 충분히 사용이 가능합니다.
또한, LocalizedStringResource 활용이 가능한 iOS16 환경에서의 예시를 보여드렸지만,
사실 저는 iOS14를 지원해야 했어서 개발에 더 많은 고생을 하였습니다.</p>
<p>iOS14의 경우 NSLocalizedString을 사용해야 합니다.
또한 crash의 위험이 존재하기 때문에 Unit Test를 적극 활용해 사고를 미연에 방지할 필요가 있습니다.
하지만, 지금까지 알려준 내용에 크게 벗어나지 않기 때문에 이를 잘 활용 하면 문제 없이 다국어 처리를 할 수 있을거라 생각합니다.
아래에는 iOS14 이하의 경우에 Unit Test를 하는 방법에 관한 간단한 코드를 남겨두고 글을 이만 마치겠습니다.</p>
<pre><code class="language-swift">extension String {
    func localized(bundle: Bundle, _ arguments: CVarArg...) -&gt; Self {
        return String(format: NSLocalizedString(self, bundle: bundle, comment: &quot;&quot;), arguments: arguments)
    }
}

final class Tests: XCTestCase {
    enum LocalizationTestError: Error {
        case invalidKoreanBundle
        case invalidEnglishBundle
    }

    private var ko: Bundle!
    private var en: Bundle!

    override func setUpWithError() throws {
        guard let koreanPath = Bundle.main.path(forResource: &quot;ko&quot;, ofType: &quot;lproj&quot;),
              let koreanBundle = Bundle(path: koreanPath) else {
            throw LocalizationTestError.invalidKoreanBundle
        }
        ko = koreanBundle

        guard let englishPath = Bundle.main.path(forResource: &quot;en&quot;, ofType: &quot;lproj&quot;),
              let englishBundle = Bundle(path: englishPath) else {
            throw LocalizationTestError.invalidEnglishBundle
        }
        en = englishBundle
    }

    override func tearDown() {
        ko = nil
        en = nil
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS에서 Xcode로 다국어 지원하기(3)]]></title>
            <link>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B03</link>
            <guid>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B03</guid>
            <pubDate>Sat, 12 Oct 2024 02:40:18 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p><a href="https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B02">https://velog.io/@sustainable-git/iOS에서-Xcode로-다국어-지원하기2</a>
이전 글에서는 String catalog를 활용해 다국어를 처리하는 방법을 소개하였습니다.
이어서 계속 진행하겠습니다.</p>
<br>

<h2 id="숫자-번역하기">숫자 번역하기</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/0a250205-5d95-4782-b61d-5c1cdf3cd8e4/image.png" alt=""></p>
<p>연필이 n개 있다 라는 String을 번역해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/76157ea5-3c7b-4583-be2d-cbd1f90e443d/image.png" alt=""></p>
<p>빌드를 하면 String catalog에 해당 string이 자동으로 들어오는걸 확인할 수 있습니다.
그런데, 이걸 영어로 번역하면 동사와 명사를 단수로 써야할지, 복수로 써야할지 고민이 되실 겁니다.</p>
<img width=150 src="https://velog.velcdn.com/images/sustainable-git/post/82a4bebe-01d7-433b-8e96-ac0026e4fb62/image.png">

<p>해당 줄을 우클릭하게 되면 <code>Vary by Plural</code>을 선택할 수 있고, 수에 따라 다르게 번역을 할 수 있게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/cd3bafea-ac66-46fe-a293-68549d3afc85/image.png" alt=""></p>
<p>영어는 단수와 복수를 구분하므로 두 가지의 경우에 대해 번역을 다르게 해주면 됩니다.
(특정 언어의 경우 3가지 이상의 구분이 있을 수 있습니다.)</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/cb527a30-afe9-43ba-9970-773b56345a47/image.png" alt=""></p>
<br>

<h2 id="여러개의-입력-순서-구분하기">여러개의 입력 순서 구분하기</h2>
<p>언어마다 어순이 다른 경우가 있습니다.
예를 들어 한국에서는 3/4를 4분의 3이라고 읽지만 영어에서는 three-fourth라고 읽습니다.
이럴 때에는 각 언어의 순서에 맞게 String catalog에서 $1, $2를 붙여주면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/e73b24e7-e43a-4ed9-9096-e29af3ac9afd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/3adcf21e-775f-4f69-b8da-1f43a8849c3c/image.png" alt=""></p>
<br>

<h2 id="기기마다-다르게-번역하기">기기마다 다르게 번역하기</h2>
<img width=250 src="https://velog.velcdn.com/images/sustainable-git/post/794b5bde-41f8-4d2b-9562-8d70d70d0a84/image.png">

<p>숫자 번역하기와 다르지 않으므로 따로 깊게 설명하지 않겠습니다.</p>
<br>

<h2 id="마무리">마무리</h2>
<p>이전 글에 이어서 String catalog를 활용해 다국어 번역을 처리하는 방법에 대해 알아보았습니다. 
특히, 숫자에 따른 번역 차이와 언어에 따른 입력 순서의 차이를 어떻게 해결할 수 있는지 살펴보았고, 기기에 따라 다르게 번역하는 방법도 간단히 짚어보았습니다.</p>
<p>다음 글에서는 String catalog를 이용해 다국어 처리를 하는 과정에서 실제로 발생할 수 있는 여러 사례들을 분석하고, 각각의 문제에 대한 해결 방법을 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS에서 Xcode로 다국어 지원하기(2)]]></title>
            <link>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B02</link>
            <guid>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B02</guid>
            <pubDate>Sat, 05 Oct 2024 04:01:15 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p><a href="https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B01">https://velog.io/@sustainable-git/iOS에서-Xcode로-다국어-지원하기1</a>
이전 글에서 Xcode 14 이하의 버전에서 <code>.strings</code>파일을 추가하여 다국어를 처리하는 방법을 다루었습니다.
하지만 Xcode 15 버전에서는 <strong>String catalog</strong>라는 새로운 기능이 도입되었습니다.
String catalog는 문자열 리소스를 하나의 파일에서 효율적으로 관리하고, 각 View에서 자동으로 해당 문자열을 가져올 수 있게 합니다.
이번 글에서는 String catalog를 활용해 다국어를 처리하는 방법을 소개하겠습니다.</p>
<br>

<h2 id="시작하기-전에">시작하기 전에</h2>
<h3 id="ios-16-미만인-경우">iOS 16 미만인 경우</h3>
<p>Xcode 15 버전으로 iOS 16 미만 버전을 대응하는 앱을 개발하는 경우도 있을 수 있습니다.
그럴 경우 <code>LocalizedStringResource</code>를 사용할 수 없고, <code>String(localized:)</code>도 또한 사용할 수 없습니다.
그런 경우 NSLocalizedString을 사용해서 다국어를 처리해야 합니다.
이전 글의 내용을 함께 활용하여 문제를 해결하시면 됩니다.
아래는 Apple의 공식문서에 적혀있는 글입니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/c2af6f1e-abe4-42da-bde4-3493334502d0/image.png" alt="https://developer.apple.com/documentation/Xcode/localizing-and-varying-text-with-a-string-catalog"></p>
<h3 id="strings-파일이-있는-상태에서-migration이-필요한-경우">strings 파일이 있는 상태에서 migration이 필요한 경우</h3>
<p>String catalog는 migration을 지원합니다.
strings 파일을 우클릭해서, migrate to string catalog... 버튼을 누르기만 하면 됩니다.</p>
<img width=200 src="https://velog.velcdn.com/images/sustainable-git/post/1cf5a13f-a1e4-4cbd-8fb3-6454bae4746f/image.png">

<p><img src="https://velog.velcdn.com/images/sustainable-git/post/99fadee1-94b9-480f-95dd-3c757b9497cf/image.png" alt=""></p>
<p>만약, 특정 언어가 번역이 안 된 상태로 string catalog가 만들어진다면 아래와 같이 stirngs 파일이 모든 언어에 대해 체크박스가 되어 있었는지 확인해 주세요.</p>
<img width=200 src="https://velog.velcdn.com/images/sustainable-git/post/df3ad711-afcc-4324-873c-f23a1a07ad02/image.png">

<h3 id="string-catalog를-쓴다면-script를-사용하지-않는게-좋습니다">String catalog를 쓴다면 script를 사용하지 않는게 좋습니다</h3>
<p>이전 글에서 script를 사용한 이유는 View에서 String을 분리하여 Human error를 제거하기 위함이었습니다.
하지만, String catalog의 자동화는 View에서 String 값을 자동으로 가져오기 때문에 View에 String이 존재해야 합니다.
Human Error는 매 빌드마다 업데이트되는 String catalog에 State 값을 통해 Human error를 확인하고 처리해야 합니다.</p>
<br>

<h2 id="string-catalog를-이용해-문자열을-자동으로-가져오기">String catalog를 이용해 문자열을 자동으로 가져오기</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/f36cfd8e-bc02-42ab-a781-6d115925e5a7/image.png" alt=""></p>
<p>위와 같은 상황에서 참치를 추가하고 빌드를 해봅시다</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/3922821e-5c8a-47b3-9ba8-8c0b96a44709/image.png" alt=""></p>
<p>그러면 String catalog에 참치가 추가되고, String catalog는 참치 Key에 대해 State에 New 태그를 붙여줍니다.
이런 변화는 각 언어 옆에 적혀있는 coverage의 변화로도 쉽게 확인할 수 있습니다.
여기서 English 부분에 번역을 적어주면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/f8a6ba7c-3f1a-42da-a659-8e0923fc26a7/image.png" alt=""></p>
<p>그런데, 일본어를 잘 모르는 사람이 임의로 참치를 다국어 처리했다고 생각해 봅시다.
잘못된 번역이 되었을 수도 있죠?
이런 경우 우클릭하여 Mark for Review를 선택해 Need Review로 State를 변경할 수 있습니다.
이렇게 되면 coverage가 올라가지 않아 다국어 처리가 더 필요함을 알 수 있고, State를 정렬하여 이 값을 쉽게 찾을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/c49c14b7-f8d4-42b5-b757-3858e9aea416/image.png" alt=""></p>
<p>번역 전문가가 다시 값을 넣어주거나, 우클릭하여 Mark as Reviewed를 선택하면 번역이 coverage가 다시 상승합니다.</p>
<h3 id="view가-아닌-곳에서-자동화를-하려면">View가 아닌 곳에서 자동화를 하려면</h3>
<p>iOS 16 미만일 경우 String catalog에 + 버튼을 눌러 하나하나 이 값을 처리해 주어야 합니다.
이 과정은 자동화가 되지 않기 때문에 string이 사라지면 개발자가 손수 string catalog의 값을 제거해야 하고,
개발자의 실수에 의해 string catalog에 값을 입력해두지 않아 번역이 되지 않는 문제가 발생할 수 있습니다.
하지만 iOS 16 이상인 경우 <code>String(localized:)</code> 함수를 이용해 이를 자동화할 수 있습니다.
이를 활용해 Model 또는 ViewModel에서 string을 다국어로 자동화하여 관리할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/e264023b-22fc-42e2-87e1-1464d8fed6a4/image.png" alt=""></p>
<pre><code class="language-swift">import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var description: String = &quot;&quot;

    func tunaButtonTouched() {
        description = String(localized: &quot;고등어목 고등엇과의 다랑어족에 속하는 어류들의 총칭.&quot;)
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        VStack {
            Text(&quot;김치&quot;)
            Button {
                viewModel.tunaButtonTouched()
            } label: {
                Text(&quot;참치&quot;)
            }
            Text(viewModel.description)
        }
    }
}

#Preview {
    ContentView()
        .environment(\.locale, .init(identifier: &quot;en&quot;))
}</code></pre>
<h3 id="서버에서-가져오는-값은-어떻게-자동으로-string-catalog에-담을-수-있나요">서버에서 가져오는 값은 어떻게 자동으로 String catalog에 담을 수 있나요?</h3>
<p>불가능합니다.
서버에서 어떤 값이 올지 미리 알 수 없기 때문에 자동으로 String catalog에 담을 수 없습니다.
때문에 String catalog에 + 버튼을 눌러 하나하나 이 값을 미리 넣어주어야 합니다.
대신 이렇게 수동으로 추가한 값은 사용자가 삭제하기 전까지는 사라지지 않아 <code>Text(_:)</code> 또는 <code>String(localized:)</code> 값이 제거되더라도 존재하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/24faa08c-a6a8-41f7-baef-a54ca8498207/image.png" alt=""></p>
<p>위 화면은 String catalog를 open as &gt; source code 로 확인하면 됩니다.
(오늘 실습에서 김치는 migrate 되었기 때문에 수동으로 추가한 것으로 남아 있습니다.)</p>
<br>

<h2 id="마무리">마무리</h2>
<p>오늘은 간단하게 String catalog를 사용해서 자동으로 string을 관리하는 방법을 주로 다루었습니다.
다국어 처리는 소개할게 너무 많아서 다음 글에서 추가 내용을 더 담도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS에서 Xcode로 다국어 지원하기(1)]]></title>
            <link>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B01</link>
            <guid>https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B01</guid>
            <pubDate>Tue, 01 Oct 2024 06:43:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sustainable-git/post/73b26e39-3c73-414e-b029-b55ca11f2a8b/image.png" alt=""></p>
<h2 id="서론">서론</h2>
<p>글로벌 사용자를 대상으로 iOS 앱을 개발한다면 다국어 지원은 필수적입니다.
다행히도 Xcode에는 다국어 처리를 쉽게 구현할 수 있도록 되어 있습니다.
오늘은 Xcode 14 이하의 버전에서 다국어 처리를 하는 방법에 대해 알아보겠습니다.
String catalog를 이용한 방법은 다음 포스트에 작성하겠습니다.</p>
<br>

<h2 id="시작하기-전에">시작하기 전에</h2>
<p>다국어 처리는 무엇을 기준으로 해야 할까요?
&quot;국가마다 다르게&quot; 라고 생각할 수도 있지만 그렇지 않습니다.
한국으로 여행온 일본인일 수도 있잖아요!
그렇다면 기준을 &quot;사용자가 사용하는 언어&quot;로 해야 합니다.
조금 더 나아가서, <strong>&quot;사용자가 기기에 설정한 언어&quot;</strong> 로 다국어 처리를 하는게 맞겠죠.
우리는 GIF 파일 경로의 언어 설정에 맞도록 처리할 예정입니다.</p>
<img width=200 src="https://velog.velcdn.com/images/sustainable-git/post/3e2b4b46-0537-4cd7-ae01-7f1b06f672de/image.gif">

<br>

<h2 id="strings를-이용한-다국어-처리legacy">Strings를 이용한 다국어 처리(Legacy)</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/ca1eb593-f58b-4086-bab7-7ab6f852e335/image.png" alt=""></p>
<p>프로젝트에 Strings file을 추가해 줍니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/72a86b37-a0c8-468b-bab2-a111aae4f668/image.png" alt=""></p>
<p>간단하게 김치를 번역해보겠습니다.
이 때 반드시 세미콜론(;)을 끝에 붙여줘야 합니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/d2013708-d13a-40b9-a7e3-dcbaf5856781/image.png" alt=""></p>
<p>Text(&quot;김치&quot;)로 표현한 것이 kimchi로 표현이 되었습니다.
아주 간단하게 다국어 처리가 완료된 것입니다!
여기서 잠깐. 저는 한국어를 사용하는 한국인인데 우리말을 왜 영어로 번역하고 있나요?</p>
<br>

<h3 id="여러-언어-추가하기">여러 언어 추가하기</h3>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/e15bb0d8-bbf1-4975-a8ff-e2d3ba131b37/image.png" alt=""></p>
<p>프로젝트 파일에서 언어를 더 추가하겠습니다.
Localizations 아래에 + 를 눌러 한국어, 일본어를 추가하겠습니다.
그리고 이번 예제에서는 한국어를 기본 언어(default)로 설정하겠습니다.</p>
<img width=200 src="https://velog.velcdn.com/images/sustainable-git/post/50335d4d-55a4-41ff-9f99-be1cee39bc7b/image.png">


<img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/e0eea5f9-e4cb-4168-83d6-377569bc3ec0/image.png">

<p>이제 Localizable.strings 파일로 돌아가서 인스펙터 영역을 연 후, <code>Localize...</code> 버튼을 눌러줍니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/b330c800-21b2-491b-906d-3dd0d4647140/image.png" alt=""></p>
<p>위 버튼을 모두 선택해주면 모든 언어로 번역할 수 있게 strings 파일이 바뀝니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/0c48588b-defc-4714-b38c-856c0fcfea64/image.png" alt=""></p>
<p>이렇게 해준 후, 각 strings 파일에서 원하는 형태로 번역을 해주면 됩니다.
우리가 아까 한국어를 기본 언어(default)로 설정했죠? 그렇기 때문에 이제 한국어를 한국어로 번역(?)을 하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/3b621b78-dd2a-47f4-a39b-d118babc9e27/image.png" alt=""></p>
<br>

<h3 id="어-저는-preview에서-영어로-번역이-잘-되는지-확인하고-싶은데요">어... 저는 Preview에서 영어로 번역이 잘 되는지 확인하고 싶은데요?</h3>
<p>#Preview에 <code>.environment(\.locale, .init(identifier: &quot;en&quot;))</code> 를 붙여주시면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/193956c4-fb67-4a55-8b44-7059e3859666/image.png" alt=""></p>
<p>어... 했는데도 안 되는데요
-&gt; Xcode 버그입니다. 다른 버전을 열거나 종료 후 다시 열어주세요.
-&gt; 그래도 안 되면 clean build 하고 preview를 다시 열어 보세요.</p>
<br>

<h2 id="human-error를-제거하고-다국어-처리하기">Human Error를 제거하고 다국어 처리하기</h2>
<p>위 방법으로 진행하는 다국어 처리는 많은 반복 작업이 필요합니다.
또한, 번역이 누락되거나 잘못된 경우에도 컴파일 과정에서 에러로 나타나지 않습니다.
때문에 사람이 하나씩 수동으로 번역을 처리하다 보면 실수가 발생할 수 있습니다.
이런 Human Error를 줄이고, 번역 작업을 자동화하는 방법에 대해 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/55842614-6f9e-487b-982e-a1c464e49096/image.png" alt=""></p>
<p>우선 I18N.swift 파일을 만들어 줍니다.
(I18N은 Internationalization의 약자입니다. 다른 이름으로 하려면 script 일부를 수정해 주세요)</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/99f8deff-628a-4085-a695-aa98c2af1c72/image.png" alt=""></p>
<p>Target의 Build Settings에 들어간 후 User Script Sandboxing을 No로 처리해 줍니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/c91c723e-0f81-47f4-8452-ef6fa7e2aa67/image.png" alt=""></p>
<p>Target에서 Build Phase를 선택하고, + 버튼을 눌러 스크립트를 추가해 줍니다.</p>
<pre><code class="language-shell">#!/bin/sh

echo &quot;generating I18N.swift&quot;

touch tempI18N.swift
echo &quot;import Foundation&quot; &gt;&gt; tempI18N.swift
echo &gt;&gt; tempI18N.swift
echo &quot;enum I18N {&quot; &gt;&gt; tempI18N.swift

inputfile=${SRCROOT}/${PROJECT}/ko.lproj/Localizable.strings
while IFS= read -r line

do
echo $line
pattern=&#39;^.+=.+$&#39;

comment=&#39;\/\/.+&#39;

if [[ $line =~ $pattern ]]
then
    echo &quot;Yes👌&quot;
    variableWithQuote=$(echo ${line%%=*})
    variableWithoutQuote=$(echo &quot;$variableWithQuote&quot; | sed &#39;s/^&quot;\(.*\)&quot;$/\1/&#39;)
    variableName=$(echo &quot;$variableWithoutQuote&quot; | tr -d &#39;. /-&#39;)
    echo $variableName
    if [ &quot;$variableName&quot; != &quot;&quot; ]; then
        echo &quot;    static let $variableName: String = \&quot;$variableName\&quot;.localized&quot; &gt;&gt; tempI18N.swift
    fi
else
    if [[ $line =~ $comment ]]
    then
        echo &quot;Comment 📝&quot;
        echo &quot;\n    $line&quot; &gt;&gt; tempI18N.swift
    else
        echo &quot;No👎&quot;
    fi
fi
done &lt;&quot;$inputfile&quot;

echo &quot;}&quot; &gt;&gt; tempI18N.swift
echo &gt;&gt; tempI18N.swift
echo &quot;extension String {&quot; &gt;&gt; tempI18N.swift
echo &quot;    var localized: String {&quot; &gt;&gt; tempI18N.swift
echo &quot;        return NSLocalizedString(self, comment: \&quot;\&quot;)&quot; &gt;&gt; tempI18N.swift
echo &quot;    }&quot; &gt;&gt; tempI18N.swift
echo &quot;}&quot; &gt;&gt; tempI18N.swift

cat tempI18N.swift &gt; ${SRCROOT}/${PROJECT}/I18N.swift
rm tempI18N.swift</code></pre>
<p>아래와 같이 설정을 변경해 주세요.</p>
<img width=300 src="https://velog.velcdn.com/images/sustainable-git/post/e98a57c1-18b9-462e-ba43-fc6d318b7a6c/image.png">

<p>이후 빌드를 하게 되면 매 빌드마다 깔끔하게 관리되는 enum 파일을 만들 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/8da59131-a9bd-45c4-bd04-3064acc8f06e/image.png" alt=""></p>
<p>이렇게 되면 View에서 나타내야 할 String 값들을 I18N.swift에서 한번에 관리할 수 있습니다.
(아래 사진은 Xcode preview가 번역을 안하고 말썽을 피워서 시뮬레이터로 찍었습니다.)</p>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/1ea20b4a-27bf-4b4f-8c87-3d7b588e3b78/image.png" alt=""></p>
<br>

<h2 id="마무리">마무리</h2>
<p>Xcode 14 이하에서 다국어 처리 방법에 대해서 알려드렸습니다.
Xcode 16이 나온 시점에서 이 방법을 포스팅한 이유는 String catalog를 사용할 수 없고, iOS 버전이 낮은 사람들이 할 수 있는 최선의 방법을 알려드리기 위함입니다.
참고로 위 내용은 모두 UIKit에서 활용이 가능합니다.</p>
<p>오늘 배운 스크립트를 활용한 방법은 매 빌드마다 자동으로 enum 파일을 생성함으로써 사람이 발생시킬 수 있는 오류를 최소화하여 효율성과 안정성을 높이는 방법을 알려드렸습니다.
다음 포스팅에서는 String catalog를 활용하는 방법을 알려드리고, 과정에서 발생할 수 있는 많은 이슈들을 함께 알아보겠습니다.</p>
<p><a href="https://velog.io/@sustainable-git/iOS%EC%97%90%EC%84%9C-Xcode%EB%A1%9C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B02">https://velog.io/@sustainable-git/iOS에서-Xcode로-다국어-지원하기2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI에서 불필요한 렌더링 제거하기(3) - 다른 View의 @State 값 변화로 인해 View가 초기화되는 현상]]></title>
            <link>https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B03-%EB%8B%A4%EB%A5%B8-View%EC%9D%98-State-%EA%B0%92-%EB%B3%80%ED%99%94%EB%A1%9C-%EC%9D%B8%ED%95%B4-View%EA%B0%80-%EC%B4%88%EA%B8%B0%ED%99%94%EB%90%98%EB%8A%94-%ED%98%84%EC%83%81</link>
            <guid>https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B03-%EB%8B%A4%EB%A5%B8-View%EC%9D%98-State-%EA%B0%92-%EB%B3%80%ED%99%94%EB%A1%9C-%EC%9D%B8%ED%95%B4-View%EA%B0%80-%EC%B4%88%EA%B8%B0%ED%99%94%EB%90%98%EB%8A%94-%ED%98%84%EC%83%81</guid>
            <pubDate>Mon, 30 Sep 2024 08:34:47 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p><a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기</a>
<a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B02">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기2</a></p>
<p>앞선 포스팅에서 아래와 같은 것들을 확인하였습니다.</p>
<ol>
<li>렌더링이 다시 일어남을 확인하는 방법</li>
<li>모듈화를 통해 렌더링이 다시 일어나지 않도록 하는 방법</li>
<li>@State 값 변화에 렌더링이 다시 일어난다는 점을 활용하여 computed property를 @State 처럼 활용하는 방법</li>
<li>ViewModel의 @Published 값의 변화가 이를 참조하는 모든 View를 업데이트한다는 점</li>
<li>@EnvironmentObject를 활용해 자식 View를 렌더링하지 않고 손자 View를 업데이트 하는 방법</li>
</ol>
<p>오늘은 실제로 일어날만한(저에게 실제로 일어났던) 일을 소개해드리겠습니다.</p>
<br>

<h2 id="서로-다른-picker의-값이-서로를-초기화시키는-이슈">서로 다른 Picker의 값이 서로를 초기화시키는 이슈</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/6f931a4a-d27a-4a77-8fe3-50c6fb957e68/image.gif" alt=""></p>
<p>시, 분, 초를 나타내야 하는 Picker가 있고, 각 Picker는 독립적이게 움직여야 합니다.
하지만 영상을 자세히 보면 분이 움직이고 있는 와중 초가 변경되고 있습니다.
14분이던 분 Picker를 가속해서 돌리는 와중 초를 변경하면 분 Picker가 초기화되어 다시 14분으로 돌아갑니다.
해당 코드는 시, 분 초 중 하나라도 값이 정해지면 body 전체를 다시 그리면서 정해지지 않은(움직이는 Picker) 값은 초기화하여 그리게 됩니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var hours: Int = 0
    @State private var minutes: Int = 0
    @State private var seconds: Int = 0

    var body: some View {
        HStack {
            Picker(&quot;시&quot;, selection: $hours) {
                ForEach(0...5, id: \.self) { hour in
                    Text(&quot;\(hour)시&quot;)
                }
            }
            Picker(&quot;분&quot;, selection: $minutes) {
                ForEach(0...60, id: \.self) { minute in
                    Text(&quot;\(minute)분&quot;)
                }
            }
            Picker(&quot;초&quot;, selection: $seconds) {
                ForEach(0...60, id: \.self) { second in
                    Text(&quot;\(second)초&quot;)
                }
            }
        }
        .pickerStyle(.wheel)
    }
}</code></pre>
<br>

<h2 id="서로-다른-picker가-서로의-랜더링에-영향을-주지-않으려면">서로 다른 Picker가 서로의 랜더링에 영향을 주지 않으려면</h2>
<p>앞선 포스팅들을 살펴보면 이 이슈의 원인을 쉽게 이해할 수 있습니다.
하나라도 @State 값이 정해지면 body를 다시 그려야 합니다.
모듈화가 되어 있다면 해당 @State에 영향을 받는 모듈만 업데이트 되지만, 그렇지 않다면 전부 다시 그립니다.
때문에 각 Picker를 모듈화 하면 이를 쉽게 해결할 수 있을 것입니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var hours: Int = 0
    @State private var minutes: Int = 0
    @State private var seconds: Int = 0

    var body: some View {
        HStack {
            MyPickerView(unit: &quot;시&quot;, range: 0...5, bindedTime: $hours)
            MyPickerView(unit: &quot;분&quot;, range: 0...60, bindedTime: $minutes)
            MyPickerView(unit: &quot;초&quot;, range: 0...60, bindedTime: $seconds)
        }
        .pickerStyle(.wheel)
    }
}

fileprivate struct MyPickerView: View {
    let unit: String
    let range: ClosedRange&lt;Int&gt;
    @Binding var bindedTime: Int


    var body: some View {
        Picker(unit, selection: $bindedTime) {
            ForEach(range, id: \.self) { time in
                Text(&quot;\(time)\(unit)&quot;)
            }
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/6ece1896-ef51-490b-8f2d-9958825a28ad/image.gif" alt=""></p>
<br>

<h2 id="결론">결론</h2>
<p>위 현상은 SwiftUI를 조금만 쓰다 보면 여러분도 겪게될 일입니다.
하지만, 이를 모르고 지나친다면 영원히 발견하지 못할 수도 있습니다.
특히 <strong>개발 편의를 위해 하나의 ViewModel을 여러 View에서 참조하도록 개발하는 경우</strong>가 많은데, 이 때 View가 다시 그려지는 것에 많은 주의가 필요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI에서 불필요한 렌더링 제거하기(2)]]></title>
            <link>https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B02</link>
            <guid>https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B02</guid>
            <pubDate>Sat, 28 Sep 2024 02:19:03 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p><a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기</a>
앞선 포스팅에서 렌더링이 다시 일어남을 확인하는 방법, 모듈화를 통해 이를 해결하는 방법을 알아보았습니다.
이번에는 이를 좀 더 깊이 알아보는 시간입니다.</p>
<br>

<h2 id="computed-property-값의-변화에-view를-다시-그리는-방법">Computed property 값의 변화에 View를 다시 그리는 방법</h2>
<p>우선 두 개의 버튼을 가진 View를 만듭니다.
각각의 View는 @State 값을 변경시키지만, 해당 값이 View에 미치는 영향이 없기 때문에 View는 다시 렌더링하지 않습니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var isFirstButtonOn: Bool = false
    @State private var isSecondButtonOn: Bool = false

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;First Button&quot;) {
                isFirstButtonOn.toggle()
            }
            Button(&quot;Second Button&quot;) {
                isSecondButtonOn.toggle()
            }
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/29d42cdc-0a41-4586-acc6-282cb5a75039/image.gif" alt=""></p>
<p>여기서 두 @State 값으로 연산하는 computed property를 만듭니다.
이렇게 되면 각각의 @State 값의 변화에 View는 자신을 다시 그리게 됩니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    private var isBothButtonOn: Bool {
        isFirstButtonOn &amp;&amp; isSecondButtonOn
    }
    @State private var isFirstButtonOn: Bool = false
    @State private var isSecondButtonOn: Bool = false

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;First Button&quot;) {
                isFirstButtonOn.toggle()
            }
            Button(&quot;Second Button&quot;) {
                isSecondButtonOn.toggle()
            }
            Text(&quot;&amp;&amp; = \(isBothButtonOn ? &quot;true&quot; : &quot;false&quot;)&quot;)
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/d65ff600-d395-47e3-a0a7-d913b8c9b2f5/image.gif" alt=""></p>
<br>

<h2 id="viewmodel의-published-값이-변경되면-모든-view가-업데이트-됨">ViewModel의 @Published 값이 변경되면 모든 View가 업데이트 됨</h2>
<p>부모 View가 ViewModel을 소유한 상황에서 ViewModel 값의 변화로 부모 View를 업데이트하는 상황입니다.
먼저 자식 View가 해당 ViewModel을 참조하지 않는 상황을 만들면, 당연히 ViewModel의 값의 변화에 자식 View가 다시 그려지지 않습니다.</p>
<pre><code class="language-swift">import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0

    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;count = \(viewModel.count)&quot;) {
                viewModel.upCount()
            }
            AnotherView()
        }
    }
}

struct AnotherView: View {
    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;나는 값이 안변하는데&quot;)
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/854210ac-eca8-479c-ba2c-97d35e3357c1/image.gif" alt=""></p>
<p>자식 View가 아무런 이유 없이 ViewModel을 참조한다고 하면, 신기하게도 @Published 값의 변화에 자식 View가 다시 그려집니다.
자식 View가 해당 값의 변화에 다시 그려질 필요가 없는데도 말이죠!</p>
<pre><code class="language-swift">import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0

    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;count = \(viewModel.count)&quot;) {
                viewModel.upCount()
            }
            AnotherView(viewModel: viewModel)
        }
    }
}

struct AnotherView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;나는 값이 안변하는데&quot;)
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/3509524f-9891-4c25-86ff-4cc400a83196/image.gif" alt=""></p>
<br>

<h2 id="손자-view는-업데이트-해야하지만-자식-view는-업데이트-하지-않으려면">손자 View는 업데이트 해야하지만, 자식 View는 업데이트 하지 않으려면</h2>
<p>위에서 알아보았듯 ViewModel이 업데이트 되면 해당 ViewModel을 소유하고 있는 모든 View는 다시 그려져야 합니다.
자식 View와 손자 View가 있는 경우 자식 View를 업데이트 하지 않고 현재 View와 손자 View만 업데이트 할 수 는 없을까요?
우선 ViewModel을 @ObservedObject로 가지고 이를 주입하게 되면 문제가 발생합니다.</p>
<pre><code class="language-swift">import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0

    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;count = \(viewModel.count)&quot;) {
                viewModel.upCount()
            }
            AnotherView(viewModel: viewModel)
        }
    }
}

struct AnotherView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Text(&quot;나는 값이 안변하는데&quot;)
            OtherView(viewModel: viewModel)
        }
    }
}

struct OtherView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;나는 변해야 해 count = \(viewModel.count)&quot;)
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/8fbda28f-3b9b-40f3-8d53-9abfc87b767f/image.gif" alt=""></p>
<p>해결방법은 간단합니다. 자식 View가 ViewModel을 참조하지 않으면 됩니다.
이를 위해 @EnvironmentObject를 사용합시다.</p>
<pre><code class="language-swift">import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0

    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button(&quot;count = \(viewModel.count)&quot;) {
                viewModel.upCount()
            }
            AnotherView()
                .environmentObject(viewModel)
        }
    }
}

struct AnotherView: View {
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Text(&quot;나는 값이 안변하는데&quot;)
            OtherView()
        }
    }
}

struct OtherView: View {
    @EnvironmentObject private var viewModel: ViewModel

    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;나는 변해야 해 count = \(viewModel.count)&quot;)
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/5bdfe696-905d-4d33-bf2c-aa1fe22105d9/image.gif" alt=""></p>
<br>

<h2 id="오늘의-결론">오늘의 결론</h2>
<p>SwiftUI는 참 편리하지만, 완벽하게 개발하려면 정말 많이 생각하고 고려해야하는것 같습니다.
@State 값의 변화에 모듈화되지 않은 모든 View가 다시 렌더링되고,
@ObservedObject 값의 변화에는 반드시 모든 View가 다시 그려집니다.
단순히 View만 모듈화를 잘 하면 되는것이 아닌, ViewModel까지 모듈화를 잘 해야 합니다.
개발 편의를 위해 하나의 ViewModel을 여러 View에서 참조하도록 개발하는 경우가 많은데, 이 때 View가 다시 그려지는 것에 많은 주의가 필요합니다.</p>
<p>다음 포스트 : <a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B03-%EB%8B%A4%EB%A5%B8-View%EC%9D%98-State-%EA%B0%92-%EB%B3%80%ED%99%94%EB%A1%9C-%EC%9D%B8%ED%95%B4-View%EA%B0%80-%EC%B4%88%EA%B8%B0%ED%99%94%EB%90%98%EB%8A%94-%ED%98%84%EC%83%81">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기3-다른-View의-State-값-변화로-인해-View가-초기화되는-현상</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI에서 불필요한 렌더링 제거하기]]></title>
            <link>https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 22 Sep 2024 02:16:09 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>SwiftUI는 함수형 프로그래밍으로 직관적이고 가독성 높게 UI 로직을 작성할 수 있습니다.
특히 View의 구조를 바깥에서 안쪽으로 쉽게 파악할 수 있어 디버깅이 쉬우며
순수 함수로 로직을 구성하여 body 외부에서 발생할 수 있는 부작용을 최소화할 수 있습니다.</p>
<p>그러나 View 로직을 구성할 때 @State, @Binding 과 같은 값의 변화를 지속적으로 관찰하고,
그에 따라 UI를 다시 계산하고 렌더링해야 합니다.
이 과정에서 쉽게 발생할 수 있는 개발자의 실수로 인해 다시 렌더링할 필요가 없는 View까지 렌더링되는 문제가 자주 발생합니다.</p>
<br>

<h2 id="swiftui에서-렌더링">SwiftUI에서 렌더링</h2>
<img width=660 src="https://velog.velcdn.com/images/sustainable-git/post/0b184540-ef78-4abb-bbf4-e55ccd8c8353/image.png">

<img width=690 src="https://velog.velcdn.com/images/sustainable-git/post/647d0c2b-2420-443e-a84b-020887b480ef/image.png">

<p>SwiftUI의 View에서 사용하는 property wrapper들은 모두 DynamicProperty 프로토콜을 채택하고 있습니다.
해당 wrapper로 감싸진 property는 View의 body를 계산하기 위한 값들을 저장하고 있습니다.</p>
<img width=440 src="https://velog.velcdn.com/images/sustainable-git/post/4de5d614-1c6c-40a6-ade9-ddf365426976/image.png">

<p>SwiftUI는 위의 property 값을 관찰하고 있습니다.
만약 property 값이 변하게 되면 SwiftUI가 렌더링이 일어나기 전(body를 다시 그리기 전에) <code>update()</code> 함수를 호출하게 되고, 이에 따라 View가 다시 그려지면서 렌더링이 일어나게 됩니다.</p>
<br>

<h2 id="렌더링을-확인하는-방법">렌더링을 확인하는 방법</h2>
<p>SwiftUI에서 View가 렌더링 되는 것을 확인하려면 View에 print문을 사용하면 됩니다.
이 때, SwiftUI View 제공해주는 기능인 <code>Self._printChanges()</code> 를 활용하면 더욱 좋습니다.
이 함수는 View가 어떤 값의 변화에 의해 다시 렌더링 되는지를 확인시켜 줍니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var value: Int = 0

    var body: some View {
        let _ = Self._printChanges()
        Button {
            value += 1
        } label: {
            Text(&quot;Value = \(value)&quot;)
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/2b2e93bd-25d7-4247-b105-6828a4463dca/image.gif" alt=""></p>
<br>

<h2 id="불필요한-렌더링-예시">불필요한 렌더링 예시</h2>
<p>ContentView에 VStack을 이용해 두 개의 View를 두었습니다.
targetView는 <code>value: Int</code> 와 무관한 View이고, Button은 value가 변함에 따라 label을 업데이트 해야 합니다.
이럴 경우 단순한 방법으로 View 로직을 구성하게 되면 <code>value: Int</code> 값 변화에 targetView도 다시 렌더링이 일어날 수 있습니다.</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var value: Int = 0

    var body: some View {
        VStack {
            targetView
            Button {
                value += 1
            } label: {
                Text(&quot;Value = \(value)&quot;)
            }
        }
    }

    @ViewBuilder
    var targetView: some View {
        let _ = Self._printChanges()
        Text(&quot;렌더링 되고 싶지 않아요&quot;)
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/350ffce5-5ac2-4e94-8496-7abc5be4ea55/image.gif" alt=""></p>
<br>

<h2 id="해결-방법--별개의-view로-모듈화-하기">해결 방법 : 별개의 View로 모듈화 하기</h2>
<p>targetView를 별도의 struct로 분리하여 모듈화를 합니다.
이렇게 되면 <code>value: Int</code>와 완벽하게 분리가 되어 Text가 다른 View에 존재하게 됩니다.
불필요한 렌더링이 다시 일어나지 않도록 문제를 해결할 수 있습니다!</p>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    @State private var value: Int = 0

    var body: some View {
        VStack {
            TargetView()
            Button {
                value += 1
            } label: {
                Text(&quot;Value = \(value)&quot;)
            }
        }
    }
}

struct TargetView: View {
    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;렌더링 되고 싶지 않아요&quot;)
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/b9862cd0-533b-48bc-aafa-09242dcfe134/image.gif" alt=""></p>
<br>

<h2 id="결론">결론</h2>
<p>SwiftUI의 View를 사용할 때는 <code>@State</code> 또는 <code>@Binding</code> 값의 변화에 View가 다시 그려질 수 있음을 항상 생각해야 합니다.
이 때문에 @State 값에 관계가 없어 보이는 View의 경우에는 모듈화를 잘 시켜 주는 것이 좋습니다.
하지만, 이렇게 되면 너무 많은 재사용 되지 않을 View 파일이 생길 수 있습니다.
그러므로 재사용이 되지 않는다면 <code>fileprivate</code>와 같은 접근 제어자를 적절히 사용하여 같은 파일에 모듈화된 여러개의 View를 두는것이 좋아 보입니다.</p>
<p>다음 포스트 : <a href="https://velog.io/@sustainable-git/SwiftUI%EC%97%90%EC%84%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B02">https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 메모리 - 스택 영역이 컴파일 타임에 결정된다]]></title>
            <link>https://velog.io/@sustainable-git/iOS-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%8A%A4%ED%83%9D-%EC%98%81%EC%97%AD%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%ED%83%80%EC%9E%84%EC%97%90-%EA%B2%B0%EC%A0%95%EB%90%9C%EB%8B%A4</link>
            <guid>https://velog.io/@sustainable-git/iOS-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%8A%A4%ED%83%9D-%EC%98%81%EC%97%AD%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%ED%83%80%EC%9E%84%EC%97%90-%EA%B2%B0%EC%A0%95%EB%90%9C%EB%8B%A4</guid>
            <pubDate>Sat, 14 Sep 2024 00:46:36 GMT</pubDate>
            <description><![CDATA[<h2 id="스택-영역이-컴파일-타임에-결정된다는게">스택 영역이 컴파일 타임에 결정된다는게</h2>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/5bacb497-a309-43b7-a068-7cfaba8a0607/image.png" alt=""></p>
<p>도대체 무슨 말일까요?
많은 블로그에서 말하고 있습니다.
스택 영역의 크기가 컴파일 타임에 결정된다고
그런데 이상합니다. 우리는 &quot;런타임&quot;에 객체를 생성하고 있어요!
분명 우리는 &quot;런타임&quot;에 struct를 생성하고 있잖아요! 분명 스택에 가변적으로 메모리를 할당하고 있지 않나요?
우리가 struct를 여러개 생성할 수 있잖아요? 이걸 컴파일 타임에 몇 개나 생성될지 어떻게 알고 미리 메모리를 정할 수 있죠?</p>
<br>

<h3 id="오해의-시작">오해의 시작</h3>
<p>iOS에서 스택(Stack) 영역에 &quot;컴파일 타임에 크기가 결정된다&quot;는 말의 뜻은 <strong>스택 영역에 할당되는 메모리의 크기가 프로그램이 실행되기 전에 고정된다</strong>에서 출발했을 겁니다.
그런데, 이 말의 뜻은 스택 영역이 아주 고정돼서 더 이상 새로운 것이 할당되지 않는다는 뜻이 아니라
<strong>지역 변수, 함수 매개변수, 함수 호출 정보(리턴 구조) 등의 크기가 미리 결정</strong>된다는 것을 의미합니다.</p>
<br>

<h3 id="그렇다면-struct를-생성한다는-것은">그렇다면 struct를 생성한다는 것은</h3>
<p>스택의 특성과 메모리 할당 방식에 대한 혼동을 이해해야 합니다.
struct 하나의 크기는 컴파일 타임에 결정됩니다.
그러나 스택에서 struct를 할당하는것은 런타임에 결정됩니다.
즉, <strong>할당되는 메모리의 크기는 컴파일 타임에 결정되지만, 할당 자체는 런타임</strong>에 이루어집니다.
스택은 컴파일 타임에 딱 정해져서 변경이 없는게 아니고, struct를 생성한다는 것은 런타임에 이루어지는 행위입니다.</p>
<br>

<h2 id="스택은-컴파일-타임에-결정되는게-아니다">스택은 컴파일 타임에 결정되는게 아니다</h2>
<p>스택 영역의 크기는 컴파일 타임에 결정된다 -&gt; 오해가 많이 발생할 수 있는 표현입니다.
스택 영역에 할당되는 객체의 크기, 지역 변수, 매개 변수, 함수 호출 정보 등이 컴파일 타임에 결정됩니다.
실제 메모리 할당은 런타임에 진행됩니다.
그러니까, struct를 여러개 생성하는 미래를 컴퓨터가 예측해서 미리 할당해 두는게 아니니까 스택이 컴파일 타임에 결정된다는 오해를 만드는 표현을 좀 더 개선하면 좋지 않을까 싶네요...</p>
<br>

<h3 id="참고로">참고로...</h3>
<p>iOS 스택 영역이 크기는 대략 1MB인 것으로 파악하고 있습니다.
<a href="https://github.com/sustainable-git/Development/tree/main/Contents/iOS%20Memory">(MacOS의 경우 8MB)</a>
때문에 스택 영역에 너무 많은 것을 할당하게 되면 stack overflow가 발생하게 되죠.
우리가 힙 영역을 사용하는 이유가 이것에 있기도 합니다.
동적 메모리 할당(힙)을 적극적으로 활용해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@autoclosure @MainActor @Sendable @escaping () -> Void]]></title>
            <link>https://velog.io/@sustainable-git/autoclosure-MainActor-Sendable-escaping-Void</link>
            <guid>https://velog.io/@sustainable-git/autoclosure-MainActor-Sendable-escaping-Void</guid>
            <pubDate>Sat, 07 Sep 2024 02:26:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sustainable-git/post/5c058802-4704-4af0-9b33-5ac02d153352/image.png" alt=""></p>
<h2 id="escaping">@escaping</h2>
<p>Swift에서 @escaping은 클로저가 함수나 메서드의 실행이 종료된 후에도 호출될 수 있음을 나타냅니다.
기본적으로 클로저는 함수 내에서만 유효하고, 함수가 종료되면 클로저도 메모리에서 해제됩니다.
그러나, 클로저가 함수 밖에서 나중에 호출되거나 저장하려면 @escaping으로 명시해야 합니다.</p>
<pre><code class="language-swift">import Foundation

class MyClass {
    var storedClosure: () -&gt; () = {}

    func store(closure: () -&gt; ()) {
        storedClosure = closure
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/30a0a346-4bd0-48fc-b68f-8814a3471b60/image.png" alt=""></p>
<pre><code class="language-swift">func async(closure: () -&gt; ()) {
    DispatchQueue.main.async {
        closure()
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/1af99c2f-5da8-465e-9365-62b8d3eaeb01/image.png" alt=""></p>
<h3 id="escaping을-사용할-때-주의사항">@escaping을 사용할 때 주의사항</h3>
<p>@escaping을 사용하면 reference counting을 이용하여 클로저가 함수의 스택 프레임에 벗어나도 클로저를 메모리에 유지합니다.
때문에 [weak self] 와 같은 방법을 이용하여 메모리 관리에 신경을 써야 합니다.</p>
<h2 id="sendable">@Sendable</h2>
<p>여러 thread에서 동시에 함수나 클로저를 실행하는 경우 race condition이 발생할 수 있습니다.
@Sendable은 다른 thread에서 실행될 때에도 안전하게 동기화될 수 있도록 보장하는 attribute입니다.</p>
<pre><code class="language-swift">import Foundation

actor Counter {
    var count = 0
}

let counter = Counter()

let closure: @Sendable () -&gt; Void = {
    counter.count += 1
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/c005be25-5978-4fa9-aafc-84b8440e0ce6/image.png" alt=""></p>
<h3 id="sendable의-규칙">@Sendable의 규칙</h3>
<ol>
<li>클로저에서 캡쳐한 변수, 상수를 thread-safe하게 다루어야 합니다. 클로저 내에서 mutating한 변수를 캡쳐하려고 할 경우 컴파일 에러가 발생합니다.</li>
<li>값 타입을 주로 사용합니다.</li>
<li>non-Sendable 타입을 캡처하지 않습니다.</li>
</ol>
<h2 id="mainactor">@MainActor</h2>
<p>iOS에서 UI 업데이트는 반드시 main thread에서 실행되도록 제한됩니다.
UI 업데이트는 최적화를 위해 thread safe하게 설계되지 않았고, 업데이트에 순서가 정해져 있기 때문입니다.</p>
<pre><code class="language-swift">import Foundation

@MainActor func mainActorFunction() {
    print(&quot;run on main&quot;)
}

func runClosure(_ closure: @escaping @Sendable () -&gt; Void) {
    Task {
        closure()
    }
}

runClosure {
    mainActorFunction()
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sustainable-git/post/8b5149f6-974e-4ff3-a48d-3d49f4e7f6fd/image.png" alt=""></p>
<p>이를 해결하려면 아래와 같이 @MainActor를 closure에 붙여주면 됩니다.</p>
<pre><code class="language-swift">func runClosure(_ closure: @escaping @Sendable @MainActor () -&gt; Void) {
    Task {
        await closure()
    }
}</code></pre>
<h2 id="autoclosure">@autoclosure</h2>
<p>클로저를 명시적으로 작성하지 않고도 자동으로 변환해주는 속성입니다.
쉽게 말해 클로저를 인자로 전달할 때 { } 를 사용하지 않아도 동작할 수 있도록 해줍니다.</p>
<pre><code class="language-swift">import Foundation

func bool(_ closure: () -&gt; Bool) {
    closure() ? print(&quot;true&quot;) : print(&quot;false&quot;)
}

bool { 2 &gt; 1 }

func bool2(_ closure: @autoclosure () -&gt; Bool) {
    closure() ? print(&quot;true&quot;) : print(&quot;false&quot;)
}

bool2 (2 &gt; 1)
</code></pre>
<h1 id="결론">결론</h1>
<p>이제 우리는 아래와 같은 함수를 이해할 수 있습니다. <del>아마도</del></p>
<pre><code class="language-swift">func nowWeCanUnderstand(_ closure: @autoclosure @MainActor @Sendable @escaping () -&gt; Void) {
    Task {
        await closure()
    }
}

nowWeCanUnderstand(print(&quot;hello world&quot;))
</code></pre>
]]></description>
        </item>
    </channel>
</rss>