<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>iOS 개발 기록</title>
    <link>https://leetaek.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 7 Apr 2026 10:48:34 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>택꽁이</managingEditor>
    <image>
      <title>iOS 개발 기록</title>
      <url>https://tistory1.daumcdn.net/tistory/5320325/attach/7be7190826a548069ae5901bf68811e8</url>
      <link>https://leetaek.tistory.com</link>
    </image>
    <item>
      <title>[AI] Codex - Worktree와 Local</title>
      <link>https://leetaek.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 내용이거나 수정사항이 있다면 알려주세욥 ...!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex를 쓰다 보면 같은 작업이어도&amp;nbsp;&lt;b&gt;Local에서는 잘 되는데 Worktree에서는 막히는 경우&lt;/b&gt;가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b93AKi/dJMcafF7lD6/1Vr9ndINxixg7fcyUuuRc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b93AKi/dJMcafF7lD6/1Vr9ndINxixg7fcyUuuRc0/img.png&quot; data-alt=&quot;worktree에서 테스트 돌렸다 발생한 에러&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b93AKi/dJMcafF7lD6/1Vr9ndINxixg7fcyUuuRc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb93AKi%2FdJMcafF7lD6%2F1Vr9ndINxixg7fcyUuuRc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;789&quot; height=&quot;212&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;worktree에서 테스트 돌렸다 발생한 에러&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;먼저 결론&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코드 수정, 브랜치 분리, PR 생성 중심 작업&lt;/b&gt;&amp;nbsp;&amp;rarr;&amp;nbsp;&lt;b&gt;Worktree&lt;/b&gt;가 적합하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Xcode, Simulator, 로컬 개발 환경에 의존하는 검증 작업&lt;/b&gt;&amp;nbsp;&amp;rarr;&amp;nbsp;&lt;b&gt;Local&lt;/b&gt;이 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worktree는 &lt;b&gt;격리된 작업 공간&lt;/b&gt;, Local은&amp;nbsp;&lt;b&gt;내가 실제로 개발하는 환경&lt;/b&gt;이라고 이해했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 이런 차이가 생기나 ?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Worktree&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex 공식 Troubleshooting 문서의 &lt;a href=&quot;https://developers.openai.com/codex/app/troubleshooting#code-doesnt-run-on-a-worktree&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Code doesn&amp;rsquo;t run on a worktree&lt;/b&gt;&lt;/a&gt;섹션에서는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worktree가 &lt;b&gt;기존 로컬 프로젝트와는 다른 디렉토리&lt;/b&gt;에 생성되며, &lt;b&gt;Git에 체크인된 파일들만 상속&lt;/b&gt;한다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소에 포함되지 않은 로컬 설정이나 의존성, 개발 중 생성된 파일들은 그대로 따라오지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프로젝트에 따라서는 worktree에서 &lt;b&gt;별도의 setup script를 다시 실행해야 할 수 있고&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대안으로는 기존의 regular local project에서 변경사항을 checkout해서 작업할 수도 있다고 안내한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Local&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex 공식 &lt;a href=&quot;https://developers.openai.com/codex/app/local-environments&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Local environments&lt;/b&gt;&lt;/a&gt; 문서에서도 비슷한 맥락을 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worktree는 local task와 &lt;b&gt;다른 디렉토리에서 실행되기 때문에&lt;/b&gt;, 프로젝트가 완전히 셋업되지 않았을 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소에 체크인되지 않은 의존성이나 파일이 빠질 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Codex는 worktree를 만들 때 필요한 환경을 맞추기 위해 &lt;b&gt;setup scripts를 함께 실행하는 방식&lt;/b&gt;을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서 기준으로 정리하면 이렇게 이해할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Worktree &lt;/b&gt;&amp;rarr; 브랜치 분리, 코드 수정, PR 생성 같은 작업에 유리, Git 기준으로 격리된 병렬 작업 공간&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Local &lt;/b&gt;&amp;rarr; Xcode, Simulator, 런타임, 체크인되지 않은 파일, 빌드 아티팩트에 민감한 작업에 더 유리할 수 있음&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Worktree가 잘 맞는 경우&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worktree는 현재 작업 중인 로컬 변경과 분리된 별도 공간에서 Codex가 작업하도록 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 같은 작업에 특히 잘 맞는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 기능 구현&lt;/li&gt;
&lt;li&gt;리팩토링&lt;/li&gt;
&lt;li&gt;테스트 코드 작성&lt;/li&gt;
&lt;li&gt;문서 수정&lt;/li&gt;
&lt;li&gt;브랜치 생성, 커밋, Draft PR 생성&lt;/li&gt;
&lt;li&gt;현재 작업 중인 변경사항과 섞이면 안 되는 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이전에 포스팅한 nightly work처럼 &amp;ldquo;이슈 하나 처리하고 브랜치 따서 PR까지 만들기&amp;rdquo; 같은 Git 중심의 흐름은 Worktree가 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Worktree의 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 작업 중인 변경사항과 충돌이 적다&lt;/li&gt;
&lt;li&gt;자동화에 올리기 좋다&lt;/li&gt;
&lt;li&gt;브랜치/PR 단위 작업이 깔끔하다&lt;/li&gt;
&lt;li&gt;실험적인 변경을 격리해서 다룰 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Worktree의 한계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Worktree는&amp;nbsp;&lt;b&gt;내 로컬 개발 환경 그 자체&lt;/b&gt;는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 같은 작업에서는 한계가 드러날 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Xcode가 실제로 열려 있어야 하는 경우&lt;/li&gt;
&lt;li&gt;iOS Simulator 접근이 필요한 경우&lt;/li&gt;
&lt;li&gt;CoreSimulatorService에 의존하는 테스트&lt;/li&gt;
&lt;li&gt;로컬에 이미 구성된 개발 환경을 그대로 써야 하는 경우&lt;/li&gt;
&lt;li&gt;앱 실행, 디버깅, UI 확인이 필요한 경우&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Local이 잘 맞는 경우&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Local은 내가 지금 실제로 쓰고 있는 프로젝트 디렉터리와 환경에서 Codex가 작업하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&amp;nbsp;내가 직접 개발하던 맥 환경을 그대로 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 같은 작업은 Local이 훨씬 안정적이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;xcodebuild test&lt;/li&gt;
&lt;li&gt;iPad/iPhone Simulator 기반 테스트&lt;/li&gt;
&lt;li&gt;Xcode, DerivedData, Simulator runtime 등 로컬 환경을 활용하는 작업&lt;/li&gt;
&lt;li&gt;직접 앱을 띄워 확인해야 하는 작업&lt;/li&gt;
&lt;li&gt;기존 workspace, scheme, simulator 상태를 그대로 써야 하는 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 iOS 프로젝트에서 테스트 코드는 Worktree에서도 잘 만들지만, 실제 test run은 시뮬레이터 검증은 Local이 더 안정적으로 돌아가는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Local의 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 개발 환경과 가장 가깝다&lt;/li&gt;
&lt;li&gt;Xcode/Simulator 의존 작업에 강하다&lt;/li&gt;
&lt;li&gt;실제 빌드/테스트 재현성이 높다&lt;/li&gt;
&lt;li&gt;로컬에서 이미 되던 흐름을 그대로 가져갈 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Local의 한계&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 작업 중인 변경사항을 직접 건드릴 수 있다&lt;/li&gt;
&lt;li&gt;자동화 시 격리가 덜 된다&lt;/li&gt;
&lt;li&gt;실험적 변경과 기존 작업이 섞일 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Xcode가 실제로 열려 있어야 하거나&lt;/li&gt;
&lt;li&gt;Simulator 상태를 그대로 활용해야 하거나&lt;/li&gt;
&lt;li&gt;DerivedData, runtime, local dependency 상태에 영향을 받는 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은 Worktree보다 &lt;b&gt;Local 환경이 더 안정적으로 동작할 가능성이 높다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 코드 수정 자체나 브랜치/PR 중심의 자동화는 Worktree가 더 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;reference&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.openai.com/codex/learn/best-practices?utm_source=chatgpt.com&quot;&gt;https://developers.openai.com/codex/learn/best-practices?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://developers.openai.com/codex/learn/best-practices?utm_source=chatgpt.com&quot;&gt;Best practices &amp;ndash; Codex | OpenAI Developers Getting started with Codex and proven practices for better results developers.openai.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775178510835&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Best practices &amp;ndash; Codex | OpenAI Developers&quot; data-og-description=&quot;Getting started with Codex and proven practices for better results&quot; data-og-host=&quot;developers.openai.com&quot; data-og-source-url=&quot;https://developers.openai.com/codex/learn/best-practices?utm_source=chatgpt.com&quot; data-og-url=&quot;https://developers.openai.com/codex/learn/best-practices/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dBJbXA/dJMb9gxlOxq/DkUKzxCFbF5kLBf4P9ucZ0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/sOvPE/dJMb84XY8yw/SLltK7OKruf0tPo2bReiPk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://developers.openai.com/codex/learn/best-practices?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.openai.com/codex/learn/best-practices?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dBJbXA/dJMb9gxlOxq/DkUKzxCFbF5kLBf4P9ucZ0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/sOvPE/dJMb84XY8yw/SLltK7OKruf0tPo2bReiPk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Best practices &amp;ndash; Codex | OpenAI Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Getting started with Codex and proven practices for better results&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.openai.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI</category>
      <category>AI</category>
      <category>automation</category>
      <category>ChatGPT</category>
      <category>codex</category>
      <category>github</category>
      <category>IOS</category>
      <category>자동화</category>
      <category>프롬프트</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/90</guid>
      <comments>https://leetaek.tistory.com/90#entry90comment</comments>
      <pubDate>Thu, 2 Apr 2026 18:01:00 +0900</pubDate>
    </item>
    <item>
      <title>[AI] Codex Automation으로 &amp;ldquo;이슈 1개 &amp;rarr; Draft PR&amp;rdquo; 밤새 돌려보기 (worktree/sandbox/gh/prompt)</title>
      <link>https://leetaek.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 글은 &amp;ldquo;Codex Automation&amp;rdquo;을 활용한 에이전트 코딩 관련 포스트의 3편이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://leetaek.tistory.com/87&quot;&gt;[AI]&amp;nbsp; 자동화를 위한 GitHub Issue 운영 체계 만들기 (라벨/템플릿/분류)&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;a href=&quot;https://leetaek.tistory.com/88&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[AI] GitHub Actions + BigQuery로 Nightly Report 자동 수집 파이프라인 구축하기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;b&gt;[AI] Codex Automation으로 &amp;ldquo;이슈 1개 &amp;rarr; Draft PR&amp;rdquo; 밤새 돌려보기 (worktree/sandbox/gh/prompt)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞선 1~2편에서 &lt;/span&gt;이슈를 모으고(운영 체계 + 라벨/템플릿)&lt;span&gt;, &lt;/span&gt;Nightly report로 신호를 쌓는 파이프라인&lt;span&gt;까지 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;&lt;span style=&quot;color: #0e0e0e; text-align: start;&quot;&gt;밤새 자동화가 이슈 하나를 가져가서, &amp;ldquo;수정 &amp;rarr; PR 생성&amp;rdquo;까지 해주는 흐름을 실제로 굴려보는&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #0e0e0e; text-align: start;&quot;&gt; 작업이 남았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 여기서 목표는 &amp;ldquo;자동 merge&amp;rdquo;가 아니다. 내가 기대하는 작업은 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안전하게(=격리된 환경에서)&lt;/li&gt;
&lt;li&gt;최소 변경으로&lt;/li&gt;
&lt;li&gt;Draft PR로 결과물을 남겨주기&lt;/li&gt;
&lt;li&gt;나는 낮에 PR을 보고 &lt;span&gt;결정만&lt;/span&gt; 하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기대하는 플로우&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Codex Automation이 조건에 맞는 이슈 1개 선택&lt;/li&gt;
&lt;li&gt;git worktree로 &lt;b&gt;격리된 작업 공간&lt;/b&gt; 생성&lt;/li&gt;
&lt;li&gt;sandbox 규칙 아래에서 최소 변경으로 수정&lt;/li&gt;
&lt;li&gt;커밋 생성&lt;/li&gt;
&lt;li&gt;gh로 Draft PR 생성&lt;/li&gt;
&lt;li&gt;(선택) PR 본문에 &amp;ldquo;변경 요약/테스트 플랜/리스크&amp;rdquo;까지 자동 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;위 플로우에서 내가 제일 먼저 챙긴 건 작업 공간 격리였다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;에이전트가 밤새 코드를 만지는 동안, 내 로컬 레포(내가 쓰는 브랜치)가 더러워지는 순간부터 짤막한 내 기억력으로는 관리 난이도가 확 올라간다 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 자동화에서는 git worktree를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worktree는 하나의 Git 레포(.git)는 공유하면서, 브랜치별로 별도의 작업 폴더(Working Directory)를 추가로 만들어 동시에 작업할 수 있게 해주는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worktree를 사용하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이전트가 파일을 많이 건드려도 내 메인 작업 폴더는 안전하게 유지되고&lt;/li&gt;
&lt;li&gt;이슈별로 작업 결과가 폴더 단위로 분리돼서 추적/리뷰가 쉬워지고&lt;/li&gt;
&lt;li&gt;실패하거나 마음에 안 들면 worktree 폴더를 지우는 것만으로 깨끗하게 정리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 밤에 자동화가 실수하더라도, 낮에 수습 가능한 형태로 남기기에 용이할 것이라 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이슈는 어떻게 고를까? (gh로 후보 추리기)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈를 골랐다면, 이제는 에이전트가 수정할 수 있는 범위를 제한해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 sandbox 규칙을 먼저 정해두고, 작업 범위를 강제로 좁혔다. 여기서 의외로 도움이 된 게 모듈화였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능이 모듈 단위로 분리되어 있으니 area:* 라벨만으로도 '어느 폴더/모듈을 건드리면 되는지'가 명확해졌고, 프롬프트에서 수정 범위를 딱 잘라 말할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;금지 사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 업데이트 / 빌드 설정 변경&lt;/li&gt;
&lt;li&gt;이슈와 무관한 리팩토링&lt;/li&gt;
&lt;li&gt;테스트가 없는 상태에서 대규모 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;완료 조건(반드시 남길 것)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;어떻게 검증했는지&amp;rdquo; 테스트 플랜 작성&lt;/li&gt;
&lt;li&gt;가능한 최소 수정&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;prompt&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 정리한 원칙(worktree 격리, 모듈 경계, 최소 변경, Draft PR)는 프롬프트에 박아넣었다. 프롬프트는 크게 네 덩어리로 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 &lt;b&gt;Preflight&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밤새 돌아가다가 인증 문제나 레이트리밋 때문에 1분 만에 허무한 상황을 방지한다. 그래서 시작하자마자 gh auth status와 rate_limit을 확인하고, GitHub API 호출이 불가능하면 그 자리에서 종료하도록 했다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;Preflight (log outputs in memo)
pwd
git rev-parse --show-toplevel
curl -sS -I &amp;lt;https://api.github.com&amp;gt; | head -n 1 || true
gh auth status || true
gh api &amp;lt;https://api.github.com/rate_limit&amp;gt; &amp;gt;/dev/null || { echo &quot;gh_api=FAIL&quot;; exit 0; }
echo &quot;gh_api=OK&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 &lt;b&gt;Issue selection&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화가 매번 흔들리지 않게 기준을 고정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go:fix + status:ready만 대상으로 삼고, 진행 중인 이슈(status:in-progress)는 제외한다. 우선순위는 prio:p0 &amp;rarr; p1 &amp;rarr; p2 순으로 내려가고, 같은 우선순위 안에서는 가장 최근 업데이트된 이슈 1개만 고른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서 이슈가 없으면 &amp;ldquo;할 게 없다&amp;rdquo;는 의미이므로 아무 변경도 하지 않고 멈춘다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;Issue selection (pick ONE, via gh)
- Try p0:
  gh issue list -R LeeTaek/Carve -S 'is:issue is:open label:go:fix label:status:ready -label:status:in-progress label:prio:p0' --json number,title,updatedAt,labels --limit 20
- Else p1, else p2 with the same format.
- Choose exactly one issue: the most recently updated (max updatedAt).
- If none, stop without making any changes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째가 &lt;b&gt;Scope boundary&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 모듈화해 둔 덕분에 area:* 라벨 하나만으로도 &amp;ldquo;고칠 범위&amp;rdquo;를 명확히 자를 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프롬프트에서 area:* 라벨을 수정 범위의 경계로 강제했다. area:* 라벨이 없거나, 여러 개가 붙어 범위가 애매하면 코딩을 시작하지 않고 이슈에 코멘트를 남기고 종료한다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;Issue selection (pick ONE, via gh)
- Try p0:
  gh issue list -R LeeTaek/Carve -S 'is:issue is:open label:go:fix label:status:ready -label:status:in-progress label:prio:p0' --json number,title,updatedAt,labels --limit 20
- Else p1, else p2 with the same format.
- Choose exactly one issue: the most recently updated (max updatedAt).
- If none, stop without making any changes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막은 결과물이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 &amp;ldquo;이슈 1개 &amp;rarr; Draft PR 1개&amp;rdquo;로 단순화했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈를 가져오면 먼저 한국어로 Expected/Actual을 1~3줄로 요약하고, 중복 작업을 막기 위해 status:ready를 제거하고 status:in-progress로 잠그도록 했다. 수정은 최소 단위로 하고, 가능하면 회귀 테스트를 추가한다&lt;br /&gt;(특히 TCA라면 TestStore 기반 리듀서 테스트를 우선).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 tuist test를 우선 시도하고, 안 되면 xcodebuild로 내려가며, 실행이 불가능한 환경이라면 실패 이유와 로컬에서 실행할 정확한 커맨드를 PR 본문에 남기게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막에 gh로 Draft PR을 생성하되, PR 본문에는 요약/변경 내용/테스트 방법/리스크&amp;middot;롤백/Closes #이슈번호를 반드시 포함하도록 강제했다. 이슈에도 PR 링크와 테스트 결과를 코멘트로 남기고, merge 전까지는 status:in-progress를 유지한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Workflow
1) Fetch the selected issue details and summarize in Korean (1-3 bullets) what must be fixed (Expected vs Actual).
2) Lock the issue:
   - Remove label &quot;status:ready&quot; and add label &quot;status:in-progress&quot;.
3) Create a branch: codex/issue-&amp;lt;ISSUE_NUMBER&amp;gt;-&amp;lt;short-slug&amp;gt;.
4) Implement the fix (respect the `area:*` scope boundary).
5) Add regression tests (prefer TCA TestStore reducer tests when applicable).
6) Run tests (best effort). Prefer Tuist:
   - Try: `tuist test` (or `tuist test &amp;lt;scheme&amp;gt;`).
   - Fallback: `xcodebuild test ...`.
   - If tests cannot run due to sandbox restrictions, record the exact failure and provide local commands to run.
7) Open a Draft PR targeting main:
   - Title format: [&amp;lt;area:...&amp;gt;] &amp;lt;issue title&amp;gt; (fallback: [BUG])
   - Body (Korean) must include:
     - 요약(무엇/왜)
     - 변경 내용(핵심 3줄)
     - 테스트 방법/결과(로컬 실행 커맨드 포함)
     - 리스크/롤백
     - Closes #&amp;lt;ISSUE_NUMBER&amp;gt;
8) Apply labels to the PR (best effort): copy `area:*` and `prio:*` from the issue.
9) Comment on the issue in Korean with PR link + summary + test results/commands.
10) Keep &quot;status:in-progress&quot; on the issue until merged. Do NOT re-add &quot;status:ready&quot; automatically.

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전문을 정리하면 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774510659070&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;You are working in the repository &quot;\{git repo}&amp;rdquo;.
Use gh CLI for all GitHub operations (issue selection/label changes/comments/PR creation).

Worktree / workspace

- Assume you are running inside an isolated git worktree directory created for this issue.
- Do NOT operate on any other working directory. All changes must stay within the current worktree.
- Log `pwd` and `git rev-parse --show-toplevel` to confirm the workspace.

Preflight (log outputs in memo)
pwd
git rev-parse --show-toplevel
curl -sS -I [https://api.github.com](https://api.github.com/) | head -n 1 || true
gh auth status || true
gh api https://api.github.com/rate_limit &amp;gt;/dev/null || { echo &quot;gh_api=FAIL&quot;; exit 0; }
echo &quot;gh_api=OK&quot;

Goal

- Every run, pick exactly ONE GitHub issue and produce a Draft PR that fixes it.
- Outputs (PR body + issue comment) must be written in Korean.

Issue selection (pick ONE, via gh)

- Try p0:
gh issue list -R LeeTaek/Carve -S 'is:issue is:open label:go:fix label:status:ready -label:status:in-progress label:prio:p0' --json number,title,updatedAt,labels --limit 20
- Else p1, else p2 with the same format.
- Choose exactly one issue: the most recently updated (max updatedAt).
- If none, stop without making any changes.

Scope boundary (module-aware / uses modularization)

- This repo is modularized. Use the issue's `area:*` label as the scope boundary.
- Identify exactly one `area:*` label on the issue.
    - If there is no `area:*`, stop: comment on the issue in Korean requesting an `area:*` label, and DO NOT code.
    - If there are multiple `area:*`, stop: comment asking to pick exactly one, and DO NOT code.
- Only modify files under the module/directory corresponding to that `area:*`.
- Do NOT broaden scope &amp;ldquo;just in case&amp;rdquo;. Keep the diff minimal and reversible.

Safety rules

- Do NOT change unrelated code. Keep the fix minimal.
- One issue = one PR. Do not bundle multiple issues.
- Do NOT merge. Create a Draft PR only.
- Do NOT upgrade dependencies, change build settings, or perform large-scale refactors.
- If you cannot run tests in this environment, still add tests and write exact local commands to run.

Workflow

1. Fetch the selected issue details and summarize in Korean (1-3 bullets) what must be fixed (Expected vs Actual).
2. Lock the issue:
    - Remove label &quot;status:ready&quot; and add label &quot;status:in-progress&quot;.
3. Create a branch: codex/issue-&amp;lt;ISSUE_NUMBER&amp;gt;-&amp;lt;short-slug&amp;gt;.
4. Implement the fix (respect the `area:*` scope boundary).
5. Add regression tests (prefer TCA TestStore reducer tests when applicable).
6. Run tests (best effort). Prefer Tuist:
    - Try: `tuist test` (or `tuist test &amp;lt;scheme&amp;gt;`).
    - Fallback: `xcodebuild test ...`.
    - If tests cannot run due to sandbox restrictions, record the exact failure and provide local commands to run.
7. Open a Draft PR targeting main:
    - Title format: [area:...] &amp;lt;issue title&amp;gt; (fallback: [BUG])
    - Body (Korean) must include:
        - 요약(무엇/왜)
        - 변경 내용(핵심 3줄)
        - 테스트 방법/결과(로컬 실행 커맨드 포함)
        - 리스크/롤백
        - Closes #&amp;lt;ISSUE_NUMBER&amp;gt;
8. Apply labels to the PR (best effort): copy `area:*` and `prio:*` from the issue.
9. Comment on the issue in Korean with PR link + summary + test results/commands.
10. Keep &quot;status:in-progress&quot; on the issue until merged. Do NOT re-add &quot;status:ready&quot; automatically.

Hints

- Respect TCA + MicroArchitecture.
- Variable names must be at least 2 characters long.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트를 만들었으면, 이제는 &amp;ldquo;진짜로 PR이 하나 올라오는지&amp;rdquo;를 확인해야 한다. 그래서 이슈 하나를 직접 만들어서, Codex Automation을 테스트해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;span data-token-index=&quot;0&quot;&gt;GitHub Issues에 테스트용 이슈를 작성&lt;/span&gt;하고 status:ready로 올렸다.(가능하면 범위가 좁고, area:* 라벨이 명확한 이슈로.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-26 오후 4.25.11.png&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVEkJp/dJMcahKBQ01/PzZGIV0zQPKUw5oWYVqAkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVEkJp/dJMcahKBQ01/PzZGIV0zQPKUw5oWYVqAkK/img.png&quot; data-alt=&quot;이미 테스트해서 Label이 status:in-progress로 바꼈다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVEkJp/dJMcahKBQ01/PzZGIV0zQPKUw5oWYVqAkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVEkJp%2FdJMcahKBQ01%2FPzZGIV0zQPKUw5oWYVqAkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1319&quot; height=&quot;879&quot; data-filename=&quot;스크린샷 2026-03-26 오후 4.25.11.png&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미 테스트해서 Label이 status:in-progress로 바꼈다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Codex에서 &lt;span data-token-index=&quot;1&quot;&gt;프롬프트 테스트 버튼&lt;/span&gt;을 눌러 한 번 실행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-26 오후 4.40.03.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBTnUY/dJMcacWNP26/x4N1mBynwJ0kWKAJiX9qH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBTnUY/dJMcacWNP26/x4N1mBynwJ0kWKAJiX9qH1/img.png&quot; data-alt=&quot;우측 상단 테스트 버튼&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBTnUY/dJMcacWNP26/x4N1mBynwJ0kWKAJiX9qH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBTnUY%2FdJMcacWNP26%2Fx4N1mBynwJ0kWKAJiX9qH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1028&quot; height=&quot;332&quot; data-filename=&quot;스크린샷 2026-03-26 오후 4.40.03.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우측 상단 테스트 버튼&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.실행이 정상이라면 다음 변화가 순서대로 일어난다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이슈의 라벨이 status:ready &amp;rarr; status:in-progress로 바뀌고(중복 작업 방지)&lt;/li&gt;
&lt;li&gt;작업 브랜치가 생성된 뒤&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Draft PR이 올라온다.&lt;/b&gt; PR 본문에는 요약/변경 내용/테스트 방법/리스크(롤백)까지 포함된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-26 오후 4.40.44.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;1124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rvnrI/dJMcaa5MeJB/OlbyKTOj0sLuotG3l9hib1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rvnrI/dJMcaa5MeJB/OlbyKTOj0sLuotG3l9hib1/img.png&quot; data-alt=&quot;생각보다 잘 올라온다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rvnrI/dJMcaa5MeJB/OlbyKTOj0sLuotG3l9hib1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrvnrI%2FdJMcaa5MeJB%2FOlbyKTOj0sLuotG3l9hib1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1308&quot; height=&quot;1124&quot; data-filename=&quot;스크린샷 2026-03-26 오후 4.40.44.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;생각보다 잘 올라온다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 앱을 배포하지 않은 상태라 Analytics 이벤트는 실제 데이터로 검증하진 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 단계에서는 파이프라인(Export/권한/워크플로/Issue 누적 기록)이 정상 동작하는지만 확인했고, Analytics 자체는 배포 후 실제 이벤트가 쌓이는 시점에 다시 테스트해봐야 겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 동작하는 것 같으니 일단 사소한 버그들부터 Issue에 좀 올려놔야겠다.&amp;nbsp;&lt;/p&gt;</description>
      <category>AI</category>
      <category>Analytics</category>
      <category>automation</category>
      <category>ChatGPT</category>
      <category>codex</category>
      <category>github</category>
      <category>IOS</category>
      <category>자동화</category>
      <category>프롬프트</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/89</guid>
      <comments>https://leetaek.tistory.com/89#entry89comment</comments>
      <pubDate>Thu, 26 Mar 2026 16:44:22 +0900</pubDate>
    </item>
    <item>
      <title>[AI] GitHub Actions + BigQuery로 Nightly Report 자동 수집 파이프라인 구축하기</title>
      <link>https://leetaek.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 글은 &amp;ldquo;Codex Automation&amp;rdquo;을 활용해 &lt;/span&gt;밤새 버그를 수집&amp;middot;분석하고 수정 제안까지 연결하는 파이프라인&lt;span&gt;을 구축한 &lt;/span&gt;두 번째 포스트&lt;span&gt;다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://leetaek.tistory.com/87&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[AI]&amp;nbsp; 자동화를 위한 GitHub Issue 운영 체계 만들기 (라벨/템플릿/분류)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[AI] GitHub Actions + BigQuery로 Nightly Report 자동 수집 파이프라인 구축하기 &lt;/b&gt;(작성중 ...)&lt;/li&gt;
&lt;li&gt;[AI] Codex Automation으로 &amp;ldquo;이슈 1개 &amp;rarr; Draft PR&amp;rdquo; 밤새 돌려보기 (worktree/sandbox/gh/prompt)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(현재 작성중인 미완성 글입니다....)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Nightly Report 워크플로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nightly Report는 다음과 같이 동작한다 .&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱에서 Firebase Analytics(GA4) 이벤트 기록&lt;/li&gt;
&lt;li&gt;BigQuery로 설정(GA4 데이터 Export, GitHub Action 조회를 위한 인증/권한 설정)&lt;/li&gt;
&lt;li&gt;GitHub Actions가 매일밤 스케줄로 Python 스크립트 실행(BigQuery 조회, 최근 데이터 쿼리, markdown로 렌더링)&lt;/li&gt;
&lt;li&gt;결과를 Markdown으로 만들고, 지정한 GitHub Issue 본문에 날짜 섹션으로 누적 기록&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Firebase Analytics 이벤트 기록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 앱에서 Firebase Analytics로 이벤트를 찍어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 그동안 화면 사용 시간에 대한 집계와 crash 로그만 집계하고 있었는데, 이 참에 사용성에 지장이 있을것 같은 포인트에 error_shown 이벤트를 로그를 찍어두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;즉, &amp;ldquo;버그를 전부 수집&amp;rdquo;하기보다 가장 먼저 잡아야 할 에러 신호부터 모으는 구조로 출발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계는 이 이벤트가 &lt;b&gt;GA4 &amp;rarr; BigQuery&lt;/b&gt;로 실제 적재되는지 확인하고, GitHub actions의 스크립트에서 조회 가능한 형태로 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BigQuery - GA4 데이터 Export 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery는 이번 파이프라인에서 GA4 이벤트가 쌓이는 저장소 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nightly Report는 이 데이터를 최근 24시간 기준으로 집계해서 GitHub Issue에 남기므로, 먼저 GA4 &amp;rarr; BigQuery Export 연결이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-27 오후 2.56.18.png&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBBc3t/dJMcafTiuon/pmlsXPVvLkbOot3z41YXR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBBc3t/dJMcafTiuon/pmlsXPVvLkbOot3z41YXR1/img.png&quot; data-alt=&quot;GCP 콘솔 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBBc3t/dJMcafTiuon/pmlsXPVvLkbOot3z41YXR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBBc3t%2FdJMcafTiuon%2FpmlsXPVvLkbOot3z41YXR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;621&quot; data-filename=&quot;스크린샷 2026-02-27 오후 2.56.18.png&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GCP 콘솔 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;준비물&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Firebase 프로젝트에 &lt;b&gt;Google Analytics(GA4)&lt;/b&gt; 가 연결되어 있어야 함&lt;/li&gt;
&lt;li&gt;BigQuery를 붙일 &lt;b&gt;Google Cloud 프로젝트&lt;/b&gt;가 있어야 함(권한 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) GA4 &amp;rarr; BigQuery Export 연결&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적&lt;/b&gt;: Firebase Analytics(GA4) 이벤트를 BigQuery dataset에 자동 적재&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위치&lt;/b&gt;: Firebase 콘솔 &amp;rarr; &lt;b&gt;프로젝트 설정 &amp;rarr; 통합(Integrations) &amp;rarr; BigQuery&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 BigQuery 연결을 설정하면, 연결된 GCP 프로젝트에 GA4 Export용 dataset이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세한 GA4 &amp;rarr; BigQuery Export 연결 방법은 &lt;a href=&quot;https://firebase.google.com/docs/ab-testing/bigquery?utm_source=chatgpt.com&amp;amp;hl=ko#enable-bigquery-export&quot;&gt;문서 참고&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772783299843&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;BigQuery로 A/B 테스팅 데이터 검사 &amp;nbsp;|&amp;nbsp; Firebase A/B Testing&quot; data-og-description=&quot;쿼리 예시를 비롯하여 BigQuery에서 Firebase A/B 테스팅 실험 데이터를 검사하고 분석하는 방법을 안내합니다.&quot; data-og-host=&quot;firebase.google.com&quot; data-og-source-url=&quot;https://firebase.google.com/docs/ab-testing/bigquery?utm_source=chatgpt.com&amp;amp;hl=ko#enable-bigquery-export&quot; data-og-url=&quot;https://firebase.google.com/docs/ab-testing/bigquery?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/ab-testing/bigquery?utm_source=chatgpt.com&amp;amp;hl=ko#enable-bigquery-export&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://firebase.google.com/docs/ab-testing/bigquery?utm_source=chatgpt.com&amp;amp;hl=ko#enable-bigquery-export&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;BigQuery로 A/B 테스팅 데이터 검사 &amp;nbsp;|&amp;nbsp; Firebase A/B Testing&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;쿼리 예시를 비롯하여 BigQuery에서 Firebase A/B 테스팅 실험 데이터를 검사하고 분석하는 방법을 안내합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;firebase.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Export가 정상인지 확인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적&lt;/b&gt;: &amp;ldquo;연결이 됐는지&amp;rdquo;를 BigQuery에서 바로 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위치&lt;/b&gt;: Google Cloud 콘솔 &amp;rarr; &lt;b&gt;BigQuery &amp;rarr; Explorer&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상이라면 아래가 보인다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dataset: analytics_XXXXXXX 형태의 dataset 생성&lt;/li&gt;
&lt;li&gt;테이블: events_YYYYMMDD 테이블 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경에 따라 당일 데이터는 events_intraday_YYYYMMDD로 먼저 쌓이고, 이후 events_YYYYMMDD로 확정되는 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 단계에서 dataset/테이블 생성만 먼저 확인했고, error_shown 이벤트는 아직 실제 데이터가 없어서 Nightly report에 &amp;ldquo;No events found&amp;rdquo;로 찍혔다. 즉, Export는 정상이고 이벤트가 없었던 상태였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-27 오후 3.02.41.png&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KP85c/dJMcabwzGVy/eyKAwyRBTBo8v7l2Ut7RY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KP85c/dJMcabwzGVy/eyKAwyRBTBo8v7l2Ut7RY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KP85c/dJMcabwzGVy/eyKAwyRBTBo8v7l2Ut7RY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKP85c%2FdJMcabwzGVy%2FeyKAwyRBTBo8v7l2Ut7RY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;491&quot; data-filename=&quot;스크린샷 2026-02-27 오후 3.02.41.png&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 앱에서 수집된 GA4 데이터를 BigQuery로 연결하는 과정은 끝났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 GitHub에서 조회할 수 있도록 인증/권한을 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BigQuery - GitHub Actions 조회를 위한 인증/권한 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 BigQuery를 직접 읽지 않고, &lt;b&gt;Service Account를 &amp;lsquo;대리(impersonate)&amp;rsquo;해서&lt;/b&gt; 읽는다. 그래서 &amp;ldquo;대리할 수 있는 권한&amp;rdquo;과 &amp;ldquo;BigQuery 읽기 권한&amp;rdquo; 두 종류가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;준비물&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Service Account 1개(예: nightly-report@...)&lt;/li&gt;
&lt;li&gt;Workload Identity Federation(Pool + Provider)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Service Account 만들기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적: BigQuery를 조회할 실제 주체(서비스 계정) 생성&lt;/li&gt;
&lt;li&gt;위치: &lt;a href=&quot;https://console.cloud.google.com/welcome&quot;&gt;GCP 콘솔&lt;/a&gt; &amp;rarr; IAM &amp;amp; Admin (IAM 및 관리자)&amp;rarr; Service Accounts(서비스 계정)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Workload Identity Pool + Provider 만들기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적: GitHub OIDC 토큰을 신뢰하고 내 레포만에서 실행된 워크플로우만 허용&lt;/li&gt;
&lt;li&gt;위치: &lt;a href=&quot;https://console.cloud.google.com/welcome&quot;&gt;GCP 콘솔&lt;/a&gt; &amp;rarr; IAM &amp;amp; Admin(IAM 및 관리자) &amp;rarr; Workload Identity Federation(워크로드 아이덴티티 제휴)&lt;/li&gt;
&lt;li&gt;역할:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pool: 외부 인증(여기선 GitHub OIDC)을 관리하기 위해 담아두는 컨테이너.&lt;/li&gt;
&lt;li&gt;Provider: 실제로 &amp;ldquo;GitHub OIDC를 신뢰한다&amp;rdquo;고 설정하는 곳. pool 안에서 provider를 만들고, repository == LeeTaek/Carve 조건(내 경우)을 걸어 허용 범위를 레포 단위로 제한한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) Service Account(서비스 계정)에 대리 권한 부여&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적: GitHub Actions를 대리할 서비스 계정 설정&lt;/li&gt;
&lt;li&gt;위치: Service Accounts(서비스 계정) &amp;rarr; (대상 SA) &amp;rarr; Permissions(권한) &amp;rarr; 서비스 계정 권한 관리 &amp;rarr; 엑세스 관리 &amp;rarr; principalSet(레포 제한) 추가 및 권한 부여&lt;/li&gt;
&lt;li&gt;부여할 권한과 역할:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Workload Identity User(작업 부하 ID 사용자)&lt;/li&gt;
&lt;li&gt;Service Account Token Creator(서비스 계정 토큰 사용자): 요게 빠지면 Codex 스크립트 실행시 iam.serviceAccounts.getAccessToken 403으로 막힌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) Service Account(서비스 계정)에 BigQuery 권한 부여&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적: 서비스 계정이 BigQuery를 조회할 수 있는 권한 부여&lt;/li&gt;
&lt;li&gt;위치:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://console.cloud.google.com/welcome&quot;&gt;GCP 콘솔&lt;/a&gt; &amp;rarr; IAM &amp;amp; Admin(IAM 및 관리자) &amp;rarr; IAM &amp;rarr; 서비스 계정 &amp;rarr; 역할 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;역할:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BigQuery Job User: 쿼리 실행할 수 있는 권한&lt;/li&gt;
&lt;li&gt;BigQuery Data Viewer: GA4 Export dataset을 읽을 수 있는 권한&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery 쪽 설정(Export + 인증/권한)이 끝났다면, 이제 GitHub Actions가 그 설정값들을 참조할 수 있게 GitHub 레포에 값을 등록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 것은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Actions가 사용할 &lt;b&gt;Workload Identity Provider 경로&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;GitHub Actions가 대리할 &lt;b&gt;Service Account 이메일&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;GitHub Actions에서 조회/리포트 할 &lt;b&gt;Dataset 번호&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값들은 워크플로(yml)에 직접 하드코딩할 수도 있지만, GitHub Settings에 저장해두는 편이 안전하고 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;준비물&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Workload Identity Provider 경로&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예:&lt;/li&gt;
&lt;li&gt;projects/&amp;lt;Project_Number(숫자)&amp;gt;/locations/global/workloadIdentityPools/&amp;lt;Pool_ID&amp;gt;/providers/&amp;lt;provider_ID&amp;gt;&lt;/li&gt;
&lt;li&gt;위치: &lt;a href=&quot;https://console.cloud.google.com/welcome&quot;&gt;GCP 콘솔&lt;/a&gt; &amp;rarr; IAM &amp;amp; Admin(IAM 및 관리자)&amp;rarr; Workload Identity Federation(워크로드 아이덴티티 제휴) &amp;rarr; PoolID 복사 &amp;rarr; ProviderID 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Service Account 이메일&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치: GCP 콘솔 &amp;rarr; IAM &amp;amp; Admin(IAM 및 관리자) &amp;rarr; Service Accounts(서비스 계정) &amp;rarr; 이메일 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) BQ_DATASET&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHubAction에서 조회할 DataSet 번호&lt;/li&gt;
&lt;li&gt;위치: GCP 콘솔 &amp;rarr; BigQuery &amp;rarr; 프로젝트 &amp;rarr; analytics_xxxxxxxxx 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) NIGHTLY_ISSUE_NUMBER&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nightly report 이슈를 누적 기록할 GitHub Issue 번호.&lt;/li&gt;
&lt;li&gt;위치: GitHub &amp;rarr; Issues &amp;rarr; Nightly report 이슈 열기 &amp;rarr; URL에서 번호 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: .../issues/2 이면 NIGHTLY_ISSUE_NUMBER=2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;없다면 Issue에 만들어서 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub Settings에 값 등록하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비물(Provider 경로, Service Account 이메일, BQ_DATASET, NIGHTLY_ISSUE_NUMBER)을 확인했다면, 이제 GitHub 레포에 값을 등록한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치: GitHub 레포 &amp;rarr; &lt;b&gt;Settings &amp;rarr; Secrets and variables &amp;rarr; Actions&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Secrets(인증 관련)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WORKLOAD_IDENTITY_PROVIDER&lt;/li&gt;
&lt;li&gt;GCP_SERVICE_ACCOUNT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Variables(스크립트 설정값)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BQ_DATASET&lt;/li&gt;
&lt;li&gt;NIGHTLY_ISSUE_NUMBER&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트: Nightly Report를 수동으로 한 번 돌려보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적: &amp;ldquo;GCP 인증/권한 + GitHub Secrets/Variables + 스크립트 동작&amp;rdquo;을 한 번에 확인&lt;/li&gt;
&lt;li&gt;방법: GitHub Actions에서 해당 워크플로를 &lt;span&gt;&lt;b&gt;Run workflow&lt;/b&gt;&lt;/span&gt;로 수동 실행&lt;/li&gt;
&lt;li&gt;기대 결과:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Actions 로그에서 BigQuery 쿼리 수행 로그가 찍힘&lt;/li&gt;
&lt;li&gt;지정한 Issue 본문에 &lt;span&gt;YYYY-MM-DD&lt;/span&gt; 섹션으로 결과가 append 됨&lt;/li&gt;
&lt;li&gt;이벤트가 없으면 &lt;span&gt;No events found&lt;/span&gt;가 찍혀도 정상(파이프라인은 통과한 것)&lt;/li&gt;
&lt;/ol&gt;
&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-18 오후 5.24.43.png&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyLDRH/dJMcaa5EX0J/BTnwqTK2z5JtkX6DrXk620/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyLDRH/dJMcaa5EX0J/BTnwqTK2z5JtkX6DrXk620/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyLDRH/dJMcaa5EX0J/BTnwqTK2z5JtkX6DrXk620/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyLDRH%2FdJMcaa5EX0J%2FBTnwqTK2z5JtkX6DrXk620%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1095&quot; height=&quot;248&quot; data-filename=&quot;스크린샷 2026-03-18 오후 5.24.43.png&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-18 오후 5.25.19.png&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s0pw1/dJMcaibzkrv/qMzYqEf5aExcrF9TqCbDC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s0pw1/dJMcaibzkrv/qMzYqEf5aExcrF9TqCbDC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s0pw1/dJMcaibzkrv/qMzYqEf5aExcrF9TqCbDC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs0pw1%2FdJMcaibzkrv%2FqMzYqEf5aExcrF9TqCbDC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;855&quot; height=&quot;179&quot; data-filename=&quot;스크린샷 2026-03-18 오후 5.25.19.png&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 동작한다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>AI</category>
      <category>Analytics</category>
      <category>automation</category>
      <category>ChatGPT</category>
      <category>codex</category>
      <category>Firebase</category>
      <category>github</category>
      <category>IOS</category>
      <category>에이전트코딩</category>
      <category>자동화</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/88</guid>
      <comments>https://leetaek.tistory.com/88#entry88comment</comments>
      <pubDate>Fri, 6 Mar 2026 16:54:38 +0900</pubDate>
    </item>
    <item>
      <title>[AI] Codex 자동화를 위한 GitHub Issue 운영 체계 만들기 (라벨/템플릿/분류)</title>
      <link>https://leetaek.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 &amp;ldquo;Codex Automation&amp;rdquo;을 활용한 에이전트 코딩 관련 포스트이면서도, 사실은 그보다 앞단의 이야기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글은 다음과 같이 세 편으로 나눠 정리하려고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;[AI]&amp;nbsp; 자동화를 위한 GitHub Issue 운영 체계 만들기 (라벨/템플릿/분류)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetaek.tistory.com/88&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[AI] GitHub Actions + BigQuery로 Nightly Report 자동 수집 파이프라인 구축하기&lt;/a&gt; (작성중 ...)&lt;/li&gt;
&lt;li&gt;[AI] Codex Automation으로 &amp;ldquo;이슈 1개 &amp;rarr; Draft PR&amp;rdquo; 밤새 돌려보기 (worktree/sandbox/gh/prompt)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요번 글은 첫번째 글이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 자동화가 필요했나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 일이 바빠지면서, 1인으로 진행하던 사이드 프로젝트가 생각보다 빠르게 소홀해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 아예 안 보는 건 아닌데, &amp;lsquo;기능 추가&amp;rsquo;는커녕 인지하고 있는 버그와 레포트로 들어오는 버그가 산더미처럼 쌓이기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 내가 힘이 빠졌던 지점은 &amp;lsquo;개발이 어렵다&amp;rsquo;기보다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴근 후 남는 에너지로는 사이드 프로젝트를 제대로 굴리기가 어렵다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집에 돌아와 저녁 먹으면 9시, 집안일 마치면 11시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 시간에 사이드 프로젝트를 켜면 &amp;ldquo;오늘은 어디부터 해야 하지?&amp;rdquo;에서 이미 지친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 공백에 대한 부담도 그중 하나였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그를 하나 고치고도 &amp;lsquo;이거 다른 데 망가뜨린 건 아닐까?&amp;rsquo; 하는 찝찝함이 남고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 찝찝함이 결국 다음 작업의 진입장벽이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 바랐던 건 &amp;ldquo;밤새 다 고쳐주기&amp;rdquo;가 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;발견된 이슈나 버그를 &lt;span&gt;정리해두고&lt;/span&gt;, 가능하면 &lt;span&gt;초안(PR)&lt;/span&gt; 까지 만들어두는 흐름&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화를 통해 밤 시간에는 반복 작업(이슈 수집/정리, PR 초안)을 맡기고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 낮에 결과를 한 번에 모아 &lt;span&gt;&lt;b&gt;리뷰하고 결정하고(필요하면 테스트까지)&lt;/b&gt;&lt;/span&gt; 처리하는 식으로 에너지를 분리하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-27 오후 4.31.16.png&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C2sze/dJMcabi0Zao/T4ermfE9slAxoUnRQ5iiFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C2sze/dJMcabi0Zao/T4ermfE9slAxoUnRQ5iiFk/img.png&quot; data-alt=&quot;Codex Automation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C2sze/dJMcabi0Zao/T4ermfE9slAxoUnRQ5iiFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC2sze%2FdJMcabi0Zao%2FT4ermfE9slAxoUnRQ5iiFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;740&quot; data-filename=&quot;스크린샷 2026-02-27 오후 4.31.16.png&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Codex Automation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 발견한 게 Codex Automation이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI로 사용하던 &lt;b&gt;Codex가 최근 MacOS용 앱을 출시했는데 함께 소개된 기능이&amp;nbsp; Automation 이다.&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능을 적극적으로 사용해보고 싶었다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기대하는 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-27 오후 4.17.02.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8elxT/dJMcaaYFXEZ/gvFb0gxpgWCWw4jtsXS15k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8elxT/dJMcaaYFXEZ/gvFb0gxpgWCWw4jtsXS15k/img.png&quot; data-alt=&quot;Codex Automation이 GitHub 에 올린 Issue를 읽고 수정해서 올린 PR&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8elxT/dJMcaaYFXEZ/gvFb0gxpgWCWw4jtsXS15k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8elxT%2FdJMcaaYFXEZ%2FgvFb0gxpgWCWw4jtsXS15k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1318&quot; height=&quot;1253&quot; data-filename=&quot;스크린샷 2026-02-27 오후 4.17.02.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1253&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Codex Automation이 GitHub 에 올린 Issue를 읽고 수정해서 올린 PR&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 기대하는 구조는 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밤 시간에 자동화가 할 수 있는 일을 맡기고, 나는 낮에 결과물을 한 번에 모아 리뷰하고 결정만 하는 방식.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 &amp;ldquo;개발을 대신한다&amp;rdquo;기보다는 영향이 크지 않은 버그 위주로 정리해주는 작업을 기대했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플로우는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 앱에서 개선할 점들을 GitHub Issue에 수집&lt;br /&gt;&amp;nbsp; &amp;nbsp;(1-2) 개발자가 직접 개선 방향/할 일을 이슈로 수동 작성&lt;br /&gt;&amp;nbsp; &amp;nbsp;(1-1) analytics 기반 오류 로그를 Nightly report로 자동 수집&lt;br /&gt;2. Codex Automation이 우선 순위에 따라 이슈 하나를 골라 밤새 작업&lt;br /&gt;3. 작업한 내용을 PR로 남김&lt;br /&gt;4.개발자가 시간 될 때 PR 확인&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 되려면, 무엇보다 이슈가 잘 모이고 잘 정리되어 있어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 버그/개선 포인트를 자동으로 모아 GitHub Issue로 쌓는 파이프라인부터 만들기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트에서 다루는 아래 내용은 Github Issue 수집에 해당하는 1번 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub Issue label 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이프라인을 구성하기 전에 Agent가 수행할 작업을 결정하는데 판단 기준이 되도록 label을 정리하는게 적절하다 생각이 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 아래와 같이 라벨을 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-27 오후 4.18.14.png&quot; data-origin-width=&quot;1269&quot; data-origin-height=&quot;1262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nrDQH/dJMcabJ5vfW/Qjg3VTl3YEWdOx2L0RpJSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nrDQH/dJMcabJ5vfW/Qjg3VTl3YEWdOx2L0RpJSK/img.png&quot; data-alt=&quot;정리한 Label&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nrDQH/dJMcabJ5vfW/Qjg3VTl3YEWdOx2L0RpJSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnrDQH%2FdJMcabJ5vfW%2FQjg3VTl3YEWdOx2L0RpJSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;779&quot; height=&quot;775&quot; data-filename=&quot;스크린샷 2026-02-27 오후 4.18.14.png&quot; data-origin-width=&quot;1269&quot; data-origin-height=&quot;1262&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정리한 Label&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우선순위: 작업을 결정하는 우선순위&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;prio:p0 매우 시급함(사용성/핵심 기능 영향)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;prio:p1 빠른 시일 내&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;prio:p2 여유 있을 때&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;상태&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;status:needs-repre: 아직 증거/재현 정보 부족(추가 로그 필요)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;status:ready 바로 작업해도 되는 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;status:in-progress: 작업 진행 중&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;작업 범위&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;area: carve, area:chart 등: 어느 영역의 문제인지 빠르게 좁히기 위한 라벨(모듈 이름으로 설정)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;자동화 대상 표시&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;go:fix:&lt;/span&gt;&amp;nbsp;Codex Automation이 가져가도 되는 이슈&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;go:test: test개선 PR 대상&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;go:dox: 문서 업데이트 PR 대상&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇게 정리해두면 자동화는 아래 조건으로 오늘 밤 처리할 이슈를 단순하게 고를 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Codex Automation에 작업 요청 - label:go:fix + label:status:ready + label:prio:p0/p1/p2&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;진행 중인 이슈는 제외: -label:status:in-progress&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;+) 에이전트 코딩과 모듈화&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;라벨을 정리하면서 모듈화의 생각하지 못한 이점도 다시 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능이 모듈 단위로 분리하고, label의 area를 모듈이름으로 달아두니 `area:*`로 범위를 지정할 때 어디를 고쳐야 하는지가 더 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결합도가 높은 코드에서는 작은 수정도 전역에 영향을 주기 쉬운데, 모듈화는 의존성/컴파일 경계를 강제해서 변경 범위를 자연스럽게 좁혀졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 특정 기능 구현은 기가 막히게 잘하지만, 레포 전체의 암묵적 의존성과 맥락을 완전히 이해하기는 어려운 한계가 있다고 생각했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 책임이 명확히 분리된 모듈 구조일수록, 에이전트가 안전하게 작업할 수 있는 단위가 작아지고 결과도 더 안정적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Nightly Report 워크플로&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nightly Report는 다음과 같이 동작한다 .&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱에서 Firebase Analytics(GA4) 이벤트 기록&lt;/li&gt;
&lt;li&gt;GA4 이벤트가 BigQuery로 Export 되어 적재&lt;/li&gt;
&lt;li&gt;GitHub Actions가 매일밤 스케줄로 Python 스크립트 실행(BigQuery 조회, 최근 데이터 쿼리, Markdown로 렌더링)&lt;/li&gt;
&lt;li&gt;결과를 Markdown으로 만들고, 지정한 GitHub Issue 본문에 날짜 섹션으로 누적 기록&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 연결은 다음글에서 ...&amp;nbsp;&lt;/p&gt;</description>
      <category>AI</category>
      <category>AI</category>
      <category>automation</category>
      <category>ChatGPT</category>
      <category>codex</category>
      <category>github</category>
      <category>IOS</category>
      <category>에이전트코딩</category>
      <category>자동화</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/87</guid>
      <comments>https://leetaek.tistory.com/87#entry87comment</comments>
      <pubDate>Fri, 27 Feb 2026 16:34:52 +0900</pubDate>
    </item>
    <item>
      <title>[Tuist] 모듈화 후에 SwiftUI Preview 에러 (Static &amp;amp; Dynamic Frameworks와 BuildSystem)</title>
      <link>https://leetaek.tistory.com/86</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI에서 프리뷰(Preview) 기능은 뷰를 코드 작성과 동시에 빠르게 확인할 수 있는 유용한 도구입니다. 빌드와 시뮬레이터 실행 없이도 바로 결과를 볼 수 있기 때문에 View의 레이아웃이나 UI 요소의 개발 효율성을 크게 향상시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, &lt;span&gt;Tuist&lt;/span&gt;로 모듈화한 프로젝트에서 TCA(TheComposableArchitecture)를 적용한 SwiftUI 뷰에 프리뷰를 적용하려던 순간, 예상치 못한 에러가 발생했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-25 오후 4.39.10.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t1oGp/btsMxE9HHnz/uGx29LWdPoRNTmnYcfNmqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t1oGp/btsMxE9HHnz/uGx29LWdPoRNTmnYcfNmqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t1oGp/btsMxE9HHnz/uGx29LWdPoRNTmnYcfNmqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft1oGp%2FbtsMxE9HHnz%2FuGx29LWdPoRNTmnYcfNmqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;584&quot; height=&quot;74&quot; data-filename=&quot;스크린샷 2025-02-25 오후 4.39.10.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-25 오후 4.36.01.png&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IlOij/btsMvuU6u4x/AXejlAa7hKhKdJbOCDwbbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IlOij/btsMvuU6u4x/AXejlAa7hKhKdJbOCDwbbk/img.png&quot; data-alt=&quot;Preview 화면에 뜨는 Runtime linking Failure와 에러의 상세 내용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IlOij/btsMvuU6u4x/AXejlAa7hKhKdJbOCDwbbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIlOij%2FbtsMvuU6u4x%2FAXejlAa7hKhKdJbOCDwbbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;286&quot; data-filename=&quot;스크린샷 2025-02-25 오후 4.36.01.png&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Preview 화면에 뜨는 Runtime linking Failure와 에러의 상세 내용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Build, DerivedData 초기화, Tuist Clean 등을 해봐도 여전히 프리뷰가 작동하지 않았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 에러로 다시 돌아와 상세히 살펴보면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740557528001&quot; class=&quot;asciidoc&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;== PREVIEW UPDATE ERROR:

    [Remote] JITError: Runtime linking failure&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- SwiftUI 프리뷰는 뷰를 렌더링 할 때에 JIT(Just In Time) 컴파일러를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;런타임&lt;/b&gt;에 뷰를 로드함.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 뷰와 관련된 모듈을 런타임에 불러와야 하는데 정적 프레임워크는 빌드 시점에 앱 바이너리에 통합되어 런타임에 로딩이 불가능.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740557528001&quot; class=&quot;groovy&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    Additional Link Time Errors:
    Symbols not found: [ ___isPlatformVersionAtLeast ]
    Symbols not found: [ _swift_getFunctionTypeMetadataGlobalActorBackDeploy ]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 프리뷰가 동작하기 위해서는 요런 Symbol를 찾아야하는데 찾을수가 없음.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;여기서 &lt;/span&gt;Tuist로 모듈화 된 프레임워크들의 연결에 문제가 생겼다 가설을 세우고, 프리뷰와 프레임워크 연결방법의 상관관계에 대해 찾아 보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 프레임워크 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Static Framework VS Dynamic Framework&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Framework&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크는 여러 프로젝트에서 공통으로 사용할 기능을 하나의 모듈로 만들어 두고 필요할때마다 불러와서 사용할 수 있도록 하는 캡슐화 하는 계층 구조 파일 디렉토리입니다. 구성 요소로 dynamic shared library(.dylib, .framework), Nib파일, asset, 헤더파일 등 다양한 공유 리소스를 포함할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크는 앱이 빌드되고 실행되는 방식에 따라 정적 프레임워크(Static Framework)와 동적 프레임워크(Dynamic Framework)로 나뉩니다. 이를 잘못 구성하면 런타임 충돌과 같은 문제를 일으킬 수 있지만 모듈화 구조에서 적절하게 조합한다면 빌드 시간과 메모리 사용량, 앱 성능 등을 개선시킬 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Static Framework&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 프레임워크(static framework)는 정적 라이브러리(static library, .a파일)과 리소스를 포함하는 프레임워크 입니다. 일반적으로 정적 라이브러리는 컴파일 시점에 실행파일에 포함되어, 앱이 실행될 때에 별도의 로딩 과정 없이 바로 사용할 수 있습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (1).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJgLED/btsMxFu6QUb/ZTBMWG4KZdqegWX0f1FOck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJgLED/btsMxFu6QUb/ZTBMWG4KZdqegWX0f1FOck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJgLED/btsMxFu6QUb/ZTBMWG4KZdqegWX0f1FOck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJgLED%2FbtsMxFu6QUb%2FZTBMWG4KZdqegWX0f1FOck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;363&quot; data-filename=&quot;다운로드 (1).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 프레임워크에서는 정적 라이브러리의 코드가 앱 코드 내의 Heap 메모리에 상주하므로 동일한 정적 라이브러리를&lt;b&gt; 여러 정적 프레임워크에서 사용하게 되면 코드 중복이 발생하게 됩니다. &lt;/b&gt;추후에 다행히 알게된 것이지만 동일한 라이브러리를 여러 정적 프레임워크에서 사용하는 경우 중복되지 않도록 컴파일러가 알아서 정리해준다고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 실행 속도 향상: 런타임 로딩이 필요 없고 실행 파일에 포함되어 빠르게 실행됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 배포시 별도의 파일 필요 없음: 모든 코드가 실행 파일에 포함됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 단점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 크기 증가: 실행 파일의 크기가 커질 수 있음&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크 업데이트가 어려움: 새 버전이 나올 떄마다 전체 앱을 다시 빌드해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Dynamic Framework&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 프레임워크(Dynamic Framework)는 동적 라이브러리(.dylib)와 리소스를 포함하는 프레임워크입니다. 정적 프레임워크와 달리 앱 실행 파일에 직접 포함되지 않고 앱 실행 시 동적으로 로드됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 빌드 시에는 링크 되지만 &lt;b&gt;실행 파일에는 포함되지 않으며 대신 Dynamic Library Reference가 포함됩니다.&lt;/b&gt;이를 통해 모듈 호출 시에 시에 Stack에 있는 라이브러리가 로드 되어 메모리에 올라갑니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmwV1x/btsMx42yqpm/OYtudaTKL8TevABHJ7GxOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmwV1x/btsMx42yqpm/OYtudaTKL8TevABHJ7GxOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmwV1x/btsMx42yqpm/OYtudaTKL8TevABHJ7GxOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmwV1x%2FbtsMx42yqpm%2FOYtudaTKL8TevABHJ7GxOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;386&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Xcode에서는 프레임워크를 만들면 기본적으로 동적 프레임워크로 만들어집니다. 동시에 여러 곳에서 사용하는 경우 동일한 버전의 코드 사본을 가지고 공유하므로, 메모리를 효율적으로 사용합니다. 또한 동적으로 연결되어 있으므로 전체 빌드를 다시 하지 않아도 새로운 버전의 프레임워크 사용이 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 장점&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 크기 절감: 실행 파일에 포함되지 않고 필요시 메모리에 로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유: 여러 앱에서 동일한 버전의 프레임 워크 사용&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 업데이트 가능: 프레임워크만 별도로 교체하여 업데이트 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-단점&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 실행시 속도 저하 가능성: 실행 시 동적으로 로딩해야 하므로 속도가 느려질 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 실행시 프레임워크 파일 필요: 프레임워크 파일이 존재하지 않으면 앱이 실행되지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Dynamic Framework에서 Static Libraries를 가지고 있는 경우는 ?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT 선생님이 다음과 같다고 알려주셨습니다 ...&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;⚙️ Dynamic Framework에서 Static Library를 포함하는 경우 처리 방식&lt;br /&gt;&lt;br /&gt;Dynamic Framework이 Static Library를 포함하면, 컴파일 과정에서 정적 라이브러리의 코드가 동적 프레임워크 내부에 병합.&lt;br /&gt;즉,&amp;nbsp;정적 라이브러리의 개별적인 독립성은 사라지고, 동적 프레임워크의 일부로 동작하게 됨.&lt;br /&gt;&lt;br /&gt;1️⃣ 컴파일 타임에서의 동작&lt;br /&gt;&amp;bull;&amp;nbsp;Static Library가 Dynamic Framework에 포함되면,&amp;nbsp;정적 라이브러리의 모든 개체 코드(object code)&amp;nbsp;가&amp;nbsp;Dynamic Framework 바이너리에 직접 병합됨.&lt;br /&gt;&amp;bull;&amp;nbsp;링커(ld)는 Static Library의 개별적인 존재를 유지하지 않고, 그 안의 코드를 Dynamic Framework의 실행 파일(.framework/.dylib)에&amp;nbsp;일반적인 object code처럼 삽입함.&lt;br /&gt;&amp;bull;&amp;nbsp;결과적으로,&amp;nbsp;&lt;b&gt;Dynamic Framework 내부에서 Static Library의 모든 심볼이 포함된 하나의 실행 바이너리가 생성됨.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;2️⃣ 런타임에서의 동작&lt;br /&gt;&amp;bull;&amp;nbsp;Dynamic Framework은 실행 시간(런타임)에서&amp;nbsp;heap에 올라가지만, 포함된 Static Library의 코드는 독립적인 라이브러리가 아니라&amp;nbsp;이미 Dynamic Framework의 일부로 병합되어 있음.&lt;br /&gt;&amp;bull;&amp;nbsp;&lt;b&gt;즉,&amp;nbsp;Static Library 내부 코드도 Dynamic Framework의 코드와 함께 heap에 올라감.&lt;/b&gt;&lt;br /&gt;&amp;bull; 이때, Static Library 내부에서 전역 심볼이나 전역 변수를 사용하면, 해당 심볼이 Dynamic Framework에서 직접 제공하는 것처럼 동작함.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Preview와 StaticFrameworks(Libraries)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 저는 Tuist를 통해서 TCA를 정적 프레임워크(StaticFrameworks)로 가져와 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 라이브러리가 내부적으로 정적 링크로 연결되어있는 TCA를 여러곳에서 링크할 때에 중복 링크를 피하기 위해서 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Feature들을 동적 프레임워크로 설정하고 정적 라이브러리인 TCA를 사용할 때에 Tuist에서 띄우는 경고입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-27 오후 1.01.13.png&quot; data-origin-width=&quot;2718&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cariUK/btsMwB1okVU/AwCavM02Key1nlj6It0SqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cariUK/btsMwB1okVU/AwCavM02Key1nlj6It0SqK/img.png&quot; data-alt=&quot;정적 라이브러리인 TCA에 중복으로 링크될 수 있다는 경고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cariUK/btsMwB1okVU/AwCavM02Key1nlj6It0SqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcariUK%2FbtsMwB1okVU%2FAwCavM02Key1nlj6It0SqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2718&quot; height=&quot;692&quot; data-filename=&quot;스크린샷 2025-02-27 오후 1.01.13.png&quot; data-origin-width=&quot;2718&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정적 라이브러리인 TCA에 중복으로 링크될 수 있다는 경고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 프레임워크 A와 B가 Static Library(TCA)를 의존하면, 컴파일 때에 각각 TCA를 포함한 실행파일(.framework / .dylib)를 만들게 되고, A와 B가 함께 로드될 때에 duplicate symbol 오류를 발생시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 정적 프레임워크로 만들게 된다면 컴파일 시에 정적 라이브러리가 앱 바이너리에 병합되고, 이를 컴파일러가 중복 링크되지 않도록 자동으로 정리해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 Feature 모듈을 정적 프레임워크로 만들어 사용하고 있었는데, 이게 Preview와 연관이 있는 것 같아 중점적으로 찾아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 부분이 핵심 요인인데 &lt;a href=&quot;https://developer.apple.com/forums/thread/704910&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Developer Forums에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;애플&lt;span&gt;&amp;nbsp;&lt;/span&gt;엔지니어의 답변&lt;/a&gt;에 따르면 &lt;b&gt;SwiftUI의 Preview 기능은 정적 프레임워크(라이브러리)에서 지원되지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-25 오후 6.15.28.png&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ0tiH/btsMve52Kcj/ELVAT4jpdiJPvDq5Q3qZuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ0tiH/btsMve52Kcj/ELVAT4jpdiJPvDq5Q3qZuk/img.png&quot; data-alt=&quot;정답이다 개발자 ...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ0tiH/btsMve52Kcj/ELVAT4jpdiJPvDq5Q3qZuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ0tiH%2FbtsMve52Kcj%2FELVAT4jpdiJPvDq5Q3qZuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;265&quot; data-filename=&quot;스크린샷 2025-02-25 오후 6.15.28.png&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정답이다 개발자 ...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Static Framework에서 프리뷰를 사용하기 위해서는 어떻게 사용해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;Static에서 Preview 사용법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Dynamic Library(framework)로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static Library를 Library로 변경하여 사용하는 방법입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzL8JE/btsMzf99EGC/gR3cjpLwB3HoBuTHTQYN10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzL8JE/btsMzf99EGC/gR3cjpLwB3HoBuTHTQYN10/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2158&quot; data-origin-height=&quot;1208&quot; data-filename=&quot;스크린샷 2025-02-27 오후 3.02.51.png&quot; style=&quot;width: 49.5659%; margin-right: 10px;&quot; data-widthpercent=&quot;50.15&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzL8JE/btsMzf99EGC/gR3cjpLwB3HoBuTHTQYN10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzL8JE%2FbtsMzf99EGC%2FgR3cjpLwB3HoBuTHTQYN10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2158&quot; height=&quot;1208&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brsYYO/btsMxCS6ZLJ/FtUBqBVciNH8iuJ4tBAK7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brsYYO/btsMxCS6ZLJ/FtUBqBVciNH8iuJ4tBAK7K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2202&quot; data-origin-height=&quot;1240&quot; data-filename=&quot;스크린샷 2025-02-27 오후 2.59.41.png&quot; style=&quot;width: 49.2713%;&quot; data-widthpercent=&quot;49.85&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brsYYO/btsMxCS6ZLJ/FtUBqBVciNH8iuJ4tBAK7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrsYYO%2FbtsMxCS6ZLJ%2FFtUBqBVciNH8iuJ4tBAK7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2202&quot; height=&quot;1240&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Dynamic 을 지원하는 라이브러리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RxSwift나 Alamofire와 같이 동적 라이브러리를 지원한다면 해당 라이브러리를 사용하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원하지 않는 경우도 있지만, Tuist를 사용한다면 외부 의존성을 추가할 때에 Package.swift에서 가져오는 설정을 변경할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(Tuist에서는 기본적으로 Static Library로 가져옵니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/2cb84cb0cd9b9428505d696fbefef219.js&quot;&gt;&lt;/script&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740637808018&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 프로젝트 생성시 
TUIST_FOR_PREVIEW=TRUE tuist generate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 Tuist에서 환경 변수를 설정할 수 있도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.tuist.dev/en/guides/develop/projects/dynamic-configuration&quot;&gt;Dynamic Configuration&lt;/a&gt;을 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 특별한 설정을 하지 않아도 Tuist generate 명령어 실행 시&amp;nbsp; 환경 변수를 지정할 수 있습니다.(TUIST_XXX 명령어, Environment.&amp;lt;variable&amp;gt; 형식)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 프리뷰가 정상적으로 잘 동작 하지만 어떤 사이드 이펙트가 생길지 몰라서 UI개발을 위해 프리뷰를 사용할 때만 환경 변수를 설정하고 개발하고 있습니다 ...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-27 오후 4.01.23.png&quot; data-origin-width=&quot;2536&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VcO1N/btsMxn9Mgwd/F51K4XyAKwA4tAfPx4kvC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VcO1N/btsMxn9Mgwd/F51K4XyAKwA4tAfPx4kvC0/img.png&quot; data-alt=&quot;드디어 돌아가는 Preview&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VcO1N/btsMxn9Mgwd/F51K4XyAKwA4tAfPx4kvC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVcO1N%2FbtsMxn9Mgwd%2FF51K4XyAKwA4tAfPx4kvC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2536&quot; height=&quot;1472&quot; data-filename=&quot;스크린샷 2025-02-27 오후 4.01.23.png&quot; data-origin-width=&quot;2536&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;드디어 돌아가는 Preview&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 공용 프레임워크로 감싸서 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture?tab=readme-ov-file#installation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TCA&lt;/a&gt;와 &lt;a href=&quot;https://github.com/tuist/tuist/tree/main/fixtures/app_with_previews&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tuist&lt;/a&gt;가 멀티 모듈을 사용할 때에 추천하는 방법입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 링크 문제를 해결하기 위해 공통으로 사용할 의존성을 감싸는 프레임워크를 만들고, 다른 모듈에서 해당 프레임워크에 의존하는 형태로 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-27 오후 4.56.29.png&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAshzo/btsMzQoRMod/d9JmKoFGGtkah0QBlCMfKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAshzo/btsMzQoRMod/d9JmKoFGGtkah0QBlCMfKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAshzo/btsMzQoRMod/d9JmKoFGGtkah0QBlCMfKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAshzo%2FbtsMzQoRMod%2Fd9JmKoFGGtkah0QBlCMfKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;422&quot; height=&quot;625&quot; data-filename=&quot;스크린샷 2025-02-27 오후 4.56.29.png&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구조에서 FeatureA, FeatureB, WrapperedTCA는 Dynamic Framework로 선언되어있고, WrappedTCA만&amp;nbsp; ComposableArchitecture에 직접적인 의존성을 가지고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pQHoV/btsMx5AIPfH/yhqu3mkelTto8jX7a4wWT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pQHoV/btsMx5AIPfH/yhqu3mkelTto8jX7a4wWT1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;1290&quot; data-filename=&quot;스크린샷 2025-02-27 오후 4.46.02.png&quot; style=&quot;width: 53.0727%; margin-right: 10px;&quot; data-widthpercent=&quot;53.7&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pQHoV/btsMx5AIPfH/yhqu3mkelTto8jX7a4wWT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpQHoV%2FbtsMx5AIPfH%2Fyhqu3mkelTto8jX7a4wWT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2542&quot; height=&quot;1290&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9YYmr/btsMxIlp1GT/JDm5jVbbHQDJEuiWMLMbTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9YYmr/btsMxIlp1GT/JDm5jVbbHQDJEuiWMLMbTK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;1496&quot; data-filename=&quot;스크린샷 2025-02-27 오후 4.46.14.png&quot; style=&quot;width: 45.7645%;&quot; data-widthpercent=&quot;46.3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9YYmr/btsMxIlp1GT/JDm5jVbbHQDJEuiWMLMbTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9YYmr%2FbtsMxIlp1GT%2FJDm5jVbbHQDJEuiWMLMbTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2542&quot; height=&quot;1496&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;FeatureA 모듈과 App의 프리뷰 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구조에서도 Wrappered에 링크된 TCA를 사용하므로 다른 모듈에서 링크가 중복될 가능성이 없으며, Preview 또한 잘 동작합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 github 링크도 같이 남겨둡니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/LeeTaek/WrappedTCAToDynamicFramework&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WrapperedTCAtoDynamicFramework git 링크 주소&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Xcode의 Build System&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Preview에 대한 해결방법을 찾다가 또 이상한 부분을 발견했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCA의&lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture#installation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; Documents의 Installation 부분&lt;/a&gt;을 보면 멀티 타겟(멀티 모듈)을 구성할 때에 TCA를 의존하는 공유 프레임워크(Shared Frameworks)를 만들고 그 후 해당 타겟을 의존성으로 추가하는 방식으로 사용할 것을 권합니다.(Preview 사용법의 2번)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 TCA의 프로젝트에 포함된&lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/TicTacToe&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; TicTacToe라는 예시 프로젝트&lt;/a&gt;를 보여주는데, 해당 프로젝트의 의존성은 SPM으로 관리됩니다. 프로젝트의 대략적인 구조 중에서 GameSwiftUI 라는 정적 프레임워크의 의존성을 확인하면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GameSwiftUI &amp;nbsp;&lt;br /&gt;└── GameCore&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;└── ComposableArchitecture (TCA)&amp;nbsp;&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상하다고 느낀 점은 GameCore와 GameSwiftUI가 정적 프레임워크라는 점입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구조에서 TCA를 감싼 모듈과 View를 구현한 모듈 모두 프레임워크로 선언되어 있는데, 프리뷰를 돌려보면 잘 돌아갑니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-28 오후 2.51.06.png&quot; data-origin-width=&quot;2348&quot; data-origin-height=&quot;1258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yEoPJ/btsMzNTDjb7/m5wLi9mKmSriGsUnkQj2Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yEoPJ/btsMzNTDjb7/m5wLi9mKmSriGsUnkQj2Zk/img.png&quot; data-alt=&quot;너는 왜 잘 동작하는거니 ...?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yEoPJ/btsMzNTDjb7/m5wLi9mKmSriGsUnkQj2Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyEoPJ%2FbtsMzNTDjb7%2Fm5wLi9mKmSriGsUnkQj2Zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;418&quot; data-filename=&quot;스크린샷 2025-02-28 오후 2.51.06.png&quot; data-origin-width=&quot;2348&quot; data-origin-height=&quot;1258&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;너는 왜 잘 동작하는거니 ...?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 이해한 바에 따르면 프리뷰에서 Runtime Linking Failure 에러가 떠야하는데 말이죠.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Phy6n/btsMy39ZdzL/2j9PvpfnrttnFJVXbRVyN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Phy6n/btsMy39ZdzL/2j9PvpfnrttnFJVXbRVyN1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;369&quot; data-filename=&quot;스크린샷 2025-02-26 오후 5.07.02.png&quot; style=&quot;width: 52.2114%; margin-right: 10px;&quot; data-widthpercent=&quot;52.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Phy6n/btsMy39ZdzL/2j9PvpfnrttnFJVXbRVyN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPhy6n%2FbtsMy39ZdzL%2F2j9PvpfnrttnFJVXbRVyN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;428&quot; height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4tjOd/btsMzg2p2Or/uYhDBVwiPaRtXNJZQ03VZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4tjOd/btsMzg2p2Or/uYhDBVwiPaRtXNJZQ03VZK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;405&quot; data-origin-height=&quot;391&quot; data-filename=&quot;스크린샷 2025-02-26 오후 5.11.00.png&quot; style=&quot;width: 46.6258%;&quot; data-widthpercent=&quot;47.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4tjOd/btsMzg2p2Or/uYhDBVwiPaRtXNJZQ03VZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4tjOd%2FbtsMzg2p2Or%2FuYhDBVwiPaRtXNJZQ03VZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;debug와 release용으로 빌드된 TicTacToe 앱의 패키지 내용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;빌드된&lt;span&gt; &lt;/span&gt;&lt;/span&gt;TicTacToe 앱의 번들 패키지를 까보면 정적 linking이지만 빌드된 앱의 패키지를 까보면 __preview.dylib와 \(앱이름).debug.dylib라는 파일이 존재하는걸 확인할 수 있습니다. &lt;br /&gt;이 파일들은 Debug용으로 빌드된 앱의 패키지에만 존재하며, Release로 빌드된 패키지 내에는 존재하지 않습니다.&amp;nbsp;&lt;br /&gt;이름부터 __preview.dylib인게 심상치 않아 찾아보니 관련된 문서가 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740645061786&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Understanding build product layout changes in Xcode | Apple Developer Documentation&quot; data-og-description=&quot;There's never been a better time to develop for Apple platforms.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b4fpPK/hyYjzXFW28/n2AUybad08ekpw52zxMrDK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/UM0G9/hyYmVx11LL/RXizu0W6gXeLHaQXZdk5i1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b4fpPK/hyYjzXFW28/n2AUybad08ekpw52zxMrDK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/UM0G9/hyYmVx11LL/RXizu0W6gXeLHaQXZdk5i1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Understanding build product layout changes in Xcode | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;There's never been a better time to develop for Apple platforms.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용을 요약해보면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;Xcode는 Preview 실행을 위해 __preview.dylib 를 생성하여 동적으로 로드한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Xcode는 빠른 Preview를 위해__preview.dylib 파일과 \(앱이름).debug.dylib 파일로 나누어 최적화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SwiftUI Preview와 일반 Debug 빌드 간에 빌드 산출물을 공유하여 동일한 레이아웃을 그릴 수 있도록 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 &lt;b&gt;Static Framework 자체는 Preview에서 지원되지 않지만, Xcode가 Preview 실행을 위해 __preview.dylib을 생성하면서 동적으로 실행을 가능하게 한다. &lt;/b&gt;는 것 같은데 ...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 왜 내 프로젝트에서는 안됐던거지 ...?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;++)&amp;nbsp; 추측: 모듈의 의존성을 SPM 으로 설정하는 방법과 Tuist로 관리하는 것의 차이인것 같은데...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 해보려고 Static Library를 만들어 TCA에 의존성을 추가하고 xcodeBuild에서 .xcframeworks를 만들어보려고 하니 다음과 같은 에러가 뜹니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-26 오후 10.02.22.png&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cf8w2r/btsMyVqhHYA/tIrfMu9nppWa5ybW16Tcc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cf8w2r/btsMyVqhHYA/tIrfMu9nppWa5ybW16Tcc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cf8w2r/btsMyVqhHYA/tIrfMu9nppWa5ybW16Tcc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcf8w2r%2FbtsMyVqhHYA%2FtIrfMu9nppWa5ybW16Tcc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1409&quot; height=&quot;735&quot; data-filename=&quot;스크린샷 2025-02-26 오후 10.02.22.png&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 설정값들이 무엇을 의미하는지는.... 아마 SPM으로 설정할 때에는 프리뷰로 확인할 수 있도록 적절하게 dylib로 변경해주는 것 같은데... 일단 여기까지만 알아보고 추후에 알아봐야겠습니다ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결론&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본적으로 Static Framework에서는 SwiftUI의 Preview가 동작하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Static Framework과 Dynamic Framework를 적절하게 조합하여 사용하면 개발효용성, 앱의 성능 등을 높일 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SPM으로 의존성을 관리하면 Xcode가 뒤에서 암튼 뭔가 해서 preview용 dynamic library를 만들어 쓰는거 같은데.. 이건 다음시간에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devmjun.github.io/archive/FrameworkVsLibrary&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devmjun.github.io/archive/FrameworkVsLibrary&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture/discussions/1680&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/pointfreeco/swift-composable-architecture/discussions/1680&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://eunjin3786.tistory.com/625&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://eunjin3786.tistory.com/625&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/xcode/understanding-build-product-layout-changes&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://neogurhub.tistory.com/entry/%EB%B0%B0%ED%8F%AC-%EA%B0%80%EB%8A%A5%ED%95%9C-FrameWork-%EB%A7%8C%EB%93%A4%EA%B8%B0-XCFramework&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://neogurhub.tistory.com/entry/%EB%B0%B0%ED%8F%AC-%EA%B0%80%EB%8A%A5%ED%95%9C-FrameWork-%EB%A7%8C%EB%93%A4%EA%B8%B0-XCFramework&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://minsone.github.io/ios/mac/ios-framework-part-1-static-framework-dynamic-framework&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://minsone.github.io/ios/mac/ios-framework-part-1-static-framework-dynamic-framework&lt;/a&gt;&lt;/p&gt;</description>
      <category>Swift</category>
      <category>composablearchitecture</category>
      <category>framework</category>
      <category>IOS</category>
      <category>SWIFT</category>
      <category>SWIFTUI</category>
      <category>TCA</category>
      <category>Tuist</category>
      <category>xcode</category>
      <category>앱구조</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/86</guid>
      <comments>https://leetaek.tistory.com/86#entry86comment</comments>
      <pubDate>Thu, 27 Feb 2025 17:43:37 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftData] iOS18이상에서 ModelContext.reset 호출로 인해 나타나는 런타임 오류 (TCA+SwiftData)</title>
      <link>https://leetaek.tistory.com/85</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-30 오후 3.00.03.png&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;24&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCYIq/btsJRBu7fGW/QyDc5Al33VkdoLDzcrv5x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCYIq/btsJRBu7fGW/QyDc5Al33VkdoLDzcrv5x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCYIq/btsJRBu7fGW/QyDc5Al33VkdoLDzcrv5x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCYIq%2FbtsJRBu7fGW%2FQyDc5Al33VkdoLDzcrv5x1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1372&quot; height=&quot;24&quot; data-filename=&quot;스크린샷 2024-09-30 오후 3.00.03.png&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;24&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Fatal error: This model instance was destroyed by calling ModelContext.reset and is no longer usable.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;발생 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iPadOS18 이상으로 업데이트하니 갑자기 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;사이드 프로젝트&lt;/span&gt;&amp;nbsp;앱에서 다음과 같은 에러와 함께 강제 종료가 되기 시작했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS17까지는 잘 돌아가던 코드가 18부터 오류가 발생하기 시작한 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱은 TCA 아키텍처 구조에서 SwiftData를 사용하고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하고 있던 방법은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp; SwiftData의 데이터를 관리하고 저장소에 접근하기 위한 ModelContainer를 생성&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/5bf0e59369711f5b2079bc03f9e15503.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 동시성 문제 없이 데이터를 관리하기 위해 ModelActor로 데이터를 관리.&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/6b0ea0785bc637d2a9d0e9adb98f0be3.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 2-1. 후에 재사용할 수 있을거 같아서 해당 Actor 등은 위해 모듈로 분리함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 2-2. 재사용성을 위해 Actor의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CRUD &lt;/span&gt;메서든는 제네릭으로 모델 타입을 받도록 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 구체적인 모델의 CRUD를 위한 struct를 구현. struct는 TCA의 Dependencies로 등록후 사용.&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/644a6f35b00e811b40e8664406a8df0d.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번의 구현에서 불편한 부분이 있으시다면 그 부분이 원인 맞습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제의 원인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftData에 대한 자료가 많지 않아서 찾을수 있을까 싶었지 이미 이 내용에 대한 포럼이 열려있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 보면 ModelActor를 사용할 때에 &lt;b&gt;모델 인스턴스가 액터보다 더 오래 살아있는 경우 &lt;/b&gt;발생할 수 있는 에러였다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오히려 iOS17에서 정상동작 하는것이 버그&lt;/b&gt;였다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 모델의 인스턴스가 액터보다 오래 살아있는 경우 문제가 생길까?&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. actor의 역할&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;actor는 모델의 인스턴스에 대한 접근을 큐를 통해 직렬화 해 순차적으로 진행한다. 때문에 동시접근이나 불일치를 방지하여 데이터에 안전한 접근을 보장한다. ModelActor는 모델의 인스턴스를 관리하는 주체로 여러 스레드에서 동일한 인스턴스에 접근할 때에 발생할 수 있는 문제를 예방한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 모델 인스턴스의 생명주기&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 인스턴스는 액터에 연결된 ModelContext에서 가져온 데이터이다. 모델의 인스턴스는 ModelContext가 살아있을 때에 유효하다. &lt;b&gt;actor가 존재하지 않는다면 ModelContext도 해제되고, 모델 인스턴스도 함께 해제된다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-30 오후 6.26.34.png&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AjPbX/btsJSeT4HAl/MkQmK3hJt7fOzh1DziI1L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AjPbX/btsJSeT4HAl/MkQmK3hJt7fOzh1DziI1L1/img.png&quot; data-alt=&quot;modelContext를 통해 인스턴스를 가져온다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AjPbX/btsJSeT4HAl/MkQmK3hJt7fOzh1DziI1L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAjPbX%2FbtsJSeT4HAl%2FMkQmK3hJt7fOzh1DziI1L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;79&quot; data-filename=&quot;스크린샷 2024-09-30 오후 6.26.34.png&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;modelContext를 통해 인스턴스를 가져온다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 생명주기로 인해 발생할 수 있는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;런타임 오류&lt;/b&gt;: 인스턴스가 actor보다 오래 살아남으면, 인스턴스에 접근할 때에 해제된 객체에 접근하여 런타임 오류가 발생할 수 있다.&lt;br /&gt;- &lt;b&gt;데이터 무결성&lt;/b&gt;: 혹여나 actor가 해제된 상태에서 값에 접근하여 수정이 된다면, actor가 관리하던 ModelContext의 상태와 불일치해서 데이터의 무결성이 보장되지 않을 수 있을 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;메모리 누수&lt;/b&gt;: 사용하지 않는 actor에 대한 참조가 남아 메모리 누수로 이어질 수도 있을 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 방안&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서 ModelContainer나 actor를 singleton으로 구현하는 것을 추천하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 나는 ModelContainer는 이미 singleton이고, actor의 경우도 한 상황에 Drawing 모델에 대한 인스턴스에만 접근하는데 왜 에러가 떴을까...?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-02 오후 2.22.06.png&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvZiPJ/btsJSLkD5ml/61VPcdTnjiBY1IjQyELka0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvZiPJ/btsJSLkD5ml/61VPcdTnjiBY1IjQyELka0/img.png&quot; data-alt=&quot;메서드들에 actor가 지역변수로 선언되어 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvZiPJ/btsJSLkD5ml/61VPcdTnjiBY1IjQyELka0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvZiPJ%2FbtsJSLkD5ml%2F61VPcdTnjiBY1IjQyELka0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;460&quot; data-filename=&quot;스크린샷 2024-10-02 오후 2.22.06.png&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메서드들에 actor가 지역변수로 선언되어 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 구체화한 struct의 메서드에 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp; 각 메서드 내에 modelActor가 지역 변수로 선언된다. 이 actor는 메서드의 생명주기와 함께하며, 메서드 종료시 ModelContext도 함께 해제된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 메서드가 종료된 후에 모델 인스턴스에 접근하려고 하면, 유효하지 않은 메모리 접근으로 런타임 에러가 발생된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/031bf850fae3e1b03b83b205300b6201.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 해당 액터를 지역 변수가 아니라 전역 변수로 선언하면 actor의 생명주기는 struct가 존재하는 한 유지되며 해당 문제는 해결된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 메서드들에서 중복으로 사용하는 변수들이기 때문에 진작에 전역변수로 빼놨으면 이런 문제가 발생하지도 않았을 일이긴 하다...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결론&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 돌아가던 코드가 안돌아간다고 버그가 아니다. 오히려 돌아갔던게 버그일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;객체의 생명주기를 고려하며 로직을 설계하자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 공통코드를 왜 반복해서 작성함?...?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 문제의 원인은 생각보다 간단할 수 있다. 자료가 적은 최신 API라 하더라도 당황하지 말자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://forums.developer.apple.com/forums/thread/757521&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://forums.developer.apple.com/forums/thread/757521&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1727848670940&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;iOS 18 SwiftData ModelContext reset | Apple Developer Forums&quot; data-og-description=&quot;I have the exact same problem. Sometimes when I build and run the app, it crashes and shows me the error. This happens randomly . Sometimes it builds fine and sometimes it crashes. Also, when it crashes and I relaunch the app without building again, it nev&quot; data-og-host=&quot;forums.developer.apple.com&quot; data-og-source-url=&quot;https://forums.developer.apple.com/forums/thread/757521&quot; data-og-url=&quot;https://forums.developer.apple.com/forums/thread/757521&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://forums.developer.apple.com/forums/thread/757521&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://forums.developer.apple.com/forums/thread/757521&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;iOS 18 SwiftData ModelContext reset | Apple Developer Forums&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I have the exact same problem. Sometimes when I build and run the app, it crashes and shows me the error. This happens randomly . Sometimes it builds fine and sometimes it crashes. Also, when it crashes and I relaunch the app without building again, it nev&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;forums.developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://forums.developer.apple.com/forums/thread/764281&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://forums.developer.apple.com/forums/thread/764281&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1727848679638&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;iOS 18 strange SwiftData fatal err&amp;hellip; | Apple Developer Forums&quot; data-og-description=&quot;Yeah, the code you provided in your comment creates the model container and holds it with a local variable, which is released after fetchEvents is done, which triggers the error when you try to access the events. Your new code seems to hold the model conta&quot; data-og-host=&quot;forums.developer.apple.com&quot; data-og-source-url=&quot;https://forums.developer.apple.com/forums/thread/764281&quot; data-og-url=&quot;https://forums.developer.apple.com/forums/thread/764281&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://forums.developer.apple.com/forums/thread/764281&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://forums.developer.apple.com/forums/thread/764281&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;iOS 18 strange SwiftData fatal err&amp;hellip; | Apple Developer Forums&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Yeah, the code you provided in your comment creates the model container and holds it with a local variable, which is released after fetchEvents is done, which triggers the error when you try to access the events. Your new code seems to hold the model conta&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;forums.developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS/에러</category>
      <category>IOS</category>
      <category>ios18</category>
      <category>swiftdata</category>
      <category>SWIFTUI</category>
      <category>TCA</category>
      <category>uikit</category>
      <category>개발</category>
      <category>모바일</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/85</guid>
      <comments>https://leetaek.tistory.com/85#entry85comment</comments>
      <pubDate>Wed, 2 Oct 2024 14:45:12 +0900</pubDate>
    </item>
    <item>
      <title>[TCA] Composable Architecture에서 NavigationSplitView 사용하기</title>
      <link>https://leetaek.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Composable Architecture(이하 TCA)에서 NavigationSplitView를 통해 화면전환을 주절주절 다루는 포스트입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;iOS 17.0 이상, TCA 1.9 이상 버전을 기준으로 작성된 코드입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQHuma/btsIIBwCupk/hJJqVoCNYSdZebzgy8qMxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQHuma/btsIIBwCupk/hJJqVoCNYSdZebzgy8qMxk/img.png&quot; data-origin-width=&quot;2142&quot; data-origin-height=&quot;1860&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2024-07-23 오전 9.18.40.png&quot; style=&quot;width: 52.5495%; margin-right: 10px;&quot; data-widthpercent=&quot;53.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQHuma/btsIIBwCupk/hJJqVoCNYSdZebzgy8qMxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQHuma%2FbtsIIBwCupk%2FhJJqVoCNYSdZebzgy8qMxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2142&quot; height=&quot;1860&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mdgR9/btsIJV8KON7/pxxoEhy1Ne3k8I4Ws8L9ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mdgR9/btsIJV8KON7/pxxoEhy1Ne3k8I4Ws8L9ak/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;1390&quot; data-filename=&quot;edited_스크린샷 2024-07-23 오전 9.19.04.png&quot; data-widthpercent=&quot;46.83&quot; style=&quot;width: 46.2877%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mdgR9/btsIJV8KON7/pxxoEhy1Ne3k8I4Ws8L9ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmdgR9%2FbtsIJV8KON7%2FpxxoEhy1Ne3k8I4Ws8L9ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1410&quot; height=&quot;1390&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;MacOS나 iPadOS에서 많이 사용되는 Sidebar가 있는 네비게이션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;iPad나 MacOS에서 흔히 볼수 있는 네비게이션의 방법 중에, 측면에 네비게이션을 위한 분할 뷰를 열로 생성하는 위와 같은 형태가 있다. 이를 쉽게 구현할 수 있도록 &lt;b&gt;SwiftUI에서 &lt;a href=&quot;https://developer.apple.com/documentation/swiftui/navigationsplitview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NavigationSplitView&lt;/a&gt;라는 API를 지원한다.&lt;/b&gt; NavigationSplitView는 iOS 16.0 이상부터 사용할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 설정 앱을 기준으로 section으로 구분되는 sidebar를 가진 간단한 NavigationSplitView 화면을 구현해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Tree-base VS Stack-base&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TCA Documents에 따르면&lt;b&gt; TCA의 네비게이션은 트리 기반과 스택 기반으로 구분&lt;/b&gt;할 수 있다. &lt;a href=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/whatisnavigation#Tree-based-vs-stack-based-navigation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;각각의 특징과 장단점&lt;/a&gt;은 문서 참고.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;나는 다음과 같은 요소들 때문에 둘 중에 &lt;b&gt;NavigationSplitView는 트리 기반이 더 적합하다고 판단했다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;Sidebar, Content, Detail의 계층 구조를 가지고 있는 NavigationSplitView는 스택 기반보다는 트리 기반의 네비게이션이 더 유리하게 보였다&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;NavigationSplitView는 Detail에서 &lt;b&gt;NavigationStackView을 중첩해서 사용도 가능&lt;/b&gt;하다. 때문에 NavigationSplitView는 Sidebar의 항목에 대한 간단한 화면전환을 구현하고, 필요에 따라 경로를 쌓아 복잡한 구조를 구현하는 경우에는 Detail에서 NavigationStack을 중첩해 구현하면 된다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구현하려고 하는 화면은 한번에 두개의 화면 상태를 가질수 없다. 하나의 Sidebar 항목만을 선택할 수 있도록 상태관리 할 것.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Reducer&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발자 문서를 참고해 enum으로 상태관리 하는 Tree 기반의 네비게이션을 따랐다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/96e3134dffca2395a06ec1ede514952a.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Path&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한번에 두개의 상태를 가질수 없게 하도록, 네비게이션 경로는 enum으로 설정했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/68f618caa3d83dc513aad0c5c2b802fd.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;설정 항목 시작시 초기값으로 iCloud 설정 화면으로 지정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Sidebar에서 List의 Selection 파라미터로 Path의 State를 받을건데, 해당 값이 Hashable 프로토콜을 필요로 해서 State를 hashable로 설정했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-23 오후 2.05.43.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beVtAB/btsIJx8OizT/Ux6Skhtlyo9Kaej0lnbgE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beVtAB/btsIJx8OizT/Ux6Skhtlyo9Kaej0lnbgE1/img.png&quot; data-alt=&quot;List나 Foreach에서는 아이템 식별과 관리에 Hashable을 요구한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beVtAB/btsIJx8OizT/Ux6Skhtlyo9Kaej0lnbgE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeVtAB%2FbtsIJx8OizT%2FUx6Skhtlyo9Kaej0lnbgE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;346&quot; data-filename=&quot;스크린샷 2024-07-23 오후 2.05.43.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;List나 Foreach에서는 아이템 식별과 관리에 Hashable을 요구한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;View&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Sidebar 1개의 열을 가지는 NavigationSplitView의 형태이다.&lt;/span&gt;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/30952606bc8f0b2ecdbb514d4fb086af.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Detail&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Sidebar View는 고정되지만, Detail은 Reducer에 따라 변화될 수 있기 @ViewBuilder메소드로 분리했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;switch를 통해 store의 path에 따라 view가 전환되도록 설정했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/8017de87fc8ef0cb606d06ff6281b794.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실은 .navigationDestination modifier를 통해 설정하려고 했는데. 요렇게 한 이유는 애먹은점에서 추가 설명.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Sidebar&lt;/span&gt;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/21591976d7fa6bcbf9b8c8285082a354.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;NavigationLink를 통해, 선택한 값을 push로 보내서 처리할 수 있도록 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애먹은 점&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Sidebar에서 주석에 번호를 단 것으로 코드설명을 조금 더 하면&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. List 파라미터&lt;/span&gt;&lt;br /&gt;List의 selection 파라미터를 통해 선택된 List 항목을 표시할 수 있다. selection에 넘겨줄 @State 변수를 따로 하나를 만들어서 선택된 값을 관리할 수도 있지만&lt;b&gt; store에 이미 선택된 값을 가지고 있는데, 이중으로 만들어 상태관리하고 싶지 않았다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 TCA는 단방향 아키텍처이기 때문에 action을 통해 reducer에서만 상태값을 변경할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-23 오후 1.57.01.png&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y992k/btsIIJWccPc/Og4xR5k707oxkiV0oNWv41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y992k/btsIIJWccPc/Og4xR5k707oxkiV0oNWv41/img.png&quot; data-alt=&quot;직접적으로 state에 binding을 하려고 하면 get-only라는 에러가 든다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y992k/btsIIJWccPc/Og4xR5k707oxkiV0oNWv41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy992k%2FbtsIIJWccPc%2FOg4xR5k707oxkiV0oNWv41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;54&quot; data-filename=&quot;스크린샷 2024-07-23 오후 1.57.01.png&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;직접적으로 state에 binding을 하려고 하면 get-only라는 에러가 든다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;View에서 바인딩이 필요한 경우, sending이라는 메서드를 통해 미리 store에 미리 지정된 action으로 변경된 값을 처리할 수 있다. 바인딩을 위한 action도 reducer에 추가해주었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/49569f9d3e94f70b522ed5a487a7c8be.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. List 내부 구현&lt;/span&gt;&lt;br /&gt;사실 가장 애먹었던 부분인데, 최종적으로는 NavigationLink를 사용한 방법으로 이용했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;원래는 Reducer에 각 뷰로 이동하는 action case를 설정하고, Button으로 해당 액션들을 store로 보내 실행하려고 했었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드로 보면 다음과 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/365cfdb6124e38a73196853c9c018b16.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/ea7859979d989e5cdced03686784b700.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그런데 이 경우 state.path가 설정된 후에 바로 nil로 재설정 되는 문제가 생겼다.&amp;nbsp; 원인을 보자면.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- Button눌러 실행되는 action을 통해 reducer에서 state.path를 할당된다. &lt;br /&gt;- 그런데 Path.State의 해시값과 Button에 tag로 설정한 Path.State의 해시값이 같지 않았다.&amp;nbsp;&lt;br /&gt;- List는 이에 선택된 Tag가 없다고 판단하고 selection에 nil을 보내게 되고, 이를 reducer의 push action에서 받아 다시 nil로 처리하는 문제였다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;List에서 선택한 값을 push 액션을 보내 설정하면 되기 때문에 코드에서 사용한 NavigationLink(_ titleKey, value: )을 사용하던, 혹은 Button 대신 Text으로 교체해 .tag를 붙여 사용하던 잘 동작한다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. navigationDestination Modifier&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;NavigationSplitView 관련해서 알아보던 중에 &lt;a href=&quot;https://developer.apple.com/documentation/localauthentication/localauthenticationview/navigationdestination(item:destination:)/#discussion&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;navigationDestination modifier&lt;/a&gt;를 이용하는 방법이 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;iOS17.0 이상부터는 해당 api를 NavigationSplitView에서도 사용할 수 있다는 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/9aa33e0a3c558ab7fd1b8641d340b705.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위에서 설명한대로 NavigationLink와 Text 둘 다 동일하게 잘 동작한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그런데 Detail에서 세부적인 네비게이션을 구현하기 위해 NavigationStack을 중첩하여 사용하는 경우에 문제가 생겼다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/bc53005aaa5d7d359f00c814df60e3a0.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-23 오후 3.33.12.png&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;55&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmqOpK/btsIK7VlWYA/QwDXiYiPXXVON4xF1Wuom0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmqOpK/btsIK7VlWYA/QwDXiYiPXXVON4xF1Wuom0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmqOpK/btsIK7VlWYA/QwDXiYiPXXVON4xF1Wuom0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmqOpK%2FbtsIK7VlWYA%2FQwDXiYiPXXVON4xF1Wuom0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1057&quot; height=&quot;55&quot; data-filename=&quot;스크린샷 2024-07-23 오후 3.33.12.png&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;55&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예제 코드에서는 중첩된 코드를 Stack-base로 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;런타임 에러의 설명을 보면 &lt;b&gt;SwiftUI에서는 NavigationLink를 통해 NavigationStack이 포함된 뷰를 직접적으로 여는것을 허용하지 않는다&lt;/b&gt;는 내용이었다.&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 에러는 NavigationLink가 아닌 Text로 구현해도 동일하게 발생했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; letter-spacing: 0px;&quot;&gt;아마 NavigationSplitView의 Sidebar에서 내부적으로 NavigationLink를 활용하기 때문에 그 안에 NavigationStack을 포함한 뷰를 .navigationDestination을 통해 전달해서 이런 메세지가 뜨는게 아닌가 싶다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;때문에 이를 회피하고자 Detail을 분리해 iflet으로 view를 처리했다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-3-6c75adfa74.gif&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkL23R/btsILhjkAuH/KNarU7snKUejqJYXib3Qak/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkL23R/btsILhjkAuH/KNarU7snKUejqJYXib3Qak/img.gif&quot; data-alt=&quot;완성~~~&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkL23R/btsILhjkAuH/KNarU7snKUejqJYXib3Qak/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bkL23R/btsILhjkAuH/KNarU7snKUejqJYXib3Qak/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;871&quot; height=&quot;600&quot; data-filename=&quot;ezgif-3-6c75adfa74.gif&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완성~~~&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최종 코드&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/948dfa911c36fa461e5deb87072c6151.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참조&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;NavigationSplitView Apple 개발자 문서&quot; href=&quot;https://developer.apple.com/documentation/swiftui/navigationsplitview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/swiftui/navigationsplitview&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;TCA의 Navigation 개발자 문서&quot; href=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/navigation&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/navigation&lt;/a&gt;&lt;/p&gt;</description>
      <category>SwiftUI/Composable Architecture</category>
      <category>composablearchitecture</category>
      <category>IOS</category>
      <category>Navigation</category>
      <category>navigationsplitview</category>
      <category>SWIFT</category>
      <category>SWIFTUI</category>
      <category>TCA</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/84</guid>
      <comments>https://leetaek.tistory.com/84#entry84comment</comments>
      <pubDate>Tue, 23 Jul 2024 16:17:39 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] 정규표현식</title>
      <link>https://leetaek.tistory.com/83</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정규표현식이란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특정 패턴언어 가지는 문자열의 집합을 표현하는데 사용되는 형식 언어.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;핸드폰번호, 이메일 등 패턴을 가진 문자열의 유효성 검증할때 핵꿀.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정규표현식 문법&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메타문자&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;사실 메타문자는 봐도봐도 까먹는다... 또 까먹을것 같지만 한번 정리는 해두자&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메타문자를 정규문자로 사용하고 싶으면 \를 통해 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 230px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 17px;&quot;&gt;메타문자&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 17px;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 17px;&quot;&gt;예시&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 17px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 17px;&quot;&gt;.&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 17px;&quot;&gt;문자 일치&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 17px;&quot;&gt;...123 ( = abc123, def123 등)&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 17px;&quot;&gt;개행문자를 제외한 문자 1개와 일치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 17px;&quot;&gt;+&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 17px;&quot;&gt;1개 이상&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 33px;&quot; rowspan=&quot;2&quot;&gt;\w+ ( = Hello 등), \d+ (= 010 등),&amp;nbsp;&lt;br /&gt;\d+\s\d(= 010 0000등)&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 17px;&quot;&gt;1개 이상의 문자 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 16px;&quot;&gt;\w \d \s&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 16px;&quot;&gt;영문자, 숫자, 공백&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 16px;&quot;&gt;[A-Za-z0-9], [0-9], 공백문자를 의미&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 17px;&quot;&gt;\W \D \S&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 17px;&quot;&gt;영문자, 숫자, 공백이 아님&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 17px;&quot;&gt;&lt;span&gt;\D+\s\D+ (= 나는 택꽁이 등)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 17px;&quot;&gt;[A-Za-z0-9], [0-9], 공백문자가 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 38px;&quot;&gt;[]&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 38px;&quot;&gt;문자 집합&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 38px;&quot;&gt;&lt;span&gt;[a, b, c]+ (= a or b or c 로만 이루어진 문자열)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 38px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #180204; text-align: start;&quot;&gt;[a-z]는 a부터 z까지 중 하나를 의미함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 19px;&quot;&gt;[^]&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 19px;&quot;&gt;문자집합 부정&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 19px;&quot;&gt;&lt;span&gt;[^a, b, c]+ (= 얘네만 아니면 됨)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 19px;&quot;&gt;[^a-z]는 알파벳 소문자가 아닌 문자를 의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 19px;&quot;&gt;^ $&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 19px;&quot;&gt;시작과 끝&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 19px;&quot;&gt;문자열의 시작과 끝 의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 17px;&quot;&gt;*&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 17px;&quot;&gt;0개 이상&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 17px;&quot;&gt;a*b ( = b, ab, aaab 등)&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 17px;&quot;&gt;0개 이상의 문자 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 17px;&quot;&gt;?&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 17px;&quot;&gt;0 ~ 1개&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 17px;&quot;&gt;a?b (= b 또는 ab)&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 17px;&quot;&gt;0 또는 1개만 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.7442%;&quot;&gt;{n}&lt;/td&gt;
&lt;td style=&quot;width: 20.465%;&quot;&gt;n개&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%;&quot;&gt;\d{3} (= 010 등)&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%;&quot;&gt;n개의 수를 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 19px;&quot;&gt;{m, n}&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 19px;&quot;&gt;m개 이상 n개 이하&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 19px;&quot;&gt;\d{3, 4} ( = 010, 1234 등)&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 19px;&quot;&gt;m개 이상 n개 이하의 개수를 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.7442%; height: 17px;&quot;&gt;()&lt;/td&gt;
&lt;td style=&quot;width: 20.465%; height: 17px;&quot;&gt;병합&lt;/td&gt;
&lt;td style=&quot;width: 28.4885%; height: 17px;&quot;&gt;&lt;span&gt;(\+\d{1,3})?&amp;nbsp; ( = +82, +192 등)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.3023%; height: 17px;&quot;&gt;여러 표현식을 그룹화한다.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 많은 정규 있다는데 궁금하면 &lt;a href=&quot;https://regexr.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;요기&lt;/a&gt; 참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://www.debuggex.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;정규 표현식이 맞는지 테스트 해주는 곳&lt;/a&gt;도 있다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Swift에서 정규표현식&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. range 메서드&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Foundation 프레임워크 안에 &lt;b&gt;NSRange 타입의 range 메서드&lt;/b&gt;를 활용하여 정규 표현식과 매칭되는지 검증할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/ba09942307742e153091eeea0856ff93.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.53.22.png&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXwehg/btsIDWgMLCO/BiJkLK9yNKcN0byX3ocib0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXwehg/btsIDWgMLCO/BiJkLK9yNKcN0byX3ocib0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXwehg/btsIDWgMLCO/BiJkLK9yNKcN0byX3ocib0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXwehg%2FbtsIDWgMLCO%2FBiJkLK9yNKcN0byX3ocib0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2070&quot; height=&quot;568&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.53.22.png&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;isValidateEmail은 매칭되는 범위을 반환하는데, 매칭되는 값이 없다면 nil을 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. NSPredicate&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Foundation 프레임워크의 &lt;b&gt;NSPredicate 클래스&lt;/b&gt;를 통해서도 정규 표현식 패턴을 만족하는지 검증할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/b1132215ae3573203d3be40a2e111ad3.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.51.59.png&quot; data-origin-width=&quot;1724&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpYZdr/btsIFUB5g6Y/YRmSHu42pVT0Ihtk276aK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpYZdr/btsIFUB5g6Y/YRmSHu42pVT0Ihtk276aK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpYZdr/btsIFUB5g6Y/YRmSHu42pVT0Ihtk276aK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpYZdr%2FbtsIFUB5g6Y%2FYRmSHu42pVT0Ihtk276aK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1724&quot; height=&quot;482&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.51.59.png&quot; data-origin-width=&quot;1724&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;주요 NSPredicate 연산자&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;NSPredicate ****연산자는 다양하게 있는데, 이건 NSPredicate 포스트가 아니므로 정규 표현식과 함께 사용될만한 연산자만 정리.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;나머지는 필요한 경우 검색해서 찾아보자.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;SELF&lt;/b&gt;: 배열의 각 요소를 나타냄&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;CONTAINS: 문자열이 특정 문자열을 포함하고 있는지 검사함&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/ecf96986f58cbabb4e35afdd2db89a63.js&quot;&gt;&lt;/script&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;MATCHES&lt;/b&gt;: 문자열이 정규표현식 패턴과 일치하는지 검사 함&lt;/span&gt;&lt;/li&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/a45c36ab8b16a8bd54ec1d9c5d8f1d93.js&quot;&gt;&lt;/script&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;BEGINSWITH/ENDSWITH: 문자열이 특정 접두사, 접미사를 포함하는지 검사함&lt;/span&gt;&lt;/li&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/91579c251456af2a745899cd4e7620ca.js&quot;&gt;&lt;/script&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AND/OR: 복수 조건을 조합할 때 사용&lt;/span&gt;&lt;/li&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/81a97d319bf90692fd8421eeb32368f8.js&quot;&gt;&lt;/script&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. NSRegularExpression&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Foundation 프레임워크에 존재하는 NSRegularExtpression 클래스를 통해서도 정규표현식 패턴을 만족하는지 검증할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/70b8b3b2ede9960f1610aa0835af3e30.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-19_%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB_9.10.09.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qtjCM/btsIE0b4ZN8/wkX1fQPt3IRAJezsmTkKd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qtjCM/btsIE0b4ZN8/wkX1fQPt3IRAJezsmTkKd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qtjCM/btsIE0b4ZN8/wkX1fQPt3IRAJezsmTkKd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqtjCM%2FbtsIE0b4ZN8%2FwkX1fQPt3IRAJezsmTkKd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1514&quot; height=&quot;890&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-19_%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB_9.10.09.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 NSRange를 사용할 때 input.count가 아니라 input.utf16.count를 사용하는 것을 볼 수 있다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 위의 코드에서 input.count로 돌려보면 결과값이 잘 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.49.51.png&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzzZ8/btsIFiXIBXO/oXQshPJzDKYtNOZIWkWGT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzzZ8/btsIFiXIBXO/oXQshPJzDKYtNOZIWkWGT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzzZ8/btsIFiXIBXO/oXQshPJzDKYtNOZIWkWGT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlzzZ8%2FbtsIFiXIBXO%2FoXQshPJzDKYtNOZIWkWGT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1754&quot; height=&quot;992&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.49.51.png&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;992&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그런데 count를 찍어보면 다르다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Swift의 문자열은 유니코드 스칼라의 값으로 이루어져있는데, &lt;b&gt;Objective-C의 NSString은 UTF-16 인코딩을 사용한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;때문에 Objective-C 기반의 API인 NSRegularExpression에서도 문자열의 범위를 정확하게 지정하기 위해서는 UTF-16을 사용하는 것이 필요하다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. RegexBuilder (iOS 16.0 +)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;WWDC22에서 발표된 새로운 정규표현식 방법. Swift 5.7부터 지원되며 iOS 16 이상에서부터 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;패턴 정의&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;String으로 정의하기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/b8053c4e716654205d14c7c3adedee99.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: left;&quot;&gt;기존의 정규표현식을 ##로 감싸서 Regex 생성자에 넣는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 기존의 정규표현식 작성 방법과 유사하게 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: 정규표현식 문법을 모르면 가독성이 떨어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;// 으로 정의하기&lt;/span&gt;&lt;/li&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/4b2d4f8912b78d20e4c990dfac6bb314.js&quot;&gt;&lt;/script&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;// 사이에 정규표현식을 넣으면 컴파일러가 알아서 Regex타입으로 바꿔준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 간결하고 직관적이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: 정규 표현식 문법을 모르면 가독성이 떨어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;RegexBuilder로 정의하기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/12d234a4dc6d73db0787b07981281583.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: left;&quot;&gt;&lt;/span&gt;Regex를 이용해 선언형의 Swift 언어와 비슷하게 정규 표현식을 나타낼 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 구조화 된 방식으로 가독성과 유지보수성이 뛰어남&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: RegexBuilder 프레임워크를 import 해야함, 간단한 식에는 굳이&amp;hellip;?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.30.45.png&quot; data-origin-width=&quot;373&quot; data-origin-height=&quot;561&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xUJkj/btsIENDWrJp/uGCL9hUy1JI7ORqO2lwyVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xUJkj/btsIENDWrJp/uGCL9hUy1JI7ORqO2lwyVk/img.png&quot; data-alt=&quot;Docs에 나와있는 컴포넌트 목록&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xUJkj/btsIENDWrJp/uGCL9hUy1JI7ORqO2lwyVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxUJkj%2FbtsIENDWrJp%2FuGCL9hUy1JI7ORqO2lwyVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;373&quot; height=&quot;561&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.30.45.png&quot; data-origin-width=&quot;373&quot; data-origin-height=&quot;561&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docs에 나와있는 컴포넌트 목록&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지원하는 컴포넌트. 여기서 필요한것 가져다 쓰면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.26.37.png&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNWLbw/btsIEcYnH1Q/qEd8vZkhBSHDcU8Pt1WKV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNWLbw/btsIEcYnH1Q/qEd8vZkhBSHDcU8Pt1WKV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNWLbw/btsIEcYnH1Q/qEd8vZkhBSHDcU8Pt1WKV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNWLbw%2FbtsIEcYnH1Q%2FqEd8vZkhBSHDcU8Pt1WKV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;843&quot; height=&quot;482&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.26.37.png&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 // 으로 정의된 정규표현식은 Xcode에서 RegexBuilder로 변환이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존의 사용하던 정규표현식이 있다면 끝을 //로 감싼 다음에&lt;b&gt; 우클릭-refactor-Conver to Regex Builder&lt;/b&gt;를 누르면 쉽게 변환이 된다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;정규표현식 매칭&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;firstMatch: 첫번째로 패턴에 매칭되는 문자열 (옵셔널로 )반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.47.07.png&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r3gG1/btsIE2gzzYO/WPevZEnconpR526SYbx0ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r3gG1/btsIE2gzzYO/WPevZEnconpR526SYbx0ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r3gG1/btsIE2gzzYO/WPevZEnconpR526SYbx0ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr3gG1%2FbtsIE2gzzYO%2FWPevZEnconpR526SYbx0ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1658&quot; height=&quot;310&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.47.07.png&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;wholeMatch: 문장 전체가 패턴에 매칭되는 문자열 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.46.45.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sr0Df/btsIFHbLisH/rLkUKzXMwkLbkFgUnh9Mvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sr0Df/btsIFHbLisH/rLkUKzXMwkLbkFgUnh9Mvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sr0Df/btsIFHbLisH/rLkUKzXMwkLbkFgUnh9Mvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsr0Df%2FbtsIFHbLisH%2FrLkUKzXMwkLbkFgUnh9Mvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1474&quot; height=&quot;258&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.46.45.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;matches: 매칭되는 문자열 전부 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.46.12.png&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ds2coW/btsID40x1YU/189J8q2YC2F1DU32WPwAC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ds2coW/btsID40x1YU/189J8q2YC2F1DU32WPwAC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ds2coW/btsID40x1YU/189J8q2YC2F1DU32WPwAC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fds2coW%2FbtsID40x1YU%2F189J8q2YC2F1DU32WPwAC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1684&quot; height=&quot;372&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.46.12.png&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Capture&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Capture: Capture를 통해 원하는 문자열를 캡쳐할 수 잇다.Capture 로 캡쳐해둔 부분만 가져와 사용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.45.13.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNYG7V/btsIFFyhgh9/sWkk5w3nTIWqiwn51TL8tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNYG7V/btsIFFyhgh9/sWkk5w3nTIWqiwn51TL8tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNYG7V/btsIFFyhgh9/sWkk5w3nTIWqiwn51TL8tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNYG7V%2FbtsIFFyhgh9%2FsWkk5w3nTIWqiwn51TL8tK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1632&quot; height=&quot;1116&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_4.45.13.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;결론&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- 사실 정규표현식 문법을 익히는게 제일 빡신 것 같다. 막상 한번 정리하고 나니까 눈에 잘 익고 다음번엔 술술 작성할것 같은 근자감도 드는데,&amp;nbsp; 고때 가선 어떨지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- 눈에 바로 들어오는 RegexBuilder를 쓰는게 마음 편할것 같은데 iOS16 이상인게 아쉽다. 코테같은데선 쓸수 있으려나?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;참고&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swift/regex?changes=latest_major&amp;amp;language=objc&quot;&gt;https://developer.apple.com/documentation/swift/regex?changes=latest_major&amp;amp;language=objc&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://green1229.tistory.com/280&quot;&gt;https://green1229.tistory.com/280&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@eunjin3786/281&quot;&gt;https://brunch.co.kr/@eunjin3786/281&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://onelife2live.tistory.com/35&quot;&gt;https://onelife2live.tistory.com/35&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Swift</category>
      <category>IOS</category>
      <category>regex</category>
      <category>SWIFT</category>
      <category>정규표현식</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/83</guid>
      <comments>https://leetaek.tistory.com/83#entry83comment</comments>
      <pubDate>Fri, 19 Jul 2024 09:20:57 +0900</pubDate>
    </item>
    <item>
      <title>[TCA] SharedState의 UserDefaults 확장하기 (CodableAppStorageKey)</title>
      <link>https://leetaek.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TCA에서 사용되는 &lt;a href=&quot;http://Sharing%20state Learn techniques for sharing state throughout many parts of your application, and how to persist data to user defaults, the file system, and other external mediums.  Overview Sharing state is the process of letting many features have access to the same data so that when any feature makes a change to this data it is instantly visible to every other feature. Such sharing can be really handy, but also does not play nicely with value types, which are copied rather than shared. Because the Composable Architecture highly prefers modeling domains with value types rather than reference types, sharing state can be tricky.  This is why the library comes with a few tools for sharing state with many parts of your application. There are two main kinds of shared state in the library: explicitly passed state and persisted state. And there are 3 persistence strategies shipped with the library: in-memory, user defaults, and file storage. You can also implement your own persistence strategy if you want to use something other than user defaults or the file system, such as SQLite.&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SharedState&lt;/a&gt;에 대한 내용입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서는 SharedState이 무엇인지 소개하는 내용 보다도 사용하면서 조금은 편하게 쓰고자 애쓴 흔적들을 &lt;u&gt;&lt;b&gt;주절주절&lt;/b&gt;&lt;/u&gt; 적습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SharedState&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;어느날 갑자기 TCA 제작사인 Point-Free 에서 메일이 겁나 날라왔다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;TCA 새로운 에피소드, 바로 Sharing State입니다~~~ 하는 내용이었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-07-17 오후 4.51.41.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n5Xsp/btsICEtsA2B/9UD9pMkJAw5rvShBnoiLk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n5Xsp/btsICEtsA2B/9UD9pMkJAw5rvShBnoiLk0/img.png&quot; data-alt=&quot;꾸준히 날라오는 그들의 에피소드 ...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n5Xsp/btsICEtsA2B/9UD9pMkJAw5rvShBnoiLk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn5Xsp%2FbtsICEtsA2B%2F9UD9pMkJAw5rvShBnoiLk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;623&quot; data-filename=&quot;edited_스크린샷 2024-07-17 오후 4.51.41.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;꾸준히 날라오는 그들의 에피소드 ...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예전에 Github에서 SharedState에 대한 논의가 활발히 다뤄지는 것만 봤었다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;느낌상 큰 업데이트일것 같아서 &lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture/tree/shared-state-beta&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Shared-State-beta 브랜치&lt;/a&gt;에서 개발되고 있는 코드를 사이드 프로젝트에 미리 적용봤었었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SharedState는&amp;nbsp;ChildFeature로 상태를 굳이 하나하나 전달하지 않아도 되는, 왕편리함 그 자체였다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 중에 자주 사용하게 되는 부분이 TCA에서 제공해주는 Persisted SharedStated의 &lt;a href=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/sharingstate/#User-defaults&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UserDefaults&lt;/a&gt;였다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정말 편한데 UserDefaults의 단점은 &lt;b&gt;간단한 자료형&lt;/b&gt;들만 사용 가능하다는 것이었다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-17 오후 5.02.02.png&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T1A5H/btsIEefRCcL/Y0KIOdSDLS3zWDTdZHl55K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T1A5H/btsIEefRCcL/Y0KIOdSDLS3zWDTdZHl55K/img.png&quot; data-alt=&quot;간단한게 아니면 다른걸 써라 ...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T1A5H/btsIEefRCcL/Y0KIOdSDLS3zWDTdZHl55K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT1A5H%2FbtsIEefRCcL%2FY0KIOdSDLS3zWDTdZHl55K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;833&quot; height=&quot;112&quot; data-filename=&quot;스크린샷 2024-07-17 오후 5.02.02.png&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;간단한게 아니면 다른걸 써라 ...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 UserDefaults에 좀 더 복잡한 자료형(사실 그렇게 복잡한것도 아닌데...)들도 저장하기 위해 찾다가 감사하게도&lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture/discussions/2857#discussioncomment-9087925&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; Github에 누군가 올려둔 것을 발견했다.&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드의 내용은 UserDefaults에 기존 자료형 저장하던 것 처럼, &lt;b&gt;Data형태로 encoding/deconding 저장하는 것&lt;/b&gt;이었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CodableAppStorageKey&lt;/span&gt;&lt;/h3&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/3c4b20d4d34b1f6e3c45952cd1fe711e.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;간단하게 코드를 분석해보면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. Generic으로 Codable을 준수하는 자료형이 들어오면 이를 CodableAppStorageKey를 통해 처리하도록 &lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture/blob/shared-state-beta/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md#custom-persistence&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PersistenceReaderKey에 등록한다.&amp;nbsp;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. CodableAppStorageKey는 들어온 값을 Data 형태로 encoding/decoding하여 이미 &lt;b&gt;TCA에 구현되어 있는 AppStorageKey를 통해 넘겨, UserDefaults로 save, load, subscribe 한다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용하려면 물론 Codable 프로토콜을 준수하는 객체를 보내야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 코드는 매우 효과적으로 잘 동작했었다. 분명히 잘 동작을 했었다...&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제 발생&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;어느날 작업한 내용을 Push하고 업무를 보고 있었는데 Xcode Cloud에서 메일이 하나 날라왔다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-17 오후 5.21.20.png&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZBGpe/btsIDvWUWlX/bf5cbfcsdjHpA6RHYDd1tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZBGpe/btsIDvWUWlX/bf5cbfcsdjHpA6RHYDd1tK/img.png&quot; data-alt=&quot;?!???&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZBGpe/btsIDvWUWlX/bf5cbfcsdjHpA6RHYDd1tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZBGpe%2FbtsIDvWUWlX%2Fbf5cbfcsdjHpA6RHYDd1tK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;120&quot; data-filename=&quot;스크린샷 2024-07-17 오후 5.21.20.png&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;?!???&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;빌드 자체가 안된댄다 ... &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;분명 실기기 테스트까지 하고 올렸는데 빌드가 안된다니,&amp;nbsp;cloud로 올린 코드를 확인해봤는데 잘만 돌아갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 로그를 따라서 Composable architecture 라이브러리의 AppStorageKey를 따라가봤는데 생성자도 로그랑은 다르게 public으로 선언되어있었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;환경상의 문제가 있나 싶어 Xcode Cloud의 환경도 확인해봤는데 별다른 문제가 없어보였다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러다가 생각났다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uRNxO/btsIDsMJ4bK/rWemW3TyDsKioGVJXxUzDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uRNxO/btsIDsMJ4bK/rWemW3TyDsKioGVJXxUzDK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;144&quot; data-filename=&quot;스크린샷 2024-07-17 오후 5.37.10.png&quot; style=&quot;width: 47.0498%; margin-right: 10px;&quot; data-widthpercent=&quot;47.6&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uRNxO/btsIDsMJ4bK/rWemW3TyDsKioGVJXxUzDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuRNxO%2FbtsIDsMJ4bK%2FrWemW3TyDsKioGVJXxUzDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bay1mj/btsIDxtHyzF/UY5pP4zklFfxH6yZAWVU5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bay1mj/btsIDxtHyzF/UY5pP4zklFfxH6yZAWVU5k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;105&quot; data-filename=&quot;스크린샷 2024-07-17 오후 5.38.28.png&quot; style=&quot;width: 51.7874%;&quot; data-widthpercent=&quot;52.4&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bay1mj/btsIDxtHyzF/UY5pP4zklFfxH6yZAWVU5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbay1mj%2FbtsIDxtHyzF%2FUY5pP4zklFfxH6yZAWVU5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;496&quot; height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;beta 브랜치와 main브랜치에 적용된 생성자...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;내가 앱에 적용했던것은 shared-state-beta 브랜치였다는 것을...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SharedState가&amp;nbsp; main으로 merge되면서 생성자의 접근제어자가 filePrivate로 변경되어 있었다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AppStorageKey를 Init해 사용하는 CodableAppStorage은 애초에 빌드가 될수 없었던 것이었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 기존에 구현된 AppStoragekey를 참고해서 다시 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아래는 기존에 구현된 코드를 바탕으로 무식하게 따라 만든 CodableAppStorageKey이다 ...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;다시 만든 MusicCodableAppStorageKey&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위의 Music은 음악이 아니라 무식입니다...&lt;/span&gt;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/b9a3f73d7db5282596a27212b7d28994.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;큰 틀은 동일하다. Codable한 객체를 받아서 Data 타입으로 encoding/decoding 한 후, 이를 UserDefaults로 저장하는 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주석으로 쓴 번호를 따라 간단하게 코드 설명을 하면&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. AppStorage로 Codable한 값을 받을 경우 CodableAppStorageKey로 처리하도록&lt;b&gt; PersistenceReaderKey를 등록&lt;/b&gt;한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. &lt;b&gt;PersistenceKey protocol을 준수&lt;/b&gt;하는 struct를 만든다. 이전에는 이미 정의된 AppStorageKey를 그대로 가져와 사용했기 때문에 프로퍼티로 AppStorage를 가져와 encoding/decoding한 Data를 넘겨줘 처리했지만, 이제는 AppStorageKey를 못가져오기 때문에 AppStorageKey의 init을 못사용하기 때문에 저장에 사용할 key값과 store를 그대로 정의한다. store는 init에서 swift-dependencies로 가져와 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3-5. AppStorageKey의 경우 save, load를 구현하는 Lookup이라는 프로토콜을 사용했다. 들어오는 데이터 타입에 따라 적합한 Lookup을 통해 로직을 구현했다. 나는 Data를 저장하는 부분만 필요해 해당 부분이 구현된 &lt;b&gt;CastableLookup을 가져와서 encoding/decoding 과정만 추가&lt;/b&gt;했다. Subscribe는 그대로 활용.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용은 SharedState의 userDefaults와 동일하게 사용하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-18 오전 9.59.04.png&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pDP0b/btsIDqu09QH/2zRYK1elDLThbt29SrInZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pDP0b/btsIDqu09QH/2zRYK1elDLThbt29SrInZ1/img.png&quot; data-alt=&quot;Codable을 준수하는 PencilPalatte를 appStorage로 저장하고 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pDP0b/btsIDqu09QH/2zRYK1elDLThbt29SrInZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpDP0b%2FbtsIDqu09QH%2F2zRYK1elDLThbt29SrInZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;264&quot; data-filename=&quot;스크린샷 2024-07-18 오전 9.59.04.png&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Codable을 준수하는 PencilPalatte를 appStorage로 저장하고 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-18 오전 10.10.11.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MqJly/btsIEjIAo1i/xJUA1W53CLFroEkTNXTjH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MqJly/btsIEjIAo1i/xJUA1W53CLFroEkTNXTjH1/img.png&quot; data-alt=&quot;xcode cloud에서도 잘 처리가 됐다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MqJly/btsIEjIAo1i/xJUA1W53CLFroEkTNXTjH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMqJly%2FbtsIEjIAo1i%2FxJUA1W53CLFroEkTNXTjH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;228&quot; data-filename=&quot;스크린샷 2024-07-18 오전 10.10.11.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;xcode cloud에서도 잘 처리가 됐다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 복잡한 자료나 대규모 데이터를 다루는 경우라면 fileStorage를 쓰는게 이득이겠지만, 그게 아니고 Codable만 준수한다면 편하게 AppStorage를 사용해 저장할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/sharingstate/#Overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TCA ShardState Docs&amp;nbsp;&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721266062170&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Documentation&quot; data-og-description=&quot;&quot; data-og-host=&quot;pointfreeco.github.io&quot; data-og-source-url=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/sharingstate/#Overview&quot; data-og-url=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/sharingstate/#Overview&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/sharingstate/#Overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/sharingstate/#Overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pointfreeco.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture/discussions/2857#discussioncomment-9087925&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TCA SharedState-beta Discussions&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721266140231&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Shared state beta &amp;middot; pointfreeco swift-composable-architecture &amp;middot; Discussion #2857&quot; data-og-description=&quot;Hello all, today we are beginning a very exciting beta for the Composable Architecture. It brings all new tools to the library for sharing state in your application and persisting data. To try out ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/pointfreeco/swift-composable-architecture/discussions/2857#discussioncomment-9087925&quot; data-og-url=&quot;https://github.com/pointfreeco/swift-composable-architecture/discussions/2857&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AxVnV/hyWzBXtcHH/ktQg4stwWlogfKn3F2gTA0/img.png?width=1200&amp;amp;height=600&amp;amp;face=1018_146_1063_194&quot;&gt;&lt;a href=&quot;https://github.com/pointfreeco/swift-composable-architecture/discussions/2857#discussioncomment-9087925&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/pointfreeco/swift-composable-architecture/discussions/2857#discussioncomment-9087925&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AxVnV/hyWzBXtcHH/ktQg4stwWlogfKn3F2gTA0/img.png?width=1200&amp;amp;height=600&amp;amp;face=1018_146_1063_194');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Shared state beta &amp;middot; pointfreeco swift-composable-architecture &amp;middot; Discussion #2857&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hello all, today we are beginning a very exciting beta for the Composable Architecture. It brings all new tools to the library for sharing state in your application and persisting data. To try out ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SwiftUI/Composable Architecture</category>
      <category>composablearchitecture</category>
      <category>github</category>
      <category>IOS</category>
      <category>SWIFT</category>
      <category>SWIFTUI</category>
      <category>TCA</category>
      <category>xcodecloud</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/82</guid>
      <comments>https://leetaek.tistory.com/82#entry82comment</comments>
      <pubDate>Thu, 18 Jul 2024 10:20:18 +0900</pubDate>
    </item>
    <item>
      <title>[Tuist] Xcode Cloud 적용하기 (SwiftLint, FirebaseCrashlytics)</title>
      <link>https://leetaek.tistory.com/81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;사이드 프로젝트에 Xcode Cloud를 적용해보면서 겪은 과정을 정리한 글입니다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Xcode Cloud에 대한 소개와 사용법 보다는 Tuist로 관리되는 프로젝트를 Xcode Cloud 환경에 맞게 설정한 내용을 기록하기 위해 적었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Xcode Cloud&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Tuist로 관리하는 프로젝트를 실행할 때에 tuist install, tuist generate와 같은 명령어가 필요하다.&amp;nbsp;&lt;/span&gt;Tuist를 통해 관리하는 프로젝트는 Xcode Cloud를 어떻게 적용시킬까? 다행히 Xcode Cloud에서는 빌드 스크립트를 작성해서 필요한 설정을 구성할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-23 오후 4.45.12.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oo9GG/btsHyUdksi1/LhLJBvRvDkJZemHMMV5Ft0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oo9GG/btsHyUdksi1/LhLJBvRvDkJZemHMMV5Ft0/img.png&quot; data-alt=&quot;Xcode Cloud에서 지원하는 build script 실행 시점&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oo9GG/btsHyUdksi1/LhLJBvRvDkJZemHMMV5Ft0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foo9GG%2FbtsHyUdksi1%2FLhLJBvRvDkJZemHMMV5Ft0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;144&quot; data-filename=&quot;스크린샷 2024-05-23 오후 4.45.12.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Xcode Cloud에서 지원하는 build script 실행 시점&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;post-clone 타이밍에 Tuist의 설치와 실행 명령어를 스크립트로 작성하면 될 것 같다. Xcode Cloud의 Custom Build Script를 사용하는 방법은 &lt;b&gt;프로젝트의 루트경로에 ci_scripts라는 폴더를 만들고, ci_post_clone.sh 파일을 생성해 원하는 스크립트를 작성하면 된다.&lt;/b&gt; 그럼 Xcode Cloud가 워크플로우 수행할 때에 해당 스크립트를 실행하게 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;ci_post_clone.sh의 내용&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Tuist 의 버전이 4.0 이상으로 올라가면서 이제는&lt;b&gt; mise로 설치할 것을 권장한다.&lt;/b&gt; mise로 설치하게 되면서 Tuist 의 버전 트래킹이 용이하게 되는 장점이 생긴다. &lt;span style=&quot;text-align: center;&quot;&gt;mise로 Tuist를 설치했다면 프로젝트 루트 경로에 자동으로 .mise.toml파일이 생성되어 있을 것이다.&amp;nbsp; &lt;/span&gt;.mise.toml 파일에 mise를 통해 설치한 도구의 버전이 기록되기 때문에 &lt;span style=&quot;text-align: center;&quot;&gt;&lt;b&gt;.tuist-version 파일을 더이상 사용하지 않아도 된다.&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-23 오후 4.34.26.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbusha/btsHyQ91XFq/My3Jn0hRMS2Wrl5Hr4Lci1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbusha/btsHyQ91XFq/My3Jn0hRMS2Wrl5Hr4Lci1/img.png&quot; data-alt=&quot;.mise.toml 파일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbusha/btsHyQ91XFq/My3Jn0hRMS2Wrl5Hr4Lci1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdbusha%2FbtsHyQ91XFq%2FMy3Jn0hRMS2Wrl5Hr4Lci1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;713&quot; height=&quot;365&quot; data-filename=&quot;스크린샷 2024-05-23 오후 4.34.26.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.mise.toml 파일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;때문에 Xcode Cloud의 환경에도 mise를 설치하고, .mise.toml을 참조해 설치된 Tuist를 실행하면 버전을 굳이 명시하지 않아도 tuist가 정상 동작한다. 각 코드는 대략적으로 주석을 달아놨으니 참고하면 될 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/b1a5a58ddeef77ceacc61ea8d3c5dbeb.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;SwiftLint와 FirebaseClashlytics&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그런데 내 경우 Tuist 설정까지는 잘 동작했는데 Xcode Cloud 환경에서 Swiftlint와 FirebaseClashlytics에서 &quot;&lt;span style=&quot;color: #ef5369;&quot;&gt;command phasescriptexecution failed with a nonzero exit code&lt;/span&gt;&quot; 같은 에러가 떴다. 두 라이브러리를 적용하며 Build Phase에 작성한 스크립트에서 발생한 에러였다. 두가지 원인이었는데&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;1. Swiftlint는 기존에 Homebrew로 설치해서 사용했었다.&amp;nbsp; -&amp;gt; mise 말고 homebrew도 설치해야하나?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;2. FirebaseCrashlytics의 스크립트 경로 설정이 잘못되었다.&amp;nbsp; -&amp;gt; 경로를 재설정해주면 그만&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그런데 고맙게도 &lt;b&gt;mise에서 SwiftLint도 지원해준다!&lt;/b&gt; mise를 통해 swiftlint를 설치하면 .mise.toml에 자동으로 기록된다. 그러면 Xcode Cloud 환경에도 SwiftLint가 설치되기 때문에 homebrew가 아닌 mise를 사용하도록 변경했다. 터미널에서 다음 명령어를 통해 Mise로 SwiftLint를 설치할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/99cb963d2147cb4f45ce56c9def4cccc.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그리고 brew로 사용하던 SwiftLint를 Mise를 통해 사용하도록 Build Phase의 스크립트도 변경해주었다. 아래는 프로젝트의 구성 경로와 해당 경로를 기준으로 Tuist edit에서 작성한 스크립트 내용이다. SwiftLint의 룰 파일인 .swiftlint.yml는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;프로젝트의 루트 경로에 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KFBK9/btsHA4ew0Rt/PCwybUEWj2R5txzI2GwDk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KFBK9/btsHA4ew0Rt/PCwybUEWj2R5txzI2GwDk1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1778&quot; data-filename=&quot;스크린샷 2024-05-24 오전 9.33.20.png&quot; style=&quot;width: 46.8132%; margin-right: 10px;&quot; data-widthpercent=&quot;47.36&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KFBK9/btsHA4ew0Rt/PCwybUEWj2R5txzI2GwDk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKFBK9%2FbtsHA4ew0Rt%2FPCwybUEWj2R5txzI2GwDk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1350&quot; height=&quot;1778&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yzwxd/btsHAJuTzHJ/WGzhFb0pg0NBfrtlBzXPtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yzwxd/btsHAJuTzHJ/WGzhFb0pg0NBfrtlBzXPtk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;1370&quot; data-filename=&quot;스크린샷 2024-05-24 오전 9.33.36.png&quot; style=&quot;width: 52.024%;&quot; data-widthpercent=&quot;52.64&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yzwxd/btsHAJuTzHJ/WGzhFb0pg0NBfrtlBzXPtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYzwxd%2FbtsHAJuTzHJ%2FWGzhFb0pg0NBfrtlBzXPtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1156&quot; height=&quot;1370&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;프로젝트 파일 경로 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/LeeTaek/dec6b2e498f21d4b7699ec4fc4c93747.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;결과&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-23 오후 5.31.40.png&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpzmtl/btsHyOLgTZP/kxlrRYtPATTkRfiYWudDS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpzmtl/btsHyOLgTZP/kxlrRYtPATTkRfiYWudDS1/img.png&quot; data-alt=&quot;Xcode Cloud 환경에서 성공적으로 진행됐다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpzmtl/btsHyOLgTZP/kxlrRYtPATTkRfiYWudDS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdpzmtl%2FbtsHyOLgTZP%2FkxlrRYtPATTkRfiYWudDS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;987&quot; height=&quot;298&quot; data-filename=&quot;스크린샷 2024-05-23 오후 5.31.40.png&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Xcode Cloud 환경에서 성공적으로 진행됐다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-23 오후 5.41.45.png&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhAWm5/btsHAp4kIL5/cTaXykItP0mm0rqDkKpwJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhAWm5/btsHAp4kIL5/cTaXykItP0mm0rqDkKpwJk/img.png&quot; data-alt=&quot;그리고 도착한 Success 메일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhAWm5/btsHAp4kIL5/cTaXykItP0mm0rqDkKpwJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhAWm5%2FbtsHAp4kIL5%2FcTaXykItP0mm0rqDkKpwJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;734&quot; height=&quot;353&quot; data-filename=&quot;스크린샷 2024-05-23 오후 5.41.45.png&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그리고 도착한 Success 메일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;성공적으로 빌드되었다. 스크립트 작성 때문에 16번정도 삽질한 것 같다. &lt;span style=&quot;text-align: start;&quot;&gt;Success 메일이 날라왔을 때 왕감격스러웠다 ㅠ_ㅠ&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;결론&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;- 기존의 다른 분들이 작성했던 글들을 보면 Tuist가 Mise를 환경설정에 훨씬 용이해진 것 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;- CI/CD를 처음 적용해봤는데 테스트 플라이트, 배포 등이 경험하게 될 장점이 기대된다. (1인으로 진행하는 프로젝트지만...)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;- 터미널 경로를 잘 확인하자. ci_scripts경로에서 tuist generate가 실행되지 않아서 무지하게 당황해하며 진땀뺐다....&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Reference&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.tuist.io/guide/introduction/installation&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.tuist.io/guide/introduction/installation&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716453474922&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Installation | Tuist&quot; data-og-description=&quot;&quot; data-og-host=&quot;docs.tuist.io&quot; data-og-source-url=&quot;https://docs.tuist.io/guide/introduction/installation&quot; data-og-url=&quot;https://docs.tuist.io/guide/introduction/installation&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.tuist.io/guide/introduction/installation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.tuist.io/guide/introduction/installation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Installation | Tuist&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.tuist.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.apple.com/documentation/xcode/writing-custom-build-scripts&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/xcode/writing-custom-build-scripts&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716452154973&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Writing custom build scripts | Apple Developer Documentation&quot; data-og-description=&quot;Extend your Xcode Cloud workflows with custom build scripts that perform custom tasks or install additional tools.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/xcode/writing-custom-build-scripts&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/xcode/writing-custom-build-scripts&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dngfqc/hyV9S5mya4/LTFbILK5NZ6942RCNKUDok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/TlUGW/hyV9OaO314/djA1oWFSm3U70YtYXoe4qk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/writing-custom-build-scripts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/xcode/writing-custom-build-scripts&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dngfqc/hyV9S5mya4/LTFbILK5NZ6942RCNKUDok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/TlUGW/hyV9OaO314/djA1oWFSm3U70YtYXoe4qk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Writing custom build scripts | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Extend your Xcode Cloud workflows with custom build scripts that perform custom tasks or install additional tools.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/klundberg/asdf-swiftlint&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/klundberg/asdf-swiftlint&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716453518320&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - klundberg/asdf-swiftlint: An asdf plugin for swiftlint&quot; data-og-description=&quot;An asdf plugin for swiftlint. Contribute to klundberg/asdf-swiftlint development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/klundberg/asdf-swiftlint&quot; data-og-url=&quot;https://github.com/klundberg/asdf-swiftlint&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Th6C0/hyV9OWcMSu/QUwK9h2iX1GGtS2y2rQbT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/klundberg/asdf-swiftlint&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/klundberg/asdf-swiftlint&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Th6C0/hyV9OWcMSu/QUwK9h2iX1GGtS2y2rQbT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - klundberg/asdf-swiftlint: An asdf plugin for swiftlint&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An asdf plugin for swiftlint. Contribute to klundberg/asdf-swiftlint development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/tuist/tuist/issues/5863&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/tuist/tuist/issues/5863&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716453523771&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Xcode Cloud + Mise doesn't work &amp;middot; Issue #5863 &amp;middot; tuist/tuist&quot; data-og-description=&quot;What problem or need do you have? Based on documentation, I've created a ci_post_clone.sh for my XcodeCloud: #!/bin/sh curl https://mise.jdx.dev/install.sh | sh echo &amp;quot;  Version:&amp;quot; ~/.local/bin/mise ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/tuist/tuist/issues/5863&quot; data-og-url=&quot;https://github.com/tuist/tuist/issues/5863&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cbXYBf/hyV9OIGEK5/Nq3DfGyKtEsJTJjnb7t801/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_108_1045_162&quot;&gt;&lt;a href=&quot;https://github.com/tuist/tuist/issues/5863&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/tuist/tuist/issues/5863&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cbXYBf/hyV9OIGEK5/Nq3DfGyKtEsJTJjnb7t801/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_108_1045_162');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Xcode Cloud + Mise doesn't work &amp;middot; Issue #5863 &amp;middot; tuist/tuist&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;What problem or need do you have? Based on documentation, I've created a ci_post_clone.sh for my XcodeCloud: #!/bin/sh curl https://mise.jdx.dev/install.sh | sh echo &quot;  Version:&quot; ~/.local/bin/mise ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://green1229.tistory.com/351&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://green1229.tistory.com/351&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716452157310&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Tuist를 통해 SwiftLint 사용하기&quot; data-og-description=&quot;안녕하세요. 그린입니다  이번 포스팅에서는 Tuist로 환경 구축을 하며 코드 및 파일 컨벤션을 잡아주기 위한 SwiftLint를 도입하면서 조금 더 라이트하게 사용해보려합니다  우선, swiftLint는&quot; data-og-host=&quot;green1229.tistory.com&quot; data-og-source-url=&quot;https://green1229.tistory.com/351&quot; data-og-url=&quot;https://green1229.tistory.com/351&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wxQtd/hyV9OomCs5/K2M5lZkkpBCJz6q1K7x070/img.png?width=800&amp;amp;height=194&amp;amp;face=0_0_800_194,https://scrap.kakaocdn.net/dn/bBTv5k/hyV9YYMI4w/9imx8e7S8nbUNfqprz81d0/img.png?width=800&amp;amp;height=194&amp;amp;face=0_0_800_194,https://scrap.kakaocdn.net/dn/o6MgM/hyV9XFBxFQ/dfzlzo0memVUjk8wd8jyw0/img.png?width=1466&amp;amp;height=357&amp;amp;face=0_0_1466_357&quot;&gt;&lt;a href=&quot;https://green1229.tistory.com/351&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://green1229.tistory.com/351&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wxQtd/hyV9OomCs5/K2M5lZkkpBCJz6q1K7x070/img.png?width=800&amp;amp;height=194&amp;amp;face=0_0_800_194,https://scrap.kakaocdn.net/dn/bBTv5k/hyV9YYMI4w/9imx8e7S8nbUNfqprz81d0/img.png?width=800&amp;amp;height=194&amp;amp;face=0_0_800_194,https://scrap.kakaocdn.net/dn/o6MgM/hyV9XFBxFQ/dfzlzo0memVUjk8wd8jyw0/img.png?width=1466&amp;amp;height=357&amp;amp;face=0_0_1466_357');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Tuist를 통해 SwiftLint 사용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 그린입니다  이번 포스팅에서는 Tuist로 환경 구축을 하며 코드 및 파일 컨벤션을 잡아주기 위한 SwiftLint를 도입하면서 조금 더 라이트하게 사용해보려합니다  우선, swiftLint는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;green1229.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://nsios.tistory.com/190&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nsios.tistory.com/190&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716453468883&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Swift] Xcode Cloud(CI/CD) + Tuist(프로젝트관리툴) + dSYMs 업로드까지 자동화 배포하기 (feat. 스크립트쉘)&quot; data-og-description=&quot;이번글은 나름 Xcode Cloud 사용에 있어서 심화?편인 것같네요 Xcode Cloud를 설정하면서 겪었던 이슈를 해결한 내용을 써내려가려고해요! Tuist를 사용하는데 CI/CD를 적용하고싶어졌어요 Xcode Cloud환경&quot; data-og-host=&quot;nsios.tistory.com&quot; data-og-source-url=&quot;https://nsios.tistory.com/190&quot; data-og-url=&quot;https://nsios.tistory.com/190&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dX6jOJ/hyV9NiHHPT/fVG1Uc7qa1z6yv4z6j4aT0/img.jpg?width=391&amp;amp;height=200&amp;amp;face=0_0_391_200,https://scrap.kakaocdn.net/dn/cpaYLU/hyV9XeymEJ/2dPhzaMRQnfK4YWy4Kfs81/img.jpg?width=391&amp;amp;height=200&amp;amp;face=0_0_391_200,https://scrap.kakaocdn.net/dn/duscN5/hyV9TpFqWv/fp8adzaENDEoklO3hskTE0/img.png?width=2298&amp;amp;height=844&amp;amp;face=0_0_2298_844&quot;&gt;&lt;a href=&quot;https://nsios.tistory.com/190&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nsios.tistory.com/190&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dX6jOJ/hyV9NiHHPT/fVG1Uc7qa1z6yv4z6j4aT0/img.jpg?width=391&amp;amp;height=200&amp;amp;face=0_0_391_200,https://scrap.kakaocdn.net/dn/cpaYLU/hyV9XeymEJ/2dPhzaMRQnfK4YWy4Kfs81/img.jpg?width=391&amp;amp;height=200&amp;amp;face=0_0_391_200,https://scrap.kakaocdn.net/dn/duscN5/hyV9TpFqWv/fp8adzaENDEoklO3hskTE0/img.png?width=2298&amp;amp;height=844&amp;amp;face=0_0_2298_844');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Swift] Xcode Cloud(CI/CD) + Tuist(프로젝트관리툴) + dSYMs 업로드까지 자동화 배포하기 (feat. 스크립트쉘)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번글은 나름 Xcode Cloud 사용에 있어서 심화?편인 것같네요 Xcode Cloud를 설정하면서 겪었던 이슈를 해결한 내용을 써내려가려고해요! Tuist를 사용하는데 CI/CD를 적용하고싶어졌어요 Xcode Cloud환경&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nsios.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>tuist #xcodecloud #ios #ci/cd #swiftlint #firebase</category>
      <author>택꽁이</author>
      <guid isPermaLink="true">https://leetaek.tistory.com/81</guid>
      <comments>https://leetaek.tistory.com/81#entry81comment</comments>
      <pubDate>Thu, 23 May 2024 17:50:53 +0900</pubDate>
    </item>
  </channel>
</rss>