<?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>Swift on Kelly Dev</title><link>https://kelly-chui.github.io/tags/swift/</link><description>Recent content in Swift on Kelly Dev</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 18 Aug 2026 23:30:00 +0900</lastBuildDate><atom:link href="https://kelly-chui.github.io/tags/swift/index.xml" rel="self" type="application/rss+xml"/><item><title>TIL. Aug 18, 2026</title><link>https://kelly-chui.github.io/til/2026/til-2026-08-18/</link><pubDate>Tue, 18 Aug 2026 23:30:00 +0900</pubDate><guid>https://kelly-chui.github.io/til/2026/til-2026-08-18/</guid><description>&lt;h2 id="오늘-한-내용"&gt;오늘 한 내용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Stack Day
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalDay&lt;/code&gt; 날짜 모델 도입&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Clock&lt;/code&gt;과 &lt;code&gt;FixedClock&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;완료 기록·취소·보관·삭제 UseCase를 날짜 모델에 맞게 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InMemoryHabitRepository&lt;/code&gt;, &lt;code&gt;InMemoryCompletionRepository&lt;/code&gt; 구현 및 테스트 추가&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/posts/stackday-dev-log-localday-time-boundary/"&gt;StackDay. Date로 하루를 표현하면 생기는 문제&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="배운-내용"&gt;배운 내용&lt;/h2&gt;
&lt;h3 id="localday-도입하기"&gt;LocalDay 도입하기&lt;/h3&gt;
&lt;p&gt;Stack Day에서는 &lt;code&gt;Date&lt;/code&gt; 하나로 특정 시점과 특정 날짜를 모두 표현하고 있었다. 그래서 달력상의 날짜만 필요한 값을 &lt;code&gt;LocalDay&lt;/code&gt;로 분리했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalDay&lt;/code&gt;: &lt;code&gt;startedOn&lt;/code&gt;, &lt;code&gt;archivedOn&lt;/code&gt;, &lt;code&gt;completedOn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Date&lt;/code&gt;: &lt;code&gt;createdAt&lt;/code&gt;, &lt;code&gt;updatedAt&lt;/code&gt;, &lt;code&gt;recordedAt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현재 시각은 &lt;code&gt;Clock&lt;/code&gt;이 제공하고, 그 시각을 어느 날짜로 해석할지는 TimeZone이 결정하도록 분리했다. 전체 설계와 구현 코드는 &lt;a href="https://kelly-chui.github.io/posts/stackday-dev-log-localday-time-boundary/"&gt;개발일지&lt;/a&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>TIL. Aug 17, 2026</title><link>https://kelly-chui.github.io/til/2026/til-2026-08-17/</link><pubDate>Mon, 17 Aug 2026 23:30:00 +0900</pubDate><guid>https://kelly-chui.github.io/til/2026/til-2026-08-17/</guid><description>&lt;h2 id="오늘-한-내용"&gt;오늘 한 내용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;PS
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/ps/2026/ps-leetcode-1563-stone-game-v/"&gt;LeetCode 1563. Stone Game V&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;WWDC
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/posts/wwdc-swift-concurrency-explore-structured-concurrency-in-swift-2/"&gt;Swift Concurrency. Explore Structured Concurrency in Swift (2) - WWDC21&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마이그레이션
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/posts/cs-algorithm-kadanes-algorithm/"&gt;Algorithm. Kadane&amp;rsquo;s Algorithm&lt;/a&gt; 마이그레이션&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/posts/mibangmurok-dev-log-colorpicker/"&gt;미방문록. 대표 색상 추출 Picker 만들기&lt;/a&gt; 마이그레이션&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="배운-내용"&gt;배운 내용&lt;/h2&gt;
&lt;h3 id="unstructured-task"&gt;Unstructured Task&lt;/h3&gt;
&lt;p&gt;WWDC21 Explore Structured Concurrency in Swift 세션을 정리를 마무리했다.&lt;/p&gt;
&lt;p&gt;1편과 다르게 구조화된 task tree에 속하지 않는 unstructured task와 detached task를 정리했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Task { ... }&lt;/code&gt;로 만든 unstructured task는 생성한 scope가 끝나도 계속 실행될 수 있다. 생성 지점의 actor, priority, task-local value를 상속한다.&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>TIL. Aug 13, 2026</title><link>https://kelly-chui.github.io/til/2026/til-2026-08-13/</link><pubDate>Thu, 13 Aug 2026 21:30:00 +0900</pubDate><guid>https://kelly-chui.github.io/til/2026/til-2026-08-13/</guid><description>&lt;h2 id="오늘-한-내용"&gt;오늘 한 내용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/posts/cs-data-structure-segment-tree/"&gt;Data Structure. Segment Tree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/ps/2026/ps-leetcode-2213-longest-substring-of-one-repeating-character/"&gt;LeetCode 2213. Longest Substring of One Repeating Character&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/posts/stackday-dev-log-separating-responsibility-and-types/"&gt;StackDay. 책임 분리와 타입 분리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="배운-내용"&gt;배운 내용&lt;/h2&gt;
&lt;h3 id="stackday-통계-계산"&gt;StackDay 통계 계산&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HabitStatistics&lt;/code&gt;를 구현하면서 계산 결과를 표현하는 타입과 계산 로직을 분리했다. 다만 &lt;code&gt;HabitStatisticsCalculator&lt;/code&gt;가 &lt;code&gt;HabitStreakCalculator&lt;/code&gt;에 의존하게 되면서, 여러 Calculator가 같은 날짜 정규화와 완료 기록 필터링을 반복하는 문제가 보였다.&lt;/p&gt;
&lt;p&gt;타입을 나누는 것만으로 항상 구조가 좋아지는 것은 아니고, 공유하는 계산 맥락까지 함께 살펴봐야 한다는 점을 배웠다. 자세한 고민은 &lt;a href="https://kelly-chui.github.io/posts/stackday-dev-log-separating-responsibility-and-types/"&gt;StackDay 개발일지&lt;/a&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>TIL. Aug 12, 2026</title><link>https://kelly-chui.github.io/til/2026/til-2026-08-12/</link><pubDate>Wed, 12 Aug 2026 23:30:00 +0900</pubDate><guid>https://kelly-chui.github.io/til/2026/til-2026-08-12/</guid><description>&lt;h2 id="오늘-한-내용"&gt;오늘 한 내용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/ps/2026/ps-leetcode-2958-length-of-longest-subarray-with-at-most-k-frequency/"&gt;LeetCode 2958. Length of Longest Subarray With at Most K Frequency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/ps/2026/ps-leetcode-2996-smallest-missing-integer-greater-than-sequential-prepix-sum/"&gt;LeetCode 2996. Smallest Missing Integer Greater Than Sequential Prefix Sum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kelly-chui.github.io/posts/stackday-dev-log-derived-value-responsibility/"&gt;StackDay. 엔티티와 계산 책임 분리하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="배운-내용"&gt;배운 내용&lt;/h2&gt;
&lt;h3 id="파생-값을-구현할-때의-책임-분리"&gt;파생 값을 구현할 때의 책임 분리&lt;/h3&gt;
&lt;p&gt;StackDay의 &lt;code&gt;HabitEntry&lt;/code&gt;와 &lt;code&gt;HabitStreak&lt;/code&gt;을 구현했다. 모델링 단계에서 정의한 파생 값을 실제 코드로 옮기면서, 값을 표현하는 타입과 값을 계산하는 로직을 분리했다.&lt;/p&gt;
&lt;p&gt;AI 에이전트가 작성한 내용중에 별로인 것들을 몇개 직접 수정했는데 리스트로 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HabitEntry&lt;/code&gt;의 이니셜라이저 전체 &lt;code&gt;Completion&lt;/code&gt; 목록을 직접 탐색하고, 생성 실패/성공을 판정했다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HabitStreak&lt;/code&gt;의 이니셜라이저 내부에 &lt;code&gt;Streak&lt;/code&gt; 계산 로직이 들어있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;둘 다 경계가 제대로 구분되지 않았다. &lt;code&gt;HabitEntry&lt;/code&gt; 내부에서 전체 &lt;code&gt;Completion&lt;/code&gt;을 탐색하면서 조건에 맞는 &lt;code&gt;Completion&lt;/code&gt;을 찾아내는건 비효율적이기도 하고, 이건 유즈케이스에서 해야 될 일이다.&lt;/p&gt;</description></item><item><title>SniffMEET. 닉네임 검증 개선 고민</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-nickname-validation-combine/</link><pubDate>Thu, 13 Feb 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-nickname-validation-combine/</guid><description>&lt;h2 id="작업-내역"&gt;작업 내역&lt;/h2&gt;
&lt;p&gt;닉네임 TextField의 검증 방식을 다시 정리했다. 현재는 &lt;code&gt;UITextFieldDelegate&lt;/code&gt;로 길이 검증과 버튼 활성화를 처리하고 있었는데, 중복 체크까지 같은 흐름에 넣을 경우 네트워크 요청이 너무 자주 발생할 수 있어 보였다.&lt;/p&gt;
&lt;h3 id="현재-구현"&gt;현재 구현&lt;/h3&gt;
&lt;p&gt;현재는 Delegate 기반으로 다음 두 가지를 처리하고 있었다.&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;extension&lt;/span&gt; &lt;span class="nc"&gt;ProfileCreateViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UITextFieldDelegate&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;textFieldDidChangeSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;textField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UITextField&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="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;textCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textField&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="bp"&gt;count&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&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;submitButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;textCount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;9&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="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;textField&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;_&lt;/span&gt; &lt;span class="n"&gt;textField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UITextField&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;shouldChangeCharactersIn&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NSRange&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;replacementString&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&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="nb"&gt;Bool&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="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;newLength&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="bp"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&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;inputTextValid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newLength&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inputTextValid&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;/p&gt;
&lt;p&gt;문제는 중복 체크처럼 네트워크 요청이 들어가는 검증까지 같은 방식으로 처리하기에는 부담이 커진다는 점이었다.&lt;/p&gt;</description></item><item><title>SniffMEET. AuthManager와 SessionManager의 책임 분리하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-auth-session-responsibilities/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-auth-session-responsibilities/</guid><description>&lt;h2 id="작업-배경"&gt;작업 배경&lt;/h2&gt;
&lt;p&gt;AuthManager와 SessionManager는 서로 매우 밀접하게 연관된 작업을 한다. AuthManager는 사용자의 계정 인증을 담당하고 세션을 받아오며, SessionManager는 그 세션을 관리한다.&lt;/p&gt;
&lt;p&gt;하지만 기존 코드에서는 세션 관리를 포함한 대부분의 작업을 &lt;code&gt;AuthManager&lt;/code&gt;가 처리하고, &lt;code&gt;SessionManager&lt;/code&gt;는 세션을 소유하는 역할만 하고 있었다.&lt;/p&gt;
&lt;h3 id="역할-분리"&gt;역할 분리&lt;/h3&gt;
&lt;p&gt;기존에는 &lt;code&gt;AuthManager&lt;/code&gt;에서 세션 갱신, 복원, 저장, 로드 작업을 처리하고 있었고, &lt;code&gt;SessionManager&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="c1"&gt;// AuthManager 인터페이스 프로토콜&lt;/span&gt;
&lt;/span&gt;&lt;/span&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;AuthManager&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;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AuthManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&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;authStateSubject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PassthroughSubject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Never&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="kr"&gt;set&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;signInAnonymously&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="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;restoreSession&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="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;refreshSession&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="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;loadTokens&lt;/span&gt;&lt;span class="p"&gt;()&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;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="c1"&gt;// SessionManager 클래스&lt;/span&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;SessionManager&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;static&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SessionManager&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;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SupabaseSession&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;isExpired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&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="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;// 세션 만료를 파악할 때는 30초의 여유시간을 줍니다.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeIntervalSince1970&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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="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;private&lt;/span&gt; &lt;span class="kd"&gt;init&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="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;SessionManager&lt;/code&gt;, 인증은 &lt;code&gt;AuthManager&lt;/code&gt;가 맡도록 기능을 다시 정의했다.&lt;/p&gt;</description></item><item><title>SniffMEET. 세션 갱신과 정보 접근을 SessionManager에 캡슐화하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-session-manager-encapsulation/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-session-manager-encapsulation/</guid><description>&lt;h2 id="sessionmanager-사용성-개선"&gt;SessionManager 사용성 개선&lt;/h2&gt;
&lt;h3 id="세션을-갱신하는-방법"&gt;세션을 갱신하는 방법&lt;/h3&gt;
&lt;p&gt;Supabase 서버와의 통신이 필요할 때, 우선 세션의 유효성을 검증해야 한다.&lt;/p&gt;
&lt;p&gt;기존 방식에선 직접 AuthManager의 싱글톤 객체에서 세션을 갱신하는 작업을 했지만&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AuthManager와 SessionManager의 역할 명확하게 분리&lt;/li&gt;
&lt;li&gt;AuthManager의 싱글톤 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;와 같은 변화점이 있었고, 기존 AuthManager가 담당하던 세션 갱신, 복원 작업이 SessionManager로 옮겨졌다. SessionManager의 싱글톤 객체에서 세션의 유효성을 체크하는 컴퓨티드 프로퍼티의 부울리언 값을 체크하여 세션을 갱신하는 메소드를 호출하는 방식을 사용했다:&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="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;SessionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExpired&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="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SessionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refreshSession&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;이 방식은 SessionManager의 싱글톤 객체 호출이 두 번 일어나고, &lt;code&gt;refreshSession&lt;/code&gt;이라는 메소드를 직접 노출해서, SessionManager가 세션을 갱신하는 방법을 직접 노출하는 문제가 있다.&lt;/p&gt;</description></item><item><title>SniffMEET. 세션 저장 메소드를 공개해도 괜찮을까?</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-session-save-api/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-session-save-api/</guid><description>&lt;h2 id="세션-저장하기"&gt;세션 저장하기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AuthManager&lt;/code&gt;와 &lt;code&gt;SessionManager&lt;/code&gt;의 역할을 분리하고 나니, 세션을 받아오는 쪽과 저장하는 쪽이 서로 달라지는 문제가 발생했다.&lt;/p&gt;
&lt;p&gt;(AuthManager가 세션도 관리하던)기존 구조에서는 &lt;code&gt;saveSession(for:)&lt;/code&gt;가 외부에 노출되어 있지 않았는데, 역할이 옮겨진 뒤에는 AuthManager에서 받아온 세션을 SessionManager로 전달해야 했다.&lt;/p&gt;
&lt;h3 id="고민한-점"&gt;고민한 점&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;saveSession(for:)&lt;/code&gt;는 사실상 &lt;code&gt;AuthManager&lt;/code&gt;가 세션을 받아온 상황에서만 필요한 메소드다.&lt;/p&gt;
&lt;p&gt;그런데 이 메소드를 공개하면 캡슐화가 약해진다는 생각을 했고, 다시 AuthManager로 돌리면 SessionManager가 맡아야 할 책임을 되돌리는 것이 되었다.&lt;/p&gt;
&lt;p&gt;따라서 몇 가지 방법을 떠올려 봤다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(최종 선택) &lt;code&gt;saveSession(for:)&lt;/code&gt;를 공개한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;saveSession(for:)&lt;/code&gt;를 &lt;code&gt;AuthManager&lt;/code&gt;로 다시 옮긴다&lt;/li&gt;
&lt;li&gt;같은 로직을 &lt;code&gt;AuthManager&lt;/code&gt;와 &lt;code&gt;SessionManager&lt;/code&gt;에 각각 작성한다&lt;/li&gt;
&lt;li&gt;세션이 갱신됐다는 이벤트를 따로 전달할 수 있는 통신 레이어를 만든다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;처음에 가장 괜찮아 보였던 건 세션 갱신 이벤트를 따로 전달하는 방식이었다.&lt;/p&gt;</description></item><item><title>SniffMEET. DTO와 Entity의 경계 점검하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-dto-entity-boundaries/</link><pubDate>Sat, 11 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-dto-entity-boundaries/</guid><description>&lt;h2 id="들어가며"&gt;들어가며&lt;/h2&gt;
&lt;p&gt;SniffMeet에 기능이 붙으면서 DTO와 Entity가 빠르게 늘어났다. 처음에는 필요한 타입을 그때그때 만들면 됐지만, 어느 순간부터 어떤 타입이 어느 레이어에서 쓰이는지 헷갈리기 시작했다.&lt;/p&gt;
&lt;p&gt;특히 &lt;code&gt;UserInfoDTO&lt;/code&gt;, &lt;code&gt;UserInfo&lt;/code&gt;, &lt;code&gt;DogInfo&lt;/code&gt;, &lt;code&gt;Mate&lt;/code&gt;처럼 비슷한 정보를 담는 타입들이 여러 곳에서 서로 다른 이름으로 사용되고 있었다. 그래서 바로 코드를 고치기보다, 먼저 현재 타입들이 어디에서 쓰이고 있는지 정리해보기로 했다.&lt;/p&gt;
&lt;h2 id="먼저-한-일-사용처-조사"&gt;먼저 한 일: 사용처 조사&lt;/h2&gt;
&lt;p&gt;가장 먼저 DTO, Entity, Supabase 관련 Request/Response 타입을 나눠서 사용처를 훑어봤다.&lt;/p&gt;
&lt;p&gt;전체 목록을 보면 타입 수가 많은 것도 문제였지만, 더 큰 문제는 몇몇 타입의 사용처가 너무 넓다는 점이었다.&lt;/p&gt;</description></item><item><title>SniffMEET. 비동기 태스크와 액터 점검하기 (1)</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-task-mainactor-boundaries-part-1/</link><pubDate>Wed, 08 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-task-mainactor-boundaries-part-1/</guid><description>&lt;h2 id="작업-내역"&gt;작업 내역&lt;/h2&gt;
&lt;p&gt;이번 작업에서는 &lt;code&gt;Task&lt;/code&gt;, &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;@MainActor&lt;/code&gt;가 섞여 있던 부분을 화면별로 점검했다.&lt;/p&gt;
&lt;p&gt;앱 시작 흐름부터 프로필 등록, 메이트 목록, 산책 응답까지 비동기 경계를 다시 살펴보고, 각 화면에서 책임이 어디에 있어야 하는지 정리했다.&lt;/p&gt;
&lt;h3 id="앱-시작-플로우"&gt;앱 시작 플로우&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;UIWindow&lt;/code&gt;의 루트 뷰 컨트롤러를 결정하는 흐름은 &lt;code&gt;@MainActor&lt;/code&gt;로 두고, 그 안에서 세션 복원이 끝나기를 기다렸다. &lt;code&gt;await&lt;/code&gt;로 대기하는 동안에는 메인 액터를 점유하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;displayInitialScreen&lt;/code&gt;이 &lt;code&gt;MainActor&lt;/code&gt; 안에서 실행되는지 breakpoint로 확인했고, &lt;code&gt;displayOnBoardingView&lt;/code&gt;는 그대로 &lt;code&gt;MainActor&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="c1"&gt;// AppRouter.swift&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;func&lt;/span&gt; &lt;span class="nf"&gt;displayInitialScreen&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;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;MainActor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;do&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="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SupabaseAuthManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;restoreSession&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;displayTabBar&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 class="k"&gt;catch&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;displayOnBoardingView&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 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;h3 id="프로필-입력-및-등록"&gt;프로필 입력 및 등록&lt;/h3&gt;
&lt;p&gt;프로필 입력 부분은 전달만 하는 역할이라 비동기 처리가 필요하지 않았다.&lt;/p&gt;</description></item><item><title>SniffMEET. 비동기 태스크와 액터 점검하기 (2)</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-task-mainactor-boundaries-part-2/</link><pubDate>Wed, 08 Jan 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-task-mainactor-boundaries-part-2/</guid><description>&lt;h3 id="메이트-요청"&gt;메이트 요청&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;didTabAcceptButton&lt;/code&gt;과 &lt;code&gt;saveMateInfo&lt;/code&gt;를 따라가 보니 유스케이스 실행을 위해 &lt;code&gt;Task&lt;/code&gt;가 존재했고, Combine의 &lt;code&gt;sink&lt;/code&gt; 클로저 안에서도 &lt;code&gt;Task&lt;/code&gt;를 생성하고 있었다. Interactor에서 태스크를 묶으면 저장 완료를 알리는 메서드가 따로 필요해 보였다.&lt;/p&gt;
&lt;p&gt;Presenter 메서드는 &lt;code&gt;async&lt;/code&gt;로 바꾸고, &lt;code&gt;View&lt;/code&gt;의 &lt;code&gt;sink&lt;/code&gt;에서는 &lt;code&gt;Task&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="n"&gt;acceptButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touchUpInside&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 class="n"&gt;sink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;weak&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&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;await&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;didTapAcceptButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;DogProfileDTO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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;self&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;closeTheView&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 class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cancellables&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="c1"&gt;// RequestMatePresenter&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;closeTheView&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="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;view&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;router&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;dismissView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&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 class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;didTapAcceptButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;async&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;SNMLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;id: &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&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;await&lt;/span&gt; &lt;span class="n"&gt;interactor&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;saveMateInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id="산책-요청-및-응답"&gt;산책 요청 및 응답&lt;/h3&gt;
&lt;h4 id="respondwalkpresenter"&gt;&lt;code&gt;RespondWalkPresenter&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;산책 요청은 &lt;code&gt;Task&lt;/code&gt;로 Interactor에 존재했고, 위치 변환은 Presenter에서 &lt;code&gt;await&lt;/code&gt;로 처리했다.&lt;/p&gt;</description></item></channel></rss>