<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>IOS on Kelly Dev</title><link>https://kelly-chui.github.io/tags/ios/</link><description>Recent content in IOS on Kelly Dev</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 18 Aug 2026 17:52:46 +0900</lastBuildDate><atom:link href="https://kelly-chui.github.io/tags/ios/index.xml" rel="self" type="application/rss+xml"/><item><title>StackDay. 0. Concept</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-concept/</link><pubDate>Sat, 08 Aug 2026 15:36:21 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-concept/</guid><description>&lt;h2 id="시작하기"&gt;시작하기&lt;/h2&gt;
&lt;p&gt;애플 리마인더나 마이크로소프트 To do를 쓰면 매일 해야하는 일들을 관리할 수 있다.&lt;/p&gt;
&lt;p&gt;나도 매일 LeetCode Daily를 풀고, Bing 출석체크를 하고, 매일 TIL 을 적으려고 한다.&lt;/p&gt;
&lt;figure class="media-caption align-center"&gt;&lt;img src="https://kelly-chui.github.io/posts/stackday-dev-log-concept/image-001-optimized-image.webp" alt="" loading="lazy" width="360"&gt;
&lt;/figure&gt;
&lt;p&gt;하지만 기본적으로 Reminder, Todo 앱과 습관 형성 앱은 차이가 있다.&lt;/p&gt;
&lt;p&gt;첫 번째로 기능 요구사항이 다르다. 애플 리마인더는 매일 반복해야 하는 태스크를 하지 않았을 경우에, 날짜가 새로 갱신되지 않고, 그 날짜에 계속 남아있는다. 마이크로소프트 To do는 하루를 빠뜨리면 그 태스크가 사라지지 않고 누적된다.&lt;/p&gt;
&lt;p&gt;내가 원하는 습관 형성 앱은 리마인더의 방식, 마이크로소프트 투 두의 방식 등등을 포괄할 수 있는 앱이었다.&lt;/p&gt;</description></item><item><title>StackDay. 엔티티와 계산 책임 분리하기</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-derived-value-responsibility/</link><pubDate>Wed, 12 Aug 2026 16:51:04 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-derived-value-responsibility/</guid><description>&lt;p&gt;앞서 도메인, 데이터 모델링한 내용을 실제 코드로 옮기기 시작했다.&lt;/p&gt;
&lt;p&gt;모델 자체의 구조는 이미 정해져 있었지만 코드로 구현하니 &lt;code&gt;Habit Entry&lt;/code&gt;같은 파생 값들은 어디서 생성해야 하는지 책임이 정해지지 않았다.&lt;/p&gt;
&lt;h2 id="completion"&gt;Completion&lt;/h2&gt;
&lt;p&gt;먼저 습관을 수행했다는 사실을 기록하는 &lt;code&gt;Completion&lt;/code&gt;을 구현했다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Equatable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;habitID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;completedOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;recordedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Completion&lt;/code&gt;은 저장되는 원본 데이터이므로 완료 사실을 표현하는 데 필요한 값만 가진다.&lt;/p&gt;
&lt;p&gt;미래의 날짜인지, &lt;code&gt;Habit&lt;/code&gt;의 시작일 이전인지, 같은 날짜의 &lt;code&gt;Completion&lt;/code&gt;이 이미 존재하는지와 같은 검증은 &lt;code&gt;Completion&lt;/code&gt; 하나만으로 판단할 수 없기 때문에 넣지 않았다.&lt;/p&gt;</description></item><item><title>StackDay. 데이터 모델링, Entry가 아닌 Completion을 관리해야 하는 이유</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-entry-vs-completion/</link><pubDate>Tue, 11 Aug 2026 16:48:37 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-entry-vs-completion/</guid><description>&lt;p&gt;StackDay의 MVP 범위와 정책을 가지고 앱의 도메인과 데이터를 모델링한다.&lt;/p&gt;
&lt;p&gt;도메인 모델링 -&amp;gt; 데이터 모델링 -&amp;gt; 유즈케이스 검증 순서로 진행하고, 각각의 단계가 이전 단계를 검증하는 방향이다.&lt;/p&gt;
&lt;h2 id="앱의-핵심-동작"&gt;앱의 핵심 동작&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://kelly-chui.github.io/posts/stackday-dev-log-concept/"&gt;이전 포스트&lt;/a&gt;에서 정한 앱의 컨셉과 정책에서부터, 이 앱이 어떤 동작을 해야 할지 먼저 정리했다.&lt;/p&gt;
&lt;pre class="mermaid" data-mermaid-source="flowchart&amp;#43;LR%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;A%5B%EC%8A%B5%EA%B4%80&amp;#43;%EC%83%9D%EC%84%B1%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;B%5B%EC%8A%B5%EA%B4%80&amp;#43;%EC%B6%94%EC%A0%81%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;C%5B%EB%A7%A4%EC%9D%BC&amp;#43;%EC%88%98%ED%96%89&amp;#43;%EC%97%AC%EB%B6%80&amp;#43;%EA%B8%B0%EB%A1%9D%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;D%5B%EC%8A%A4%ED%8A%B8%EB%A6%AD%EA%B3%BC&amp;#43;%ED%86%B5%EA%B3%84&amp;#43;%EA%B3%84%EC%82%B0%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;E%5B%EC%8A%B5%EA%B4%80&amp;#43;%EC%A2%85%EB%A3%8C%5D%0A%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;A&amp;#43;--%3E&amp;#43;B&amp;#43;--%3E&amp;#43;C&amp;#43;--%3E&amp;#43;D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;B&amp;#43;--%3E&amp;#43;E"&gt;&lt;/pre&gt;&lt;p&gt;사용자가 습관을 만들면 그날부터 습관의 추적이 시작된다. 사용자는 매일 습관을 수행했는지 기록하고, 앱은 누적된 기록을 바탕으로 현재 스트릭과 통계를 보여준다. 더 이상 이어가지 않을 습관은 추적을 종료할 수 있다.&lt;/p&gt;</description></item><item><title>StackDay. Date는 날짜가 아니다</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-localday-time-boundary/</link><pubDate>Tue, 18 Aug 2026 17:52:46 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-localday-time-boundary/</guid><description>&lt;p&gt;StackDay는 일정 주기의 습관 수행 여부를 기록하는 앱이고, MVP에서는 그 주기를 매일로 고정했다.&lt;/p&gt;
&lt;p&gt;지금까지 구현할 때, &amp;ldquo;어느 날에 수행했는가&amp;quot;와 &amp;ldquo;정확히 언제 기록했는가&amp;quot;를 모두 Foundation의 &lt;code&gt;Date&lt;/code&gt;로 표현했다.&lt;/p&gt;
&lt;p&gt;두 정보는 비슷해 보이지만 다르다. 이 차이를 타입으로 구분하지 않으면 날짜 검증, 중복 기록 검사, 통계 계산마다 같은 해석을 반복하게 된다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 그 문제가 실제 코드에서 어떻게 드러났고, &lt;code&gt;LocalDay&lt;/code&gt;·&lt;code&gt;Clock&lt;/code&gt;·TimeZone을 어떤 경계로 나눴는지 정리한다.&lt;/p&gt;
&lt;h2 id="date타입이-저장하는-것은-순간"&gt;Date타입이 저장하는 것은 순간&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Date&lt;/code&gt;는 연, 월, 일이나 자정 같은 달력 개념을 저장하지 않는다.&lt;/p&gt;</description></item><item><title>StackDay. 비즈니스 규칙과 Entity Identity 분리하기</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-completion-identity/</link><pubDate>Sun, 16 Aug 2026 15:23:36 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-completion-identity/</guid><description>&lt;p&gt;현재 StackDay MVP에서는 하나의 &lt;code&gt;Habit&lt;/code&gt;에 대해 하루 한 번만 &lt;code&gt;Completion&lt;/code&gt;을 기록할 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 처음에는 &lt;code&gt;(habitID, completedOn)&lt;/code&gt; 조합을 Completion의 identity처럼 사용해도 될 것 같았고, 실제로 그런 코드도 있었다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;code&gt;Completion&lt;/code&gt; 삭제를 구현하면서 이 둘을 분리해야 한다는 점을 알게 되었다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Equatable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Identifiable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;habitID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;completedOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;recordedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id="같은-날의-기록을-찾는-조건"&gt;같은 날의 기록을 찾는 조건&lt;/h2&gt;
&lt;p&gt;같은 &lt;code&gt;Habit&lt;/code&gt;의 같은 날 &lt;code&gt;Completion&lt;/code&gt;을 찾는 데에는 &lt;code&gt;habitID&lt;/code&gt;와 &lt;code&gt;completedOn&lt;/code&gt;이 필요하다. 현재 정책에서는 이 조건으로 중복 기록도 막을 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 이것은 현재 정책에서 필요한 조회 조건이다. 이후 시간 단위로 습관을 기록하거나, 하루에 여러 번 수행하는 &lt;code&gt;Habit&lt;/code&gt;이 생기면 같은 조합으로 여러 &lt;code&gt;Completion&lt;/code&gt;이 존재할 수 있다.&lt;/p&gt;</description></item><item><title>StackDay. UseCase와 Entity는 각각 어디까지 책임져야 할까</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-usecase-entity-responsibilities/</link><pubDate>Fri, 14 Aug 2026 16:31:52 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-usecase-entity-responsibilities/</guid><description>&lt;p&gt;&lt;code&gt;CreateHabitUseCase&lt;/code&gt;를 시작으로 여러 유즈케이스를 구현하면서, 도메인 규칙을 어디에 두어야 할지 계속 고민하게 됐다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Habit&lt;/code&gt;을 생성할 때 이름의 유효성을 누가 검증해야 하는지, &lt;code&gt;Completion&lt;/code&gt;을 남길 때 해당 날짜에 기록할 수 있는지는 누가 판단해야 하는지처럼 비슷한 문제가 반복해서 나타났다.&lt;/p&gt;
&lt;p&gt;처음에는 유즈케이스가 사용자의 작업을 처리하니 이런 검증도 함께 담당하면 된다고 생각할 수 있었다. 하지만 구현을 진행할수록 엔티티가 스스로 보장해야 하는 규칙과 유즈케이스 작업의 흐름을 위해 판단해야 하는 규칙을 구분할 필요가 있었다.&lt;/p&gt;
&lt;p&gt;이 포스트에서는 &lt;code&gt;Habit&lt;/code&gt;의 생성과 &lt;code&gt;Completion&lt;/code&gt; 기록을 중심으로 어떤 고민을 했는지와 어떻게 해결했는지를 정리한다.&lt;/p&gt;</description></item><item><title>StackDay. Repository의 save를 insert와 update로 나누기</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-repository-contracts/</link><pubDate>Fri, 14 Aug 2026 14:15:52 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-repository-contracts/</guid><description>&lt;p&gt;유즈케이스를 구현하면서 &lt;code&gt;Habit&lt;/code&gt;과 &lt;code&gt;Completion&lt;/code&gt;을 저장할 Repository 계약을 좀 정리했다.&lt;/p&gt;
&lt;p&gt;처음에는 create와 update 둘 다 &lt;code&gt;save&lt;/code&gt; 하나로 저장하면 간단해 보였지만, 두 기능이 기대하는 실패 조건이 서로 다르다는 점에서 계약을 나눌 필요가 있었다.&lt;/p&gt;
&lt;h2 id="save는-어떤-동작인가"&gt;save는 어떤 동작인가&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;save&lt;/code&gt;를 upsert로 구현하면 호출하는 쪽은 편하다. ID가 없으면 삽입하고, 이미 있으면 갱신하면 된다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="nc"&gt;HabitRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;habit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Habit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;문제는 이 동작이 실패해야 하는 상황까지 조용히 성공으로 바꾼다는 점이다.&lt;/p&gt;
&lt;p&gt;(물론 UUID라 현실적으로 겹칠 일은 없지만) &lt;code&gt;Habit&lt;/code&gt;을 생성하는 작업은 같은 ID가 이미 있으면 실패해야 하고, 보관처럼 기존 &lt;code&gt;Habit&lt;/code&gt;을 바꾸는 작업은 대상이 없으면 실패해야 한다.&lt;/p&gt;</description></item><item><title>StackDay. 책임 분리와 타입 분리</title><link>https://kelly-chui.github.io/posts/stackday-dev-log-separating-responsibility-and-types/</link><pubDate>Thu, 13 Aug 2026 18:32:51 +0900</pubDate><guid>https://kelly-chui.github.io/posts/stackday-dev-log-separating-responsibility-and-types/</guid><description>&lt;p&gt;이전 작업에서 &lt;code&gt;HabitStreak&lt;/code&gt;을 구현하면서 계산 결과와 계산 로직을 분리했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HabitStreak&lt;/code&gt;은 현재 스트릭과 최장 스트릭이라는 값만 표현하고, 계산은 &lt;code&gt;HabitStreakCalculator&lt;/code&gt;가 담당한다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;HabitStreak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Equatable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;longest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;HabitStreakCalculator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;habit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Habit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Completion&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;referenceDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HabitStreak&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;같은 원리로 &lt;code&gt;HabitStatistics&lt;/code&gt;도 파생 값과 계산 책임을 분리해서 구현했다.&lt;/p&gt;
&lt;h2 id="habitstatistics"&gt;HabitStatistics&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HabitStatistics&lt;/code&gt;는 Habit의 수행 기록을 요약한 파생 값이다.&lt;/p&gt;
&lt;p&gt;MVP에서는 다음 값을 제공한다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;HabitStatistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Equatable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;totalCompletedDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;eligibleTrackingDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;completionRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Double&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;streak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HabitStreak&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;totalCompletedDays&lt;/code&gt;는 추적 기간 안에서 완료한 날짜의 수이고, &lt;code&gt;eligibleTrackingDays&lt;/code&gt;는 시작일부터 기준일까지 추적 대상이 된 날짜의 수다. 아카이브된 Habit은 &lt;code&gt;archivedOn&lt;/code&gt;을 마지막 추적일로 사용한다.&lt;/p&gt;</description></item><item><title>SniffMEET. Xcode 프로젝트 파일 정리하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-xcode-project-cleanup/</link><pubDate>Thu, 05 Jun 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-xcode-project-cleanup/</guid><description>&lt;h2 id="작업-내역"&gt;작업 내역&lt;/h2&gt;
&lt;img loading="lazy" src="https://kelly-chui.github.io/posts/sniffmeet-dev-log-xcode-project-cleanup/image-001-optimized-image.webp"&gt;&lt;p&gt;Xcode에서 프로젝트 파일 관련 경고가 많이 떠서 정리했다.&lt;/p&gt;
&lt;h2 id="작업-내역-1"&gt;작업 내역&lt;/h2&gt;
&lt;h3 id="null-파일-제거"&gt;null 파일 제거&lt;/h3&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;320043702CDC9A0D00D08B6D /* (null) in Sources */ = {isa = PBXBuildFile; };
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;320043722CDC9E5F00D08B6D /* (null) in Sources */ = {isa = PBXBuildFile; };
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;320043762CDCA18E00D08B6D /* (null) in Sources */ = {isa = PBXBuildFile; };
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;320043782CDCA49100D08B6D /* (null) in Sources */ = {isa = PBXBuildFile; };
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3200437C2CDCA6F300D08B6D /* (null) in Sources */ = {isa = PBXBuildFile; };&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;이전에 있었던 구조 변경이나, git 충돌/머지 등 다양한 이유로 프로젝트 파일 안에 &lt;code&gt;(null)&lt;/code&gt; 파일을 참조하는 라인이 많이 있었다.&lt;/p&gt;</description></item><item><title>SniffMEET. Diffable Data Source에서 이미지가 갱신되지 않은 이유</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-diffable-data-source-image-update/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-diffable-data-source-image-update/</guid><description>&lt;h2 id="들어가며"&gt;들어가며&lt;/h2&gt;
&lt;p&gt;SniffMeet의 메이트 리스트 화면은 &lt;code&gt;UITableViewDataSource&lt;/code&gt;를 직접 구현해서 셀을 구성하고 있었다. 이후 테이블 뷰와 컬렉션 뷰에서 Diffable DataSource를 사용해보기로 했고, 먼저 메이트 리스트 화면에 적용해보려고 했다.&lt;/p&gt;
&lt;p&gt;처음에는 어렵지 않을 거라고 생각했다. 섹션과 아이템 타입을 만들고, snapshot을 적용하면 기존 &lt;code&gt;reloadData()&lt;/code&gt;보다 깔끔하게 업데이트할 수 있을 것 같았다. 하지만 실제로는 이미지 갱신이 제대로 동작하지 않았다. 더 정확히는 프로필 이미지 데이터는 받아오는데, Diffable DataSource가 셀을 다시 구성할 때는 이미지가 계속 &lt;code&gt;nil&lt;/code&gt;로 남아 있었다.&lt;/p&gt;
&lt;p&gt;결국 이 작업은 성공적으로 마무리하지 못했다. 그래도 실패한 이유를 다시 정리해보니, 문제는 Diffable DataSource API 자체가 아니라 item의 identity와 표시 상태를 제대로 구분하지 못한 데 있었다.&lt;/p&gt;</description></item><item><title>SniffMEET. 프로필 이미지 다운샘플링과 썸네일 분리하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-profile-image-downsampling-thumbnails/</link><pubDate>Fri, 17 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-profile-image-downsampling-thumbnails/</guid><description>&lt;h2 id="개요"&gt;개요&lt;/h2&gt;
&lt;p&gt;사용자가 고해상도 사진을 그대로 업로드하면, 앱에서는 훨씬 작은 크기로만 이미지를 표시하는데도 원본 이미지를 계속 전송하고 저장하게 된다.&lt;/p&gt;
&lt;p&gt;SniffMeet에서도 프로필 이미지는 홈 화면의 프로필 카드나 메이트 리스트의 작은 썸네일로 표시되는 경우가 대부분이었다. 그런데 원본 이미지를 그대로 업로드하고, 목록에서도 같은 이미지를 다시 내려받고 있었다.&lt;/p&gt;
&lt;p&gt;불필요한 네트워크 사용량과 메모리 사용을 줄이기 위해 두 가지를 적용했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;업로드 전에 프로필 이미지를 앱에서 필요한 크기로 다운샘플링한다.&lt;/li&gt;
&lt;li&gt;메이트 리스트에서는 원본 프로필 이미지가 아니라 별도의 썸네일 이미지를 내려받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="다운샘플링-기준-정하기"&gt;다운샘플링 기준 정하기&lt;/h2&gt;
&lt;p&gt;이미지를 어느 크기까지 줄일지 먼저 정해야 했다. 기준은 앱에서 프로필 이미지가 가장 크게 표시되는 홈 화면의 프로필 카드로 잡았다.&lt;/p&gt;</description></item><item><title>SniffMEET. NI·MPC 프로필 드랍 흐름 소유권 고민하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-ni-mpc-profile-drop-ownership/</link><pubDate>Tue, 14 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-ni-mpc-profile-drop-ownership/</guid><description>&lt;h2 id="들어가며"&gt;들어가며&lt;/h2&gt;
&lt;p&gt;SniffMeet의 프로필 드랍 기능은 주변 사용자를 찾고, 기기 간 연결을 만들고, 거리와 방향 조건을 만족했을 때 프로필을 전달하는 흐름이다.&lt;/p&gt;
&lt;p&gt;겉으로 보면 &amp;ldquo;주변 사람 찾기&amp;quot;와 &amp;ldquo;프로필 보내기&amp;quot;를 나누면 될 것 같았다. 하지만 실제 흐름을 펼쳐보니 MPC, NI, 유즈케이스, 인터랙터가 서로 맞물려 있었고, 단순히 기능 이름으로 유즈케이스를 나누기 어려웠다.&lt;/p&gt;
&lt;p&gt;이 글은 프로필 드랍 흐름을 유즈케이스로 나누면서 고민했던 지점을 정리한 기록이다.&lt;/p&gt;
&lt;h2 id="기존에-생각한-흐름"&gt;기존에 생각한 흐름&lt;/h2&gt;
&lt;p&gt;당시 프로필 드랍은 대략 다음 순서로 동작한다고 봤다.&lt;/p&gt;</description></item><item><title>SniffMEET. OSSignposter로 앱 내부 작업 구간 측정하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-os-signposter-intervals/</link><pubDate>Tue, 14 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-os-signposter-intervals/</guid><description>&lt;h2 id="들어가며"&gt;들어가며&lt;/h2&gt;
&lt;p&gt;성능 개선을 하려면 먼저 어떤 작업이 얼마나 걸리는지 볼 수 있어야 한다. CPU 사용량만 보면 어느 시점에 부하가 생겼는지는 알 수 있지만, 앱 내부의 어떤 작업이 그 구간에 실행됐는지는 바로 드러나지 않는다.&lt;/p&gt;
&lt;p&gt;그래서 Instruments에서 앱 내부 작업 구간을 이름으로 확인할 수 있도록 &lt;code&gt;OSSignposter&lt;/code&gt;를 래핑해보기로 했다.&lt;/p&gt;
&lt;h2 id="어디에-붙일까"&gt;어디에 붙일까?&lt;/h2&gt;
&lt;p&gt;처음에는 측정용 타입을 따로 만들지, 기존 &lt;code&gt;SNMLogger&lt;/code&gt;에 붙일지 고민했다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subsystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;SniffMeet&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;SNMLogger&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;별도의 &lt;code&gt;SNMBenchMarker&lt;/code&gt; 같은 타입을 만들 수도 있었지만, 이미 프로젝트에 로그 시스템이 있었기 때문에 기존 &lt;code&gt;SNMLogger&lt;/code&gt;에 통합하는 쪽을 선택했다.&lt;/p&gt;</description></item><item><title>SniffMEET. Instruments로 성능 개선 후보 찾기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-instruments-performance-candidates/</link><pubDate>Tue, 07 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-instruments-performance-candidates/</guid><description>&lt;h2 id="들어가며"&gt;들어가며&lt;/h2&gt;
&lt;p&gt;성능 개선이 필요해 보이는 지점을 감으로 고르지 않기 위해, 먼저 SniffMeet의 주요 흐름에서 CPU와 네트워크 사용량을 훑어봤다.&lt;/p&gt;
&lt;p&gt;이 측정만으로 병목 원인을 확정할 수는 없다. 다만 어떤 화면과 기능을 먼저 의심해야 하는지, 이후 리팩토링 후보를 정하는 기준으로는 충분했다.&lt;/p&gt;
&lt;h2 id="cpu-사용량-측정"&gt;CPU 사용량 측정&lt;/h2&gt;
&lt;p&gt;Instruments의 Time Profiler로 주요 사용자 흐름을 따라가며 CPU 사용량이 튀는 구간을 확인했다.&lt;/p&gt;
&lt;img alt="Time Profiler에서 CPU 사용량이 튀는 구간" loading="lazy" src="https://kelly-chui.github.io/posts/sniffmeet-dev-log-instruments-performance-candidates/image-001-optimized-image.webp"&gt;&lt;p&gt;측정 중 비교적 사용량이 높았던 지점은 다음과 같았다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시점&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;관찰&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0:26&lt;/td&gt;
&lt;td&gt;프로필 입력 뷰 로드&lt;/td&gt;
&lt;td&gt;CPU 70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0:53&lt;/td&gt;
&lt;td&gt;텍스트 필드 입력&lt;/td&gt;
&lt;td&gt;CPU 70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1:44&lt;/td&gt;
&lt;td&gt;사진, 닉네임 입력 뷰 로드&lt;/td&gt;
&lt;td&gt;CPU 70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2:08&lt;/td&gt;
&lt;td&gt;포토피커&lt;/td&gt;
&lt;td&gt;CPU 90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3:00&lt;/td&gt;
&lt;td&gt;등록 완료 버튼 터치&lt;/td&gt;
&lt;td&gt;CPU 100%, 약 0.7초 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3:28&lt;/td&gt;
&lt;td&gt;메이트 리스트 뷰 로드&lt;/td&gt;
&lt;td&gt;CPU 50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3:41&lt;/td&gt;
&lt;td&gt;산책 요청 보내기 터치&lt;/td&gt;
&lt;td&gt;CPU 80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4:10&lt;/td&gt;
&lt;td&gt;지도 로드&lt;/td&gt;
&lt;td&gt;CPU 95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5:05&lt;/td&gt;
&lt;td&gt;요청 보내기&lt;/td&gt;
&lt;td&gt;CPU 100%, 약 0.2초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6:36&lt;/td&gt;
&lt;td&gt;MPC 연결&lt;/td&gt;
&lt;td&gt;CPU 100%, 약 1.2초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;특히 회원가입 완료 버튼을 눌렀을 때와 MPC 연결 시점이 눈에 띄었다. 회원가입 완료 시점에는 메인 스레드에서만 작업이 몰리는 것처럼 보였고, MPC 연결 시점에는 MultipeerConnectivity 관련 스레드가 지속적으로 동작했다.&lt;/p&gt;</description></item><item><title>SniffMEET. 여러 VIPER 구현에서 Router 구조 비교하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-viper-router-comparison/</link><pubDate>Tue, 12 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-viper-router-comparison/</guid><description>&lt;p&gt;VIPER에서 Router가 화면 전환을 담당한다는 원칙은 분명하다. 하지만 현재 화면을 어떻게 참조하는지, 다음 모듈은 누가 만드는지, AppRouter가 반드시 필요한지는 구현마다 달랐다.&lt;/p&gt;
&lt;p&gt;SniffMEET의 화면 구조를 정하기 전에 여러 VIPER 프로젝트를 비교하며 Router의 책임 범위를 확인했다.&lt;/p&gt;
&lt;h2 id="무엇을-확인했나"&gt;무엇을 확인했나&lt;/h2&gt;
&lt;p&gt;분석 기준은 다음 세 가지였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Router가 현재 ViewController를 어떻게 참조하는가&lt;/li&gt;
&lt;li&gt;화면 전환 시 다음 VIPER 모듈을 누가 조립하는가&lt;/li&gt;
&lt;li&gt;여러 모듈에 걸친 전환을 AppRouter가 담당하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="구현-비교"&gt;구현 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구현&lt;/th&gt;
&lt;th&gt;모듈 조립&lt;/th&gt;
&lt;th&gt;화면 전환&lt;/th&gt;
&lt;th&gt;AppRouter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/amitshekhariitbhu/iOS-Viper-Architecture"&gt;iOS-Viper-Architecture&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;WireFrame의 정적 메서드&lt;/td&gt;
&lt;td&gt;출발 View를 인자로 전달&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/tailec/ios-architecture"&gt;ios-architecture&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;정적 팩토리 메서드&lt;/td&gt;
&lt;td&gt;전환 사례가 적음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ferranabello/Viperit"&gt;Viperit&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;프레임워크가 모듈을 관리&lt;/td&gt;
&lt;td&gt;모듈이 직접 자신을 표시&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AppModules&lt;/code&gt;로 모듈 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/giftbott/iOS-Architecture-Sample"&gt;iOS-Architecture-Sample&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;모듈별 생성&lt;/td&gt;
&lt;td&gt;앱 단위 Router가 화면 계층 관리&lt;/td&gt;
&lt;td&gt;싱글톤 AppRouter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;구현은 달랐지만 공통점도 있었다. View나 Presenter가 직접 다음 화면을 생성하지 않았고, 화면 조립과 전환을 UI 바깥의 객체로 분리하고 있었다.&lt;/p&gt;</description></item><item><title>SniffMEET. UITabBarController VIPER 컨테이너로 다루기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-viper-tab-bar-container/</link><pubDate>Mon, 11 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-viper-tab-bar-container/</guid><description>&lt;p&gt;VIPER 모듈을 사용하더라도, 직접 탭 바를 구현하지 않는 이상 &lt;code&gt;UITabBarController&lt;/code&gt;는 사용해야 한다. 일반적인 VIPER 화면처럼 View, Presenter, Interactor, Router를 모두 갖춘 독립 모듈로 보기엔 애매한 것 같다.&lt;/p&gt;
&lt;p&gt;특히 Interactor는 여기서 아무 의미가 없어보이고, Router는 &lt;code&gt;UITabBarController&lt;/code&gt;에 내장되어 있으니까&amp;hellip;&lt;/p&gt;
&lt;h2 id="모듈-빌더"&gt;모듈 빌더&lt;/h2&gt;
&lt;p&gt;router, interactor를 생성하고 이를 가지고 있는 presenter를 생성해서 뷰에 집어넣는 방식&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HomeModuleBuilder&lt;/code&gt;는 Home 화면에 필요한 VIPER 구성 요소를 한 곳에서 조립한다. ViewController를 만들고, Router와 Interactor를 생성한 뒤 Presenter에 주입한다. 마지막으로 NavigationFactory를 통해 화면을 &lt;code&gt;UINavigationController&lt;/code&gt;로 감싸서 반환한다.&lt;/p&gt;</description></item><item><title>Software Engineering. VIPER 아키텍처</title><link>https://kelly-chui.github.io/posts/se-viper/</link><pubDate>Mon, 11 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/se-viper/</guid><description>&lt;h2 id="viper란"&gt;VIPER란&lt;/h2&gt;
&lt;img alt="VIPER 구성 요소와 데이터 흐름" loading="lazy" src="https://kelly-chui.github.io/posts/se-viper/image-001-optimized-image.webp"&gt;&lt;p&gt;VIPER는 화면을 View, Interactor, Presenter, Entity, Router로 나누는 아키텍처 패턴이다. 각 객체의 역할과 의존 방향을 제한해 UI, 비즈니스 로직, 화면 전환을 분리한다.&lt;/p&gt;
&lt;h2 id="구성-요소"&gt;구성 요소&lt;/h2&gt;
&lt;h3 id="view"&gt;View&lt;/h3&gt;
&lt;p&gt;화면을 그리고 사용자 입력을 Presenter에 전달한다. Presenter가 전달한 값을 UI에 반영하며, 비즈니스 로직은 직접 처리하지 않는다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="nc"&gt;ExampleViewProtocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;AnyObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;displayMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;displayError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExampleViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UIViewController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="n"&gt;ExamplePresenterProtocol&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kc"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;@IBAction&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;buttonTapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UIButton&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;handleButtonTapped&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;ExampleViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExampleViewProtocol&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;displayMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;displayError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;errorLabel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;pre class="mermaid" data-mermaid-source="flowchart&amp;#43;LR%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;View%5B%22View%22%5D&amp;#43;--%3E%7C%22%EC%82%AC%EC%9A%A9%EC%9E%90&amp;#43;%EC%9D%B4%EB%B2%A4%ED%8A%B8%22%7C&amp;#43;Presenter%5B%22Presenter%22%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;Presenter&amp;#43;-.-%3E%7C%22weak&amp;#43;%C2%B7&amp;#43;%ED%99%94%EB%A9%B4&amp;#43;%EA%B0%B1%EC%8B%A0%22%7C&amp;#43;View"&gt;&lt;/pre&gt;&lt;h3 id="presenter"&gt;Presenter&lt;/h3&gt;
&lt;p&gt;View의 입력을 해석해 Interactor나 Router에 작업을 요청한다. Interactor가 반환한 Entity는 화면에 표시할 값으로 가공해 View에 전달한다.&lt;/p&gt;</description></item></channel></rss>