<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://indextrown.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://indextrown.github.io/" rel="alternate" type="text/html" /><updated>2026-06-30T08:03:06+00:00</updated><id>https://indextrown.github.io/feed.xml</id><title type="html">DongHyeon Kim’s Blog</title><subtitle>Swift와 크롤링 기술을 통해 안전한 세상을 만들고 싶습니다.</subtitle><author><name>Dong Hyeon</name></author><entry><title type="html"></title><link href="https://indextrown.github.io/Makefile/" rel="alternate" type="text/html" title="" /><published>2026-06-30T00:00:00+00:00</published><updated>2026-06-30T00:00:00+00:00</updated><id>https://indextrown.github.io/Makefile</id><content type="html" xml:base="https://indextrown.github.io/Makefile/"><![CDATA[<!-- <img src="/%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B2%BD%EB%A1%9C" alt="이미지" width="30%"> -->]]></content><author><name>Dong Hyeon</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">[TCA Deep Dive] @CasePathable</title><link href="https://indextrown.github.io/TCA-tca1/" rel="alternate" type="text/html" title="[TCA Deep Dive] @CasePathable" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BTCA%5D%20tca1</id><content type="html" xml:base="https://indextrown.github.io/TCA-tca1/"><![CDATA[<!-- `Text("Hello World")`, `Text(isOn ? "On" : "Off")`, `Button` -->

<!-- <img src="/%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B2%BD%EB%A1%9C" alt="이미지" width="30%"> -->

<!-- <img src="https://github.com/user-attachments/assets/3938e583-0fc4-4620-99b0-bf761e60a1ba" width="60%" align="left"> -->

<h2 id="keypath">KeyPath</h2>
<blockquote class="custom-double-quote">
  <p>Swift의 구조체와 클래슨느 프로퍼티를 참조할 수 있는 KeyPath를 사용할 수 있다.
KeyPath는 특정 타입의 프로퍼티 위치를 값처럼 표현하는 기능이다.</p>
</blockquote>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">User</span> <span class="p">{</span> 
    <span class="k">let</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">Int</span> 
    <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span> 
<span class="p">}</span>

<span class="p">\</span><span class="kt">User</span><span class="o">.</span><span class="n">id</span> <span class="c1">// KeyPath&lt;User, Int&gt; </span>
<span class="p">\</span><span class="kt">User</span><span class="o">.</span><span class="n">name</span> <span class="c1">// WritableKeyPath&lt;User, String&gt;</span>
</code></pre></div></div>
<ul>
  <li>KeyPath&lt;Root, Value&gt;는 Root 타입 안에 있는 Value 타입 프로퍼티를 가리킨다.<br />
<br /><br /><br /></li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">user</span> <span class="o">=</span> <span class="kt">User</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">name</span><span class="p">:</span> <span class="s">"Donghyeon"</span><span class="p">)</span> 
<span class="k">let</span> <span class="nv">nameKeyPath</span> <span class="o">=</span> <span class="p">\</span><span class="kt">User</span><span class="o">.</span><span class="n">name</span> 
<span class="nf">print</span><span class="p">(</span><span class="n">user</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">nameKeyPath</span><span class="p">])</span> <span class="c1">// Donghyeon</span>
</code></pre></div></div>
<ul>
  <li>예를 들어 \User.name은 User 안의 name 프로퍼티를 가리키는 값이다.<br />
<br /><br /><br /></li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">user</span> <span class="o">=</span> <span class="kt">User</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">name</span><span class="p">:</span> <span class="s">"Donghyeon"</span><span class="p">)</span> 
<span class="n">user</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="p">\</span><span class="kt">User</span><span class="o">.</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="s">"Kim"</span> 
<span class="nf">print</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">name</span><span class="p">)</span> <span class="c1">// Kim</span>
</code></pre></div></div>
<ul>
  <li>var 프로퍼티라면 값을 변경하는 것도 가능하다.<br />
<br /><br /><br /></li>
</ul>

<p>즉, KeyPath를 사용하면 어떤 프로퍼티를 읽거나 수정할지 코드로 직접 고정하지 않고, 프로퍼티 접근 자체를 값처럼 전달할 수 있다.<br />
이 개념은 SwiftUI에서도 자주 사용된다. 예를 들어 Binding, Environment, dynamicMemberLookup 같은 API 내부에서 KeyPath가 자연스럽게 활용된다.<br />
<br /><br /><br /></p>

<h2 id="enum에서는-keypath를-바로-사용할-수-없다">enum에서는 KeyPath를 바로 사용할 수 없다</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span> 
<span class="p">}</span>

<span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="n">home</span> <span class="c1">// 🛑 Key path cannot refer to enum case 'home'</span>
</code></pre></div></div>
<p>enum의 case에는 일반적인 KeyPath를 사용할 수 없다.<br />
KeyPath는 기본적으로 구조체나 클래스의 저장 프로퍼티를 가리키는 기능이기 떄문이다.<br />
enum의 case는 저장 프로퍼티가 아니다. enum은 case중 하나의 상태를 표현하는 타입이고, associated value는 특정 case일 때만 존재한다.<br />
<br /><br /><br /></p>

<h2 id="enum에서는-keypath를-바로-사용할-수-없는-이유">enum에서는 KeyPath를 바로 사용할 수 없는 이유</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span> 
<span class="p">}</span>

<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">())</span>
</code></pre></div></div>
<ul>
  <li>home은 항상 존재하는 프로퍼티가 아니다.</li>
  <li>let action 상태는 home이라는 값이 존재하지 않는다. 따라서 Swift 기본 KeyPath 시스템은 \UserAction.home처럼 enum case 내부 값을 직접 가리킬 수 없다.<br />
<br /><br /><br /></li>
</ul>

<h2 id="enum에서는-기본적으로-switch로-꺼낸다">enum에서는 기본적으로 switch로 꺼낸다</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span> 
<span class="p">}</span>

<span class="k">switch</span> <span class="n">action</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">homeAction</span><span class="p">):</span> 
        <span class="nf">print</span><span class="p">(</span><span class="s">"home action:"</span><span class="p">,</span> <span class="n">homeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">settings</span><span class="p">(</span><span class="n">settingsAction</span><span class="p">):</span> 
        <span class="nf">print</span><span class="p">(</span><span class="s">"settings action:"</span><span class="p">,</span> <span class="n">settingsAction</span><span class="p">)</span> 
<span class="p">}</span>
</code></pre></div></div>
<ul>
  <li>Swift에서 enum의 associated value를 다루는 가장 기본적인 방법은 switch 또는 if case를 사용하는 것이다.<br />
<br /><br /><br /></li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">homeAction</span><span class="p">)</span> <span class="o">=</span> <span class="n">action</span> <span class="p">{</span> 
    <span class="nf">print</span><span class="p">(</span><span class="n">homeAction</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
  <li>특정 case만 보고 싶다면 if case를 사용할 수 있다.<br />
<br /><br /><br /></li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">Destination</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeState</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsState</span><span class="p">)</span> 
<span class="p">}</span> 

<span class="k">var</span> <span class="nv">destination</span> <span class="o">=</span> <span class="kt">Destination</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="kt">HomeState</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Home"</span><span class="p">))</span> 
<span class="k">switch</span> <span class="n">destination</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="kd">var</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">state</span><span class="p">):</span> 
        <span class="n">state</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="s">"New Home"</span> 
        <span class="n">destination</span> <span class="o">=</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">state</span><span class="p">)</span> 
    <span class="k">case</span> <span class="o">.</span><span class="nv">settings</span><span class="p">:</span> 
        <span class="k">break</span> 
<span class="p">}</span>
</code></pre></div></div>
<ul>
  <li>값을 수정해야 한다면, enum을 switch로 분해한 뒤 새로운 enum 값으로 다시 만들어줘야 한다.</li>
  <li>즉, enum에서는 구조체처름 특정 내부 값을 KeyPath로 바로 수정하는 것이 아니라 현재 case를 확인하고 associated value를 꺼낸 뒤 다시 감싸는 방식으로 처리한다.<br />
<br /><br /><br /></li>
</ul>

<h2 id="정리">정리</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">\</span><span class="kt">User</span><span class="o">.</span><span class="n">name</span> <span class="c1">// 가능</span>
</code></pre></div></div>
<ul>
  <li>구조체와 클래스에서는 KeyPath를 사용해 프로퍼티 접근을 값처럼 다룰 수 있다. <br />
<br /><br /><br /></li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="n">home</span> <span class="c1">// 불가능</span>

<span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">homeAction</span><span class="p">)</span> <span class="o">=</span> <span class="n">action</span> <span class="p">{</span> 
    <span class="c1">// homeAction 사용 </span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
  <li>하지만 enum case는 저장 프로퍼티가 아니기 때문에 Swift의 기본 KeyPath로 접근할 수 없다.
    <ul>
      <li>그래서 enum의 associated value를 다룰 때는 기본적으로 switch, if case, guard case를 사용한다.</li>
    </ul>
  </li>
  <li>Swift의 기본 KeyPath는 구조체와 클래스의 프로퍼티 접근에는 강력하지만, enum case까지 표현하지는 못한다. 그래서 enum을 더 일반적으로 다루고 싶다면 Swift의 기본 KeyPath가 아니라, case를 추출하고 다시 삽입할 수 있는 별도의 추상화가 필요하다.<br />
<br /><br /><br /></li>
</ul>

<h2 id="casepath">CasePath</h2>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span> 
<span class="p">}</span>

<span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="n">home</span> <span class="c1">// 🛑 Key path cannot refer to enum case 'home'</span>
</code></pre></div></div>
<ul>
  <li>앞에서 살펴본 것처럼 Swift의 기본 KeyPath는 구조체와 클래스의 프로퍼티에는 사용할 수 있지만, enum의 case에는 사용할 수 없다.</li>
  <li>하지만 실제 앱을 개발하다 보면 enum의 associated value를 일반화해서 다루고 싶은 경우가 매우 많다.</li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">Destination</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeState</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsState</span><span class="p">)</span> 
<span class="p">}</span>
</code></pre></div></div>
<ul>
  <li>예를 들어 위와 같은 상태가 있다고 가정해 보자.</li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 방법 1</span>
<span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">state</span><span class="p">)</span> <span class="o">=</span> <span class="n">destination</span> <span class="p">{</span> 
    <span class="c1">// state 사용 </span>
<span class="p">}</span>

<span class="c1">// 방법 2</span>
<span class="k">switch</span> <span class="n">destination</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="kd">var</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">state</span><span class="p">):</span> 
        <span class="n">state</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="s">"새 제목"</span> 
        <span class="n">destination</span> <span class="o">=</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">state</span><span class="p">)</span> 
    <span class="k">case</span> <span class="o">.</span><span class="nv">settings</span><span class="p">:</span> 
        <span class="k">break</span> 
<span class="p">}</span>
</code></pre></div></div>
<ul>
  <li>Destination이 현재 home인지 확인하고, if case let 나 switch로 수정한 뒤 다시 enum으로 감싸야 한다.</li>
  <li>이러한 코드는 한두 번은 괜찮지만 enum을 자주 다루게 되면 반복되는 switch와 패턴 매칭 코드가 계속 등장하게 된다.</li>
</ul>

<h2 id="casepath-1">CasePath</h2>
<p>앞에서 살펴본 것처럼 Swift의 기본 KeyPath는 구조체와 클래스의 프로퍼티에는 사용할 수 있지만, enum의 case에는 사용할 수 없다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span>
  <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span>
<span class="p">}</span>

<span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="n">home</span>
<span class="c1">// 🛑 key path cannot refer to static member 'home'</span>
</code></pre></div></div>
<ul>
  <li>KeyPath가 구조체나 클래스 내부의 프로퍼티를 가리킨다면, CasePath는 enum의 특정 case를 가리킨다.</li>
  <li>Point-Free의 swift-case-paths는 이 개념을 Swift에서 사용할 수 있게 해주는 라이브러리다.</li>
  <li>공식 설명에서도 CasePath를 “enum case를 위한 key path”라고 설명한다.</li>
</ul>

<h2 id="casepath를-활성화하는-방법">CasePath를 활성화하는 방법</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">CasePaths</span> 

<span class="kd">@CasePathable</span> 
<span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span> 
<span class="p">}</span>

<span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="kt">Cases</span><span class="o">.</span><span class="n">home</span> <span class="c1">// CaseKeyPath&lt;UserAction, HomeAction&gt; </span>
<span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="kt">Cases</span><span class="o">.</span><span class="n">settings</span> <span class="c1">// CaseKeyPath&lt;UserAction, SettingsAction&gt;</span>

<span class="c1">// 타입을 추론할 수 있는 상황에서는 더 짧게 쓸 수도 있다.</span>
<span class="p">\</span><span class="o">.</span><span class="n">home</span> <span class="k">as</span> <span class="kt">CaseKeyPath</span><span class="o">&lt;</span><span class="kt">UserAction</span><span class="p">,</span> <span class="kt">HomeAction</span><span class="o">&gt;</span> 
<span class="p">\</span><span class="o">.</span><span class="n">settings</span> <span class="k">as</span> <span class="kt">CaseKeyPath</span><span class="o">&lt;</span><span class="kt">UserAction</span><span class="p">,</span> <span class="kt">SettingsAction</span><span class="o">&gt;</span>
</code></pre></div></div>
<ul>
  <li>CasePath를 사용하려면 enum에 @CasePathable 매크로를 붙인다.</li>
  <li>그러면 enum의 Cases 네임스페이스를 통해 case path를 만들 수 있다.</li>
</ul>

<h2 id="casepath가-할-수-있는-일">CasePath가 할 수 있는 일</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// KeyPath는 어떤 값의 프로퍼티를 읽고 수정할 수 있다.</span>
<span class="n">user</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="p">\</span><span class="kt">User</span><span class="o">.</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="s">"Blob"</span> 
<span class="n">user</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">name</span><span class="p">]</span> <span class="c1">// "Blob"</span>

<span class="c1">// CasePath는 enum에서 특정 case의 associated value를 꺼내거나 수정할 수 있다.</span>
<span class="c1">// 현재 enum 값이 원하는 case라면 associated value를 반환하고, 다른 case라면 nil을 반환한다.</span>
<span class="k">var</span> <span class="nv">userAction</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span> 
<span class="n">userAction</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span> <span class="c1">// Optional(HomeAction.onAppear) </span>
<span class="n">userAction</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">settings</span><span class="p">]</span> <span class="c1">// nil</span>

<span class="c1">// 값을 수정하는 것도 가능하다.</span>
<span class="c1">// 이 코드는 현재 userAction이 .home case일 때 내부의 HomeAction 값을 수정한다.</span>
<span class="n">userAction</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span> <span class="o">=</span> <span class="o">.</span><span class="n">buttonTapped</span>
</code></pre></div></div>

<h2 id="embed-값을-다시-enum으로-감싸기">Embed: 값을 다시 enum으로 감싸기</h2>
<p>CasePath는 enum에서 값을 꺼내는 것뿐만 아니라, associated value를 다시 enum case로 감쌀 수도 있다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">homeCase</span> <span class="o">=</span> <span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="kt">Cases</span><span class="o">.</span><span class="n">home</span> 
<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="nf">homeCase</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span> 
<span class="c1">// UserAction.home(.onAppear)</span>

<span class="kt">HomeAction</span><span class="o">.</span><span class="n">onAppear</span> 
          <span class="err">↓</span>
<span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>
</code></pre></div></div>
<ul>
  <li>이 부분이 일반 KeyPath와 다른 중요한 차이다. KeyPath는 이미 존재하는 구조체나 클래스 안으로 들어가 프로퍼티를 읽고 수정한다. 반면 CasePath는 associated value를 새로운 enum 값으로 만들 수도 있다.</li>
</ul>

<h2 id="case인지-확인하기">case인지 확인하기</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">userAction</span><span class="o">.</span><span class="nf">is</span><span class="p">(\</span><span class="o">.</span><span class="n">home</span><span class="p">)</span> <span class="c1">// true</span>
<span class="n">userAction</span><span class="o">.</span><span class="nf">is</span><span class="p">(\</span><span class="o">.</span><span class="n">settings</span><span class="p">)</span> <span class="c1">// false</span>

<span class="c1">// 배열에서도 유용하다.</span>
<span class="k">let</span> <span class="nv">actions</span><span class="p">:</span> <span class="p">[</span><span class="kt">UserAction</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
  <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">),</span>
  <span class="o">.</span><span class="nf">settings</span><span class="p">(</span><span class="o">.</span><span class="n">purchaseButtonTapped</span><span class="p">),</span>
  <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">buttonTapped</span><span class="p">)</span>
<span class="p">]</span>

<span class="k">let</span> <span class="nv">homeActionsCount</span> <span class="o">=</span> <span class="n">actions</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="nf">is</span><span class="p">(\</span><span class="o">.</span><span class="n">home</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// 2</span>
</code></pre></div></div>
<ul>
  <li>@CasePathable을 사용하면 특정 enum 값이 원하는 case인지 확인할 수도 있다.</li>
</ul>

<h2 id="associated-value를-제자리에서-수정하기">associated value를 제자리에서 수정하기</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">result</span> <span class="o">=</span> <span class="kt">Result</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">Error</span><span class="o">&gt;.</span><span class="nf">success</span><span class="p">(</span><span class="s">"Blob"</span><span class="p">)</span> 

<span class="c1">// 기본 Swift만 사용했다면 switch로 case를 꺼내고, 값을 수정한 뒤, 다시 enum으로 감싸야 한다.</span>
<span class="k">switch</span> <span class="n">result</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="kd">var</span> <span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="n">value</span><span class="p">):</span> 
        <span class="n">value</span> <span class="o">+=</span> <span class="s">", Jr."</span> 
       <span class="n">result</span> <span class="o">=</span> <span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> 
    <span class="k">case</span> <span class="o">.</span><span class="nv">failure</span><span class="p">:</span> 
        <span class="k">break</span> 
<span class="p">}</span>

<span class="c1">// modify를 사용하면 이 반복 코드를 줄일 수 있다.</span>
<span class="n">result</span><span class="o">.</span><span class="nf">modify</span><span class="p">(\</span><span class="o">.</span><span class="n">success</span><span class="p">)</span> <span class="p">{</span> 
    <span class="nv">$0</span> <span class="o">+=</span> <span class="s">", Jr."</span> 
<span class="p">}</span> 
<span class="n">result</span> <span class="c1">// .success("Blob, Jr.")</span>
</code></pre></div></div>
<ul>
  <li>CasePath는 특정 case의 associated value를 제자리에서 수정하는 modify도 제공한다.</li>
</ul>

<h2 id="casepath도-조합할-수-있다">CasePath도 조합할 수 있다</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@CasePathable</span> 
<span class="kd">enum</span> <span class="kt">AppAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">user</span><span class="p">(</span><span class="kt">UserAction</span><span class="p">)</span> 
<span class="p">}</span> 

<span class="kd">@CasePathable</span> 
<span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span> 
<span class="p">}</span>
</code></pre></div></div>
<ul>
  <li>KeyPath를 조합할 수 있듯이 CasePath도 조합할 수 있다.</li>
  <li>예를 들어 액션이 중첩되어 있다고 가정해 보자.</li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 그러면 AppAction에서 바로 HomeAction까지 들어가는 CasePath를 만들 수 있다.</span>
<span class="p">\</span><span class="kt">AppAction</span><span class="o">.</span><span class="kt">Cases</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">home</span> <span class="c1">// CaseKeyPath&lt;AppAction, HomeAction&gt;</span>

<span class="c1">// 또는 직접 append할 수도 있다.</span>
<span class="k">let</span> <span class="nv">appActionToUser</span> <span class="o">=</span> <span class="p">\</span><span class="kt">AppAction</span><span class="o">.</span><span class="kt">Cases</span><span class="o">.</span><span class="n">user</span>
<span class="k">let</span> <span class="nv">userActionToHome</span> <span class="o">=</span> <span class="p">\</span><span class="kt">UserAction</span><span class="o">.</span><span class="kt">Cases</span><span class="o">.</span><span class="n">home</span>
<span class="k">let</span> <span class="nv">appActionToHome</span> <span class="o">=</span> <span class="n">appActionToUser</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="nv">path</span><span class="p">:</span> <span class="n">userActionToHome</span><span class="p">)</span> <span class="c1">// CaseKeyPath&lt;AppAction, HomeAction&gt;</span>
</code></pre></div></div>
<ul>
  <li>이 덕분에 큰 enum을 작은 enum 단위로 나누고, 필요한 case만 선택해서 다룰 수 있다.</li>
</ul>

<h2 id="dynamic-member-lookup과-함께-사용하기">Dynamic Member Lookup과 함께 사용하기</h2>
<p>@CasePathable과 @dynamicMemberLookup을 함께 사용하면 enum case를 프로퍼티처럼 접근할 수도 있다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@CasePathable</span> 
<span class="kd">@dynamicMemberLookup</span> 
<span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span> 
<span class="p">}</span>
<span class="k">let</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">UserAction</span> <span class="o">=</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>
<span class="n">action</span><span class="o">.</span><span class="n">home</span> <span class="c1">// Optional(HomeAction.onAppear)</span>
<span class="n">action</span><span class="o">.</span><span class="n">settings</span> <span class="c1">// nil</span>
</code></pre></div></div>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">actions</span><span class="p">:</span> <span class="p">[</span><span class="kt">UserAction</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span> 
    <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">),</span> 
    <span class="o">.</span><span class="nf">settings</span><span class="p">(</span><span class="o">.</span><span class="n">purchaseButtonTapped</span><span class="p">),</span> 
    <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">buttonTapped</span><span class="p">)</span> 
<span class="p">]</span>
<span class="n">actions</span><span class="o">.</span><span class="nf">compactMap</span><span class="p">(\</span><span class="o">.</span><span class="n">home</span><span class="p">)</span> <span class="c1">// [.onAppear, .buttonTapped]</span>
</code></pre></div></div>
<ul>
  <li>배열에서도 key path 표현식처럼 사용할 수 있다.</li>
  <li>이 문법은 enum을 구조체 프로퍼티처럼 다룰 수 있게 만들어준다.</li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @dynamicMemberLookup 없음</span>
<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="n">action</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span>
<span class="c1">// Optional(.onAppear)</span>

<span class="c1">// @dynamicMemberLookup 있음</span>
<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="n">action</span><span class="o">.</span><span class="n">home</span>
<span class="c1">// Optional(.onAppear)</span>
</code></pre></div></div>

<h2 id="swiftui-binding과-casepath">SwiftUI Binding과 CasePath</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">user</span><span class="p">:</span> <span class="kt">Binding</span><span class="o">&lt;</span><span class="kt">User</span><span class="o">&gt;</span> <span class="o">=</span> <span class="o">...</span> 
<span class="k">let</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">Binding</span><span class="o">&lt;</span><span class="kt">String</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">name</span>
</code></pre></div></div>
<ul>
  <li>공식 README에서도 Binding 예시를 통해 CasePath의 필요성을 설명한다.</li>
  <li>SwiftUI의 Binding은 WritableKeyPath를 이용해 하위 프로퍼티의 바인딩을 만들 수 있다.</li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">Destination</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeState</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsState</span><span class="p">)</span> 
<span class="p">}</span>

<span class="k">let</span> <span class="nv">destination</span><span class="p">:</span> <span class="kt">Binding</span><span class="o">&lt;</span><span class="kt">Destination</span><span class="o">&gt;</span> <span class="o">=</span> <span class="o">...</span> 
<span class="n">destination</span><span class="o">.</span><span class="n">home</span> <span class="c1">// 기본 Swift만으로는 불가능</span>
</code></pre></div></div>
<ul>
  <li>하지만 enum에서는 기본적으로 이런 식의 접근이 불가능하다.</li>
</ul>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@CasePathable</span>
<span class="kd">enum</span> <span class="kt">Destination</span> <span class="p">{</span> 
    <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeState</span><span class="p">)</span> 
    <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsState</span><span class="p">)</span> 
<span class="p">}</span>

<span class="n">destination</span><span class="o">.</span><span class="n">home</span> <span class="c1">// Binding&lt;HomeState&gt;? </span>
<span class="n">destination</span><span class="o">.</span><span class="n">settings</span> <span class="c1">// Binding&lt;SettingsState&gt;?</span>
</code></pre></div></div>
<ul>
  <li>CasePath를 사용하면 enum의 특정 case에 대한 바인딩을 만들 수 있다.</li>
</ul>

<h2 id="dynamicmemberlookup-유무-차이"><code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code> 유무 차이</h2>

<p>CasePath를 이해할 때는 먼저 두 가지를 구분해야 한다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1. CasePath 없음</span>
<span class="c1">// Swift 기본 enum만 사용하는 상태</span>

<span class="c1">// 2. CasePath 있음</span>
<span class="c1">// @CasePathable을 붙여 enum case를 CasePath로 다룰 수 있는 상태</span>

<span class="c1">// 3. @dynamicMemberLookup 있음</span>
<span class="c1">// CasePath 접근을 점 문법으로 더 짧게 사용할 수 있는 상태</span>
</code></pre></div></div>

<p>즉, <code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code>은 CasePath 자체를 만들어주는 기능이 아니다.<br />
CasePath를 더 편한 문법으로 사용할 수 있게 해주는 옵션에 가깝다.</p>

<hr />

<h4 id="1-casepath-없음">1. CasePath 없음</h4>

<p>Swift 기본 enum만 사용하면 associated value를 꺼내기 위해 <code class="language-plaintext highlighter-rouge">switch</code> 또는 <code class="language-plaintext highlighter-rouge">if case</code>를 사용해야 한다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span>
  <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>

<span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">=</span> <span class="n">action</span> <span class="p">{</span>
  <span class="nf">print</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// .onAppear</span>
</code></pre></div></div>

<p>이 상태에서는 아래 문법을 사용할 수 없다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">action</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span> <span class="c1">// 불가능</span>
<span class="n">action</span><span class="o">.</span><span class="n">home</span>          <span class="c1">// 불가능</span>
</code></pre></div></div>

<hr />

<h4 id="2-casepath-있음-dynamicmemberlookup-없음">2. CasePath 있음, <code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code> 없음</h4>

<p><code class="language-plaintext highlighter-rouge">@CasePathable</code>을 붙이면 enum case를 CasePath로 다룰 수 있다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">CasePaths</span>

<span class="kd">@CasePathable</span>
<span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span>
  <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>

<span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="n">action</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span>
<span class="c1">// Optional(.onAppear)</span>
</code></pre></div></div>

<p>이때는 CasePath 기능은 사용할 수 있지만, 점 문법은 사용할 수 없다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">action</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span> <span class="c1">// 가능</span>
<span class="n">action</span><span class="o">.</span><span class="n">home</span>          <span class="c1">// 불가능</span>
</code></pre></div></div>

<hr />

<h4 id="3-casepath-있음-dynamicmemberlookup-있음">3. CasePath 있음, <code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code> 있음</h4>

<p><code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code>까지 붙이면 CasePath 접근을 점 문법으로 더 짧게 쓸 수 있다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">CasePaths</span>

<span class="kd">@CasePathable</span>
<span class="kd">@dynamicMemberLookup</span>
<span class="kd">enum</span> <span class="kt">UserAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">home</span><span class="p">(</span><span class="kt">HomeAction</span><span class="p">)</span>
  <span class="k">case</span> <span class="nf">settings</span><span class="p">(</span><span class="kt">SettingsAction</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>

<span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="n">action</span><span class="o">.</span><span class="n">home</span>
<span class="c1">// Optional(.onAppear)</span>
</code></pre></div></div>

<p>이 코드는 사실상 아래 코드와 같은 의미다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="n">action</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span>
<span class="c1">// Optional(.onAppear)</span>
</code></pre></div></div>

<p>즉, <code class="language-plaintext highlighter-rouge">action.home</code>은 <code class="language-plaintext highlighter-rouge">action[case: \.home]</code>의 축약 문법이라고 보면 된다.</p>

<hr />

<h3 id="최종-비교">최종 비교</h3>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CasePath 없음</span>
<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>

<span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">=</span> <span class="n">action</span> <span class="p">{</span>
  <span class="nf">print</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// .onAppear</span>
</code></pre></div></div>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CasePath 있음</span>
<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>

<span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="n">action</span><span class="p">[</span><span class="nv">case</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">home</span><span class="p">]</span>
<span class="c1">// Optional(.onAppear)</span>
</code></pre></div></div>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CasePath 있음 + @dynamicMemberLookup 있음</span>
<span class="k">let</span> <span class="nv">action</span> <span class="o">=</span> <span class="kt">UserAction</span><span class="o">.</span><span class="nf">home</span><span class="p">(</span><span class="o">.</span><span class="n">onAppear</span><span class="p">)</span>

<span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="n">action</span><span class="o">.</span><span class="n">home</span>
<span class="c1">// Optional(.onAppear)</span>
</code></pre></div></div>

<p>정리하면 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>상태</th>
      <th>가능한 문법</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CasePath 없음</td>
      <td><code class="language-plaintext highlighter-rouge">if case let .home(value)</code></td>
      <td>Swift 기본 패턴 매칭</td>
    </tr>
    <tr>
      <td>CasePath 있음</td>
      <td><code class="language-plaintext highlighter-rouge">action[case: \.home]</code></td>
      <td>CasePath로 associated value 추출</td>
    </tr>
    <tr>
      <td>CasePath 있음 + <code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code> 있음</td>
      <td><code class="language-plaintext highlighter-rouge">action.home</code></td>
      <td>CasePath 접근을 점 문법으로 축약</td>
    </tr>
  </tbody>
</table>

<p>따라서 핵심은 다음과 같다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">@CasePathable</code>은 enum case를 CasePath로 다룰 수 있게 해준다.</li>
  <li><code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code>은 <code class="language-plaintext highlighter-rouge">action[case: \.home]</code>을 <code class="language-plaintext highlighter-rouge">action.home</code>처럼 짧게 쓸 수 있게 해준다.</li>
  <li>즉, <code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code>은 필수가 아니라 편의 문법이다.</li>
</ul>

<!-- 
Swift는 모든 구조체와 클래스 속성에 KeyPath를 부여한다.

```swift
struct User {
  let id: Int
  var name: String
}

\User.id    // KeyPath<User, Int>
\User.name  // WritableKeyPath<User, String>
```
- 컴파일러가 생성하는 이 코드는 구조체의 일부를 추상적으로 확대하여 검사 및 변경할 수 있고, 변경 사항을 구조체 전체에 반영할 수 있다.
- SwiftUI의 바인딩 처럼 동적 멤버 조회를 사용하는 많은 최신 Swift API에서 조용히 활용되지만, SwiftUI enviroment나 unsafe mutable pointers 같이 직접적으로 나타나기도 한다.

```swift
enum UserAction {
  case home(HomeAction)
  case settings(SettingsAction)
}

// 🛑 키 경로가 정적 멤버 'home'을 참조할 수 없습니다.
\UserAction.home  // 🛑
```
- 아쉽지만 열거형 케이스에 대해서는 이러한 접근이 불가능하다.
- 그래서 열거형의 특정 사례 데이터를 수정할 수 있는 일반적인 코드 작성은 불가능하다. -->]]></content><author><name>Dong Hyeon</name></author><category term="TCA" /><summary type="html"><![CDATA[KeyPath blue &gt; Swift의 구조체와 클래슨느 프로퍼티를 참조할 수 있는 KeyPath를 사용할 수 있다. blue &gt; KeyPath는 특정 타입의 프로퍼티 위치를 값처럼 표현하는 기능이다. struct User { let id: Int var name: String } \User.id // KeyPath&lt;User, Int&gt; \User.name // WritableKeyPath&lt;User, String&gt; KeyPath&lt;Root, Value&gt;는 Root 타입 안에 있는 Value 타입 프로퍼티를 가리킨다. let user = User(id: 1, name: "Donghyeon") let nameKeyPath = \User.name print(user[keyPath: nameKeyPath]) // Donghyeon 예를 들어 \User.name은 User 안의 name 프로퍼티를 가리키는 값이다. var user = User(id: 1, name: "Donghyeon") user[keyPath: \User.name] = "Kim" print(user.name) // Kim var 프로퍼티라면 값을 변경하는 것도 가능하다. 즉, KeyPath를 사용하면 어떤 프로퍼티를 읽거나 수정할지 코드로 직접 고정하지 않고, 프로퍼티 접근 자체를 값처럼 전달할 수 있다. 이 개념은 SwiftUI에서도 자주 사용된다. 예를 들어 Binding, Environment, dynamicMemberLookup 같은 API 내부에서 KeyPath가 자연스럽게 활용된다. enum에서는 KeyPath를 바로 사용할 수 없다 enum UserAction { case home(HomeAction) case settings(SettingsAction) } \UserAction.home // 🛑 Key path cannot refer to enum case 'home' enum의 case에는 일반적인 KeyPath를 사용할 수 없다. KeyPath는 기본적으로 구조체나 클래스의 저장 프로퍼티를 가리키는 기능이기 떄문이다. enum의 case는 저장 프로퍼티가 아니다. enum은 case중 하나의 상태를 표현하는 타입이고, associated value는 특정 case일 때만 존재한다. enum에서는 KeyPath를 바로 사용할 수 없는 이유 enum UserAction { case home(HomeAction) case settings(SettingsAction) } let action = UserAction.settings(SettingsAction()) home은 항상 존재하는 프로퍼티가 아니다. let action 상태는 home이라는 값이 존재하지 않는다. 따라서 Swift 기본 KeyPath 시스템은 \UserAction.home처럼 enum case 내부 값을 직접 가리킬 수 없다. enum에서는 기본적으로 switch로 꺼낸다 enum UserAction { case home(HomeAction) case settings(SettingsAction) } switch action { case let .home(homeAction): print("home action:", homeAction) case let .settings(settingsAction): print("settings action:", settingsAction) } Swift에서 enum의 associated value를 다루는 가장 기본적인 방법은 switch 또는 if case를 사용하는 것이다. if case let .home(homeAction) = action { print(homeAction) } 특정 case만 보고 싶다면 if case를 사용할 수 있다. enum Destination { case home(HomeState) case settings(SettingsState) } var destination = Destination.home(HomeState(title: "Home")) switch destination { case var .home(state): state.title = "New Home" destination = .home(state) case .settings: break } 값을 수정해야 한다면, enum을 switch로 분해한 뒤 새로운 enum 값으로 다시 만들어줘야 한다. 즉, enum에서는 구조체처름 특정 내부 값을 KeyPath로 바로 수정하는 것이 아니라 현재 case를 확인하고 associated value를 꺼낸 뒤 다시 감싸는 방식으로 처리한다. 정리 \User.name // 가능 구조체와 클래스에서는 KeyPath를 사용해 프로퍼티 접근을 값처럼 다룰 수 있다. \UserAction.home // 불가능 if case let .home(homeAction) = action { // homeAction 사용 } 하지만 enum case는 저장 프로퍼티가 아니기 때문에 Swift의 기본 KeyPath로 접근할 수 없다. 그래서 enum의 associated value를 다룰 때는 기본적으로 switch, if case, guard case를 사용한다. Swift의 기본 KeyPath는 구조체와 클래스의 프로퍼티 접근에는 강력하지만, enum case까지 표현하지는 못한다. 그래서 enum을 더 일반적으로 다루고 싶다면 Swift의 기본 KeyPath가 아니라, case를 추출하고 다시 삽입할 수 있는 별도의 추상화가 필요하다. CasePath enum UserAction { case home(HomeAction) case settings(SettingsAction) } \UserAction.home // 🛑 Key path cannot refer to enum case 'home' 앞에서 살펴본 것처럼 Swift의 기본 KeyPath는 구조체와 클래스의 프로퍼티에는 사용할 수 있지만, enum의 case에는 사용할 수 없다. 하지만 실제 앱을 개발하다 보면 enum의 associated value를 일반화해서 다루고 싶은 경우가 매우 많다. enum Destination { case home(HomeState) case settings(SettingsState) } 예를 들어 위와 같은 상태가 있다고 가정해 보자. // 방법 1 if case let .home(state) = destination { // state 사용 } // 방법 2 switch destination { case var .home(state): state.title = "새 제목" destination = .home(state) case .settings: break } Destination이 현재 home인지 확인하고, if case let 나 switch로 수정한 뒤 다시 enum으로 감싸야 한다. 이러한 코드는 한두 번은 괜찮지만 enum을 자주 다루게 되면 반복되는 switch와 패턴 매칭 코드가 계속 등장하게 된다. CasePath 앞에서 살펴본 것처럼 Swift의 기본 KeyPath는 구조체와 클래스의 프로퍼티에는 사용할 수 있지만, enum의 case에는 사용할 수 없다. enum UserAction { case home(HomeAction) case settings(SettingsAction) } \UserAction.home // 🛑 key path cannot refer to static member 'home' KeyPath가 구조체나 클래스 내부의 프로퍼티를 가리킨다면, CasePath는 enum의 특정 case를 가리킨다. Point-Free의 swift-case-paths는 이 개념을 Swift에서 사용할 수 있게 해주는 라이브러리다. 공식 설명에서도 CasePath를 “enum case를 위한 key path”라고 설명한다. CasePath를 활성화하는 방법 import CasePaths @CasePathable enum UserAction { case home(HomeAction) case settings(SettingsAction) } \UserAction.Cases.home // CaseKeyPath&lt;UserAction, HomeAction&gt; \UserAction.Cases.settings // CaseKeyPath&lt;UserAction, SettingsAction&gt; // 타입을 추론할 수 있는 상황에서는 더 짧게 쓸 수도 있다. \.home as CaseKeyPath&lt;UserAction, HomeAction&gt; \.settings as CaseKeyPath&lt;UserAction, SettingsAction&gt; CasePath를 사용하려면 enum에 @CasePathable 매크로를 붙인다. 그러면 enum의 Cases 네임스페이스를 통해 case path를 만들 수 있다. CasePath가 할 수 있는 일 // KeyPath는 어떤 값의 프로퍼티를 읽고 수정할 수 있다. user[keyPath: \User.name] = "Blob" user[keyPath: \.name] // "Blob" // CasePath는 enum에서 특정 case의 associated value를 꺼내거나 수정할 수 있다. // 현재 enum 값이 원하는 case라면 associated value를 반환하고, 다른 case라면 nil을 반환한다. var userAction = UserAction.home(.onAppear) userAction[case: \.home] // Optional(HomeAction.onAppear) userAction[case: \.settings] // nil // 값을 수정하는 것도 가능하다. // 이 코드는 현재 userAction이 .home case일 때 내부의 HomeAction 값을 수정한다. userAction[case: \.home] = .buttonTapped Embed: 값을 다시 enum으로 감싸기 CasePath는 enum에서 값을 꺼내는 것뿐만 아니라, associated value를 다시 enum case로 감쌀 수도 있다. let homeCase = \UserAction.Cases.home let action = homeCase(.onAppear) // UserAction.home(.onAppear) HomeAction.onAppear ↓ UserAction.home(.onAppear) 이 부분이 일반 KeyPath와 다른 중요한 차이다. KeyPath는 이미 존재하는 구조체나 클래스 안으로 들어가 프로퍼티를 읽고 수정한다. 반면 CasePath는 associated value를 새로운 enum 값으로 만들 수도 있다. case인지 확인하기 userAction.is(\.home) // true userAction.is(\.settings) // false // 배열에서도 유용하다. let actions: [UserAction] = [ .home(.onAppear), .settings(.purchaseButtonTapped), .home(.buttonTapped) ] let homeActionsCount = actions.count { $0.is(\.home) } // 2 @CasePathable을 사용하면 특정 enum 값이 원하는 case인지 확인할 수도 있다. associated value를 제자리에서 수정하기 var result = Result&lt;String, Error&gt;.success("Blob") // 기본 Swift만 사용했다면 switch로 case를 꺼내고, 값을 수정한 뒤, 다시 enum으로 감싸야 한다. switch result { case var .success(value): value += ", Jr." result = .success(value) case .failure: break } // modify를 사용하면 이 반복 코드를 줄일 수 있다. result.modify(\.success) { $0 += ", Jr." } result // .success("Blob, Jr.") CasePath는 특정 case의 associated value를 제자리에서 수정하는 modify도 제공한다. CasePath도 조합할 수 있다 @CasePathable enum AppAction { case user(UserAction) } @CasePathable enum UserAction { case home(HomeAction) case settings(SettingsAction) } KeyPath를 조합할 수 있듯이 CasePath도 조합할 수 있다. 예를 들어 액션이 중첩되어 있다고 가정해 보자. // 그러면 AppAction에서 바로 HomeAction까지 들어가는 CasePath를 만들 수 있다. \AppAction.Cases.user.home // CaseKeyPath&lt;AppAction, HomeAction&gt; // 또는 직접 append할 수도 있다. let appActionToUser = \AppAction.Cases.user let userActionToHome = \UserAction.Cases.home let appActionToHome = appActionToUser.append(path: userActionToHome) // CaseKeyPath&lt;AppAction, HomeAction&gt; 이 덕분에 큰 enum을 작은 enum 단위로 나누고, 필요한 case만 선택해서 다룰 수 있다. Dynamic Member Lookup과 함께 사용하기 @CasePathable과 @dynamicMemberLookup을 함께 사용하면 enum case를 프로퍼티처럼 접근할 수도 있다. @CasePathable @dynamicMemberLookup enum UserAction { case home(HomeAction) case settings(SettingsAction) } let action: UserAction = .home(.onAppear) action.home // Optional(HomeAction.onAppear) action.settings // nil let actions: [UserAction] = [ .home(.onAppear), .settings(.purchaseButtonTapped), .home(.buttonTapped) ] actions.compactMap(\.home) // [.onAppear, .buttonTapped] 배열에서도 key path 표현식처럼 사용할 수 있다. 이 문법은 enum을 구조체 프로퍼티처럼 다룰 수 있게 만들어준다. // @dynamicMemberLookup 없음 let action = UserAction.home(.onAppear) let value = action[case: \.home] // Optional(.onAppear) // @dynamicMemberLookup 있음 let action = UserAction.home(.onAppear) let value = action.home // Optional(.onAppear) SwiftUI Binding과 CasePath let user: Binding&lt;User&gt; = ... let name: Binding&lt;String&gt; = user.name 공식 README에서도 Binding 예시를 통해 CasePath의 필요성을 설명한다. SwiftUI의 Binding은 WritableKeyPath를 이용해 하위 프로퍼티의 바인딩을 만들 수 있다. enum Destination { case home(HomeState) case settings(SettingsState) } let destination: Binding&lt;Destination&gt; = ... destination.home // 기본 Swift만으로는 불가능 하지만 enum에서는 기본적으로 이런 식의 접근이 불가능하다. @CasePathable enum Destination { case home(HomeState) case settings(SettingsState) } destination.home // Binding&lt;HomeState&gt;? destination.settings // Binding&lt;SettingsState&gt;? CasePath를 사용하면 enum의 특정 case에 대한 바인딩을 만들 수 있다. @dynamicMemberLookup 유무 차이 CasePath를 이해할 때는 먼저 두 가지를 구분해야 한다. // 1. CasePath 없음 // Swift 기본 enum만 사용하는 상태 // 2. CasePath 있음 // @CasePathable을 붙여 enum case를 CasePath로 다룰 수 있는 상태 // 3. @dynamicMemberLookup 있음 // CasePath 접근을 점 문법으로 더 짧게 사용할 수 있는 상태 즉, @dynamicMemberLookup은 CasePath 자체를 만들어주는 기능이 아니다. CasePath를 더 편한 문법으로 사용할 수 있게 해주는 옵션에 가깝다. 1. CasePath 없음 Swift 기본 enum만 사용하면 associated value를 꺼내기 위해 switch 또는 if case를 사용해야 한다. enum UserAction { case home(HomeAction) case settings(SettingsAction) } let action = UserAction.home(.onAppear) if case let .home(value) = action { print(value) } // .onAppear 이 상태에서는 아래 문법을 사용할 수 없다. action[case: \.home] // 불가능 action.home // 불가능 2. CasePath 있음, @dynamicMemberLookup 없음 @CasePathable을 붙이면 enum case를 CasePath로 다룰 수 있다. import CasePaths @CasePathable enum UserAction { case home(HomeAction) case settings(SettingsAction) } let action = UserAction.home(.onAppear) let value = action[case: \.home] // Optional(.onAppear) 이때는 CasePath 기능은 사용할 수 있지만, 점 문법은 사용할 수 없다. action[case: \.home] // 가능 action.home // 불가능 3. CasePath 있음, @dynamicMemberLookup 있음 @dynamicMemberLookup까지 붙이면 CasePath 접근을 점 문법으로 더 짧게 쓸 수 있다. import CasePaths @CasePathable @dynamicMemberLookup enum UserAction { case home(HomeAction) case settings(SettingsAction) } let action = UserAction.home(.onAppear) let value = action.home // Optional(.onAppear) 이 코드는 사실상 아래 코드와 같은 의미다. let value = action[case: \.home] // Optional(.onAppear) 즉, action.home은 action[case: \.home]의 축약 문법이라고 보면 된다. 최종 비교 // CasePath 없음 let action = UserAction.home(.onAppear) if case let .home(value) = action { print(value) } // .onAppear // CasePath 있음 let action = UserAction.home(.onAppear) let value = action[case: \.home] // Optional(.onAppear) // CasePath 있음 + @dynamicMemberLookup 있음 let action = UserAction.home(.onAppear) let value = action.home // Optional(.onAppear) 정리하면 다음과 같다. 상태 가능한 문법 의미 CasePath 없음 if case let .home(value) Swift 기본 패턴 매칭 CasePath 있음 action[case: \.home] CasePath로 associated value 추출 CasePath 있음 + @dynamicMemberLookup 있음 action.home CasePath 접근을 점 문법으로 축약 따라서 핵심은 다음과 같다. @CasePathable은 enum case를 CasePath로 다룰 수 있게 해준다. @dynamicMemberLookup은 action[case: \.home]을 action.home처럼 짧게 쓸 수 있게 해준다. 즉, @dynamicMemberLookup은 필수가 아니라 편의 문법이다.]]></summary></entry><entry><title type="html">[WWDC] Demystify SwiftUI - SwiftUI가 코드를 보는법</title><link href="https://indextrown.github.io/swiftui-demystify-swiftui/" rel="alternate" type="text/html" title="[WWDC] Demystify SwiftUI - SwiftUI가 코드를 보는법" /><published>2026-06-01T00:00:00+00:00</published><updated>2026-06-01T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BSwiftUI%5D%20Demystify%20SwiftUI</id><content type="html" xml:base="https://indextrown.github.io/swiftui-demystify-swiftui/"><![CDATA[<!-- <img src="/%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B2%BD%EB%A1%9C" alt="이미지" width="30%"> -->

<!-- <img src="https://github.com/user-attachments/assets/3938e583-0fc4-4620-99b0-bf761e60a1ba" width="60%" align="left"> -->

<h4 id="swiftui가-코드를-볼-때-무엇을-보는가">SwiftUI가 코드를 볼 때 무엇을 보는가?</h4>
<ul>
  <li>Identity(정체성) - SwiftUI가 앱의 여러 업데이트에서 동일 요소로 보거나 별개 요소로 보는 방법</li>
  <li>Lifetime(수명) - SwiftUI가 시간이 지남에 따라 View와 Data 존재를 추적하는 방법</li>
  <li>Dependencies(의존성) - SwiftUI가 인터페이스를 업데이트해야 하는 시기와 이유를 이해하는 방법</li>
</ul>

<h4 id="identity정체성---swiftui가-앱의-여러-업데이트에서-동일-요소로-보거나-별개-요소로-보는-방법">Identity(정체성) - SwiftUI가 앱의 여러 업데이트에서 동일 요소로 보거나 별개 요소로 보는 방법</h4>
<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601150619822.png" alt="image-20260601150619822" /><br />
사진에서 두 강아지가 다른 강아지인지 혹은 두 장의 같은 강아지인지 알 수 없다.<br />
정보가 충분하지 않아 사실을 말할 수 없기 때문이다. 이 질문을 SwiftUI는 Identity(정체성) 이라고 부른다.  <br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601150933679.png" alt="image-20260601150933679" /><br />
다른 예시로, 아이콘이 서로 완전히 독립 객체인지 알 수 없다.<br />
완전히 다른 객체일 수도 있고 단지 위치만 다르고 다른 색을 가진 동일한 객체일수도 있다.<br />
이러한 구분을 “한 상태에서 다른 상태로 전환하는 방식”을 변경하기 때문에 중요한 부분이다.<br />
만약 서로 같은 객체라면 현 위치에서 다른 위치로 이동하는 동일한 뷰이기 때문에 전환 중에 객체가 아래로 내려가야 한다.<br />
따라서 서로 다른 상태에 걸쳐 뷰를 연결하는 것이 중요하다. 이것이 SwiftUI가 뷰 사이를 전달하는 방법을 이해하는 방식이기 때문이다. 이게 View Identity 핵심 가치다.</p>

<h4 id="swiftui는-두-가지-다른-유형의-identity에-초점을-두고-있다">SwiftUI는 두 가지 다른 유형의 Identity에 초점을 두고 있다.</h4>
<ul>
  <li>Explicit of Identity(명시적 정체성): 맞춤형 또는 데이터 기반 식별자를 사용한다.</li>
  <li>Structural Identity(구조적 정체성): 뷰 계층 구조에서의 유형과 위치에 따라 뷰를 구별한다.<br />
<br /></li>
</ul>

<h4 id="explicit-of-identity명시적-정체성">Explicit of Identity(명시적 정체성)</h4>
<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601151352773.png" alt="image-20260601151352773" /><br />
사물을 식별하기 위해 보통 이름을 부여할 수 있다.<br />
하지만 두 사물이 똑같이 생기고 같은 계열일 가능성이 높을 수 있어서 이름으로는 고유성이 부족할 수 있다.<br />
이름이나 식별자를 할당하는 것은 Explicit of Identity(명시적 정체성) 의 한 형태이다.<br />
이는 강력하고 유연하지만 누군가 어딘가에서 이름을 추적해야 한다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601151759888.png" alt="image-20260601151759888" /><br />
이미 익숙할 수 있는 Explicit of Identity의 한 형태는 UIKit 및 AppKit 전체에서 사용되는 포인터 Identity다.<br />
SwiftUI에서는 포인터 Identity를 사용하지 않고 UIView와 NSView에서 사용한다.<br />
UIView와 NSView는 클래스이므로 메모리 할당에 필요한 고유한 포인터를 가진다. 포인터는 Explicit of Identity의 자연스러운 원천이라고 한다.<br />
포인터를 사용해 개별 뷰를 참조할 수 있으며 두 뷰가 동일한 포인터를 공유하는 경우 실제로 동일한 뷰임을 보장할 수 있다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601151819505.png" alt="image-20260601151819505" /><br />
하지만 SwiftUI View는 일반적으로 클래스 대신 구조체로 표현하는 값 타입이기 때문에 SwiftUI는 포인터를 사용하지 않는다.<br />
가장 중요한 점은 값 타입은 SwiftUI가 해당 View에 대한 지속적인 Identity로 사용할 수 있는 정식 참조가 없다는 것이다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601152414474.png" alt="image-20260601152414474" /><br />
여기서 사용된 id는 Explicit of Identity의 한 형태이다.<br />
각 강아지의 인식표 Identity는 View를 명시적으로 식별하는 데 사용된다.<br />
강아지 컬렉션이 변경되면 SwiftUI는 해당 Id를 사용해 무엇이 변경되는지 이해하고 목록 내에서 올바른 애니메이션을 생성할 수 있다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601152720076.png" alt="image-20260601152720076" /><br />
또 다른 예제인데 여기서는 ScrollViewReader를 사용해 하단 버튼을 사용하여 뷰 상단으로 이동한다. <br />
.id() Modifier는 사용자 정의 식별자를 사용해 뷰를 명시적으로 식별하는 방법을 제공한다. <br />
현재 페이지 상단의 헤더 뷰에 식별자를 추가한 것이다.</p>

<p>추가로 해당 식별자를 뷰 프록시의 scrollTo(_:) 메서드에 전달해 SwiftUI에게 특정 뷰로 이동하도록 지시할 수 있다. <br />
이 장점은 모든 뷰를 명시적으로 식별할 필요가 없고 헤더와 같이 코드의 다른 곳에서 참조해야 하는 뷰만 식별할 수 있다는 것이다.</p>

<p>ScrollViewreader, ScrollView, 텍스트 및 버튼에는 명시적인 식별자가 필요하지 않는다.<br />
그러나 정체성이 명시적이지 않다고 해서 이러한 뷰가 전혀 정체성이 없다는 것을 의미하지는 않는다.<br />
명시적이지 않더라도 모든 View는 Identity가 있기 때문이다. → <strong>여기서 구조적 정체성이 등장</strong><br />
<br /></p>

<h4 id="structural-identity구조적-정체성">Structural Identity(구조적 정체성)</h4>
<p>SwiftUI는 뷰 계층 구조를 사용하여 뷰에 대한 암시적 ID를 생성하므로 사용자가 직접 ID를 생성할 필요가 없다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601154415736.png" alt="image-20260601154415736" /><br />
비슷한 두 마리의 강아지가 있지만 이름은 모르지만 각각을 식별해야 한다고 가정해보자.<br />
움직이지 않음을 보장할 수 있다면 “왼쪽에 있는 개”, “오른쪽에 있는 개”와 같이 위치에 따라 식별할 수 있다.<br />
SwiftUI는 이런 객체를 구별하기 위해 상대적인 배열을 사용한다고 한다 이것이 Structural Identity이다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601155206703.png" alt="image-20260601155206703" /><br />
SwiftUI는 API 전체에서 Structural Identity를 활용한다.<br />
전형적인 예시로 View 코드 내에서 if 문과 기타 조건부 논리를 사용하는 경우다.<br />
조건문의 구조는 각 뷰를 식별하는 명확한 방법을 제공한다.<br />
첫 번째 뷰는 조선이 true일때만 표시되고 두번째 뷰는 false일때만 표시된다.<br />
하지만 이 경우 SwiftUI가 볼때 이 뷰가 현재 위치에 유지되고 위치가 바뀌지 않음을 정적으로 보장할 경우에만 작동한다.<br />
SwiftUI는 뷰 계층 구조의 type structure를 살펴봄으로써 이를 수행한다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601160216465.png" alt="image-20260601160216465" /><br />
SwiftUI가 View를 볼 때 generic type을 본다. <br />
이 경우 if문은 참 거짓 콘텐츠에 대해 ConditionalContent View 뷰로 변환된다. <br />
이러한 번역은 Swift의 result builder 타입의 ViewBuilder에 의해 제공된다.</p>

<p>View 프로토콜은 속성의 논리 문에서 단일 일반 뷰를 구성하는 ViewBuilder의 body 속성을 암시적으로 래핑한다.<br />
body 속성의 some View 반환 유형은 이 정적 복합 유형을 나타내는 자리 표시자로서, 코드를 복잡하게 만들지 않도록 숨겨준다.</p>

<p>이 generic type을 사용해 SwiftUI는 실제 View가 항상 AdoptionDirectory이고 False뷰가 항상 DogList임을 보장하여 암시적으로 안정적인 Id가 할당될 수 있게 한다.<br />
<br /></p>

<h4 id="some-view">some view</h4>
<ul>
  <li>“정확한 타입 이름은 숨길게 하지만 내부적으로는 항상 하나의 구체 타입을 반환해야 해”</li>
  <li>@ViewBuilder가 if else를 _ConditionalContent&lt;TrueView, FalseView&gt;라는 하나의 View 타입으로 감싸주기 때문에 some View가 성립한다.<br />
<br /></li>
</ul>

<h4 id="전략-1-swiftui에서-조건-분기마다-고유한-id를-가진다">전략 1: SwiftUI에서 조건 분기마다 고유한 id를 가진다</h4>
<div class="image-row">
  <img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601161202017.png" alt="image-20260601161202017" />
  <img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601161210464.png" alt="image-20260601161210464" />
</div>
<p>앞서 살펴본 코드이다. 상단 코드에는 각 조건 분기에 대해 서로 다른 뷰를 정의하는 if문이 있다.<br />
SwiftUI의 if문의 각 분기가 고유한 id를 가진 다른 뷰를 나타낸다는 것을 이해하기 때문에 이로 인해 뷰가 전환된다.<br />
<br /></p>

<h4 id="전략-2-identity를-유지하고-유연한-전환을-제공apple-권장">전략 2: Identity를 유지하고 유연한 전환을 제공(Apple 권장)</h4>
<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601161532491.png" alt="image-20260601161532491" /><br />
또는 동일한 뷰에 대해 레이아웃과 색상만 변경하는 단일 뷰를 가질 수도 있다.<br />
다른 상태로 전환되면 뷰는 다음 위치로 부드럽게 미끄러진다. 이는 일관된 id로 단일 뷰를 수정하기 때문이다.<br />
이 두 전략 모두 작동할 수 있자만 Apple는 두 번째 접근 방식을 권장한다.<br />
기본적으로 Identity를 유지하고 유연한 전환을 제공하도록 노력해라고 권장한다.<br />
이유는 뷰의 라이프사이클과 상태를 보존하는 데 도움이 된다고 한다.<br />
<br /></p>

<h4 id="anyview">AnyView</h4>
<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601161958629.png" alt="image-20260601161958629" /><br />
앞서 우리는 AdoptionDirectory와 DogList 사이를 전환하기 위해 이 if문을 작성했었다. SwiftUI가 이 코드를 보면 오른쪽에 제네릭 타입 구조가 보인다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601162030694.png" alt="image-20260601162030694" /><br />
이 코드는 개의 품종을 나타내는 뷰를 얻기 위한 헬퍼 함수다. 함수의 각 조건부 분기는 서로 다른 종류의 View를 반환하므로 Swift에서는 전체 함수에 대해 단일 반환 유형이 필요하므로 이를 모두 AnyView에 래핑한다.</p>

<p>불행이도 이는 SwiftUI가 내 코드의 조건부 구조를 볼 수 없다는 의미이기도 하다. 대신 AnyView를 함수의 반환 유형으로 간주한다. 이는 AnyView가 유형 삭제 래퍼 유형(type-erasing wrapper type) 라고 불리기 때문이다. 즉 generic signature에서 래핑하는 뷰 유형을 숨긴다. 그러나 더 중요한 것은 이 코드가 사람이 읽기에도 어렵다는 것이다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601162731507.png" alt="image-20260601162731507" /><br />
이 오류를 피하려면 View프로토콜이 암시적으로 ViewBuilder를 래핑하기 때문에 뷰의 body 속성은 특별하다는 점을 기억해야 한다.<br />
이는 속성의 논리를 단일 일반 뷰 구조로 변환해줄 수 있다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601162930162.png" alt="image-20260601162930162" /><br />
이제 AnyView가 제거되어 코드의 가독성이 늘어났다. 그리고 결과 타입 시그니처를 보면 조건부 콘텐츠 트리를 사용해 함수의 조건부 논리를 정확히 복제하여 해당 구성 요소의 ID를 제공한다.<br />
<br /></p>

<p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20Demystify%20SwiftUI/image-20260601163052725.png" alt="image-20260601163052725" /><br />
참고로 조건문의 synatic sugar인 switch를 사용해도 동일한 view’s type signature(뷰의 유형 서명)이 정확히 유지된다.<br />
<br /></p>

<h4 id="anyview정리">AnyView정리</h4>
<ul>
  <li>코드에서 유형 정보를 삭제할 수 있다.</li>
  <li>view builder를 활용해 불필요한 AnyView를 제거할 수 있다.</li>
  <li>AnyView는 컴파일러에서 정적 유형 정보를 숨기기 때문에 유용한 진단 오류 및 경고가 코드에 표시되지 않는 경우가 있다.</li>
  <li>필요하지 않을 때 AnyView를 사용하면 성능이 저하될 수 있다.(16:11)</li>
  <li>가능하다면 코드 주변에 AnyView를 전달하는 대신 제네릭을 사용해 정적 유형 정보를 보존해라.</li>
</ul>]]></content><author><name>Dong Hyeon</name></author><category term="Swift" /><category term="WWDC" /><summary type="html"><![CDATA[SwiftUI가 코드를 볼 때 무엇을 보는가? Identity(정체성) - SwiftUI가 앱의 여러 업데이트에서 동일 요소로 보거나 별개 요소로 보는 방법 Lifetime(수명) - SwiftUI가 시간이 지남에 따라 View와 Data 존재를 추적하는 방법 Dependencies(의존성) - SwiftUI가 인터페이스를 업데이트해야 하는 시기와 이유를 이해하는 방법 Identity(정체성) - SwiftUI가 앱의 여러 업데이트에서 동일 요소로 보거나 별개 요소로 보는 방법 사진에서 두 강아지가 다른 강아지인지 혹은 두 장의 같은 강아지인지 알 수 없다. 정보가 충분하지 않아 사실을 말할 수 없기 때문이다. 이 질문을 SwiftUI는 Identity(정체성) 이라고 부른다. 다른 예시로, 아이콘이 서로 완전히 독립 객체인지 알 수 없다. 완전히 다른 객체일 수도 있고 단지 위치만 다르고 다른 색을 가진 동일한 객체일수도 있다. 이러한 구분을 “한 상태에서 다른 상태로 전환하는 방식”을 변경하기 때문에 중요한 부분이다. 만약 서로 같은 객체라면 현 위치에서 다른 위치로 이동하는 동일한 뷰이기 때문에 전환 중에 객체가 아래로 내려가야 한다. 따라서 서로 다른 상태에 걸쳐 뷰를 연결하는 것이 중요하다. 이것이 SwiftUI가 뷰 사이를 전달하는 방법을 이해하는 방식이기 때문이다. 이게 View Identity 핵심 가치다. SwiftUI는 두 가지 다른 유형의 Identity에 초점을 두고 있다. Explicit of Identity(명시적 정체성): 맞춤형 또는 데이터 기반 식별자를 사용한다. Structural Identity(구조적 정체성): 뷰 계층 구조에서의 유형과 위치에 따라 뷰를 구별한다. Explicit of Identity(명시적 정체성) 사물을 식별하기 위해 보통 이름을 부여할 수 있다. 하지만 두 사물이 똑같이 생기고 같은 계열일 가능성이 높을 수 있어서 이름으로는 고유성이 부족할 수 있다. 이름이나 식별자를 할당하는 것은 Explicit of Identity(명시적 정체성) 의 한 형태이다. 이는 강력하고 유연하지만 누군가 어딘가에서 이름을 추적해야 한다. 이미 익숙할 수 있는 Explicit of Identity의 한 형태는 UIKit 및 AppKit 전체에서 사용되는 포인터 Identity다. SwiftUI에서는 포인터 Identity를 사용하지 않고 UIView와 NSView에서 사용한다. UIView와 NSView는 클래스이므로 메모리 할당에 필요한 고유한 포인터를 가진다. 포인터는 Explicit of Identity의 자연스러운 원천이라고 한다. 포인터를 사용해 개별 뷰를 참조할 수 있으며 두 뷰가 동일한 포인터를 공유하는 경우 실제로 동일한 뷰임을 보장할 수 있다. 하지만 SwiftUI View는 일반적으로 클래스 대신 구조체로 표현하는 값 타입이기 때문에 SwiftUI는 포인터를 사용하지 않는다. 가장 중요한 점은 값 타입은 SwiftUI가 해당 View에 대한 지속적인 Identity로 사용할 수 있는 정식 참조가 없다는 것이다. 여기서 사용된 id는 Explicit of Identity의 한 형태이다. 각 강아지의 인식표 Identity는 View를 명시적으로 식별하는 데 사용된다. 강아지 컬렉션이 변경되면 SwiftUI는 해당 Id를 사용해 무엇이 변경되는지 이해하고 목록 내에서 올바른 애니메이션을 생성할 수 있다. 또 다른 예제인데 여기서는 ScrollViewReader를 사용해 하단 버튼을 사용하여 뷰 상단으로 이동한다. .id() Modifier는 사용자 정의 식별자를 사용해 뷰를 명시적으로 식별하는 방법을 제공한다. 현재 페이지 상단의 헤더 뷰에 식별자를 추가한 것이다. 추가로 해당 식별자를 뷰 프록시의 scrollTo(_:) 메서드에 전달해 SwiftUI에게 특정 뷰로 이동하도록 지시할 수 있다. 이 장점은 모든 뷰를 명시적으로 식별할 필요가 없고 헤더와 같이 코드의 다른 곳에서 참조해야 하는 뷰만 식별할 수 있다는 것이다. ScrollViewreader, ScrollView, 텍스트 및 버튼에는 명시적인 식별자가 필요하지 않는다. 그러나 정체성이 명시적이지 않다고 해서 이러한 뷰가 전혀 정체성이 없다는 것을 의미하지는 않는다. 명시적이지 않더라도 모든 View는 Identity가 있기 때문이다. → 여기서 구조적 정체성이 등장 Structural Identity(구조적 정체성) SwiftUI는 뷰 계층 구조를 사용하여 뷰에 대한 암시적 ID를 생성하므로 사용자가 직접 ID를 생성할 필요가 없다. 비슷한 두 마리의 강아지가 있지만 이름은 모르지만 각각을 식별해야 한다고 가정해보자. 움직이지 않음을 보장할 수 있다면 “왼쪽에 있는 개”, “오른쪽에 있는 개”와 같이 위치에 따라 식별할 수 있다. SwiftUI는 이런 객체를 구별하기 위해 상대적인 배열을 사용한다고 한다 이것이 Structural Identity이다. SwiftUI는 API 전체에서 Structural Identity를 활용한다. 전형적인 예시로 View 코드 내에서 if 문과 기타 조건부 논리를 사용하는 경우다. 조건문의 구조는 각 뷰를 식별하는 명확한 방법을 제공한다. 첫 번째 뷰는 조선이 true일때만 표시되고 두번째 뷰는 false일때만 표시된다. 하지만 이 경우 SwiftUI가 볼때 이 뷰가 현재 위치에 유지되고 위치가 바뀌지 않음을 정적으로 보장할 경우에만 작동한다. SwiftUI는 뷰 계층 구조의 type structure를 살펴봄으로써 이를 수행한다. SwiftUI가 View를 볼 때 generic type을 본다. 이 경우 if문은 참 거짓 콘텐츠에 대해 ConditionalContent View 뷰로 변환된다. 이러한 번역은 Swift의 result builder 타입의 ViewBuilder에 의해 제공된다. View 프로토콜은 속성의 논리 문에서 단일 일반 뷰를 구성하는 ViewBuilder의 body 속성을 암시적으로 래핑한다. body 속성의 some View 반환 유형은 이 정적 복합 유형을 나타내는 자리 표시자로서, 코드를 복잡하게 만들지 않도록 숨겨준다. 이 generic type을 사용해 SwiftUI는 실제 View가 항상 AdoptionDirectory이고 False뷰가 항상 DogList임을 보장하여 암시적으로 안정적인 Id가 할당될 수 있게 한다. some view “정확한 타입 이름은 숨길게 하지만 내부적으로는 항상 하나의 구체 타입을 반환해야 해” @ViewBuilder가 if else를 _ConditionalContent&lt;TrueView, FalseView&gt;라는 하나의 View 타입으로 감싸주기 때문에 some View가 성립한다. 전략 1: SwiftUI에서 조건 분기마다 고유한 id를 가진다 앞서 살펴본 코드이다. 상단 코드에는 각 조건 분기에 대해 서로 다른 뷰를 정의하는 if문이 있다. SwiftUI의 if문의 각 분기가 고유한 id를 가진 다른 뷰를 나타낸다는 것을 이해하기 때문에 이로 인해 뷰가 전환된다. 전략 2: Identity를 유지하고 유연한 전환을 제공(Apple 권장) 또는 동일한 뷰에 대해 레이아웃과 색상만 변경하는 단일 뷰를 가질 수도 있다. 다른 상태로 전환되면 뷰는 다음 위치로 부드럽게 미끄러진다. 이는 일관된 id로 단일 뷰를 수정하기 때문이다. 이 두 전략 모두 작동할 수 있자만 Apple는 두 번째 접근 방식을 권장한다. 기본적으로 Identity를 유지하고 유연한 전환을 제공하도록 노력해라고 권장한다. 이유는 뷰의 라이프사이클과 상태를 보존하는 데 도움이 된다고 한다. AnyView 앞서 우리는 AdoptionDirectory와 DogList 사이를 전환하기 위해 이 if문을 작성했었다. SwiftUI가 이 코드를 보면 오른쪽에 제네릭 타입 구조가 보인다. 이 코드는 개의 품종을 나타내는 뷰를 얻기 위한 헬퍼 함수다. 함수의 각 조건부 분기는 서로 다른 종류의 View를 반환하므로 Swift에서는 전체 함수에 대해 단일 반환 유형이 필요하므로 이를 모두 AnyView에 래핑한다. 불행이도 이는 SwiftUI가 내 코드의 조건부 구조를 볼 수 없다는 의미이기도 하다. 대신 AnyView를 함수의 반환 유형으로 간주한다. 이는 AnyView가 유형 삭제 래퍼 유형(type-erasing wrapper type) 라고 불리기 때문이다. 즉 generic signature에서 래핑하는 뷰 유형을 숨긴다. 그러나 더 중요한 것은 이 코드가 사람이 읽기에도 어렵다는 것이다. 이 오류를 피하려면 View프로토콜이 암시적으로 ViewBuilder를 래핑하기 때문에 뷰의 body 속성은 특별하다는 점을 기억해야 한다. 이는 속성의 논리를 단일 일반 뷰 구조로 변환해줄 수 있다. 이제 AnyView가 제거되어 코드의 가독성이 늘어났다. 그리고 결과 타입 시그니처를 보면 조건부 콘텐츠 트리를 사용해 함수의 조건부 논리를 정확히 복제하여 해당 구성 요소의 ID를 제공한다. 참고로 조건문의 synatic sugar인 switch를 사용해도 동일한 view’s type signature(뷰의 유형 서명)이 정확히 유지된다. AnyView정리 코드에서 유형 정보를 삭제할 수 있다. view builder를 활용해 불필요한 AnyView를 제거할 수 있다. AnyView는 컴파일러에서 정적 유형 정보를 숨기기 때문에 유용한 진단 오류 및 경고가 코드에 표시되지 않는 경우가 있다. 필요하지 않을 때 AnyView를 사용하면 성능이 저하될 수 있다.(16:11) 가능하다면 코드 주변에 AnyView를 전달하는 대신 제네릭을 사용해 정적 유형 정보를 보존해라.]]></summary></entry><entry><title type="html">[SwiftUI] Diffing, 끝까지 파고들었습니다.</title><link href="https://indextrown.github.io/SwiftUI-diff/" rel="alternate" type="text/html" title="[SwiftUI] Diffing, 끝까지 파고들었습니다." /><published>2026-06-01T00:00:00+00:00</published><updated>2026-06-01T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BSwiftUI%5D%20diff</id><content type="html" xml:base="https://indextrown.github.io/SwiftUI-diff/"><![CDATA[<!-- `Text("Hello World")`, `Text(isOn ? "On" : "Off")`, `Button` -->

<!-- <img src="/%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B2%BD%EB%A1%9C" alt="이미지" width="30%"> -->

<!-- <img src="https://github.com/user-attachments/assets/3938e583-0fc4-4620-99b0-bf761e60a1ba" width="60%" align="left"> -->

<h2 id="identity">Identity</h2>
<p><a href="https://developer.apple.com/videos/play/wwdc2021/10022/">WWDC21 - Demystify SwiftUI</a> 에서 Identity를 두 경우로 나누어서 설명한다.</p>
<ul>
  <li>explicit identity (명시적 정체성)</li>
  <li>structual identity (구조적 정체성)</li>
</ul>

<p>각각 간략히 말하자면</p>
<ul>
  <li>explicit identity는 사용자 정의 또는 데이터 기반 identity를 사용하는 것이고</li>
  <li>structual identity은 View hierachy에서 타입 및 위치에 따라 뷰를 구분하는 것이다.</li>
</ul>

<p><strong>참고</strong>: 애플은 View의 프로퍼티를 뷰의 종속성(dependencies)이라고 지칭한다.</p>

<details class="notion-toggle-list">
  <summary>body 재호출 디버깅을 위한 랜덤 배경색 부여 헬퍼 메서드</summary>

  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">extension</span> <span class="kt">ShapeStyle</span> <span class="k">where</span> <span class="k">Self</span> <span class="o">==</span> <span class="kt">Color</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">random</span><span class="p">:</span> <span class="kt">Color</span> <span class="p">{</span>
        <span class="kt">Color</span><span class="p">(</span>
            <span class="nv">red</span><span class="p">:</span> <span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="mi">1</span><span class="p">),</span>
            <span class="nv">green</span><span class="p">:</span> <span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="mi">1</span><span class="p">),</span>
            <span class="nv">blue</span><span class="p">:</span> <span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="mi">1</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">extension</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">randomColorStyle</span><span class="p">(</span>
        <span class="nv">cornerRadius</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">16</span><span class="p">,</span>
        <span class="nv">padding</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">12</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">self</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="n">padding</span><span class="p">)</span>
            <span class="o">.</span><span class="n">background</span> <span class="p">{</span>
                <span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="n">cornerRadius</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">continuous</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">random</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.35</span><span class="p">))</span>
                    <span class="o">.</span><span class="n">overlay</span> <span class="p">{</span>
                        <span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="n">cornerRadius</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">continuous</span><span class="p">)</span>
                            <span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="o">.</span><span class="n">ultraThinMaterial</span><span class="p">)</span>
                    <span class="p">}</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="n">overlay</span> <span class="p">{</span>
                <span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="n">cornerRadius</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">continuous</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">white</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.35</span><span class="p">),</span> <span class="nv">lineWidth</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">shadow</span><span class="p">(</span><span class="nv">color</span><span class="p">:</span> <span class="o">.</span><span class="n">black</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.12</span><span class="p">),</span> <span class="nv">radius</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">6</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

</details>

<h2 id="1-state-프로퍼티가-변경되면-state를-소유한-view의-body는-재호출된다">1. @State 프로퍼티가 변경되면 @State를 소유한 View의 body는 재호출된다.</h2>
<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SampleDiffView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isOn</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>

            <span class="kt">Text</span><span class="p">(</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
            
            <span class="kt">Button</span> <span class="p">{</span>
                <span class="n">isOn</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
            <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">Text</span><span class="p">(</span><span class="s">"버튼"</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-01%20at%2017.26.13.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-01 at 17.26.13" /></p>

</div>
<p><code class="language-plaintext highlighter-rouge">이 예제의 목적은 body가 재호출 될 때 랜덤 색상을 부여하는 것이다. diffing 이전의 단계임을 인지해야 한다.</code><br />
이 예제에서는 SampleDiffView.body 안에서 isOn을 읽고 있다.<br />
따라서 버튼을 눌러 isOn이 변경되면 SampleDiffView의 body가 다시 평가된다.<br />
이때 VStack 내부의 subview들이 모두 같은 identity를 갖는 것은 아니다.<br />
<br /></p>

<h4 id="subview들은-structural-identity를-가진다">SubView들은 Structural Identity를 가진다.</h4>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span>        <span class="c1">// child 0</span>
    <span class="kt">Text</span><span class="p">(</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span>  <span class="c1">// child 1</span>
    <span class="kt">Button</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>             <span class="c1">// child 2</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Text("Hello World")</code>, <code class="language-plaintext highlighter-rouge">Text(isOn ? "On" : "Off")</code>, <code class="language-plaintext highlighter-rouge">Button</code>은 VStack 안에서 서로 다른 위치를 가지므로 각각 다른 Structural Identity를 가진다. 또한 두 번째 Text는 isOn 값에 따라 on/off를 보여주지만, 뷰의 위치와 타입은 그대로 유지된다. 즉, “On” 상태의 두 번째 Text와 “Off” 상태의 두 번째 Text는 같은 Structural Identity를 유지한다. 바뀌는 것은 identity가 아니라 Text의 입력 값이다.</p>

<!-- 그런데 화면에서는 isOn과 직접 관련 없어 보이는 Text("Hello World")의 배경색도 함께 바뀐다. 이는 Text("Hello World")가 같은 identity를 가져서가 아니라, SampleDiffView.body가 다시 평가되면서 .randomColorStyle() 내부의 Color.random도 다시 실행되기 때문이다.

정리하면 다음과 같다.
- isOn 변경으로 SampleDiffView.body가 다시 평가된다.
- VStack 내부의 각 subview는 서로 다른 structural identity를 가진다.
- 두 번째 Text는 "On"과 "Off" 사이에서 같은 structural identity를 유지한다.
- Text("Hello World")의 배경색이 바뀌는 이유는 identity 때문이 아니라, body 재평가 과정에서 랜덤 색상이 다시 생성되기 때문이다. -->

<h2 id="2-extract-subview-view의-일부-ui를-새로운-하위-view로-만들면-dependency을-읽지-않는-자식-body호출을-방지할-수-있다">2. Extract SubView: View의 일부 UI를 새로운 하위 View로 만들면 dependency을 읽지 않는 자식 body호출을 방지할 수 있다.</h2>
<div class="code-media-row">

  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SampleDiffView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isOn</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">ExtractSubView</span><span class="p">()</span>
            
            <span class="kt">Text</span><span class="p">(</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
            
            <span class="kt">Button</span> <span class="p">{</span>
                <span class="n">isOn</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
            <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">Text</span><span class="p">(</span><span class="s">"버튼"</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ExtractSubView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="c1">// no dependency</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-02%20at%2013.41.22.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-02 at 13.41.22" /></p>

</div>

<p><code class="language-plaintext highlighter-rouge">ExtractSubView는 isOn을 직접 읽지 않는 독립적인 View이다.</code><br />
버튼을 누르면 SampleDiffView의 @State인 isOn이 변경되므로 SampleDiffView.body는 다시 평가된다.<br />
하지만 ExtractSubView는 VStack 안에서 같은 위치에 같은 타입으로 유지되고, 내부에서 변경된 dependency(@State)를 읽지 않는다.</p>

<h4 id="부연-설명">부연 설명</h4>
<p>SwiftUI 입장에서는 ExtractSubView 안에서 @State 같은 dependency 값이 변하지 않았고, 이 예시에서는 애초에 그런 dependency를 가지고 있지도 않기 때문에 ExtractSubView.body를 다시 평가할 필요가 없다.<br />
참고로 어떤 View의 body가 다시 평가되면, 그 이후 diffing 단계에서 SwiftUI는 identity 변화를 확인해 이전 View를 재사용할지 새 View로 취급할지 판단하고, 실제 화면 업데이트가 필요한 범위를 결정한다.<br />
<code class="language-plaintext highlighter-rouge">핵심은 렌더링 단계가 가장 무겁지만, 위와 같은 구조에서는 개발자가 body 재평가 범위를 직접 줄여 불필요한 렌더링으로 이어지는 일을 막을 수 있다는 것이다.</code></p>

<h2 id="3-id로-view의-identity가-바뀌면-subview도-다시-평가된다">3. .id()로 View의 identity가 바뀌면 SubView도 다시 평가된다.</h2>
<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SampleDiffView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isOn</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">ExtractSubView</span><span class="p">()</span><span class="o">.</span><span class="nf">id</span><span class="p">(</span><span class="kt">UUID</span><span class="p">())</span> <span class="c1">// id</span>

            <span class="kt">Text</span><span class="p">(</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
            
            <span class="kt">Button</span> <span class="p">{</span>
                <span class="n">isOn</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
            <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">Text</span><span class="p">(</span><span class="s">"버튼"</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ExtractSubView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-01%20at%2017.26.13.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-01 at 17.26.13" /></p>
</div>
<p><a href="https://developer.apple.com/documentation/swiftui/view/id(_:)/">공식문서</a> 에서 id가 변경되면 View의 Identity 가 변경된다고 한다.<br />
버튼을 누르면 SampleDiffView의 @State인 isOn이 변경되므로 SampleDiffView.body가 다시 평가된다.<br />
이때 ExtractSubView().id(UUID())도 다시 만들어지는데, UUID()는 매번 새로운 값을 만들기 때문에 ExtractSubView의 명시적 identity도 매번 달라진다.</p>

<p>즉 2번 예시에서는 ExtractSubView가 같은 위치에 같은 타입으로 유지되어 재사용될 수 있었지만, 3번 예시에서는 .id(UUID()) 때문에 SwiftUI가 이전 ExtractSubView와 현재 ExtractSubView를 같은 View로 보지 않는다. 그래서 ExtractSubView.body도 다시 평가되고, 실제 렌더링도 새로 일어나는 것처럼 보인다.</p>

<p>Self._printChanges()를 찍어보면 다음처럼 나올 수 있다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">SampleDiffView</span><span class="p">:</span> <span class="kd">@self</span><span class="p">,</span> <span class="kd">@identity</span><span class="p">,</span> <span class="n">_isOn</span> <span class="n">changed</span><span class="o">.</span> <span class="c1">// 뷰 초기화</span>
<span class="kt">SampleDiffView</span><span class="p">:</span> <span class="n">_isOn</span> <span class="n">changed</span><span class="o">.</span>                   <span class="c1">// 버튼 클릭</span>
<span class="kt">SampleDiffView</span><span class="p">:</span> <span class="n">_isOn</span> <span class="n">changed</span><span class="o">.</span>                   <span class="c1">// 버튼 클릭</span>
</code></pre></div></div>

<p>여기서 _isOn changed는 @State 값인 isOn이 변경되어 SampleDiffView.body가 다시 평가됐다는 의미이다.<br />
@self는 SampleDiffView라는 View 값 자체가 새로 만들어졌다는 의미이고, @identity는 SwiftUI가 SampleDiffView의 identity 변화를 감지했거나 identity 기준으로 View를 다시 연결했다는 의미로 볼 수 있다.<br />
첫 출력은 SampleDiffView가 처음 구성되거나 identity 변화가 함께 감지된 상황이고, 이후 출력은 identity는 유지된 채 _isOn 변경만으로 SampleDiffView.body가 다시 평가된 상황이다.</p>

<h2 id="4-extract-subview-body에서-실제로-읽는-dependency가-바뀐-view만-body가-다시-호출된다">4. Extract SubView: body에서 실제로 읽는 dependency가 바뀐 View만 body가 다시 호출된다.</h2>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 버튼 1번 클릭
ExtractLightView: _isOn changed.

// 버튼 1번 클릭
ExtractLightView: _isOn changed.
</code></pre></div></div>
<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SampleDiffView5</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isOn</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">ExtractTitleView</span><span class="p">()</span>

            <span class="kt">ExtractLightView</span><span class="p">(</span><span class="nv">isOn</span><span class="p">:</span> <span class="n">$isOn</span><span class="p">)</span>
            
            <span class="kt">ExtractSubButton</span><span class="p">(</span><span class="nv">isOn</span><span class="p">:</span> <span class="n">$isOn</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ExtractTitleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="c1">// no dependency</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ExtractLightView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@Binding</span> <span class="k">var</span> <span class="nv">isOn</span><span class="p">:</span> <span class="kt">Bool</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ExtractSubButton</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@Binding</span> <span class="k">var</span> <span class="nv">isOn</span><span class="p">:</span> <span class="kt">Bool</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Button</span> <span class="p">{</span>
            <span class="n">isOn</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
        <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"버튼"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2014.32.04.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 14.32.04" /></p>
</div>

<ul>
  <li>ExtractTitleView는 변경되는 dependency를 읽지 않기 때문에 body가 다시 호출되지 않는다.</li>
  <li>ExtractLightView는 @Binding var isOn: Bool을 가지고 있고, Text(isOn ? “On” : “Off”)에서 isOn을 직접 읽는다. 따라서 isOn이 변경될 때마다 body가 다시 호출된다.</li>
  <li>ExtractSubButton도 @Binding var isOn: Bool을 가지고 있지만, 버튼을 누를 때 실행되는 action 클로저 안에서만 isOn.toggle()을 호출한다. 이 값은 화면을 그리는 body 안에서 직접 읽히는 값이 아니므로, isOn이 변경되어도 ExtractSubButton.body는 다시 호출되지 않는다.</li>
</ul>

<p>여기서 중요한 점은 @Binding으로 값을 전달받았다는 사실만으로 body가 다시 호출되는 것은 아니라는 점이다.<br />
SwiftUI에서 body가 다시 호출되는 기준은, body 안에서 해당 상태가 실제로 읽혔는지 여부에 가깝다.<br />
ExtractLightView는 Text(isOn ? “On” : “Off”)를 통해 isOn 값을 화면에 반영한다. 그래서 isOn이 바뀌면 화면 결과도 달라질 수 있고, SwiftUI는 ExtractLightView.body를 다시 호출한다.<br />
반면 ExtractSubButton은 isOn을 버튼의 action 클로저 안에서만 사용한다. action 클로저는 body가 화면을 구성하는 시점이 아니라, 사용자가 버튼을 탭한 이벤트 시점에 실행된다. 따라서 ExtractSubButton의 화면 결과는 현재 isOn 값에 직접 의존하지 않는다.</p>

<p>정리하면, @Binding 기반 예시에서는 상태를 “전달받는 것”과 상태를 “렌더링에 사용하는 것”이 다르다. SwiftUI에서 body 재호출의 기준은 단순한 상태 전달 여부가 아니라, body 안에서 해당 상태가 실제로 읽혀 화면 결과에 영향을 주는지에 가깝다.</p>

<h2 id="5-observedobject는-객체-변경-알림을-구독하기-때문에-body-호출-범위가-달라질-수-있다">5. @ObservedObject는 객체 변경 알림을 구독하기 때문에 body 호출 범위가 달라질 수 있다.</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 버튼 1번 클릭
SampleObservedObjectDiffView: _viewModel changed.
ObservedLightView: _viewModel changed.
ObservedSubButton: _viewModel changed.

// 버튼 1번 클릭
SampleObservedObjectDiffView: _viewModel changed.
ObservedLightView: _viewModel changed.
ObservedSubButton: _viewModel changed.
</code></pre></div></div>

<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">LightViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
    <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">isOn</span> <span class="o">=</span> <span class="kc">true</span>

    <span class="kd">func</span> <span class="nf">toggle</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">isOn</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">SampleObservedObjectDiffView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@StateObject</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">viewModel</span> <span class="o">=</span> <span class="kt">LightViewModel</span><span class="p">()</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">ObservedTitleView</span><span class="p">()</span>
    
            <span class="kt">ObservedLightView</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">)</span>
    
            <span class="kt">ObservedSubButton</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ObservedTitleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="c1">// no dependency</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ObservedLightView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">LightViewModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ObservedSubButton</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">LightViewModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Button</span> <span class="p">{</span>
            <span class="n">viewModel</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
        <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"버튼"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2014.47.48.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 14.47.48" /></p>
</div>

<p>4번 예시에서는 @Binding var isOn: Bool을 가지고 있더라도, body 안에서 isOn을 직접 읽지 않는 ExtractSubButton.body는 다시 호출되지 않았다.</p>

<p>하지만 @ObservedObject는 동작 방식이 조금 다르다. ObservedLightView와 ObservedSubButton은 모두 같은 LightViewModel을 @ObservedObject로 관찰한다. viewModel.isOn이 변경되면 LightViewModel.objectWillChange가 발생하고, 이 객체를 관찰 중인 View들은 변경 알림을 받는다.</p>

<p>그래서 ObservedLightView처럼 body 안에서 viewModel.isOn을 직접 읽는 View는 당연히 body가 다시 호출된다. 그리고 ObservedSubButton처럼 viewModel을 버튼 action 클로저 안에서만 사용하더라도, @ObservedObject로 객체를 관찰하고 있기 때문에 viewModel의 변경 알림에 의해 body가 다시 호출될 수 있다.</p>

<p>반면 ObservedTitleView는 LightViewModel을 전달받지도 않고, 어떤 변경되는 dependency도 읽지 않는다. 그래서 버튼을 눌러 isOn이 변경되어도 ObservedTitleView.body는 다시 호출되지 않는다.</p>

<p>정리하면, @Binding은 값이 body 안에서 실제로 읽히는지에 따라 body 재호출 여부가 더 직접적으로 갈리는 반면, @ObservedObject는 객체의 objectWillChange를 구독한다. 따라서 같은 객체를 관찰하는 View라면, 특정 프로퍼티를 body 안에서 직접 읽지 않더라도 객체 변경 알림으로 인해 body가 다시 호출될 수 있다.</p>

<h2 id="6-published-프로퍼티가-여러-개일-때-하나만-바뀌어도-같은-객체를-관찰하는-view의-body가-다시-호출될-수-있다">6. @Published 프로퍼티가 여러 개일 때 하나만 바뀌어도 같은 객체를 관찰하는 View의 body가 다시 호출될 수 있다.</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// count 버튼 1번 클릭
SampleMultiPublishedDiffView: _viewModel changed.
PublishedCountView: _viewModel changed.
PublishedTitleView: _viewModel changed.
PublishedSubButton: _viewModel changed.
</code></pre></div></div>

<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">MultiPublishedViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
    <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">title</span> <span class="o">=</span> <span class="s">"Hello World"</span>

    <span class="kd">func</span> <span class="nf">increaseCount</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">changeTitle</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">title</span> <span class="o">=</span> <span class="p">[</span><span class="s">"SwiftUI"</span><span class="p">,</span> <span class="s">"Diffing"</span><span class="p">,</span> <span class="s">"Identity"</span><span class="p">]</span><span class="o">.</span><span class="nf">randomElement</span><span class="p">()</span> <span class="p">??</span> <span class="s">"SwiftUI"</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">SampleMultiPublishedDiffView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@StateObject</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">viewModel</span> <span class="o">=</span> <span class="kt">MultiPublishedViewModel</span><span class="p">()</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">PublishedCountView</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">)</span>
    
            <span class="kt">PublishedTitleView</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">)</span>
    
            <span class="kt">PublishedSubButton</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">PublishedCountView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">MultiPublishedViewModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"count: </span><span class="se">\(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">count</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">PublishedTitleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">MultiPublishedViewModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">PublishedSubButton</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">MultiPublishedViewModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Button</span> <span class="p">{</span>
            <span class="n">viewModel</span><span class="o">.</span><span class="nf">increaseCount</span><span class="p">()</span>
        <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"count 증가"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2014.55.42.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 14.55.42" /></p>
</div>

<p>이 예제에서 count 버튼을 누르면 실제로 변경되는 값은 count뿐이다. PublishedTitleView가 읽는 title은 바뀌지 않는다.</p>

<p>하지만 count와 title은 같은 ObservableObject 안에 들어 있다. @Published var count가 변경되면 MultiPublishedViewModel.objectWillChange가 발생하고, 이 객체를 관찰 중인 View들은 변경 알림을 받는다.</p>

<p>그래서 PublishedTitleView가 title만 읽고 있더라도, 같은 viewModel을 @ObservedObject로 관찰하고 있다면 count 변경 시 body가 다시 호출될 수 있다.</p>

<p>정리하면, ObservableObject는 기본적으로 프로퍼티 단위가 아니라 객체 단위로 변경 알림을 보낸다. 여러 @Published 값을 하나의 ObservableObject에 모아두면 편하지만, 변경 범위가 넓어질 수 있다.</p>

<h2 id="7-foreach에서-identity가-불안정하면-하위-view의-state가-유지되지-않을-수-있다">7. ForEach에서 identity가 불안정하면 하위 View의 State가 유지되지 않을 수 있다.</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 부모 업데이트 버튼 1번 클릭
ForEachIdentitySampleView: _tick changed.
UnstableIdentityRow: @self, @identity, _color changed.
UnstableIdentityRow: @self, @identity, _color changed.
UnstableIdentityRow: @self, @identity, _color changed.
</code></pre></div></div>

<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">DiffItem</span><span class="p">:</span> <span class="kt">Identifiable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">id</span> <span class="o">=</span> <span class="kt">UUID</span><span class="p">()</span>
    <span class="k">let</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ForEachIdentitySampleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">tick</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">items</span> <span class="o">=</span> <span class="p">[</span>
        <span class="kt">DiffItem</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"First"</span><span class="p">),</span>
        <span class="kt">DiffItem</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Second"</span><span class="p">),</span>
        <span class="kt">DiffItem</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Third"</span><span class="p">)</span>
    <span class="p">]</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"tick: </span><span class="se">\(</span><span class="n">tick</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">Button</span><span class="p">(</span><span class="s">"부모 업데이트"</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">tick</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">ForEach</span><span class="p">(</span><span class="n">items</span><span class="p">)</span> <span class="p">{</span> <span class="n">item</span> <span class="k">in</span>
                <span class="kt">StableIdentityRow</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="n">item</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
            <span class="p">}</span>
    
            <span class="kt">ForEach</span><span class="p">(</span><span class="n">items</span><span class="p">)</span> <span class="p">{</span> <span class="n">item</span> <span class="k">in</span>
                <span class="kt">UnstableIdentityRow</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="n">item</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">id</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">item</span><span class="o">.</span><span class="n">id</span><span class="se">)</span><span class="s">-</span><span class="se">\(</span><span class="n">tick</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">StableIdentityRow</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">color</span> <span class="o">=</span> <span class="kt">Color</span><span class="o">.</span><span class="n">random</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"stable: </span><span class="se">\(</span><span class="n">title</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="n">color</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.35</span><span class="p">))</span>
            <span class="o">.</span><span class="nf">clipShape</span><span class="p">(</span><span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">continuous</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">UnstableIdentityRow</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">color</span> <span class="o">=</span> <span class="kt">Color</span><span class="o">.</span><span class="n">random</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"unstable: </span><span class="se">\(</span><span class="n">title</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="n">color</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.35</span><span class="p">))</span>
            <span class="o">.</span><span class="nf">clipShape</span><span class="p">(</span><span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">continuous</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2015.05.06.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 15.05.06" /></p>
</div>

<p>ForEach는 각 row를 구분하기 위해 identity를 사용한다. items가 안정적인 id를 가지고 있으면 SwiftUI는 이전 row와 현재 row를 같은 View로 연결할 수 있다.</p>

<p>이 예제에서는 row 내부에 @State private var color = Color.random을 두었다. row의 identity가 유지되면 이 @State 값도 유지되기 때문에 부모 View의 tick이 바뀌어도 stable row의 배경색은 유지된다.</p>

<p>반면 UnstableIdentityRow에는 .id(“(item.id)-(tick)”)를 붙였다. tick이 바뀔 때마다 row의 identity도 바뀌므로 SwiftUI는 이전 row와 현재 row를 같은 View로 연결하지 못한다. 이때 row의 로컬 @State도 새로 만들어질 수 있고, color 값이 다시 초기화되면서 배경색이 바뀐다.</p>

<p>정리하면, ForEach에서는 body 호출 여부만 보는 것보다 identity가 로컬 State를 유지시키는지 확인하는 편이 더 명확하다. identity가 안정적이면 row의 State가 유지되고, identity가 흔들리면 SwiftUI가 이전 row를 이어받지 못해 State가 초기화될 수 있다.</p>

<h2 id="8-equatable을-사용하지-않아도-입력값이-그대로라면-하위-view의-body가-다시-호출되지-않을-수-있다">8. Equatable을 사용하지 않아도 입력값이 그대로라면 하위 View의 body가 다시 호출되지 않을 수 있다.</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// unrelated 버튼 1번 클릭
NonEquatableSampleView: _unrelated changed.

// count 버튼 1번 클릭
NonEquatableSampleView: _count changed.
PlainCounterView: _count changed.
</code></pre></div></div>

<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">NonEquatableSampleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">unrelated</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"unrelated: </span><span class="se">\(</span><span class="n">unrelated</span><span class="o">.</span><span class="n">description</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">PlainCounterView</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="n">count</span><span class="p">)</span>
    
            <span class="kt">Button</span><span class="p">(</span><span class="s">"unrelated 변경"</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">unrelated</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">Button</span><span class="p">(</span><span class="s">"count 증가"</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">PlainCounterView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"count: </span><span class="se">\(</span><span class="n">count</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2015.16.21.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 15.16.21" /></p>
</div>

<p>먼저 .equatable()을 전혀 사용하지 않는 경우를 보자. unrelated가 바뀌면 NonEquatableSampleView.body는 다시 호출된다. 부모가 다시 호출되었기 때문에 body 안의 PlainCounterView(count: count) 코드도 다시 실행된다.</p>

<p>그런데 count 값이 그대로라면 PlainCounterView가 실제로 다시 계산되거나 갱신될 필요는 없다. SwiftUI는 이런 단순한 입력값에 대해 이미 이전 View와 현재 View를 비교하고, 결과가 같다고 판단하면 하위 View의 body 호출이나 갱신을 줄일 수 있다.</p>

<p>그래서 이 예제만 보면 .equatable()을 붙이지 않아도 PlainCounterView.body가 다시 호출되지 않는 것처럼 보일 수 있다. 이 지점 때문에 .equatable()을 “body 호출을 막는 마법”처럼 설명하면 오히려 헷갈린다.</p>

<p>정리하면, Equatable을 직접 사용하지 않아도 SwiftUI가 입력값이 변하지 않은 하위 View를 다시 계산하지 않는 경우가 있다. 따라서 단순한 count 전달 예제만으로는 .equatable()의 차이가 선명하게 드러나지 않을 수 있다.</p>

<h2 id="8-1-equatable은-하위-view를-비교할-때-equatable-기준을-사용하라고-명시하는-방법이다">8-1. .equatable()은 하위 View를 비교할 때 Equatable 기준을 사용하라고 명시하는 방법이다.</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// unrelated 버튼 1번 클릭
EquatableValueSampleView: _unrelated changed.

// count 버튼 1번 클릭
EquatableValueSampleView: _count changed.
EquatableValueCounterView: @self changed.
</code></pre></div></div>

<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">EquatableValueSampleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">unrelated</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"unrelated: </span><span class="se">\(</span><span class="n">unrelated</span><span class="o">.</span><span class="n">description</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">EquatableValueCounterView</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="n">count</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">equatable</span><span class="p">()</span>
    
            <span class="kt">Button</span><span class="p">(</span><span class="s">"unrelated 변경"</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">unrelated</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">Button</span><span class="p">(</span><span class="s">"count 증가"</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">EquatableValueCounterView</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">Equatable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"count: </span><span class="se">\(</span><span class="n">count</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2015.21.04.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 15.21.04" /></p>
</div>

<p>.equatable()을 붙이면 SwiftUI는 EquatableValueCounterView를 비교할 때 View가 가진 Equatable 기준을 사용할 수 있다. 이 예제에서는 EquatableValueCounterView가 count만 가지고 있으므로, Swift가 자동으로 만들어주는 Equatable 비교도 결국 count 비교가 된다.</p>

<p>unrelated만 바뀌면 부모인 EquatableValueSampleView.body는 다시 호출된다. 하지만 EquatableValueCounterView에 전달되는 count는 그대로다. 이 경우 SwiftUI는 이전 EquatableValueCounterView와 현재 EquatableValueCounterView가 같다고 판단할 수 있고, 하위 View의 body 호출이나 갱신을 줄일 수 있다.</p>

<p>반대로 count가 바뀌면 EquatableValueCounterView의 입력값 자체가 달라진다. 비교 결과가 달라졌기 때문에 EquatableValueCounterView.body는 다시 호출된다.</p>

<p>다만 이 예제도 8번과 결과가 비슷하게 보일 수 있다. count처럼 단순한 값 하나만 넘기는 경우에는 SwiftUI가 .equatable() 없이도 비슷하게 최적화할 수 있기 때문이다. 그래서 .equatable()의 의미는 “항상 새로운 최적화를 켠다”라기보다, 하위 View 비교에 Equatable 기준을 사용하겠다고 명시하는 쪽에 가깝다.</p>

<h2 id="8-2-클로저처럼-비교하기-어려운-값이-섞이면-을-직접-구현해-비교-기준에서-제외할-수-있다">8-2. 클로저처럼 비교하기 어려운 값이 섞이면 ==을 직접 구현해 비교 기준에서 제외할 수 있다.</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// unrelated 버튼 1번 클릭
EquatableClosureSampleView: _unrelated changed.

// EquatableClosureCounterView 버튼 1번 클릭
EquatableClosureSampleView: _unrelated changed.

// count 버튼 1번 클릭
EquatableClosureSampleView: _count changed.
EquatableClosureCounterView: @self changed.
</code></pre></div></div>

<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">EquatableClosureSampleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">unrelated</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"unrelated: </span><span class="se">\(</span><span class="n">unrelated</span><span class="o">.</span><span class="n">description</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">EquatableClosureCounterView</span><span class="p">(</span>
                <span class="nv">count</span><span class="p">:</span> <span class="n">count</span><span class="p">,</span>
                <span class="nv">action</span><span class="p">:</span> <span class="p">{</span>
                    <span class="n">unrelated</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">)</span>
                <span class="o">.</span><span class="nf">equatable</span><span class="p">()</span>
    
            <span class="kt">Button</span><span class="p">(</span><span class="s">"unrelated 변경"</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">unrelated</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    
            <span class="kt">Button</span><span class="p">(</span><span class="s">"count 증가"</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">EquatableClosureCounterView</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">Equatable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span>
    <span class="k">let</span> <span class="nv">action</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span>

    <span class="kd">static</span> <span class="kd">func</span> <span class="o">==</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="kt">EquatableClosureCounterView</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">EquatableClosureCounterView</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
        <span class="n">lhs</span><span class="o">.</span><span class="n">count</span> <span class="o">==</span> <span class="n">rhs</span><span class="o">.</span><span class="n">count</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Button</span> <span class="p">{</span>
            <span class="nf">action</span><span class="p">()</span>
        <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"count: </span><span class="se">\(</span><span class="n">count</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2015.27.05.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 15.27.05" /></p>
</div>

<p>이번에는 EquatableClosureCounterView가 count뿐 아니라 action 클로저도 함께 받는다. 클로저는 일반적인 값처럼 Equatable로 비교할 수 없다. 그래서 이 상태에서는 Swift가 Equatable 구현을 자동으로 만들어줄 수 없고, 직접 ==을 구현해야 한다.</p>

<p>여기서 action 클로저는 버튼을 눌렀을 때 실행될 동작이다. 화면에 표시되는 값은 Text(“count: (count)”)이고, 현재 예제에서 View의 시각적 결과를 결정하는 핵심 입력값은 count다.</p>

<p>그래서 == 구현에서 action은 비교하지 않고 count만 비교하도록 했다. unrelated가 바뀌면 부모 body가 다시 호출되고, action 클로저도 새로 만들어질 수 있다. 하지만 count가 같다면 EquatableClosureCounterView의 화면 결과는 같다고 볼 수 있다.</p>

<p>반대로 count가 바뀌면 ==의 비교 결과가 달라진다. 이때는 EquatableClosureCounterView가 다시 계산되어야 하므로 body가 다시 호출된다.</p>

<p>정리하면, .equatable()은 부모 body 호출을 막는 기능이 아니다. 더 정확히는 하위 View를 비교할 때 어떤 값을 기준으로 같다고 볼지 알려주는 장치에 가깝다. 특히 클로저처럼 비교하기 어렵거나, 화면 결과에 직접 영향을 주지 않는 값을 비교 대상에서 제외하고 싶을 때 직접 ==을 구현하는 방식이 의미가 있다.</p>

<h2 id="9-observable은-body에서-실제로-접근한-프로퍼티를-기준으로-body-호출-범위를-좁힐-수-있다">9. @Observable은 body에서 실제로 접근한 프로퍼티를 기준으로 body 호출 범위를 좁힐 수 있다.</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 버튼 1번 클릭
ObservableMacroLightView: _model changed.

// 버튼 1번 클릭
ObservableMacroLightView: _model changed.
</code></pre></div></div>

<div class="code-media-row">
  <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Observation</span>
<span class="kd">import</span> <span class="kt">SwiftUI</span>

<span class="kd">@Observable</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="kt">ObservableLightModel</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">isOn</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="k">var</span> <span class="nv">title</span> <span class="o">=</span> <span class="s">"Hello World"</span>

    <span class="kd">func</span> <span class="nf">toggle</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">isOn</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">SampleObservableMacroDiffView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">model</span> <span class="o">=</span> <span class="kt">ObservableLightModel</span><span class="p">()</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">ObservableMacroTitleView</span><span class="p">(</span><span class="nv">model</span><span class="p">:</span> <span class="n">model</span><span class="p">)</span>
    
            <span class="kt">ObservableMacroLightView</span><span class="p">(</span><span class="nv">model</span><span class="p">:</span> <span class="n">model</span><span class="p">)</span>
    
            <span class="kt">ObservableMacroSubButton</span><span class="p">(</span><span class="nv">model</span><span class="p">:</span> <span class="n">model</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ObservableMacroTitleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">model</span><span class="p">:</span> <span class="kt">ObservableLightModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="n">model</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ObservableMacroLightView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">model</span><span class="p">:</span> <span class="kt">ObservableLightModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Text</span><span class="p">(</span><span class="n">model</span><span class="o">.</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">ObservableMacroSubButton</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">model</span><span class="p">:</span> <span class="kt">ObservableLightModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">_</span> <span class="o">=</span> <span class="k">Self</span><span class="o">.</span><span class="nf">_printChanges</span><span class="p">()</span>
        <span class="kt">Button</span> <span class="p">{</span>
            <span class="n">model</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
        <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"버튼"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">randomColorStyle</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p><img src="/assets/img/2026-06-01-%5BSwiftUI%5D%20diff2/Simulator%20Screen%20Recording%20-%20iPhone%2017%20Pro%20Max%20-%202026-06-12%20at%2015.29.58.gif" alt="Simulator Screen Recording - iPhone 17 Pro Max - 2026-06-12 at 15.29.58" /></p>
</div>

<p>@Observable은 @ObservedObject와 달리 객체 전체의 변경 알림만 보는 방식이 아니다. SwiftUI가 body를 실행하는 동안 어떤 프로퍼티가 실제로 읽혔는지를 추적할 수 있다.</p>

<p>ObservableMacroTitleView는 model.title을 읽는다. 버튼을 눌러 바뀌는 값은 model.isOn이므로 title을 읽는 View의 body는 다시 호출되지 않을 수 있다.</p>

<p>ObservableMacroLightView는 model.isOn을 읽는다. 그래서 model.isOn이 바뀌면 ObservableMacroLightView.body는 다시 호출된다.</p>

<p>ObservableMacroSubButton은 model을 가지고 있지만, body 안에서 model.isOn을 읽지 않는다. 버튼 action 클로저에서 model.toggle()을 호출할 뿐이다. 이 경우 action은 렌더링 시점이 아니라 이벤트 시점에 실행되므로, body가 model.isOn에 직접 의존하지 않는다.</p>

<p>정리하면, @ObservedObject는 objectWillChange를 통해 객체 단위로 변경을 알리는 반면, @Observable은 body에서 실제로 접근한 프로퍼티를 기준으로 더 세밀하게 body 호출 범위를 줄일 수 있다.</p>

<h2 id="10-예제로-정리한-swiftui-diffing-이론">10. 예제로 정리한 SwiftUI Diffing 이론</h2>

<p>앞의 예제들을 기준으로 SwiftUI의 업데이트 흐름을 다시 정리해보자.</p>

<p>SwiftUI의 내부 구현은 공식적으로 모두 공개되어 있지 않다. 그래서 “항상 정확히 이 순서로만 동작한다”라고 단정하기보다는, 실제로 관찰할 수 있는 현상을 바탕으로 업데이트 과정을 이해하는 것이 좋다.</p>

<p>다만 상태 종류마다 “어떤 View가 다시 계산 대상으로 잡히는가”는 다르다. 그래서 하나의 흐름으로만 쓰면 오해가 생길 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// @State 값 변경
@State 변경
-&gt; @State를 소유한 View의 body가 다시 호출될 수 있음
-&gt; 그 body 안에서 자식 View 값들이 다시 만들어짐
-&gt; 자식 View가 변경된 값을 body에서 읽거나, 입력값/identity가 달라졌다면 해당 자식 body가 다시 호출될 수 있음
-&gt; 새로 계산된 View 값들을 기존 View Graph의 identity/dependency 정보와 맞춰 보며 업데이트 필요 여부를 판단
-&gt; 필요한 부분만 실제 화면에 반영

// @ObservedObject / @StateObject의 @Published 값 변경
@Published 변경
-&gt; objectWillChange 발생
-&gt; 해당 객체를 관찰하는 View들이 업데이트 대상으로 잡힘
-&gt; 해당 View들의 body가 다시 호출될 수 있음
-&gt; 새로 계산된 View 값들을 기존 View Graph의 identity/dependency 정보와 맞춰 보며 업데이트 필요 여부를 판단
-&gt; 필요한 부분만 실제 화면에 반영

// @Observable 프로퍼티 변경
@Observable 프로퍼티 변경
-&gt; body에서 해당 프로퍼티를 실제로 읽은 View를 중심으로 업데이트 대상이 잡힘
-&gt; 해당 View들의 body가 다시 호출될 수 있음
-&gt; 새로 계산된 View 값들을 기존 View Graph의 identity/dependency 정보와 맞춰 보며 업데이트 필요 여부를 판단
-&gt; 필요한 부분만 실제 화면에 반영
</code></pre></div></div>

<p>여기서 가장 중요한 점은 body 호출과 실제 렌더링이 같지 않다는 것이다.</p>

<p>body가 다시 호출된다는 사실 자체가 곧 성능 문제를 의미하지는 않는다. body가 다시 호출되면 SwiftUI는 새로운 View 값을 만든다. 그 다음 SwiftUI는 새로 만들어진 View 값들을 기존 View Graph의 identity, dependency, 저장 프로퍼티 정보와 맞춰 보며 실제 업데이트가 필요한지 판단한다. 이 판단 과정, 즉 diffing/reconciliation은 개발자가 세부 구현을 직접 제어할 수 없는 블랙박스에 가깝다.</p>

<p>하지만 개발자가 간접적으로 통제할 수 있는 지점은 있다. 바로 어떤 자식 View의 body가 다시 호출될 필요가 없도록 구조를 나누는 것이다. 자식 body 호출이 줄어들면, 그 자식이 만들어내는 하위 View Tree도 다시 계산되지 않을 수 있고, 결과적으로 그 이후 단계인 diffing 대상 범위도 줄어들 수 있다. 이것이 SwiftUI 렌더링 최적화에서 View 분리, dependency 분리, identity 안정화가 의미를 가지는 이유다.</p>

<p>즉, 목표는 “body 호출을 무조건 없애는 것”이 아니다. 변경된 상태와 직접 관련 없는 자식 View의 body 호출을 줄여서, 불필요한 View Tree 재계산과 그 뒤의 diffing 후보를 사전에 줄이는 것이다.</p>

<p>또 하나 중요한 점은 부모 body가 다시 호출되었다고 해서 모든 자식 View의 body가 반드시 다시 호출되는 것은 아니라는 점이다. 부모 body 안에서 자식 View 값은 다시 만들어질 수 있지만, SwiftUI는 dependency, identity, diffing 결과를 바탕으로 실제로 어떤 자식 body를 다시 계산할지 결정한다.</p>

<p>@State 변경 이후 자식 View의 body가 다시 호출되는 대표적인 경우는 다음과 같다.</p>

<ol>
  <li>자식 View가 변경된 값을 body에서 직접 읽는 경우</li>
  <li>자식 View에 전달되는 입력값이 바뀌고, 그 값이 화면 결과에 영향을 주는 경우</li>
  <li>.id(…)나 ForEach id가 바뀌어 자식 View의 identity가 달라지는 경우</li>
  <li>if/switch 분기나 View 계층 변화로 자식 View의 Structural Identity가 달라지는 경우</li>
  <li>자식 View가 자체적으로 @State, @ObservedObject, @Environment 같은 dependency 변경을 감지한 경우</li>
</ol>

<p>반대로 자식 View가 변경된 값을 body에서 읽지 않고, identity와 비교 기준도 그대로라면 부모 body가 다시 호출되어도 자식 body는 다시 호출되지 않을 수 있다. 4번 예제에서 ExtractSubButton이 @Binding을 가지고 있어도 body가 다시 호출되지 않았던 이유가 여기에 가깝다.</p>

<h3 id="view-tree-render-tree-view-graph">View Tree, Render Tree, View Graph</h3>

<p>SwiftUI를 이해할 때는 세 가지를 나눠서 생각하면 편하다.</p>

<p>View Tree는 body 호출 결과로 만들어지는 선언적 View 값이다. Text, VStack, Button 같은 값들이 모여 “화면이 이렇게 생겼으면 좋겠다”는 설명을 만든다. SwiftUI의 View는 대부분 값 타입이므로, body가 호출될 때마다 새로운 View 값이 만들어질 수 있다.</p>

<p>View Graph는 SwiftUI가 identity, state, dependency, layout을 연결하기 위해 유지하고 갱신하는 내부 관리 구조에 가깝다. @State가 어떤 View identity에 붙어 유지되는지, 어떤 body가 어떤 상태를 읽었는지 같은 정보는 단순한 View 값만으로 설명하기 어렵다.</p>

<p>Render Tree는 실제 화면에 가까운 구조다. UIKit/AppKit View, Layer, 렌더링 가능한 객체들을 떠올리면 된다. body 호출마다 통째로 새로 만드는 것이 아니라, diffing/reconciliation 결과에 따라 필요한 부분만 생성되거나 갱신된다.</p>

<p>상태 변경 이후 업데이트 흐름에서 각 구조가 어느 시점에 관여하는지 정리하면 다음과 같다.</p>

<ol>
  <li>업데이트 트리거 시점: @State 변경, objectWillChange, @Observable 접근 추적 등을 바탕으로 어떤 View의 body를 다시 계산할지 후보가 잡힌다.</li>
  <li>body 호출 시점: View Tree에 해당하는 선언적 View 값들이 새로 만들어질 수 있다.</li>
  <li>diffing/reconciliation 시점: 새로 만들어진 View 값들을 기존 View Graph의 identity/state/dependency 정보와 맞춰 보며 실제 화면 반영이 필요한지 판단한다.</li>
  <li>render 반영 시점: diffing/reconciliation 결과 실제 변화가 필요하다고 판단된 부분만 Render Tree에 반영된다.</li>
</ol>

<p>여기서 invalidation이라는 단어는 문맥에 따라 다르게 들릴 수 있다. objectWillChange처럼 body 재계산을 유발하는 변경 알림을 가리킬 때도 있고, diffing 이후 실제 화면 갱신이 필요한 대상으로 판정되는 의미로 쓰일 때도 있다. 그래서 이 글에서는 혼동을 줄이기 위해 “업데이트 트리거”, “body 재계산”, “화면 반영 대상 결정”으로 나눠서 본다.</p>

<p>이를 초기 표시와 상태 변경 이후 업데이트 흐름에 대입하면 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>초기 표시 단계
1. View 인스턴스 초기화
2. body 호출
3. body 결과로 선언적 View 값들이 만들어짐
4. SwiftUI가 이 View 값들을 기존/새 View Graph에 연결
5. layout, drawing, platform view/layer 반영을 거쳐 Render Tree가 생성 또는 갱신

상태 변경 이후 업데이트 단계
1. @State, objectWillChange, @Observable 접근 추적 등에 의해 body 재계산 후보가 잡힘
2. 대상 View의 body가 다시 호출됨
3. 새로운 선언적 View 값들이 만들어짐
4. SwiftUI가 새 View 값들을 기존 View Graph의 identity/state/dependency 정보와 맞춰 보며 화면 반영 여부를 판단함
5. 실제 변화가 필요한 부분만 Render Tree에 반영
</code></pre></div></div>

<p>정리하면, View Tree에 해당하는 선언적 View 값들은 자주 다시 만들어질 수 있지만 Render Tree는 매번 통째로 새로 만들어지지 않는다. SwiftUI는 새로 계산된 View 값들을 기존 View Graph와 연결해 보고, identity와 입력값 변화에 따라 실제 화면에 필요한 변경만 반영하려고 한다.</p>

<h3 id="업데이트-대상으로-잡히는-기준">업데이트 대상으로 잡히는 기준</h3>

<p>body는 View가 의존하는 값이 바뀌었을 때 다시 호출될 수 있다. 여기서 의존한다는 말은 단순히 프로퍼티를 가지고 있다는 뜻이 아니라, body를 계산하는 과정에서 그 값이 실제로 읽혔는지와 관련이 깊다.</p>

<p>@State 예제에서는 @State를 소유한 View의 body가 다시 호출됐다. @Binding 예제에서는 Binding을 전달받았더라도 body 안에서 값을 읽지 않는 버튼 View는 다시 호출되지 않았다. 이 차이가 중요하다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">Text</span><span class="p">(</span><span class="n">isOn</span> <span class="p">?</span> <span class="s">"On"</span> <span class="p">:</span> <span class="s">"Off"</span><span class="p">)</span> <span class="c1">// body가 isOn을 읽는다.</span>

<span class="kt">Button</span> <span class="p">{</span>
    <span class="n">isOn</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>          <span class="c1">// action 시점에만 isOn을 사용한다.</span>
<span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
    <span class="kt">Text</span><span class="p">(</span><span class="s">"버튼"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>첫 번째 코드는 body가 isOn을 읽고 화면 결과를 만든다. 그래서 isOn이 바뀌면 body 결과도 달라질 수 있다.</p>

<p>두 번째 코드는 isOn을 버튼 action 안에서만 사용한다. action은 body가 화면을 구성하는 시점이 아니라, 사용자가 버튼을 누르는 이벤트 시점에 실행된다. 따라서 이 버튼의 화면 결과는 현재 isOn 값에 직접 의존하지 않는다.</p>

<h3 id="observedobject와-observable의-차이">@ObservedObject와 @Observable의 차이</h3>

<p>@ObservedObject는 프로퍼티 단위가 아니라 객체 단위 변경 알림에 가깝다. ObservableObject 안의 @Published 값이 바뀌면 objectWillChange가 발생하고, 그 객체를 관찰하는 View들은 변경 알림을 받는다.</p>

<p>그래서 어떤 View가 viewModel.isOn을 화면에 직접 표시하지 않더라도, 같은 viewModel을 @ObservedObject로 관찰하고 있다면 body가 다시 호출될 수 있다.</p>

<p>반면 @Observable은 body가 실행되는 동안 실제로 접근한 프로퍼티를 추적할 수 있다. title만 읽는 View는 isOn 변경에 반응하지 않을 수 있고, isOn을 읽는 View만 다시 호출될 수 있다.</p>

<p>이 차이는 5번과 9번 예제를 비교하면 선명해진다.</p>

<ul>
  <li>@ObservedObject: 객체 변경 알림을 구독하는 View 단위로 body 호출 범위가 넓어질 수 있다.</li>
  <li>@Observable: body에서 실제로 읽은 프로퍼티 기준으로 body 호출 범위가 더 좁아질 수 있다.</li>
</ul>

<h3 id="identity는-state를-어디에-붙여둘지-결정한다">Identity는 State를 어디에 붙여둘지 결정한다</h3>

<p>SwiftUI에서 identity는 “이전 View와 현재 View를 같은 대상으로 볼 수 있는가”를 판단하는 기준이다.</p>

<p>identity에는 크게 두 가지 관점이 있다.</p>

<ul>
  <li>Structural Identity: View 계층에서의 타입과 위치를 기준으로 생기는 정체성</li>
  <li>Explicit Identity: .id(…)나 ForEach의 id처럼 개발자가 명시하는 정체성</li>
</ul>

<p>ForEach 예제에서 row의 identity가 안정적이면 SwiftUI는 이전 row와 현재 row를 같은 대상으로 연결할 수 있다. 이때 row 내부의 @State도 유지된다.</p>

<p>반대로 .id(“(item.id)-(tick)”)처럼 tick이 바뀔 때마다 identity가 흔들리면 SwiftUI는 이전 row와 현재 row를 같은 대상으로 보기 어렵다. 그러면 row에 붙어 있던 로컬 @State도 새로 만들어질 수 있다.</p>

<p>즉, identity는 단순히 diffing 성능만의 문제가 아니다. @State가 유지되는 위치, View가 재사용되는 범위, 애니메이션 연결 방식에도 영향을 준다.</p>

<h3 id="diffing과-equatable">Diffing과 Equatable</h3>

<p>body가 다시 호출되면 SwiftUI는 새로 만들어진 View 값을 기존 View Graph에 남아 있는 이전 정보와 맞춰 보며 실제 업데이트가 필요한지 판단한다. 이 판단 과정을 흔히 diffing 또는 reconciliation이라고 부른다.</p>

<p>개념적으로는 다음과 같이 이해할 수 있다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 실제 SwiftUI 구현이 아니라 이해를 위한 의사코드</span>
<span class="kd">func</span> <span class="nf">shouldUpdate</span><span class="p">(</span><span class="nv">oldView</span><span class="p">:</span> <span class="kt">ViewValue</span><span class="p">,</span> <span class="nv">newView</span><span class="p">:</span> <span class="kt">ViewValue</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">viewUsesEquatableComparison</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">oldView</span> <span class="o">!=</span> <span class="n">newView</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nf">compareStoredProperties</span><span class="p">(</span><span class="n">oldView</span><span class="p">,</span> <span class="n">newView</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Equatable을 사용하지 않아도 SwiftUI는 단순한 값 변화에 대해 내부적으로 비교를 수행할 수 있다. 그래서 count 같은 값 하나만 넘기는 예제에서는 .equatable()을 붙이지 않아도 하위 View의 body 호출이 줄어드는 것처럼 보일 수 있다.</p>

<p>.equatable()의 의미는 “부모 body 호출을 막는다”가 아니다. 더 정확히는 “이 하위 View를 비교할 때 Equatable 기준을 사용하라”는 힌트에 가깝다.</p>

<p>특히 클로저가 섞이면 이야기가 중요해진다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CellView</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">Equatable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">item</span><span class="p">:</span> <span class="kt">Item</span>
    <span class="k">let</span> <span class="nv">action</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span>

    <span class="kd">static</span> <span class="kd">func</span> <span class="o">==</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="kt">CellView</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">CellView</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
        <span class="n">lhs</span><span class="o">.</span><span class="n">item</span> <span class="o">==</span> <span class="n">rhs</span><span class="o">.</span><span class="n">item</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>클로저는 일반적인 값처럼 Equatable로 비교할 수 없다. 하지만 action 클로저가 화면 결과를 직접 결정하지 않고, item만 화면을 결정한다면 ==에서 item만 비교하도록 기준을 정할 수 있다.</p>

<p>이렇게 하면 부모 body가 다시 호출되면서 action 클로저가 새로 만들어지더라도, item이 같다면 CellView를 같은 View로 볼 수 있다.</p>

<h3 id="실전에서-가져갈-기준">실전에서 가져갈 기준</h3>

<p>이번 예제들을 통해 정리하면 다음 기준이 남는다.</p>

<ol>
  <li>body 호출과 실제 렌더링을 분리해서 생각한다.</li>
  <li>상태를 가지고 있다는 사실보다 body에서 실제로 읽는지가 중요하다.</li>
  <li>@ObservedObject는 객체 단위 변경 알림이라 body 호출 범위가 넓어질 수 있다.</li>
  <li>@Observable은 접근한 프로퍼티 기준으로 더 세밀하게 반응할 수 있다.</li>
  <li>ForEach에서는 안정적인 identity가 로컬 State 유지에 중요하다.</li>
  <li>.equatable()은 부모 body 호출을 막는 기능이 아니라, 하위 View 비교 기준을 명시하는 도구다.</li>
  <li>클로저를 하위 View에 넘길 때는 화면 결과를 결정하는 값과 이벤트 처리를 위한 값을 분리해서 생각한다.</li>
</ol>

<p>결국 SwiftUI 성능을 볼 때 핵심은 “무엇이 바뀌었는가”보다 “어떤 View가 그 값을 읽고 있는가”, “그 View의 identity는 유지되는가”, “이전 View와 현재 View를 같은 결과로 볼 수 있는가”에 가깝다.</p>

<h2 id="11-팝팡에서는-어떻게-해결했는가">11. 팝팡에서는 어떻게 해결했는가</h2>

<p><img src="https://github.com/user-attachments/assets/3938e583-0fc4-4620-99b0-bf761e60a1ba" width="80%" /></p>

<p>팝팡에서는 리스트의 특정 Cell 하나만 변경했는데도 여러 Cell의 body가 다시 호출되는 문제가 있었다.</p>

<p>처음 문제가 된 구조는 Cell이 직접 ViewModel을 관찰하는 형태였다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MARK: - Cell (ViewModel 참조)</span>
<span class="kd">struct</span> <span class="kt">CellView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">item</span><span class="p">:</span> <span class="kt">Item</span>
    <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">vm</span><span class="p">:</span> <span class="kt">ListViewModel</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"body 호출: </span><span class="se">\(</span><span class="n">item</span><span class="o">.</span><span class="n">id</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>

        <span class="k">return</span> <span class="kt">HStack</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="n">item</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>

            <span class="kt">Spacer</span><span class="p">()</span>

            <span class="kt">Button</span> <span class="p">{</span>
                <span class="n">vm</span><span class="o">.</span><span class="nf">toggleLike</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="n">item</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
            <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="n">item</span><span class="o">.</span><span class="n">isLiked</span> <span class="p">?</span> <span class="s">"heart.fill"</span> <span class="p">:</span> <span class="s">"heart"</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="n">item</span><span class="o">.</span><span class="n">isLiked</span> <span class="p">?</span> <span class="o">.</span><span class="nv">red</span> <span class="p">:</span> <span class="o">.</span><span class="n">gray</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">gray</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.1</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이 구조에서는 여러 Cell이 같은 ListViewModel을 @ObservedObject로 관찰한다. 특정 Cell의 좋아요 상태만 바뀌더라도 ListViewModel.objectWillChange가 발생하고, 같은 객체를 관찰하는 Cell들이 업데이트 대상으로 잡힐 수 있다.</p>

<p>문제의 핵심은 Cell이 화면에 필요한 값인 item만 받는 것이 아니라, 변경 알림을 발생시키는 ViewModel 자체를 직접 구독하고 있었다는 점이다. 이러면 특정 item 하나만 바뀐 상황에서도 여러 Cell의 body가 다시 호출될 수 있고, 그 이후 diffing/reconciliation 후보 범위도 넓어진다.</p>

<p>그래서 Cell에서 @ObservedObject를 제거하고, 화면 결과를 결정하는 값과 사용자 이벤트를 분리했다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MARK: - Cell (값 타입 데이터 + 액션)</span>
<span class="kd">struct</span> <span class="kt">CellView</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">Equatable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">item</span><span class="p">:</span> <span class="kt">Item</span>
    <span class="k">let</span> <span class="nv">action</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span>

    <span class="kd">static</span> <span class="kd">func</span> <span class="o">==</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="kt">CellView</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">CellView</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
        <span class="n">lhs</span><span class="o">.</span><span class="n">item</span> <span class="o">==</span> <span class="n">rhs</span><span class="o">.</span><span class="n">item</span>
    <span class="p">}</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"body 호출: </span><span class="se">\(</span><span class="n">item</span><span class="o">.</span><span class="n">id</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>

        <span class="k">return</span> <span class="kt">HStack</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="n">item</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>

            <span class="kt">Spacer</span><span class="p">()</span>

            <span class="kt">Button</span> <span class="p">{</span>
                <span class="nf">action</span><span class="p">()</span>
            <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="n">item</span><span class="o">.</span><span class="n">isLiked</span> <span class="p">?</span> <span class="s">"heart.fill"</span> <span class="p">:</span> <span class="s">"heart"</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="n">item</span><span class="o">.</span><span class="n">isLiked</span> <span class="p">?</span> <span class="o">.</span><span class="nv">red</span> <span class="p">:</span> <span class="o">.</span><span class="n">gray</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">gray</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.1</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>변경 후 Cell은 ViewModel을 직접 관찰하지 않는다. Cell은 화면을 그리는 데 필요한 item 값만 받고, 좋아요 버튼을 눌렀을 때 실행할 동작은 action 클로저로 상위 View에 위임한다.</p>

<p>여기서 item은 화면 결과를 결정하는 값이고, action은 이벤트를 처리하기 위한 값이다. action 클로저는 Equatable로 비교할 수 없고, 부모 body가 다시 호출될 때마다 새로 만들어질 수 있다. 하지만 action 자체가 Cell의 현재 화면 결과를 결정하는 값은 아니다.</p>

<p>그래서 Equatable 구현에서는 item만 비교한다.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="kd">func</span> <span class="o">==</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="kt">CellView</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">CellView</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
    <span class="n">lhs</span><span class="o">.</span><span class="n">item</span> <span class="o">==</span> <span class="n">rhs</span><span class="o">.</span><span class="n">item</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이렇게 하면 부모 View의 body가 다시 호출되어 CellView 값이 새로 만들어지더라도, item이 같다면 같은 Cell로 판단할 수 있다. 반대로 좋아요 상태처럼 item의 화면 결과가 달라지는 값이 바뀌면 비교 결과가 달라지고, 해당 Cell은 다시 계산될 수 있다.</p>

<p>정리하면 팝팡에서 적용한 방향은 다음과 같다.</p>

<ol>
  <li>Cell에서 @ObservedObject를 제거한다.</li>
  <li>Cell에는 화면 결과를 결정하는 값 타입 데이터만 전달한다.</li>
  <li>사용자 액션은 closure로 상위 View에 위임한다.</li>
  <li>Cell에 Equatable을 적용하고, closure는 비교 기준에서 제외한다.</li>
  <li>변경과 무관한 Cell의 body 호출과 그 이후 diffing/reconciliation 후보 범위를 줄인다.</li>
</ol>]]></content><author><name>Dong Hyeon</name></author><category term="Swift" /><summary type="html"><![CDATA[Identity WWDC21 - Demystify SwiftUI 에서 Identity를 두 경우로 나누어서 설명한다. explicit identity (명시적 정체성) structual identity (구조적 정체성) 각각 간략히 말하자면 explicit identity는 사용자 정의 또는 데이터 기반 identity를 사용하는 것이고 structual identity은 View hierachy에서 타입 및 위치에 따라 뷰를 구분하는 것이다. 참고: 애플은 View의 프로퍼티를 뷰의 종속성(dependencies)이라고 지칭한다. body 재호출 디버깅을 위한 랜덤 배경색 부여 헬퍼 메서드 public extension ShapeStyle where Self == Color { static var random: Color { Color( red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1) ) } } extension View { func randomColorStyle( cornerRadius: CGFloat = 16, padding: CGFloat = 12 ) -&gt; some View { self .padding(padding) .background { RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .fill(Color.random.opacity(0.35)) .overlay { RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .fill(.ultraThinMaterial) } } .overlay { RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .stroke(Color.white.opacity(0.35), lineWidth: 1) } .shadow(color: .black.opacity(0.12), radius: 12, y: 6) } } 1. @State 프로퍼티가 변경되면 @State를 소유한 View의 body는 재호출된다. struct SampleDiffView: View { @State private var isOn = true var body: some View { VStack(spacing: 20) { Text("Hello World") .frame(maxWidth: .infinity) .randomColorStyle() Text(isOn ? "On" : "Off") .frame(maxWidth: .infinity) .randomColorStyle() Button { isOn.toggle() } label: { Text("버튼") .frame(maxWidth: .infinity) } .randomColorStyle() } .padding(24) } } 이 예제의 목적은 body가 재호출 될 때 랜덤 색상을 부여하는 것이다. diffing 이전의 단계임을 인지해야 한다. 이 예제에서는 SampleDiffView.body 안에서 isOn을 읽고 있다. 따라서 버튼을 눌러 isOn이 변경되면 SampleDiffView의 body가 다시 평가된다. 이때 VStack 내부의 subview들이 모두 같은 identity를 갖는 것은 아니다. SubView들은 Structural Identity를 가진다. VStack(spacing: 20) { Text("Hello World") // child 0 Text(isOn ? "On" : "Off") // child 1 Button { ... } // child 2 } Text("Hello World"), Text(isOn ? "On" : "Off"), Button은 VStack 안에서 서로 다른 위치를 가지므로 각각 다른 Structural Identity를 가진다. 또한 두 번째 Text는 isOn 값에 따라 on/off를 보여주지만, 뷰의 위치와 타입은 그대로 유지된다. 즉, “On” 상태의 두 번째 Text와 “Off” 상태의 두 번째 Text는 같은 Structural Identity를 유지한다. 바뀌는 것은 identity가 아니라 Text의 입력 값이다. 2. Extract SubView: View의 일부 UI를 새로운 하위 View로 만들면 dependency을 읽지 않는 자식 body호출을 방지할 수 있다. struct SampleDiffView: View { @State private var isOn = true var body: some View { VStack(spacing: 20) { ExtractSubView() Text(isOn ? "On" : "Off") .frame(maxWidth: .infinity) .randomColorStyle() Button { isOn.toggle() } label: { Text("버튼") .frame(maxWidth: .infinity) } .randomColorStyle() } .padding(24) } } struct ExtractSubView: View { // no dependency var body: some View { Text("Hello World") .frame(maxWidth: .infinity) .randomColorStyle() } } ExtractSubView는 isOn을 직접 읽지 않는 독립적인 View이다. 버튼을 누르면 SampleDiffView의 @State인 isOn이 변경되므로 SampleDiffView.body는 다시 평가된다. 하지만 ExtractSubView는 VStack 안에서 같은 위치에 같은 타입으로 유지되고, 내부에서 변경된 dependency(@State)를 읽지 않는다. 부연 설명 SwiftUI 입장에서는 ExtractSubView 안에서 @State 같은 dependency 값이 변하지 않았고, 이 예시에서는 애초에 그런 dependency를 가지고 있지도 않기 때문에 ExtractSubView.body를 다시 평가할 필요가 없다. 참고로 어떤 View의 body가 다시 평가되면, 그 이후 diffing 단계에서 SwiftUI는 identity 변화를 확인해 이전 View를 재사용할지 새 View로 취급할지 판단하고, 실제 화면 업데이트가 필요한 범위를 결정한다. 핵심은 렌더링 단계가 가장 무겁지만, 위와 같은 구조에서는 개발자가 body 재평가 범위를 직접 줄여 불필요한 렌더링으로 이어지는 일을 막을 수 있다는 것이다. 3. .id()로 View의 identity가 바뀌면 SubView도 다시 평가된다. struct SampleDiffView: View { @State private var isOn = true var body: some View { VStack(spacing: 20) { ExtractSubView().id(UUID()) // id Text(isOn ? "On" : "Off") .frame(maxWidth: .infinity) .randomColorStyle() Button { isOn.toggle() } label: { Text("버튼") .frame(maxWidth: .infinity) } .randomColorStyle() } .padding(24) } } struct ExtractSubView: View { var body: some View { Text("Hello World") .frame(maxWidth: .infinity) .randomColorStyle() } } 공식문서 에서 id가 변경되면 View의 Identity 가 변경된다고 한다. 버튼을 누르면 SampleDiffView의 @State인 isOn이 변경되므로 SampleDiffView.body가 다시 평가된다. 이때 ExtractSubView().id(UUID())도 다시 만들어지는데, UUID()는 매번 새로운 값을 만들기 때문에 ExtractSubView의 명시적 identity도 매번 달라진다. 즉 2번 예시에서는 ExtractSubView가 같은 위치에 같은 타입으로 유지되어 재사용될 수 있었지만, 3번 예시에서는 .id(UUID()) 때문에 SwiftUI가 이전 ExtractSubView와 현재 ExtractSubView를 같은 View로 보지 않는다. 그래서 ExtractSubView.body도 다시 평가되고, 실제 렌더링도 새로 일어나는 것처럼 보인다. Self._printChanges()를 찍어보면 다음처럼 나올 수 있다. SampleDiffView: @self, @identity, _isOn changed. // 뷰 초기화 SampleDiffView: _isOn changed. // 버튼 클릭 SampleDiffView: _isOn changed. // 버튼 클릭 여기서 _isOn changed는 @State 값인 isOn이 변경되어 SampleDiffView.body가 다시 평가됐다는 의미이다. @self는 SampleDiffView라는 View 값 자체가 새로 만들어졌다는 의미이고, @identity는 SwiftUI가 SampleDiffView의 identity 변화를 감지했거나 identity 기준으로 View를 다시 연결했다는 의미로 볼 수 있다. 첫 출력은 SampleDiffView가 처음 구성되거나 identity 변화가 함께 감지된 상황이고, 이후 출력은 identity는 유지된 채 _isOn 변경만으로 SampleDiffView.body가 다시 평가된 상황이다. 4. Extract SubView: body에서 실제로 읽는 dependency가 바뀐 View만 body가 다시 호출된다. // 버튼 1번 클릭 ExtractLightView: _isOn changed. // 버튼 1번 클릭 ExtractLightView: _isOn changed. struct SampleDiffView5: View { @State private var isOn = true var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { ExtractTitleView() ExtractLightView(isOn: $isOn) ExtractSubButton(isOn: $isOn) } .padding(24) } } struct ExtractTitleView: View { // no dependency var body: some View { let _ = Self._printChanges() Text("Hello World") .frame(maxWidth: .infinity) .randomColorStyle() } } struct ExtractLightView: View { @Binding var isOn: Bool var body: some View { let _ = Self._printChanges() Text(isOn ? "On" : "Off") .frame(maxWidth: .infinity) .randomColorStyle() } } struct ExtractSubButton: View { @Binding var isOn: Bool var body: some View { let _ = Self._printChanges() Button { isOn.toggle() } label: { Text("버튼") .frame(maxWidth: .infinity) } .randomColorStyle() } } ExtractTitleView는 변경되는 dependency를 읽지 않기 때문에 body가 다시 호출되지 않는다. ExtractLightView는 @Binding var isOn: Bool을 가지고 있고, Text(isOn ? “On” : “Off”)에서 isOn을 직접 읽는다. 따라서 isOn이 변경될 때마다 body가 다시 호출된다. ExtractSubButton도 @Binding var isOn: Bool을 가지고 있지만, 버튼을 누를 때 실행되는 action 클로저 안에서만 isOn.toggle()을 호출한다. 이 값은 화면을 그리는 body 안에서 직접 읽히는 값이 아니므로, isOn이 변경되어도 ExtractSubButton.body는 다시 호출되지 않는다. 여기서 중요한 점은 @Binding으로 값을 전달받았다는 사실만으로 body가 다시 호출되는 것은 아니라는 점이다. SwiftUI에서 body가 다시 호출되는 기준은, body 안에서 해당 상태가 실제로 읽혔는지 여부에 가깝다. ExtractLightView는 Text(isOn ? “On” : “Off”)를 통해 isOn 값을 화면에 반영한다. 그래서 isOn이 바뀌면 화면 결과도 달라질 수 있고, SwiftUI는 ExtractLightView.body를 다시 호출한다. 반면 ExtractSubButton은 isOn을 버튼의 action 클로저 안에서만 사용한다. action 클로저는 body가 화면을 구성하는 시점이 아니라, 사용자가 버튼을 탭한 이벤트 시점에 실행된다. 따라서 ExtractSubButton의 화면 결과는 현재 isOn 값에 직접 의존하지 않는다. 정리하면, @Binding 기반 예시에서는 상태를 “전달받는 것”과 상태를 “렌더링에 사용하는 것”이 다르다. SwiftUI에서 body 재호출의 기준은 단순한 상태 전달 여부가 아니라, body 안에서 해당 상태가 실제로 읽혀 화면 결과에 영향을 주는지에 가깝다. 5. @ObservedObject는 객체 변경 알림을 구독하기 때문에 body 호출 범위가 달라질 수 있다. // 버튼 1번 클릭 SampleObservedObjectDiffView: _viewModel changed. ObservedLightView: _viewModel changed. ObservedSubButton: _viewModel changed. // 버튼 1번 클릭 SampleObservedObjectDiffView: _viewModel changed. ObservedLightView: _viewModel changed. ObservedSubButton: _viewModel changed. final class LightViewModel: ObservableObject { @Published var isOn = true func toggle() { isOn.toggle() } } struct SampleObservedObjectDiffView: View { @StateObject private var viewModel = LightViewModel() var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { ObservedTitleView() ObservedLightView(viewModel: viewModel) ObservedSubButton(viewModel: viewModel) } .padding(24) } } struct ObservedTitleView: View { // no dependency var body: some View { let _ = Self._printChanges() Text("Hello World") .frame(maxWidth: .infinity) .randomColorStyle() } } struct ObservedLightView: View { @ObservedObject var viewModel: LightViewModel var body: some View { let _ = Self._printChanges() Text(viewModel.isOn ? "On" : "Off") .frame(maxWidth: .infinity) .randomColorStyle() } } struct ObservedSubButton: View { @ObservedObject var viewModel: LightViewModel var body: some View { let _ = Self._printChanges() Button { viewModel.toggle() } label: { Text("버튼") .frame(maxWidth: .infinity) } .randomColorStyle() } } 4번 예시에서는 @Binding var isOn: Bool을 가지고 있더라도, body 안에서 isOn을 직접 읽지 않는 ExtractSubButton.body는 다시 호출되지 않았다. 하지만 @ObservedObject는 동작 방식이 조금 다르다. ObservedLightView와 ObservedSubButton은 모두 같은 LightViewModel을 @ObservedObject로 관찰한다. viewModel.isOn이 변경되면 LightViewModel.objectWillChange가 발생하고, 이 객체를 관찰 중인 View들은 변경 알림을 받는다. 그래서 ObservedLightView처럼 body 안에서 viewModel.isOn을 직접 읽는 View는 당연히 body가 다시 호출된다. 그리고 ObservedSubButton처럼 viewModel을 버튼 action 클로저 안에서만 사용하더라도, @ObservedObject로 객체를 관찰하고 있기 때문에 viewModel의 변경 알림에 의해 body가 다시 호출될 수 있다. 반면 ObservedTitleView는 LightViewModel을 전달받지도 않고, 어떤 변경되는 dependency도 읽지 않는다. 그래서 버튼을 눌러 isOn이 변경되어도 ObservedTitleView.body는 다시 호출되지 않는다. 정리하면, @Binding은 값이 body 안에서 실제로 읽히는지에 따라 body 재호출 여부가 더 직접적으로 갈리는 반면, @ObservedObject는 객체의 objectWillChange를 구독한다. 따라서 같은 객체를 관찰하는 View라면, 특정 프로퍼티를 body 안에서 직접 읽지 않더라도 객체 변경 알림으로 인해 body가 다시 호출될 수 있다. 6. @Published 프로퍼티가 여러 개일 때 하나만 바뀌어도 같은 객체를 관찰하는 View의 body가 다시 호출될 수 있다. // count 버튼 1번 클릭 SampleMultiPublishedDiffView: _viewModel changed. PublishedCountView: _viewModel changed. PublishedTitleView: _viewModel changed. PublishedSubButton: _viewModel changed. final class MultiPublishedViewModel: ObservableObject { @Published var count = 0 @Published var title = "Hello World" func increaseCount() { count += 1 } func changeTitle() { title = ["SwiftUI", "Diffing", "Identity"].randomElement() ?? "SwiftUI" } } struct SampleMultiPublishedDiffView: View { @StateObject private var viewModel = MultiPublishedViewModel() var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { PublishedCountView(viewModel: viewModel) PublishedTitleView(viewModel: viewModel) PublishedSubButton(viewModel: viewModel) } .padding(24) } } struct PublishedCountView: View { @ObservedObject var viewModel: MultiPublishedViewModel var body: some View { let _ = Self._printChanges() Text("count: \(viewModel.count)") .frame(maxWidth: .infinity) .randomColorStyle() } } struct PublishedTitleView: View { @ObservedObject var viewModel: MultiPublishedViewModel var body: some View { let _ = Self._printChanges() Text(viewModel.title) .frame(maxWidth: .infinity) .randomColorStyle() } } struct PublishedSubButton: View { @ObservedObject var viewModel: MultiPublishedViewModel var body: some View { let _ = Self._printChanges() Button { viewModel.increaseCount() } label: { Text("count 증가") .frame(maxWidth: .infinity) } .randomColorStyle() } } 이 예제에서 count 버튼을 누르면 실제로 변경되는 값은 count뿐이다. PublishedTitleView가 읽는 title은 바뀌지 않는다. 하지만 count와 title은 같은 ObservableObject 안에 들어 있다. @Published var count가 변경되면 MultiPublishedViewModel.objectWillChange가 발생하고, 이 객체를 관찰 중인 View들은 변경 알림을 받는다. 그래서 PublishedTitleView가 title만 읽고 있더라도, 같은 viewModel을 @ObservedObject로 관찰하고 있다면 count 변경 시 body가 다시 호출될 수 있다. 정리하면, ObservableObject는 기본적으로 프로퍼티 단위가 아니라 객체 단위로 변경 알림을 보낸다. 여러 @Published 값을 하나의 ObservableObject에 모아두면 편하지만, 변경 범위가 넓어질 수 있다. 7. ForEach에서 identity가 불안정하면 하위 View의 State가 유지되지 않을 수 있다. // 부모 업데이트 버튼 1번 클릭 ForEachIdentitySampleView: _tick changed. UnstableIdentityRow: @self, @identity, _color changed. UnstableIdentityRow: @self, @identity, _color changed. UnstableIdentityRow: @self, @identity, _color changed. struct DiffItem: Identifiable { let id = UUID() let title: String } struct ForEachIdentitySampleView: View { @State private var tick = 0 private let items = [ DiffItem(title: "First"), DiffItem(title: "Second"), DiffItem(title: "Third") ] var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { Text("tick: \(tick)") .frame(maxWidth: .infinity) .randomColorStyle() Button("부모 업데이트") { tick += 1 } .frame(maxWidth: .infinity) .randomColorStyle() ForEach(items) { item in StableIdentityRow(title: item.title) } ForEach(items) { item in UnstableIdentityRow(title: item.title) .id("\(item.id)-\(tick)") } } .padding(24) } } struct StableIdentityRow: View { let title: String @State private var color = Color.random var body: some View { let _ = Self._printChanges() Text("stable: \(title)") .frame(maxWidth: .infinity) .padding(12) .background(color.opacity(0.35)) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) } } struct UnstableIdentityRow: View { let title: String @State private var color = Color.random var body: some View { let _ = Self._printChanges() Text("unstable: \(title)") .frame(maxWidth: .infinity) .padding(12) .background(color.opacity(0.35)) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) } } ForEach는 각 row를 구분하기 위해 identity를 사용한다. items가 안정적인 id를 가지고 있으면 SwiftUI는 이전 row와 현재 row를 같은 View로 연결할 수 있다. 이 예제에서는 row 내부에 @State private var color = Color.random을 두었다. row의 identity가 유지되면 이 @State 값도 유지되기 때문에 부모 View의 tick이 바뀌어도 stable row의 배경색은 유지된다. 반면 UnstableIdentityRow에는 .id(“(item.id)-(tick)”)를 붙였다. tick이 바뀔 때마다 row의 identity도 바뀌므로 SwiftUI는 이전 row와 현재 row를 같은 View로 연결하지 못한다. 이때 row의 로컬 @State도 새로 만들어질 수 있고, color 값이 다시 초기화되면서 배경색이 바뀐다. 정리하면, ForEach에서는 body 호출 여부만 보는 것보다 identity가 로컬 State를 유지시키는지 확인하는 편이 더 명확하다. identity가 안정적이면 row의 State가 유지되고, identity가 흔들리면 SwiftUI가 이전 row를 이어받지 못해 State가 초기화될 수 있다. 8. Equatable을 사용하지 않아도 입력값이 그대로라면 하위 View의 body가 다시 호출되지 않을 수 있다. // unrelated 버튼 1번 클릭 NonEquatableSampleView: _unrelated changed. // count 버튼 1번 클릭 NonEquatableSampleView: _count changed. PlainCounterView: _count changed. struct NonEquatableSampleView: View { @State private var count = 0 @State private var unrelated = false var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { Text("unrelated: \(unrelated.description)") .frame(maxWidth: .infinity) .randomColorStyle() PlainCounterView(count: count) Button("unrelated 변경") { unrelated.toggle() } .frame(maxWidth: .infinity) .randomColorStyle() Button("count 증가") { count += 1 } .frame(maxWidth: .infinity) .randomColorStyle() } .padding(24) } } struct PlainCounterView: View { let count: Int var body: some View { let _ = Self._printChanges() Text("count: \(count)") .frame(maxWidth: .infinity) .randomColorStyle() } } 먼저 .equatable()을 전혀 사용하지 않는 경우를 보자. unrelated가 바뀌면 NonEquatableSampleView.body는 다시 호출된다. 부모가 다시 호출되었기 때문에 body 안의 PlainCounterView(count: count) 코드도 다시 실행된다. 그런데 count 값이 그대로라면 PlainCounterView가 실제로 다시 계산되거나 갱신될 필요는 없다. SwiftUI는 이런 단순한 입력값에 대해 이미 이전 View와 현재 View를 비교하고, 결과가 같다고 판단하면 하위 View의 body 호출이나 갱신을 줄일 수 있다. 그래서 이 예제만 보면 .equatable()을 붙이지 않아도 PlainCounterView.body가 다시 호출되지 않는 것처럼 보일 수 있다. 이 지점 때문에 .equatable()을 “body 호출을 막는 마법”처럼 설명하면 오히려 헷갈린다. 정리하면, Equatable을 직접 사용하지 않아도 SwiftUI가 입력값이 변하지 않은 하위 View를 다시 계산하지 않는 경우가 있다. 따라서 단순한 count 전달 예제만으로는 .equatable()의 차이가 선명하게 드러나지 않을 수 있다. 8-1. .equatable()은 하위 View를 비교할 때 Equatable 기준을 사용하라고 명시하는 방법이다. // unrelated 버튼 1번 클릭 EquatableValueSampleView: _unrelated changed. // count 버튼 1번 클릭 EquatableValueSampleView: _count changed. EquatableValueCounterView: @self changed. struct EquatableValueSampleView: View { @State private var count = 0 @State private var unrelated = false var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { Text("unrelated: \(unrelated.description)") .frame(maxWidth: .infinity) .randomColorStyle() EquatableValueCounterView(count: count) .equatable() Button("unrelated 변경") { unrelated.toggle() } .frame(maxWidth: .infinity) .randomColorStyle() Button("count 증가") { count += 1 } .frame(maxWidth: .infinity) .randomColorStyle() } .padding(24) } } struct EquatableValueCounterView: View, Equatable { let count: Int var body: some View { let _ = Self._printChanges() Text("count: \(count)") .frame(maxWidth: .infinity) .randomColorStyle() } } .equatable()을 붙이면 SwiftUI는 EquatableValueCounterView를 비교할 때 View가 가진 Equatable 기준을 사용할 수 있다. 이 예제에서는 EquatableValueCounterView가 count만 가지고 있으므로, Swift가 자동으로 만들어주는 Equatable 비교도 결국 count 비교가 된다. unrelated만 바뀌면 부모인 EquatableValueSampleView.body는 다시 호출된다. 하지만 EquatableValueCounterView에 전달되는 count는 그대로다. 이 경우 SwiftUI는 이전 EquatableValueCounterView와 현재 EquatableValueCounterView가 같다고 판단할 수 있고, 하위 View의 body 호출이나 갱신을 줄일 수 있다. 반대로 count가 바뀌면 EquatableValueCounterView의 입력값 자체가 달라진다. 비교 결과가 달라졌기 때문에 EquatableValueCounterView.body는 다시 호출된다. 다만 이 예제도 8번과 결과가 비슷하게 보일 수 있다. count처럼 단순한 값 하나만 넘기는 경우에는 SwiftUI가 .equatable() 없이도 비슷하게 최적화할 수 있기 때문이다. 그래서 .equatable()의 의미는 “항상 새로운 최적화를 켠다”라기보다, 하위 View 비교에 Equatable 기준을 사용하겠다고 명시하는 쪽에 가깝다. 8-2. 클로저처럼 비교하기 어려운 값이 섞이면 ==을 직접 구현해 비교 기준에서 제외할 수 있다. // unrelated 버튼 1번 클릭 EquatableClosureSampleView: _unrelated changed. // EquatableClosureCounterView 버튼 1번 클릭 EquatableClosureSampleView: _unrelated changed. // count 버튼 1번 클릭 EquatableClosureSampleView: _count changed. EquatableClosureCounterView: @self changed. struct EquatableClosureSampleView: View { @State private var count = 0 @State private var unrelated = false var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { Text("unrelated: \(unrelated.description)") .frame(maxWidth: .infinity) .randomColorStyle() EquatableClosureCounterView( count: count, action: { unrelated.toggle() } ) .equatable() Button("unrelated 변경") { unrelated.toggle() } .frame(maxWidth: .infinity) .randomColorStyle() Button("count 증가") { count += 1 } .frame(maxWidth: .infinity) .randomColorStyle() } .padding(24) } } struct EquatableClosureCounterView: View, Equatable { let count: Int let action: () -&gt; Void static func == (lhs: EquatableClosureCounterView, rhs: EquatableClosureCounterView) -&gt; Bool { lhs.count == rhs.count } var body: some View { let _ = Self._printChanges() Button { action() } label: { Text("count: \(count)") .frame(maxWidth: .infinity) } .randomColorStyle() } } 이번에는 EquatableClosureCounterView가 count뿐 아니라 action 클로저도 함께 받는다. 클로저는 일반적인 값처럼 Equatable로 비교할 수 없다. 그래서 이 상태에서는 Swift가 Equatable 구현을 자동으로 만들어줄 수 없고, 직접 ==을 구현해야 한다. 여기서 action 클로저는 버튼을 눌렀을 때 실행될 동작이다. 화면에 표시되는 값은 Text(“count: (count)”)이고, 현재 예제에서 View의 시각적 결과를 결정하는 핵심 입력값은 count다. 그래서 == 구현에서 action은 비교하지 않고 count만 비교하도록 했다. unrelated가 바뀌면 부모 body가 다시 호출되고, action 클로저도 새로 만들어질 수 있다. 하지만 count가 같다면 EquatableClosureCounterView의 화면 결과는 같다고 볼 수 있다. 반대로 count가 바뀌면 ==의 비교 결과가 달라진다. 이때는 EquatableClosureCounterView가 다시 계산되어야 하므로 body가 다시 호출된다. 정리하면, .equatable()은 부모 body 호출을 막는 기능이 아니다. 더 정확히는 하위 View를 비교할 때 어떤 값을 기준으로 같다고 볼지 알려주는 장치에 가깝다. 특히 클로저처럼 비교하기 어렵거나, 화면 결과에 직접 영향을 주지 않는 값을 비교 대상에서 제외하고 싶을 때 직접 ==을 구현하는 방식이 의미가 있다. 9. @Observable은 body에서 실제로 접근한 프로퍼티를 기준으로 body 호출 범위를 좁힐 수 있다. // 버튼 1번 클릭 ObservableMacroLightView: _model changed. // 버튼 1번 클릭 ObservableMacroLightView: _model changed. import Observation import SwiftUI @Observable final class ObservableLightModel { var isOn = true var title = "Hello World" func toggle() { isOn.toggle() } } struct SampleObservableMacroDiffView: View { @State private var model = ObservableLightModel() var body: some View { let _ = Self._printChanges() VStack(spacing: 20) { ObservableMacroTitleView(model: model) ObservableMacroLightView(model: model) ObservableMacroSubButton(model: model) } .padding(24) } } struct ObservableMacroTitleView: View { let model: ObservableLightModel var body: some View { let _ = Self._printChanges() Text(model.title) .frame(maxWidth: .infinity) .randomColorStyle() } } struct ObservableMacroLightView: View { let model: ObservableLightModel var body: some View { let _ = Self._printChanges() Text(model.isOn ? "On" : "Off") .frame(maxWidth: .infinity) .randomColorStyle() } } struct ObservableMacroSubButton: View { let model: ObservableLightModel var body: some View { let _ = Self._printChanges() Button { model.toggle() } label: { Text("버튼") .frame(maxWidth: .infinity) } .randomColorStyle() } } @Observable은 @ObservedObject와 달리 객체 전체의 변경 알림만 보는 방식이 아니다. SwiftUI가 body를 실행하는 동안 어떤 프로퍼티가 실제로 읽혔는지를 추적할 수 있다. ObservableMacroTitleView는 model.title을 읽는다. 버튼을 눌러 바뀌는 값은 model.isOn이므로 title을 읽는 View의 body는 다시 호출되지 않을 수 있다. ObservableMacroLightView는 model.isOn을 읽는다. 그래서 model.isOn이 바뀌면 ObservableMacroLightView.body는 다시 호출된다. ObservableMacroSubButton은 model을 가지고 있지만, body 안에서 model.isOn을 읽지 않는다. 버튼 action 클로저에서 model.toggle()을 호출할 뿐이다. 이 경우 action은 렌더링 시점이 아니라 이벤트 시점에 실행되므로, body가 model.isOn에 직접 의존하지 않는다. 정리하면, @ObservedObject는 objectWillChange를 통해 객체 단위로 변경을 알리는 반면, @Observable은 body에서 실제로 접근한 프로퍼티를 기준으로 더 세밀하게 body 호출 범위를 줄일 수 있다. 10. 예제로 정리한 SwiftUI Diffing 이론 앞의 예제들을 기준으로 SwiftUI의 업데이트 흐름을 다시 정리해보자. SwiftUI의 내부 구현은 공식적으로 모두 공개되어 있지 않다. 그래서 “항상 정확히 이 순서로만 동작한다”라고 단정하기보다는, 실제로 관찰할 수 있는 현상을 바탕으로 업데이트 과정을 이해하는 것이 좋다. 다만 상태 종류마다 “어떤 View가 다시 계산 대상으로 잡히는가”는 다르다. 그래서 하나의 흐름으로만 쓰면 오해가 생길 수 있다. // @State 값 변경 @State 변경 -&gt; @State를 소유한 View의 body가 다시 호출될 수 있음 -&gt; 그 body 안에서 자식 View 값들이 다시 만들어짐 -&gt; 자식 View가 변경된 값을 body에서 읽거나, 입력값/identity가 달라졌다면 해당 자식 body가 다시 호출될 수 있음 -&gt; 새로 계산된 View 값들을 기존 View Graph의 identity/dependency 정보와 맞춰 보며 업데이트 필요 여부를 판단 -&gt; 필요한 부분만 실제 화면에 반영 // @ObservedObject / @StateObject의 @Published 값 변경 @Published 변경 -&gt; objectWillChange 발생 -&gt; 해당 객체를 관찰하는 View들이 업데이트 대상으로 잡힘 -&gt; 해당 View들의 body가 다시 호출될 수 있음 -&gt; 새로 계산된 View 값들을 기존 View Graph의 identity/dependency 정보와 맞춰 보며 업데이트 필요 여부를 판단 -&gt; 필요한 부분만 실제 화면에 반영 // @Observable 프로퍼티 변경 @Observable 프로퍼티 변경 -&gt; body에서 해당 프로퍼티를 실제로 읽은 View를 중심으로 업데이트 대상이 잡힘 -&gt; 해당 View들의 body가 다시 호출될 수 있음 -&gt; 새로 계산된 View 값들을 기존 View Graph의 identity/dependency 정보와 맞춰 보며 업데이트 필요 여부를 판단 -&gt; 필요한 부분만 실제 화면에 반영 여기서 가장 중요한 점은 body 호출과 실제 렌더링이 같지 않다는 것이다. body가 다시 호출된다는 사실 자체가 곧 성능 문제를 의미하지는 않는다. body가 다시 호출되면 SwiftUI는 새로운 View 값을 만든다. 그 다음 SwiftUI는 새로 만들어진 View 값들을 기존 View Graph의 identity, dependency, 저장 프로퍼티 정보와 맞춰 보며 실제 업데이트가 필요한지 판단한다. 이 판단 과정, 즉 diffing/reconciliation은 개발자가 세부 구현을 직접 제어할 수 없는 블랙박스에 가깝다. 하지만 개발자가 간접적으로 통제할 수 있는 지점은 있다. 바로 어떤 자식 View의 body가 다시 호출될 필요가 없도록 구조를 나누는 것이다. 자식 body 호출이 줄어들면, 그 자식이 만들어내는 하위 View Tree도 다시 계산되지 않을 수 있고, 결과적으로 그 이후 단계인 diffing 대상 범위도 줄어들 수 있다. 이것이 SwiftUI 렌더링 최적화에서 View 분리, dependency 분리, identity 안정화가 의미를 가지는 이유다. 즉, 목표는 “body 호출을 무조건 없애는 것”이 아니다. 변경된 상태와 직접 관련 없는 자식 View의 body 호출을 줄여서, 불필요한 View Tree 재계산과 그 뒤의 diffing 후보를 사전에 줄이는 것이다. 또 하나 중요한 점은 부모 body가 다시 호출되었다고 해서 모든 자식 View의 body가 반드시 다시 호출되는 것은 아니라는 점이다. 부모 body 안에서 자식 View 값은 다시 만들어질 수 있지만, SwiftUI는 dependency, identity, diffing 결과를 바탕으로 실제로 어떤 자식 body를 다시 계산할지 결정한다. @State 변경 이후 자식 View의 body가 다시 호출되는 대표적인 경우는 다음과 같다. 자식 View가 변경된 값을 body에서 직접 읽는 경우 자식 View에 전달되는 입력값이 바뀌고, 그 값이 화면 결과에 영향을 주는 경우 .id(…)나 ForEach id가 바뀌어 자식 View의 identity가 달라지는 경우 if/switch 분기나 View 계층 변화로 자식 View의 Structural Identity가 달라지는 경우 자식 View가 자체적으로 @State, @ObservedObject, @Environment 같은 dependency 변경을 감지한 경우 반대로 자식 View가 변경된 값을 body에서 읽지 않고, identity와 비교 기준도 그대로라면 부모 body가 다시 호출되어도 자식 body는 다시 호출되지 않을 수 있다. 4번 예제에서 ExtractSubButton이 @Binding을 가지고 있어도 body가 다시 호출되지 않았던 이유가 여기에 가깝다. View Tree, Render Tree, View Graph SwiftUI를 이해할 때는 세 가지를 나눠서 생각하면 편하다. View Tree는 body 호출 결과로 만들어지는 선언적 View 값이다. Text, VStack, Button 같은 값들이 모여 “화면이 이렇게 생겼으면 좋겠다”는 설명을 만든다. SwiftUI의 View는 대부분 값 타입이므로, body가 호출될 때마다 새로운 View 값이 만들어질 수 있다. View Graph는 SwiftUI가 identity, state, dependency, layout을 연결하기 위해 유지하고 갱신하는 내부 관리 구조에 가깝다. @State가 어떤 View identity에 붙어 유지되는지, 어떤 body가 어떤 상태를 읽었는지 같은 정보는 단순한 View 값만으로 설명하기 어렵다. Render Tree는 실제 화면에 가까운 구조다. UIKit/AppKit View, Layer, 렌더링 가능한 객체들을 떠올리면 된다. body 호출마다 통째로 새로 만드는 것이 아니라, diffing/reconciliation 결과에 따라 필요한 부분만 생성되거나 갱신된다. 상태 변경 이후 업데이트 흐름에서 각 구조가 어느 시점에 관여하는지 정리하면 다음과 같다. 업데이트 트리거 시점: @State 변경, objectWillChange, @Observable 접근 추적 등을 바탕으로 어떤 View의 body를 다시 계산할지 후보가 잡힌다. body 호출 시점: View Tree에 해당하는 선언적 View 값들이 새로 만들어질 수 있다. diffing/reconciliation 시점: 새로 만들어진 View 값들을 기존 View Graph의 identity/state/dependency 정보와 맞춰 보며 실제 화면 반영이 필요한지 판단한다. render 반영 시점: diffing/reconciliation 결과 실제 변화가 필요하다고 판단된 부분만 Render Tree에 반영된다. 여기서 invalidation이라는 단어는 문맥에 따라 다르게 들릴 수 있다. objectWillChange처럼 body 재계산을 유발하는 변경 알림을 가리킬 때도 있고, diffing 이후 실제 화면 갱신이 필요한 대상으로 판정되는 의미로 쓰일 때도 있다. 그래서 이 글에서는 혼동을 줄이기 위해 “업데이트 트리거”, “body 재계산”, “화면 반영 대상 결정”으로 나눠서 본다. 이를 초기 표시와 상태 변경 이후 업데이트 흐름에 대입하면 다음과 같다. 초기 표시 단계 1. View 인스턴스 초기화 2. body 호출 3. body 결과로 선언적 View 값들이 만들어짐 4. SwiftUI가 이 View 값들을 기존/새 View Graph에 연결 5. layout, drawing, platform view/layer 반영을 거쳐 Render Tree가 생성 또는 갱신 상태 변경 이후 업데이트 단계 1. @State, objectWillChange, @Observable 접근 추적 등에 의해 body 재계산 후보가 잡힘 2. 대상 View의 body가 다시 호출됨 3. 새로운 선언적 View 값들이 만들어짐 4. SwiftUI가 새 View 값들을 기존 View Graph의 identity/state/dependency 정보와 맞춰 보며 화면 반영 여부를 판단함 5. 실제 변화가 필요한 부분만 Render Tree에 반영 정리하면, View Tree에 해당하는 선언적 View 값들은 자주 다시 만들어질 수 있지만 Render Tree는 매번 통째로 새로 만들어지지 않는다. SwiftUI는 새로 계산된 View 값들을 기존 View Graph와 연결해 보고, identity와 입력값 변화에 따라 실제 화면에 필요한 변경만 반영하려고 한다. 업데이트 대상으로 잡히는 기준 body는 View가 의존하는 값이 바뀌었을 때 다시 호출될 수 있다. 여기서 의존한다는 말은 단순히 프로퍼티를 가지고 있다는 뜻이 아니라, body를 계산하는 과정에서 그 값이 실제로 읽혔는지와 관련이 깊다. @State 예제에서는 @State를 소유한 View의 body가 다시 호출됐다. @Binding 예제에서는 Binding을 전달받았더라도 body 안에서 값을 읽지 않는 버튼 View는 다시 호출되지 않았다. 이 차이가 중요하다. Text(isOn ? "On" : "Off") // body가 isOn을 읽는다. Button { isOn.toggle() // action 시점에만 isOn을 사용한다. } label: { Text("버튼") } 첫 번째 코드는 body가 isOn을 읽고 화면 결과를 만든다. 그래서 isOn이 바뀌면 body 결과도 달라질 수 있다. 두 번째 코드는 isOn을 버튼 action 안에서만 사용한다. action은 body가 화면을 구성하는 시점이 아니라, 사용자가 버튼을 누르는 이벤트 시점에 실행된다. 따라서 이 버튼의 화면 결과는 현재 isOn 값에 직접 의존하지 않는다. @ObservedObject와 @Observable의 차이 @ObservedObject는 프로퍼티 단위가 아니라 객체 단위 변경 알림에 가깝다. ObservableObject 안의 @Published 값이 바뀌면 objectWillChange가 발생하고, 그 객체를 관찰하는 View들은 변경 알림을 받는다. 그래서 어떤 View가 viewModel.isOn을 화면에 직접 표시하지 않더라도, 같은 viewModel을 @ObservedObject로 관찰하고 있다면 body가 다시 호출될 수 있다. 반면 @Observable은 body가 실행되는 동안 실제로 접근한 프로퍼티를 추적할 수 있다. title만 읽는 View는 isOn 변경에 반응하지 않을 수 있고, isOn을 읽는 View만 다시 호출될 수 있다. 이 차이는 5번과 9번 예제를 비교하면 선명해진다. @ObservedObject: 객체 변경 알림을 구독하는 View 단위로 body 호출 범위가 넓어질 수 있다. @Observable: body에서 실제로 읽은 프로퍼티 기준으로 body 호출 범위가 더 좁아질 수 있다. Identity는 State를 어디에 붙여둘지 결정한다 SwiftUI에서 identity는 “이전 View와 현재 View를 같은 대상으로 볼 수 있는가”를 판단하는 기준이다. identity에는 크게 두 가지 관점이 있다. Structural Identity: View 계층에서의 타입과 위치를 기준으로 생기는 정체성 Explicit Identity: .id(…)나 ForEach의 id처럼 개발자가 명시하는 정체성 ForEach 예제에서 row의 identity가 안정적이면 SwiftUI는 이전 row와 현재 row를 같은 대상으로 연결할 수 있다. 이때 row 내부의 @State도 유지된다. 반대로 .id(“(item.id)-(tick)”)처럼 tick이 바뀔 때마다 identity가 흔들리면 SwiftUI는 이전 row와 현재 row를 같은 대상으로 보기 어렵다. 그러면 row에 붙어 있던 로컬 @State도 새로 만들어질 수 있다. 즉, identity는 단순히 diffing 성능만의 문제가 아니다. @State가 유지되는 위치, View가 재사용되는 범위, 애니메이션 연결 방식에도 영향을 준다. Diffing과 Equatable body가 다시 호출되면 SwiftUI는 새로 만들어진 View 값을 기존 View Graph에 남아 있는 이전 정보와 맞춰 보며 실제 업데이트가 필요한지 판단한다. 이 판단 과정을 흔히 diffing 또는 reconciliation이라고 부른다. 개념적으로는 다음과 같이 이해할 수 있다. // 실제 SwiftUI 구현이 아니라 이해를 위한 의사코드 func shouldUpdate(oldView: ViewValue, newView: ViewValue) -&gt; Bool { if viewUsesEquatableComparison { return oldView != newView } return compareStoredProperties(oldView, newView) } Equatable을 사용하지 않아도 SwiftUI는 단순한 값 변화에 대해 내부적으로 비교를 수행할 수 있다. 그래서 count 같은 값 하나만 넘기는 예제에서는 .equatable()을 붙이지 않아도 하위 View의 body 호출이 줄어드는 것처럼 보일 수 있다. .equatable()의 의미는 “부모 body 호출을 막는다”가 아니다. 더 정확히는 “이 하위 View를 비교할 때 Equatable 기준을 사용하라”는 힌트에 가깝다. 특히 클로저가 섞이면 이야기가 중요해진다. struct CellView: View, Equatable { let item: Item let action: () -&gt; Void static func == (lhs: CellView, rhs: CellView) -&gt; Bool { lhs.item == rhs.item } } 클로저는 일반적인 값처럼 Equatable로 비교할 수 없다. 하지만 action 클로저가 화면 결과를 직접 결정하지 않고, item만 화면을 결정한다면 ==에서 item만 비교하도록 기준을 정할 수 있다. 이렇게 하면 부모 body가 다시 호출되면서 action 클로저가 새로 만들어지더라도, item이 같다면 CellView를 같은 View로 볼 수 있다. 실전에서 가져갈 기준 이번 예제들을 통해 정리하면 다음 기준이 남는다. body 호출과 실제 렌더링을 분리해서 생각한다. 상태를 가지고 있다는 사실보다 body에서 실제로 읽는지가 중요하다. @ObservedObject는 객체 단위 변경 알림이라 body 호출 범위가 넓어질 수 있다. @Observable은 접근한 프로퍼티 기준으로 더 세밀하게 반응할 수 있다. ForEach에서는 안정적인 identity가 로컬 State 유지에 중요하다. .equatable()은 부모 body 호출을 막는 기능이 아니라, 하위 View 비교 기준을 명시하는 도구다. 클로저를 하위 View에 넘길 때는 화면 결과를 결정하는 값과 이벤트 처리를 위한 값을 분리해서 생각한다. 결국 SwiftUI 성능을 볼 때 핵심은 “무엇이 바뀌었는가”보다 “어떤 View가 그 값을 읽고 있는가”, “그 View의 identity는 유지되는가”, “이전 View와 현재 View를 같은 결과로 볼 수 있는가”에 가깝다. 11. 팝팡에서는 어떻게 해결했는가 팝팡에서는 리스트의 특정 Cell 하나만 변경했는데도 여러 Cell의 body가 다시 호출되는 문제가 있었다. 처음 문제가 된 구조는 Cell이 직접 ViewModel을 관찰하는 형태였다. // MARK: - Cell (ViewModel 참조) struct CellView: View { let item: Item @ObservedObject var vm: ListViewModel var body: some View { print("body 호출: \(item.id)") return HStack { Text(item.name) Spacer() Button { vm.toggleLike(id: item.id) } label: { Image(systemName: item.isLiked ? "heart.fill" : "heart") .foregroundColor(item.isLiked ? .red : .gray) } } .padding() .background(Color.gray.opacity(0.1)) } } 이 구조에서는 여러 Cell이 같은 ListViewModel을 @ObservedObject로 관찰한다. 특정 Cell의 좋아요 상태만 바뀌더라도 ListViewModel.objectWillChange가 발생하고, 같은 객체를 관찰하는 Cell들이 업데이트 대상으로 잡힐 수 있다. 문제의 핵심은 Cell이 화면에 필요한 값인 item만 받는 것이 아니라, 변경 알림을 발생시키는 ViewModel 자체를 직접 구독하고 있었다는 점이다. 이러면 특정 item 하나만 바뀐 상황에서도 여러 Cell의 body가 다시 호출될 수 있고, 그 이후 diffing/reconciliation 후보 범위도 넓어진다. 그래서 Cell에서 @ObservedObject를 제거하고, 화면 결과를 결정하는 값과 사용자 이벤트를 분리했다. // MARK: - Cell (값 타입 데이터 + 액션) struct CellView: View, Equatable { let item: Item let action: () -&gt; Void static func == (lhs: CellView, rhs: CellView) -&gt; Bool { lhs.item == rhs.item } var body: some View { print("body 호출: \(item.id)") return HStack { Text(item.name) Spacer() Button { action() } label: { Image(systemName: item.isLiked ? "heart.fill" : "heart") .foregroundColor(item.isLiked ? .red : .gray) } } .padding() .background(Color.gray.opacity(0.1)) } } 변경 후 Cell은 ViewModel을 직접 관찰하지 않는다. Cell은 화면을 그리는 데 필요한 item 값만 받고, 좋아요 버튼을 눌렀을 때 실행할 동작은 action 클로저로 상위 View에 위임한다. 여기서 item은 화면 결과를 결정하는 값이고, action은 이벤트를 처리하기 위한 값이다. action 클로저는 Equatable로 비교할 수 없고, 부모 body가 다시 호출될 때마다 새로 만들어질 수 있다. 하지만 action 자체가 Cell의 현재 화면 결과를 결정하는 값은 아니다. 그래서 Equatable 구현에서는 item만 비교한다. static func == (lhs: CellView, rhs: CellView) -&gt; Bool { lhs.item == rhs.item } 이렇게 하면 부모 View의 body가 다시 호출되어 CellView 값이 새로 만들어지더라도, item이 같다면 같은 Cell로 판단할 수 있다. 반대로 좋아요 상태처럼 item의 화면 결과가 달라지는 값이 바뀌면 비교 결과가 달라지고, 해당 Cell은 다시 계산될 수 있다. 정리하면 팝팡에서 적용한 방향은 다음과 같다. Cell에서 @ObservedObject를 제거한다. Cell에는 화면 결과를 결정하는 값 타입 데이터만 전달한다. 사용자 액션은 closure로 상위 View에 위임한다. Cell에 Equatable을 적용하고, closure는 비교 기준에서 제외한다. 변경과 무관한 Cell의 body 호출과 그 이후 diffing/reconciliation 후보 범위를 줄인다.]]></summary></entry><entry><title type="html">[ReactorKit] 개념</title><link href="https://indextrown.github.io/ReactorKit-ReactorKit1/" rel="alternate" type="text/html" title="[ReactorKit] 개념" /><published>2026-04-19T00:00:00+00:00</published><updated>2026-04-19T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BReactorKit%5D-ReactorKit1</id><content type="html" xml:base="https://indextrown.github.io/ReactorKit-ReactorKit1/"><![CDATA[<h2 id="reactorkit">ReactorKit</h2>
<ul>
  <li>Flux와 반응형 프로그래밍을 결합한 개념이다.</li>
  <li>Rx의 개념인 Observable과 함께 사용하여 비동기적으로 데이터를 처리하고, UI와의 상호 작용을 쉽게 관리할 수 있는 구조를 만든다.</li>
  <li>MVVM 모델의 ViewModel 역할을 하며 비즈니스 로직을 처리하고 View와 상호 작용하는 방법을 제공한다.</li>
  <li>UI관련 코드와 비즈니스 로직을 분리하고, 코드의 재사용성이 높아진다.</li>
</ul>

<h2 id="핵심-장점">핵심 장점</h2>
<ul>
  <li>사용법이 간단하다.</li>
  <li>특정 부분에만 적용할 수 있다.</li>
  <li>테스트하기 쉽다. (Reactor와 View에 대해 의존성이 없기 때문)</li>
  <li>유지보수하기 쉽다. (상태값 관리 용이)</li>
  <li>코드가 간결해진다. (ViewController에 있는 로직을 분리해서)</li>
</ul>

<p><img src="/assets/img/2026-04-19-%5BReactorKit%5D-ReactorKit1/flow1.webp" alt="flow1" /></p>
<ul>
  <li>사용자의 Action은 Reactor로, Reactor에서 방출된 State는 Observable 스트림을 통해 전달된다.</li>
  <li>View는 Action만 방출할 수 있으며, Reactor는 State만 망출할 수 있다.</li>
  <li>단방향적 흐름으로 View와 비즈니스 로직을 분리할 수 있으며 모듈 간 결합도가 낮게 개발할 수 있다.</li>
</ul>

<p><img src="/assets/img/2026-04-19-%5BReactorKit%5D-ReactorKit1/flow2.webp" alt="flow2" /></p>
<ul>
  <li>View: Reactor로 받는 State를 통해 UI를 보여주는 역할과 사용자 인터렉션을 추상화하여 Reactor로 전달하는 역할을 수행한다.</li>
  <li>Reactor: View로부터 Action Stream을 전달 받아 내부에서 mutate()와 reduce() 과정을 거쳐서 State Stream으로 바꾸어 다시 View로 전달해주는 역할을 한다. 이를 적용하기 위해 Reactor 프로토콜을 적용해야 하며, 그 내부에는 사용자 인터렉션을 의미하는 Action과 State매개체인 Mutation, View가 가질 상태를 의미하는 State를 반드시 정의해야 한다. 또한 State의 초기값을 설정하기 위해 inititlState가 필요하다.
    <ul>
      <li>mutate(action:) -&gt; Observable<Mutation>: Action 스트림을 Mutation 스트림으로 변환, 변환된 Mutation스트림을 reduce함수로 전달한다.</Mutation></li>
      <li>reduce(state:mutation:) -&gt; State: 이전 State와 Mutation을 활용하여 새로운 State를 반환한다. 이 State를 View에서 구독하고 있었다면 State 변경됨에 따라 UI가 업데이트 된다.</li>
    </ul>
  </li>
</ul>

<h2 id="reference">Reference</h2>
<ul>
  <li>https://ios-development.tistory.com/782</li>
  <li>https://oliveyoung.tech/2023-05-20/OliveYoung-iOS-ReactorKit/</li>
</ul>]]></content><author><name>Dong Hyeon</name></author><category term="ReactorKit" /><summary type="html"><![CDATA[ReactorKit Flux와 반응형 프로그래밍을 결합한 개념이다. Rx의 개념인 Observable과 함께 사용하여 비동기적으로 데이터를 처리하고, UI와의 상호 작용을 쉽게 관리할 수 있는 구조를 만든다. MVVM 모델의 ViewModel 역할을 하며 비즈니스 로직을 처리하고 View와 상호 작용하는 방법을 제공한다. UI관련 코드와 비즈니스 로직을 분리하고, 코드의 재사용성이 높아진다. 핵심 장점 사용법이 간단하다. 특정 부분에만 적용할 수 있다. 테스트하기 쉽다. (Reactor와 View에 대해 의존성이 없기 때문) 유지보수하기 쉽다. (상태값 관리 용이) 코드가 간결해진다. (ViewController에 있는 로직을 분리해서) 사용자의 Action은 Reactor로, Reactor에서 방출된 State는 Observable 스트림을 통해 전달된다. View는 Action만 방출할 수 있으며, Reactor는 State만 망출할 수 있다. 단방향적 흐름으로 View와 비즈니스 로직을 분리할 수 있으며 모듈 간 결합도가 낮게 개발할 수 있다. View: Reactor로 받는 State를 통해 UI를 보여주는 역할과 사용자 인터렉션을 추상화하여 Reactor로 전달하는 역할을 수행한다. Reactor: View로부터 Action Stream을 전달 받아 내부에서 mutate()와 reduce() 과정을 거쳐서 State Stream으로 바꾸어 다시 View로 전달해주는 역할을 한다. 이를 적용하기 위해 Reactor 프로토콜을 적용해야 하며, 그 내부에는 사용자 인터렉션을 의미하는 Action과 State매개체인 Mutation, View가 가질 상태를 의미하는 State를 반드시 정의해야 한다. 또한 State의 초기값을 설정하기 위해 inititlState가 필요하다. mutate(action:) -&gt; Observable: Action 스트림을 Mutation 스트림으로 변환, 변환된 Mutation스트림을 reduce함수로 전달한다. reduce(state:mutation:) -&gt; State: 이전 State와 Mutation을 활용하여 새로운 State를 반환한다. 이 State를 View에서 구독하고 있었다면 State 변경됨에 따라 UI가 업데이트 된다. Reference https://ios-development.tistory.com/782 https://oliveyoung.tech/2023-05-20/OliveYoung-iOS-ReactorKit/]]></summary></entry><entry><title type="html">[UIKit] UICollectionView 정복하기</title><link href="https://indextrown.github.io/UIKit-UICollectionView/" rel="alternate" type="text/html" title="[UIKit] UICollectionView 정복하기" /><published>2026-04-19T00:00:00+00:00</published><updated>2026-04-19T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BUIKit%5D-UICollectionView</id><content type="html" xml:base="https://indextrown.github.io/UIKit-UICollectionView/"><![CDATA[<h2 id="uicollectionview">UICollectionView</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@MainActor</span>
<span class="kd">class</span> <span class="kt">UICollectionView</span>
</code></pre></div></div>
<ul>
  <li>정렬된 데이터 항목 모음을 관리하고 사용자 정의 가능한 레이아웃을 사용하여 표시하는 객체이다.</li>
</ul>

<h2 id="datasource">DataSource</h2>
<ul>
  <li>컬렉션뷰는 컬렉션뷰의 속성에 저장된 데이터 소스 객체에서 데이터를 가져온다.
    <ul>
      <li>데이터 소스에는 컬렉션뷰의 데이터와 사용자 인터페이스 업데이트를 간단하고 효율적으로 관리하는 데 필요한 동적을 제공한다.</li>
      <li>프로토콜을 채택하여 사용자 지정 데이터소스 객체를 만들수도 있다.</li>
    </ul>
  </li>
  <li><a href="https://developer.apple.com/documentation/uikit/uicollectionviewdatasource">UICollectionViewDataSource</a></li>
  <li><a href="https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource-9tqpa">UICollectionViewDiffableDataSource</a></li>
</ul>

<h2 id="item">Item</h2>
<ul>
  <li>item은 표시하려는 가장 작은 데이터 단위이다.</li>
  <li>collectionView의 데이터는 개별 item으로 구분되고, 이러한 iteme들은 그룹화하여 표시할 수 있다.</li>
  <li>collectionView는 datasource가 구성하고 제공하는 클래스의 인스턴스인 셀을 사용하여 화면에 item을 표시한다.</li>
  <li><a href="https://developer.apple.com/documentation/uikit/uicollectionviewcell">UICollectionViewCell</a></li>
</ul>

<h2 id="supplementary-view">Supplementary View</h2>
<ul>
  <li>collectionView는 셀 외에도 다른 유형의 뷰를 사용하여 데이터를 표시할 수 있다. 이러한 보조 뷰는 섹션 헤더 밑 푸터가 될 수 있다.</li>
  <li>Supplementary View는 선택사항이고 collectionView의 layout 객체에서 정으된다.</li>
</ul>

<h2 id="uicollectionviewdiffabledatasource">UICollectionViewDiffableDataSource</h2>
<ul>
  <li>컬렉션뷰의 메서드를 사용해 항목의 시각적 표현이 데이터 소스 개체의 순서와 일치하도록 할 수 있다.</li>
  <li>컬렉션에서 데이터를 추가, 삭제 또는 재배열할 때마다 컬렉션 뷰의 메서드를 사용하여 해당 셀을 삽입, 삭제, 재배열한다.</li>
</ul>

<h2 id="layout">Layout</h2>
<ul>
  <li>Layout객체는 CollectionView에서 콘텐츠의 시각적 배치를 정의한다.</li>
  <li>클래스의 하위 클래스인 레이아웃 객체는 컬렉션 뷰 내의 모든 셀과 보조 뷰의 구성 위치를 정의한다.</li>
  <li>레이아웃 객체는 위치를 정의하지만 실제로 해당 정보를 해당 뷰에 적용하지는 않는다.</li>
  <li><a href="https://developer.apple.com/documentation/uikit/uicollectionviewlayout">UICollectionViewLayout</a></li>
</ul>

<h2 id="cell--supplementary-views">Cell &amp; Supplementary Views</h2>
<ul>
  <li>collectionView의 datasource는 item에 대한 콘텐츠와 해당 콘텐츠를 표시하는 데 사용되는 뷰를 모두 제공한다.</li>
  <li>컬렉션 뷰가 처음 로드 시 데이터소스에 표시되는 각 항목에 대한 뷰를 제공하도록 요청한다. 컬렉션 뷰는 데이터 소스가 재사용을 위해 표시한 뷰 객체의 큐 또는 목록을 유지한다.</li>
  <li>코드에서 명시적으로 새 뷰를 가져오는 것이 아니라 항상 큐에서 뷰를 가져온다.</li>
</ul>]]></content><author><name>Dong Hyeon</name></author><category term="UIKit" /><summary type="html"><![CDATA[UICollectionView @MainActor class UICollectionView 정렬된 데이터 항목 모음을 관리하고 사용자 정의 가능한 레이아웃을 사용하여 표시하는 객체이다. DataSource 컬렉션뷰는 컬렉션뷰의 속성에 저장된 데이터 소스 객체에서 데이터를 가져온다. 데이터 소스에는 컬렉션뷰의 데이터와 사용자 인터페이스 업데이트를 간단하고 효율적으로 관리하는 데 필요한 동적을 제공한다. 프로토콜을 채택하여 사용자 지정 데이터소스 객체를 만들수도 있다. UICollectionViewDataSource UICollectionViewDiffableDataSource Item item은 표시하려는 가장 작은 데이터 단위이다. collectionView의 데이터는 개별 item으로 구분되고, 이러한 iteme들은 그룹화하여 표시할 수 있다. collectionView는 datasource가 구성하고 제공하는 클래스의 인스턴스인 셀을 사용하여 화면에 item을 표시한다. UICollectionViewCell Supplementary View collectionView는 셀 외에도 다른 유형의 뷰를 사용하여 데이터를 표시할 수 있다. 이러한 보조 뷰는 섹션 헤더 밑 푸터가 될 수 있다. Supplementary View는 선택사항이고 collectionView의 layout 객체에서 정으된다. UICollectionViewDiffableDataSource 컬렉션뷰의 메서드를 사용해 항목의 시각적 표현이 데이터 소스 개체의 순서와 일치하도록 할 수 있다. 컬렉션에서 데이터를 추가, 삭제 또는 재배열할 때마다 컬렉션 뷰의 메서드를 사용하여 해당 셀을 삽입, 삭제, 재배열한다. Layout Layout객체는 CollectionView에서 콘텐츠의 시각적 배치를 정의한다. 클래스의 하위 클래스인 레이아웃 객체는 컬렉션 뷰 내의 모든 셀과 보조 뷰의 구성 위치를 정의한다. 레이아웃 객체는 위치를 정의하지만 실제로 해당 정보를 해당 뷰에 적용하지는 않는다. UICollectionViewLayout Cell &amp; Supplementary Views collectionView의 datasource는 item에 대한 콘텐츠와 해당 콘텐츠를 표시하는 데 사용되는 뷰를 모두 제공한다. 컬렉션 뷰가 처음 로드 시 데이터소스에 표시되는 각 항목에 대한 뷰를 제공하도록 요청한다. 컬렉션 뷰는 데이터 소스가 재사용을 위해 표시한 뷰 객체의 큐 또는 목록을 유지한다. 코드에서 명시적으로 새 뷰를 가져오는 것이 아니라 항상 큐에서 뷰를 가져온다.]]></summary></entry><entry><title type="html">[Swift] 12. 클래스</title><link href="https://indextrown.github.io/Swift-12-class/" rel="alternate" type="text/html" title="[Swift] 12. 클래스" /><published>2026-04-17T00:00:00+00:00</published><updated>2026-04-17T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BSwift-12%5D-class</id><content type="html" xml:base="https://indextrown.github.io/Swift-12-class/"><![CDATA[<h2 id="클래스">클래스</h2>]]></content><author><name>Dong Hyeon</name></author><category term="Swift" /><category term="Grammer" /><summary type="html"><![CDATA[클래스]]></summary></entry><entry><title type="html">[RxSwift]</title><link href="https://indextrown.github.io/RxLab-rx1/" rel="alternate" type="text/html" title="[RxSwift]" /><published>2026-04-17T00:00:00+00:00</published><updated>2026-04-17T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BRxLab%5D-rx1</id><content type="html" xml:base="https://indextrown.github.io/RxLab-rx1/"><![CDATA[<!-- <img src="/%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B2%BD%EB%A1%9C" alt="이미지" width="30%"> -->

<h2 id="비동기-이벤트-예시">비동기 이벤트 예시</h2>
<ul>
  <li>버튼의 클릭을 나타내는 UIButton의 Tap 이벤트는 비동기 이벤트로, 데이터의 변화에 대해 관찰이 가능한 형태다.</li>
  <li>RxSwift에서 이를 Observable 형태로 만들 수 있다.</li>
</ul>

<h2 id="observable">Observable</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="kt">Observable</span><span class="o">&lt;</span><span class="kt">Element</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">ObservableType</span> <span class="p">{}</span>
</code></pre></div></div>
<ul>
  <li>관찰이 가능한 흐름으로, 비동기 이벤트의 시퀀스를 생성할 수 있는 대상을 말한다.</li>
  <li>비동기 이벤트를 어떤 관찰 가능한 타입으로 만든다 == <code class="language-plaintext highlighter-rouge">비동기 이벤트를 제네릭 타입의 Observable이란 클래스의 인스턴스로 만든다는 것이다</code></li>
  <li><code class="language-plaintext highlighter-rouge">이벤트가 발생시 이를 알리기 위해 그 이벤트에 대한 항목을 시퀀스로 방출한다</code> 이를 obser가 받는다</li>
</ul>

<h2 id="observer">Observer</h2>
<ul>
  <li>Observer는 Observable을 구독한다</li>
  <li>Observer는 내가 원하는 비동기 이벤트를 방출하는 <code class="language-plaintext highlighter-rouge">Observable을 구독할 경우</code><br />
그 Observable이 비동기 이벤트가 실행되어 <code class="language-plaintext highlighter-rouge">항목(item)이 방출되면 그 항목을 받을 수 있다.</code></li>
  <li>Observer가 Observable을 구독할 수 있게 해주는 것이 <code class="language-plaintext highlighter-rouge">Subscrive</code>라는 메서드다.</li>
</ul>

<h2 id="subscribe">Subscribe</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">func</span> <span class="nf">subscribe</span><span class="p">(</span>
    <span class="nv">onNext</span><span class="p">:</span> <span class="p">((</span><span class="kt">Element</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)?</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">,</span>      <span class="c1">// 구독하려는 Observable이 항목(item)을 방출 시 어떤 작업을 할 것인지</span>
    <span class="nv">onError</span><span class="p">:</span> <span class="p">((</span><span class="kt">Swift</span><span class="o">.</span><span class="kt">Error</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)?</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">,</span> <span class="c1">// 해당 Observable이 오류 발생 시 어떤 작업을 할 것인지</span>
    <span class="nv">onCompleted</span><span class="p">:</span> <span class="p">(()</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)?</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">,</span>        <span class="c1">// 더 이상 이벤트 발생하지 않고 종료됐을 때 어떤 작업을 할 것인지</span>
    <span class="nv">onDisposed</span><span class="p">:</span> <span class="p">(()</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)?</span> <span class="o">=</span> <span class="kc">nil</span>         
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Disposable</span>
</code></pre></div></div>

<h2 id="uibutton-클릭-이벤트-예시observable-observer-subscribe">UIButton 클릭 이벤트 예시(Observable, Observer, Subscribe</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sodeulButton</span>
    <span class="o">.</span><span class="n">rx</span>
    <span class="o">.</span><span class="n">tap</span>
    <span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="nv">onNext</span><span class="p">:</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Observable이 항목을 방출 했다!"</span><span class="p">)</span>
    <span class="p">},</span>
    <span class="nv">onError</span><span class="p">:</span> <span class="p">{</span> <span class="n">error</span> <span class="k">in</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"에러가 발생 했다!"</span><span class="p">)</span>
    <span class="p">},</span>
    <span class="nv">onCompleted</span><span class="p">:</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"해당 이벤트가 끝났다!"</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="o">.</span><span class="nf">disposed</span><span class="p">(</span><span class="nv">by</span><span class="p">:</span> <span class="n">disposedBag</span><span class="p">)</span>
</code></pre></div></div>
<ul>
  <li>tap 이벤트 발생 시 subscribe(onNext:…)를 통해 “구독”을 해서 해당 Observable이 방출하는 항목에 대해 받을 수 있다.</li>
  <li>이 메서드의 파라미터로 아래 3가지를 각각 넘겨줄 수 있다.
    <ul>
      <li>onNext(항목이 방출 됐을 때, 즉 버튼이 눌렸을 때 실행시킬 클로저)</li>
      <li>onError(에러가 발생 했을 때 실행시킬 클로저)</li>
      <li>onCompleted(이벤트가 종료됐을 때 실행시킬 클로저)</li>
    </ul>
  </li>
</ul>

<h2 id="subscribe-메서드를-사용하면-메서드-내부에서-observer를-자체적으로-생성한다">subscribe 메서드를 사용하면 메서드 내부에서 Observer를 자체적으로 생성한다.</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 자체적으로 AnnonymousObserver라는 것을 생성해서 해당 Observable에 subscribe를 해준다.</span>
<span class="k">let</span> <span class="nv">observer</span> <span class="o">=</span> <span class="kt">AnnonymousObserver</span><span class="o">&lt;</span><span class="kt">Element</span><span class="o">&gt;</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="k">return</span> <span class="kt">Disposables</span><span class="o">.</span><span class="nf">create</span><span class="p">(</span>
    <span class="k">self</span><span class="o">.</span><span class="nf">asObservable</span><span class="p">()</span><span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">observer</span><span class="p">),</span>
    <span class="n">disposable</span>
<span class="p">)</span>
</code></pre></div></div>
<ul>
  <li>subscribe(onNext:…) 메서드를 사용할 경우, 메서드 내부에서 옵저버를 자체적으로 생성해서 <br />
우리가 직접 Observer를 생성하지 않고 파라미터로 클로저만 넘겨줘도 Observable을 구독하여 방출하는 항목을 받을 수 있게 된다.</li>
</ul>

<h2 id="reference">Reference</h2>
<ul>
  <li>https://babbab2.tistory.com/182</li>
  <li>https://babbab2.tistory.com/185</li>
</ul>]]></content><author><name>Dong Hyeon</name></author><category term="RxSwift" /><summary type="html"><![CDATA[비동기 이벤트 예시 버튼의 클릭을 나타내는 UIButton의 Tap 이벤트는 비동기 이벤트로, 데이터의 변화에 대해 관찰이 가능한 형태다. RxSwift에서 이를 Observable 형태로 만들 수 있다. Observable public class Observable&lt;Element&gt;: ObservableType {} 관찰이 가능한 흐름으로, 비동기 이벤트의 시퀀스를 생성할 수 있는 대상을 말한다. 비동기 이벤트를 어떤 관찰 가능한 타입으로 만든다 == 비동기 이벤트를 제네릭 타입의 Observable이란 클래스의 인스턴스로 만든다는 것이다 이벤트가 발생시 이를 알리기 위해 그 이벤트에 대한 항목을 시퀀스로 방출한다 이를 obser가 받는다 Observer Observer는 Observable을 구독한다 Observer는 내가 원하는 비동기 이벤트를 방출하는 Observable을 구독할 경우 그 Observable이 비동기 이벤트가 실행되어 항목(item)이 방출되면 그 항목을 받을 수 있다. Observer가 Observable을 구독할 수 있게 해주는 것이 Subscrive라는 메서드다. Subscribe public func subscribe( onNext: ((Element) -&gt; Void)? = nil, // 구독하려는 Observable이 항목(item)을 방출 시 어떤 작업을 할 것인지 onError: ((Swift.Error) -&gt; Void)? = nil, // 해당 Observable이 오류 발생 시 어떤 작업을 할 것인지 onCompleted: (() -&gt; Void)? = nil, // 더 이상 이벤트 발생하지 않고 종료됐을 때 어떤 작업을 할 것인지 onDisposed: (() -&gt; Void)? = nil ) -&gt; Disposable UIButton 클릭 이벤트 예시(Observable, Observer, Subscribe sodeulButton .rx .tap .subscribe(onNext: { print("Observable이 항목을 방출 했다!") }, onError: { error in print("에러가 발생 했다!") }, onCompleted: { print("해당 이벤트가 끝났다!") }) .disposed(by: disposedBag) tap 이벤트 발생 시 subscribe(onNext:…)를 통해 “구독”을 해서 해당 Observable이 방출하는 항목에 대해 받을 수 있다. 이 메서드의 파라미터로 아래 3가지를 각각 넘겨줄 수 있다. onNext(항목이 방출 됐을 때, 즉 버튼이 눌렸을 때 실행시킬 클로저) onError(에러가 발생 했을 때 실행시킬 클로저) onCompleted(이벤트가 종료됐을 때 실행시킬 클로저) subscribe 메서드를 사용하면 메서드 내부에서 Observer를 자체적으로 생성한다. // 자체적으로 AnnonymousObserver라는 것을 생성해서 해당 Observable에 subscribe를 해준다. let observer = AnnonymousObserver&lt;Element&gt; { ... } return Disposables.create( self.asObservable().subscribe(observer), disposable ) subscribe(onNext:…) 메서드를 사용할 경우, 메서드 내부에서 옵저버를 자체적으로 생성해서 우리가 직접 Observer를 생성하지 않고 파라미터로 클로저만 넘겨줘도 Observable을 구독하여 방출하는 항목을 받을 수 있게 된다. Reference https://babbab2.tistory.com/182 https://babbab2.tistory.com/185]]></summary></entry><entry><title type="html">[Swift] 7. 함수</title><link href="https://indextrown.github.io/Swift-07-func/" rel="alternate" type="text/html" title="[Swift] 7. 함수" /><published>2026-04-16T00:00:00+00:00</published><updated>2026-04-16T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BSwift-07%5D-func</id><content type="html" xml:base="https://indextrown.github.io/Swift-07-func/"><![CDATA[<h2 id="함수">함수</h2>
<ul>
  <li>특정 작업을 수행하는 독립적인 코드 덩어리다.</li>
  <li>함수가 하는 일을 식별하는 이름을 지정하며, 필요할 때 이 이름을 호출하여 작업을 수행한다.</li>
</ul>

<h2 id="함수-키워드">함수 키워드</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// num1, num2: Argument Label(파라미터를 외부에서 볼 때, 함수 호출 시 사용하려는 이름)</span>
<span class="c1">// a, b: parameter</span>
<span class="c1">// _(Argumant Label 생략하고 싶을 때) : 와일드카드 패턴</span>
<span class="kd">func</span> <span class="nf">plusFunc</span><span class="p">(</span><span class="n">num1</span> <span class="nv">a</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">num2</span><span class="p">:</span> <span class="nv">b</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>

<span class="p">}</span>

<span class="c1">// Int...: 가변 파라미터(Variadic Parameters)로 정해지지 않은 여러개의 파라미터를 받을 때</span>
<span class="c1">// num: Int = 5: 기본값 설정</span>
<span class="kd">func</span> <span class="nf">plusFunc</span><span class="p">(</span><span class="nv">numbers</span><span class="p">:</span> <span class="kt">Int</span><span class="o">...</span><span class="p">,</span> <span class="nv">num</span><span class="p">:</span> <span class="kt">Int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>

<span class="p">}</span>
</code></pre></div></div>

<h2 id="함수-오버로딩">함수 오버로딩</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">do</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="p">{}</span>
<span class="kd">func</span> <span class="nf">do</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{}</span>
<span class="kd">func</span> <span class="nf">do</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{}</span>
</code></pre></div></div>
<ul>
  <li>swift는 1) 함수 이름, 2) 파라미터수/타입, 3) Argument Label, 4) 리턴타입을 모두 포함해서 함수를 식별하는 Overloading을 지원한다.</li>
  <li>하나의 함수 이름에 여러개의 함수를 대응 시킬 수 있다.</li>
</ul>

<h2 id="inout">inout</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">first</span> <span class="o">=</span> <span class="mi">10</span>
<span class="k">var</span> <span class="nv">second</span> <span class="o">=</span> <span class="mi">20</span>

<span class="kd">func</span> <span class="nf">swap</span><span class="p">(</span><span class="n">_</span> <span class="nv">a</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">Int</span><span class="p">,</span> <span class="n">_</span> <span class="nv">b</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">Int</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 2. 함수 내부에서 복사된 값을 변경</span>
    <span class="k">let</span> <span class="nv">temp</span> <span class="o">=</span> <span class="n">a</span>
    <span class="n">a</span> <span class="o">=</span> <span class="n">b</span>
    <span class="n">b</span> <span class="o">=</span> <span class="n">temp</span>
<span class="p">}</span>

<span class="c1">// 1. copy-in: first, second의 값이 함수 내부로 복사됨</span>
<span class="nf">swap</span><span class="p">(</span><span class="o">&amp;</span><span class="n">first</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">second</span><span class="p">)</span>

<span class="c1">// 3. copy-out: 함수 내부에서 변경된 값이 다시 first, second에 반영됨</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"firstInt is now </span><span class="se">\(</span><span class="n">first</span><span class="se">)</span><span class="s">, and secondInt is now </span><span class="se">\(</span><span class="n">second</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>
<ul>
  <li>함수의 파라미터는 기본적으로 값처럼 전달되며, 함수 내부에서는 상수처럼 취급되기 때문에 직접 변경할 수 없다.</li>
  <li>inout은 함수 내부에서 외부 변수의 값을 변경할 수 있도록 해주는 키워드이다. 호출할 때는 &amp;를 사용해 해당 변수를 전달한다.</li>
  <li>inout은 참조 전달과 완전히 같은 개념은 아니다. 즉, 단순히 변수의 주소를 직접 넘겨서 수정하는 방식으로만 이해하면 정확하지 않다.</li>
  <li>Swift의 inout은 개념적으로 copy-in copy-out 방식으로 이해할 수 있다.</li>
  <li>즉, 함수 호출 시 원래 값을 함수 내부로 복사해 오고(copy-in), 함수 내부에서 값을 변경한 뒤, 함수가 끝날 때 변경된 값을 다시 원래 변수에 반영한다(copy-out).</li>
  <li>단, 실제 실행 시에는 성능 최적화를 통해 복사가 생략되고 참조처럼 동작할 수 있지만, 개념적으로는 항상 copy-in copy-out으로 이해하는 것이 안전하다.</li>
</ul>

<h2 id="reference">Reference</h2>
<ul>
  <li>https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID545</li>
  <li>https://jud00.tistory.com/entry/오늘의-Swift-지식-In-Out이란-무엇일까</li>
  <li>https://velog.io/@juseojo/TIL-Swift의-inout-파라미터-정말-참조를-전달할까-Copy-In-Copy-Out-파헤치기</li>
  <li>https://hyunsikwon.github.io/swift/Swift-Inout-01/</li>
</ul>]]></content><author><name>Dong Hyeon</name></author><category term="Swift" /><category term="Grammer" /><summary type="html"><![CDATA[함수 특정 작업을 수행하는 독립적인 코드 덩어리다. 함수가 하는 일을 식별하는 이름을 지정하며, 필요할 때 이 이름을 호출하여 작업을 수행한다. 함수 키워드 // num1, num2: Argument Label(파라미터를 외부에서 볼 때, 함수 호출 시 사용하려는 이름) // a, b: parameter // _(Argumant Label 생략하고 싶을 때) : 와일드카드 패턴 func plusFunc(num1 a: Int, num2: b: Int) -&gt; Int { } // Int...: 가변 파라미터(Variadic Parameters)로 정해지지 않은 여러개의 파라미터를 받을 때 // num: Int = 5: 기본값 설정 func plusFunc(numbers: Int..., num: Int = 5) -&gt; Int { } 함수 오버로딩 func do(value: Int) {} func do(value: Double) {} func do(value: String) {} swift는 1) 함수 이름, 2) 파라미터수/타입, 3) Argument Label, 4) 리턴타입을 모두 포함해서 함수를 식별하는 Overloading을 지원한다. 하나의 함수 이름에 여러개의 함수를 대응 시킬 수 있다. inout var first = 10 var second = 20 func swap(_ a: inout Int, _ b: inout Int) { // 2. 함수 내부에서 복사된 값을 변경 let temp = a a = b b = temp } // 1. copy-in: first, second의 값이 함수 내부로 복사됨 swap(&amp;first, &amp;second) // 3. copy-out: 함수 내부에서 변경된 값이 다시 first, second에 반영됨 print("firstInt is now \(first), and secondInt is now \(second)") 함수의 파라미터는 기본적으로 값처럼 전달되며, 함수 내부에서는 상수처럼 취급되기 때문에 직접 변경할 수 없다. inout은 함수 내부에서 외부 변수의 값을 변경할 수 있도록 해주는 키워드이다. 호출할 때는 &amp;를 사용해 해당 변수를 전달한다. inout은 참조 전달과 완전히 같은 개념은 아니다. 즉, 단순히 변수의 주소를 직접 넘겨서 수정하는 방식으로만 이해하면 정확하지 않다. Swift의 inout은 개념적으로 copy-in copy-out 방식으로 이해할 수 있다. 즉, 함수 호출 시 원래 값을 함수 내부로 복사해 오고(copy-in), 함수 내부에서 값을 변경한 뒤, 함수가 끝날 때 변경된 값을 다시 원래 변수에 반영한다(copy-out). 단, 실제 실행 시에는 성능 최적화를 통해 복사가 생략되고 참조처럼 동작할 수 있지만, 개념적으로는 항상 copy-in copy-out으로 이해하는 것이 안전하다. Reference https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID545 https://jud00.tistory.com/entry/오늘의-Swift-지식-In-Out이란-무엇일까 https://velog.io/@juseojo/TIL-Swift의-inout-파라미터-정말-참조를-전달할까-Copy-In-Copy-Out-파헤치기 https://hyunsikwon.github.io/swift/Swift-Inout-01/]]></summary></entry><entry><title type="html">[Swift] 8. 옵셔널</title><link href="https://indextrown.github.io/Swift-08-optional/" rel="alternate" type="text/html" title="[Swift] 8. 옵셔널" /><published>2026-04-16T00:00:00+00:00</published><updated>2026-04-16T00:00:00+00:00</updated><id>https://indextrown.github.io/%5BSwift-08%5D-optional</id><content type="html" xml:base="https://indextrown.github.io/Swift-08-optional/"><![CDATA[<h2 id="옵셔널">옵셔널</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@frozen</span>
<span class="kd">enum</span> <span class="kt">Optional</span><span class="o">&lt;</span><span class="kt">Wrapped</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">Wrapped</span> <span class="p">:</span> <span class="kt">~Copyable</span><span class="p">,</span> <span class="kt">Wrapped</span> <span class="p">:</span> <span class="o">~</span><span class="kt">Escapable</span>
</code></pre></div></div>
<ul>
  <li>래핑된 값 또는 값이 없는 경우를 나타내는 타입입니다.</li>
</ul>

<h2 id="옵셔널-구현">옵셔널 구현</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">Optional</span><span class="o">&lt;</span><span class="kt">Wrapped</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">case</span> <span class="kd">some</span><span class="o">&lt;</span><span class="kt">Wrapped</span><span class="o">&gt;</span> <span class="c1">// 연관값</span>
    <span class="k">case</span> <span class="k">none</span>          <span class="c1">// 값이 없음, none는 nil과 완전 동일</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="옵셔널은-두-가지-경우를-가진-열거형이다">옵셔널은 두 가지 경우를 가진 열거형이다</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">number</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span> <span class="o">=</span> <span class="kt">Optional</span><span class="o">.</span><span class="nf">some</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">noNumber</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span> <span class="o">=</span> <span class="kt">Optional</span><span class="o">.</span><span class="k">none</span>
<span class="nf">print</span><span class="p">(</span><span class="n">noNumber</span> <span class="o">==</span> <span class="kc">nil</span><span class="p">)</span>
<span class="c1">// Prints "true"</span>
</code></pre></div></div>

<h2 id="옵셔널-값-추출-4가지">옵셔널 값 추출 4가지</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1) 강제 추출</span>
<span class="n">num</span><span class="o">!</span>

<span class="c1">// 2) nil이 아닌지 확인후 강제추출</span>
<span class="k">if</span> <span class="n">num</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="nf">print</span><span class="p">(</span><span class="n">num</span><span class="o">!</span><span class="p">)</span> <span class="p">}</span>

<span class="c1">// 3) 바인딩(상수나 변수에 대입)이 되는 경우만 특정 작업을 진행</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">name</span> <span class="o">=</span> <span class="n">optionalName</span> <span class="p">{</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 4) Nil-Coalescing: 옵셔널 표션식 뒤에 기본값을 제시해서 옵셔널 가능성을 없앤다.</span>
<span class="n">optionalName</span> <span class="p">??</span> <span class="s">"인덱스"</span>
</code></pre></div></div>

<h2 id="접근연산자-사용법">접근연산자 사용법</h2>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 옵셔널 체이닝</span>
<span class="c1">// - 표현식이 옵셔널의 가능성이 있다는 것을 표현, 체이닝 결과는 항상 옵셔널</span>
<span class="n">human</span><span class="p">?</span><span class="o">.</span><span class="n">choco</span><span class="p">?</span><span class="o">.</span><span class="n">name</span> <span class="c1">// 언래핑 필수</span>
</code></pre></div></div>

<h2 id="reference">Reference</h2>
<ul>
  <li>https://developer.apple.com/documentation/swift/optional</li>
</ul>]]></content><author><name>Dong Hyeon</name></author><category term="Swift" /><category term="Grammer" /><summary type="html"><![CDATA[옵셔널 @frozen enum Optional&lt;Wrapped&gt; where Wrapped : ~Copyable, Wrapped : ~Escapable 래핑된 값 또는 값이 없는 경우를 나타내는 타입입니다. 옵셔널 구현 enum Optional&lt;Wrapped&gt; { case some&lt;Wrapped&gt; // 연관값 case none // 값이 없음, none는 nil과 완전 동일 } 옵셔널은 두 가지 경우를 가진 열거형이다 let number: Int? = Optional.some(42) let noNumber: Int? = Optional.none print(noNumber == nil) // Prints "true" 옵셔널 값 추출 4가지 // 1) 강제 추출 num! // 2) nil이 아닌지 확인후 강제추출 if num != nil { print(num!) } // 3) 바인딩(상수나 변수에 대입)이 되는 경우만 특정 작업을 진행 if let name = optionalName { print(name) } // 4) Nil-Coalescing: 옵셔널 표션식 뒤에 기본값을 제시해서 옵셔널 가능성을 없앤다. optionalName ?? "인덱스" 접근연산자 사용법 // 옵셔널 체이닝 // - 표현식이 옵셔널의 가능성이 있다는 것을 표현, 체이닝 결과는 항상 옵셔널 human?.choco?.name // 언래핑 필수 Reference https://developer.apple.com/documentation/swift/optional]]></summary></entry></feed>