<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>오깅중</title><link>https://jun7680.github.io/</link><description>Recent content on 오깅중</description><generator>Hugo</generator><language>ko</language><lastBuildDate>Tue, 02 Jun 2026 08:00:00 +0900</lastBuildDate><atom:link href="https://jun7680.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>URLSession 취소를 실패 알림으로 보여주지 않기</title><link>https://jun7680.github.io/p/urlsession-cancellation-error-handling/</link><pubDate>Tue, 02 Jun 2026 08:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/urlsession-cancellation-error-handling/</guid><description>&lt;p&gt;검색 화면이나 탭 전환 화면에서 네트워크 요청을 취소하는 건 흔하다. 사용자가 검색어를 바꾸면 이전 요청은 더 이상 필요 없고, 화면을 나가면 진행 중인 API도 굳이 끝까지 기다릴 이유가 없다.&lt;/p&gt;</description></item><item><title>NavigationStack path가 커질 때 Route enum으로 버티기</title><link>https://jun7680.github.io/p/navigationstack-route-enum-path/</link><pubDate>Tue, 02 Jun 2026 07:40:00 +0900</pubDate><guid>https://jun7680.github.io/p/navigationstack-route-enum-path/</guid><description>&lt;p&gt;&lt;code&gt;NavigationStack&lt;/code&gt;은 처음 붙이면 꽤 편하다. 리스트에서 값을 누르고, &lt;code&gt;navigationDestination&lt;/code&gt;만 선언하면 상세 화면으로 넘어간다. 작은 화면에서는 이 정도로 충분하다.&lt;/p&gt;</description></item><item><title>SwiftUI @State 초기값이 바인딩을 따라 바뀌지 않을 때</title><link>https://jun7680.github.io/p/swiftui-state-initial-value-binding-trap/</link><pubDate>Tue, 02 Jun 2026 07:20:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-state-initial-value-binding-trap/</guid><description>&lt;p&gt;SwiftUI에서 편집 화면을 만들다 보면 부모가 넘겨준 모델을 &lt;code&gt;@State&lt;/code&gt;에 복사해서 쓰고 싶을 때가 있다. 예를 들어 상세 화면에서 이름을 수정하다가 저장 버튼을 누를 때만 실제 모델에 반영하는 구조다.&lt;/p&gt;</description></item><item><title>SwiftUI searchable 붙이고 검색 상태가 지저분해졌을 때</title><link>https://jun7680.github.io/p/swiftui-searchable-state-management/</link><pubDate>Mon, 01 Jun 2026 16:25:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-searchable-state-management/</guid><description>&lt;p&gt;SwiftUI에서 검색창 붙이는 건 쉽다. &lt;code&gt;NavigationStack&lt;/code&gt;이나 &lt;code&gt;List&lt;/code&gt;에 &lt;code&gt;.searchable(text:)&lt;/code&gt; 한 줄 붙이면 검색 필드가 바로 생긴다.&lt;/p&gt;
&lt;p&gt;문제는 그 다음이다. 검색어가 바뀔 때마다 API를 부르고, 검색 결과 없음 화면도 보여주고, 추천어도 띄우고, 화면을 나갔다 돌아왔을 때 이전 검색어를 유지할지 말지까지 들어오면 &lt;code&gt;@State var searchText&lt;/code&gt; 하나가 갑자기 너무 많은 일을 하기 시작한다.&lt;/p&gt;</description></item><item><title>UICollectionViewCompositionalLayout 가로 스크롤 섹션에서 삐끗한 것들</title><link>https://jun7680.github.io/p/uicollectionview-compositional-layout-orthogonal-scrolling/</link><pubDate>Mon, 01 Jun 2026 10:50:00 +0900</pubDate><guid>https://jun7680.github.io/p/uicollectionview-compositional-layout-orthogonal-scrolling/</guid><description>&lt;p&gt;&lt;code&gt;UICollectionViewCompositionalLayout&lt;/code&gt;은 처음 보면 꽤 그럴듯하다. 섹션마다 다른 레이아웃을 만들 수 있고, 한 화면 안에 세로 리스트와 가로 캐러셀을 같이 넣는 것도 &lt;code&gt;orthogonalScrollingBehavior&lt;/code&gt; 한 줄이면 된다.&lt;/p&gt;</description></item><item><title>iOS 17에서만 죽는 SwiftData 크래시 — unmanaged @Model의 함정</title><link>https://jun7680.github.io/p/swiftdata-unmanaged-model-ios17-crash/</link><pubDate>Fri, 29 May 2026 14:20:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftdata-unmanaged-model-ios17-crash/</guid><description>&lt;p&gt;배포된 앱이 특정 화면 진입에서 죽는다는 제보가 들어왔다. 그런데 조건이 이상했다. iOS 18 기기는 멀쩡한데 &lt;strong&gt;iOS 17에서만&lt;/strong&gt; 죽는다. 같은 빌드, 같은 코드인데 OS만 타는 크래시였다. 비슷한 증상으로 헤매는 사람이 있을 것 같아 원인 찾던 과정을 남겨둔다.&lt;/p&gt;</description></item><item><title>SwiftUI TextField 숫자 입력이 자꾸 되돌아갈 때</title><link>https://jun7680.github.io/p/swiftui-textfield-number-input/</link><pubDate>Wed, 27 May 2026 08:45:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-textfield-number-input/</guid><description>&lt;p&gt;수량이나 금액 입력칸을 만들 때 &lt;code&gt;TextField&lt;/code&gt;에 &lt;code&gt;Int&lt;/code&gt;를 바로 묶으면 코드가 아주 예쁘다. &lt;code&gt;format: .number&lt;/code&gt;까지 붙이면 화면에 보여주는 모양도 알아서 챙겨주니 여기서 끝난 줄 알기 쉽다.&lt;/p&gt;</description></item><item><title>@Bindable vs @Binding, 둘이 뭐가 다른지 한 번 정리</title><link>https://jun7680.github.io/p/swiftui-bindable-vs-binding/</link><pubDate>Tue, 26 May 2026 14:15:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-bindable-vs-binding/</guid><description>&lt;p&gt;iOS 17에서 &lt;code&gt;@Observable&lt;/code&gt; 매크로가 들어오면서 &lt;code&gt;@Bindable&lt;/code&gt;이라는 새 property wrapper가 같이 붙어 왔다. 이름이 &lt;code&gt;@Binding&lt;/code&gt;이랑 한 글자 차이라 처음엔 &amp;ldquo;이게 뭐 다른 건가&amp;rdquo; 싶었는데, 막상 자식 view에 모델 넘기다가 &lt;code&gt;$model.name&lt;/code&gt;이 컴파일이 안 돼서 한참 헤맸다. 두 번 헷갈리기 싫어서 정리해둔다.&lt;/p&gt;</description></item><item><title>SwiftUI #Preview가 자꾸 죽을 때 점검할 패턴들</title><link>https://jun7680.github.io/p/swiftui-preview-crash-patterns/</link><pubDate>Tue, 26 May 2026 14:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-preview-crash-patterns/</guid><description>&lt;p&gt;&lt;code&gt;#Preview&lt;/code&gt; 매크로(iOS 17+)로 옮긴 뒤로 캔버스가 빨갛게 죽는 빈도가 늘었다. 대부분 뷰 코드가 잘못된 게 아니라 &lt;strong&gt;Preview에 환경(Environment)이나 의존성을 안 넘겨서&lt;/strong&gt; 터지는 거였다. 매번 같은 패턴을 까먹고 헤매서 정리해 둔다.&lt;/p&gt;</description></item><item><title>SwiftUI .task vs .onAppear, 어느 쪽이 안전한가</title><link>https://jun7680.github.io/p/swiftui-task-vs-onappear-cancellation/</link><pubDate>Tue, 26 May 2026 13:45:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-task-vs-onappear-cancellation/</guid><description>&lt;p&gt;&lt;code&gt;.task&lt;/code&gt;와 &lt;code&gt;.onAppear { Task { } }&lt;/code&gt;가 처음엔 둘 다 &amp;ldquo;뷰가 떴을 때 비동기 작업 시작&amp;quot;으로 보였다. 둘 중 하나만 골라 쓰면 되겠다 싶었는데, 화면을 빠르게 들락거리면서 네트워크 로그를 보다가 두 패턴이 같은 상황에서 전혀 다르게 동작하는 걸 봤다. 차이가 한 줄로 요약된다. &lt;strong&gt;사라진 뷰의 작업을 자동으로 취소해 주느냐 아니냐.&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>@FocusState로 키보드 다루기 — 자주 헷갈리는 자리들</title><link>https://jun7680.github.io/p/swiftui-focusstate-keyboard-traps/</link><pubDate>Tue, 26 May 2026 13:30:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-focusstate-keyboard-traps/</guid><description>&lt;p&gt;&lt;code&gt;@FocusState&lt;/code&gt;는 처음 쓸 때보다 두 번째, 세 번째 쓸 때 더 헷갈린다. 폼 화면 하나 짤 때마다 &amp;ldquo;이거 nil로 두면 되던가&amp;hellip;?&amp;rdquo; 하면서 다시 검색한다. 그래서 매번 헷갈리는 자리만 한 번 정리해둔다.&lt;/p&gt;</description></item><item><title>Swift ClosedRange&lt;Date&gt; 한 줄 때문에 한 달간 헤맨 이야기</title><link>https://jun7680.github.io/p/closed-range-date-crash/</link><pubDate>Tue, 26 May 2026 10:30:00 +0900</pubDate><guid>https://jun7680.github.io/p/closed-range-date-crash/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;어느 날 Crashlytics를 열었더니 캘린더 화면 한 곳에서만 4주간 98건이 쌓여 있었다. 신호는 &lt;code&gt;EXC_BREAKPOINT&lt;/code&gt;. 영향 사용자는 15명. 같은 사용자가 평균 9~10번씩 죽는 &lt;code&gt;SIGNAL_REPETITIVE&lt;/code&gt; 패턴.&lt;/p&gt;</description></item><item><title>Coordinator 패턴에서 부모 없는 외부 진입이 만드는 delegate nil 함정</title><link>https://jun7680.github.io/p/coordinator-external-entry-no-parent/</link><pubDate>Fri, 22 May 2026 09:40:00 +0900</pubDate><guid>https://jun7680.github.io/p/coordinator-external-entry-no-parent/</guid><description>&lt;p&gt;MVVM+C 구조에서 상세 화면을 열고 닫는 흐름은 보통 부모 Coordinator가 관리한다.&lt;/p&gt;
&lt;p&gt;리스트에서 셀을 탭하면 리스트 Coordinator가 상세 Coordinator를 만들고, 상세가 끝났다는 delegate 콜백을 받는다. 너무 익숙한 구조다. 그래서 오히려 의심을 안 했다.&lt;/p&gt;</description></item><item><title>실기기에서만 깨진 콜드스타트 푸시 race condition 디버깅 기록</title><link>https://jun7680.github.io/p/ios-cold-start-push-race-debugging/</link><pubDate>Fri, 22 May 2026 09:30:00 +0900</pubDate><guid>https://jun7680.github.io/p/ios-cold-start-push-race-debugging/</guid><description>&lt;p&gt;푸시 알림을 탭해서 앱을 콜드스타트로 열었더니 리스트는 빈 화면이고, 상세는 끝까지 안 떴다.&lt;/p&gt;
&lt;p&gt;조금 기다리면 리스트 데이터는 뒤늦게 채워졌다. 그런데 푸시가 가리키던 상세 화면은 다시 살아나지 않았다. 더 얄미운 건 시뮬레이터에서는 멀쩡했다는 점이다. 실기기 + 콜드스타트 + 특정 상세 진입. 이 세 조건이 겹칠 때만 깨졌다.&lt;/p&gt;</description></item><item><title>SwiftUI List에서 onAppear로 페이지네이션 짜다가...</title><link>https://jun7680.github.io/p/swiftui-list-onappear-pagination-pitfall/</link><pubDate>Thu, 21 May 2026 15:35:57 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-list-onappear-pagination-pitfall/</guid><description>&lt;p&gt;&lt;code&gt;onAppear&lt;/code&gt;라는 이름을 처음 봤을 때 UIKit &lt;code&gt;viewDidAppear&lt;/code&gt; 비슷한 거라고 생각했다. 화면이 나타날 때 한 번 부르는 진입 이벤트라고 본 거다.&lt;/p&gt;
&lt;p&gt;근데 &lt;code&gt;List&lt;/code&gt; row의 &lt;code&gt;onAppear&lt;/code&gt;는 그렇게 한 번만 부르고 끝나는 시그널이 아니라서 거기에 페이지네이션 API를 그대로 매달면 첫 진입에 보이는 row 수만큼 동시 호출이 나가고 스크롤을 위아래로 왔다 갔다 하면 같은 row의 &lt;code&gt;onAppear&lt;/code&gt;가 또 호출되면서 API가 그대로 또 나간다. 처음엔 서버 로그를 보고 한참 헷갈렸다.&lt;/p&gt;</description></item><item><title>NavigationStack에서 화면이 두 번 쌓이는 세 가지 케이스</title><link>https://jun7680.github.io/p/swiftui-navigationstack-duplicate-push/</link><pubDate>Thu, 21 May 2026 14:13:36 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-navigationstack-duplicate-push/</guid><description>&lt;p&gt;UIKit에서 화면 쌓던 감각으로 SwiftUI &lt;code&gt;NavigationStack&lt;/code&gt;을 다루다 보면 어딘가에서 한 번씩 같은 화면이 두 개 쌓여 있다. 뒤로 가기를 두 번 눌러야 원래 화면으로 돌아오는 그 상황이다.&lt;/p&gt;</description></item><item><title>SwiftUI 모달 세 개째부터는 Bool 말고 enum 라우팅으로</title><link>https://jun7680.github.io/p/swiftui-sheet-bool-to-identifiable-enum/</link><pubDate>Thu, 21 May 2026 13:45:26 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-sheet-bool-to-identifiable-enum/</guid><description>&lt;p&gt;처음에 SwiftUI에서 &lt;code&gt;.sheet&lt;/code&gt;를 띄울 일이 두 개 정도였을 때는 별생각 없이 &lt;code&gt;@State private var showProfile = false&lt;/code&gt;, &lt;code&gt;@State private var showSettings = false&lt;/code&gt; 식으로 Bool 두 개 잡아서 썼다. 동작도 잘 됐고 별 문제 없었다.&lt;/p&gt;</description></item><item><title>SwiftUI onChange 안에서 state 다시 set하면 무한 루프 돈다</title><link>https://jun7680.github.io/p/swiftui-onchange-infinite-loop/</link><pubDate>Thu, 21 May 2026 13:33:28 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-onchange-infinite-loop/</guid><description>&lt;p&gt;SwiftUI &lt;code&gt;onChange&lt;/code&gt;를 처음 봤을 때 그냥 &amp;ldquo;값 바뀌면 호출되는 이벤트 핸들러&amp;quot;라고 생각했다. UIKit &lt;code&gt;addTarget(_:action:for: .editingChanged)&lt;/code&gt; 같은 거라고 본 거다.&lt;/p&gt;
&lt;p&gt;근데 그렇게 쓰다 보면 어느 순간 콘솔이 미친 듯이 찍히고 운 나쁘면 CPU 한 코어가 그대로 먹힌다. 원인은 거의 매번 같은데 &lt;code&gt;onChange&lt;/code&gt; 안에서 감시 중인 state를 다시 건드렸기 때문이다.&lt;/p&gt;</description></item><item><title>UIKit 모달 여러 개가 동시에 present 될 때 큐로 줄 세운 기록</title><link>https://jun7680.github.io/p/uikit-present-modal-queue/</link><pubDate>Thu, 21 May 2026 10:20:00 +0900</pubDate><guid>https://jun7680.github.io/p/uikit-present-modal-queue/</guid><description>&lt;p&gt;앱 진입 직후에 띄워야 하는 모달이 여러 개 있었다.&lt;/p&gt;
&lt;p&gt;권한 안내, 튜토리얼, 업데이트 팝업, 서비스 공지&amp;hellip; 이런 것들이 각자 다른 publisher에서 자기 타이밍에 신호를 보낸다. 하나씩 보면 다 말이 된다. 근데 실제 앱에서는 거의 동시에 떨어진다.&lt;/p&gt;</description></item><item><title>Coordinator re-attach 때 생기는 deinit race window 막기</title><link>https://jun7680.github.io/p/coordinator-reattach-race/</link><pubDate>Thu, 21 May 2026 09:48:00 +0900</pubDate><guid>https://jun7680.github.io/p/coordinator-reattach-race/</guid><description>&lt;p&gt;Coordinator 패턴을 쓰다 보면 특정 list coordinator를 다시 붙이는 코드가 생긴다. 푸시로 특정 탭에 진입하거나, 기존 stack을 정리한 뒤 새 list를 만들어야 하는 경우가 그렇다.&lt;/p&gt;</description></item><item><title>iOS 푸시 알림 중복 진입을 막는 2층 dedupe 패턴</title><link>https://jun7680.github.io/p/ios-push-dedupe-two-layer/</link><pubDate>Thu, 21 May 2026 09:46:00 +0900</pubDate><guid>https://jun7680.github.io/p/ios-push-dedupe-two-layer/</guid><description>&lt;p&gt;iOS 푸시 알림은 생각보다 같은 이벤트가 두 번 들어오기 쉽다.&lt;/p&gt;
&lt;p&gt;사용자는 알림을 한 번 눌렀는데 앱 안에서는 notification tap, cold launch payload, pending route, ready callback 같은 경로가 겹친다. 결과는 익숙하다. 같은 상세 화면이 navigation stack에 두 번 쌓인다. 또는 이미 처리한 푸시가 host attach 시점에 다시 살아난다. 보고 있으면 좀 허탈하다.&lt;/p&gt;</description></item><item><title>RxSwift 코드베이스에서 async/await로 점진 마이그레이션하는 방법</title><link>https://jun7680.github.io/p/rxswift-to-async-await-migration/</link><pubDate>Thu, 21 May 2026 09:44:00 +0900</pubDate><guid>https://jun7680.github.io/p/rxswift-to-async-await-migration/</guid><description>&lt;p&gt;RxSwift가 오래 깔린 iOS 프로젝트를 Swift Concurrency로 옮기는 건 생각보다 무겁다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Observable&lt;/code&gt; 몇 개를 &lt;code&gt;async&lt;/code&gt;로 바꾸면 끝나는 문제가 아니었다. 보통은 Repository, UseCase, ViewModel, Coordinator까지 전 레이어에 Rx 타입이 퍼져 있다. 이런 상태에서 &amp;ldquo;이번 PR에서 RxSwift 제거합니다&amp;rdquo; 하면&amp;hellip; 거의 반드시 터진다.&lt;/p&gt;</description></item><item><title>iOS Coordinator에서 푸시 진입을 단일 라우터로 정리한 기록</title><link>https://jun7680.github.io/p/coordinator-push-routing-async/</link><pubDate>Thu, 21 May 2026 09:42:00 +0900</pubDate><guid>https://jun7680.github.io/p/coordinator-push-routing-async/</guid><description>&lt;p&gt;UIKit + MVVM + Coordinator 구조에서 푸시 알림을 처리하다 보면 어느 순간 코드가 이상한 방향으로 자란다.&lt;/p&gt;
&lt;p&gt;처음엔 되게 단순하다. A 리스트는 A 타입 이벤트를 구독하고, B 리스트는 B 타입 이벤트를 구독한다. 각 화면이 자기 관심사를 처리하니까 자연스러워 보인다.&lt;/p&gt;</description></item><item><title>@MainActor를 클래스에 통째로 붙이면 생기는 fetch 함정</title><link>https://jun7680.github.io/p/mainactor-class-fetch-trap/</link><pubDate>Thu, 21 May 2026 09:40:00 +0900</pubDate><guid>https://jun7680.github.io/p/mainactor-class-fetch-trap/</guid><description>&lt;p&gt;Swift Concurrency로 코드를 옮기다 보면 &lt;code&gt;@MainActor&lt;/code&gt;를 어디까지 붙여야 하는지 은근 애매하다.&lt;/p&gt;
&lt;p&gt;이번에 만난 케이스도 딱 그랬다. 외부 이벤트를 받으면 서버에서 상세 데이터를 단건 fetch하고, 그 결과로 상세 화면 Coordinator를 만든 다음 navigation stack에 push하는 코드였다. Coordinator 빌드는 UIKit을 만지니까 메인 스레드가 맞고&amp;hellip; 그래서 처음엔 클래스 전체에 &lt;code&gt;@MainActor&lt;/code&gt;를 붙였다.&lt;/p&gt;</description></item><item><title>Swift Concurrency 프로젝트에서 GCD가 여전히 답인 자리들</title><link>https://jun7680.github.io/p/gcd-still-needed-with-swift-concurrency/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/gcd-still-needed-with-swift-concurrency/</guid><description>&lt;p&gt;요즘 프로젝트를 Swift Concurrency(async/await, actor)로 통일해가고 있다. 그런데 코드 곳곳에 GCD를 일부러 남겨둔 자리들이 보인다. &amp;ldquo;다 옮기면 깔끔하지 않을까?&amp;rdquo; 싶어서 한 번씩 시도해봤는데, 그때마다 코드가 더 길어지거나 의미가 더 흐려지는 자리가 있더라. 그래서 이 글에서는 &amp;ldquo;Concurrency 시대에도 GCD가 답인 자리들&amp;quot;을 카탈로그로 정리해본다. GCD 기본기와 async/await 마이그레이션 입문은 예전에 쓴 &lt;a href="https://jun7680.github.io/post/concurrency/"&gt;동시성 정리 글&lt;/a&gt;에 둬뒀으니 그쪽을 먼저 봐도 좋다.&lt;/p&gt;</description></item><item><title>MCP 서버 비개발자 동료에게 배포하다 Windows에서 6번 죽었다</title><link>https://jun7680.github.io/p/mcp-windows-deploy-6-traps/</link><pubDate>Tue, 19 May 2026 10:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/mcp-windows-deploy-6-traps/</guid><description>&lt;p&gt;직접 만든 MCP 서버(&lt;code&gt;kakao-moment-mcp&lt;/code&gt;)를 비개발자 동료에게 배포하는 작업이었다. macOS에서는 &lt;code&gt;uvx&lt;/code&gt; 한 줄로 끝나는데 Windows에서 갈렸다. 내 PC도 아니라 동료 PC로 원격 디버깅을 돌리면서 6번쯤 죽었다 살아났다. 같은 길 갈 사람이 시간 덜 쓰라고 기록해둔다.&lt;/p&gt;</description></item><item><title>UIButton.Configuration 쓸 땐 layer.cornerRadius 건드리지 말자</title><link>https://jun7680.github.io/p/uibutton-configuration-layer-cornerradius-trap/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/uibutton-configuration-layer-cornerradius-trap/</guid><description>&lt;p&gt;iOS 15+ 신규 화면을 만들다가 새 API인 &lt;code&gt;UIButton.Configuration.filled()&lt;/code&gt;로 버튼을 찍었다. 둥글게 + 빨간 보더 1pt만 주면 되는 단순한 작업인데, 한 시간을 날렸다. 같은 함정에 빠진 분이 있으면 이 글 한 편으로 빠르게 빠져나오길 바라며 정리한다.&lt;/p&gt;</description></item><item><title>xcrun simctl launch에 환경변수 안 먹던 이야기</title><link>https://jun7680.github.io/p/simctl-launch-env-vars/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/simctl-launch-env-vars/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;QA 시나리오 캡처 자동화를 짜다가 막혔다.&lt;/p&gt;
&lt;p&gt;서버 응답에 따라 분기되는 화면 3종을 캡처하고 싶어서, 디버그 빌드에 환경변수
&lt;code&gt;FORCE_LABS_ENABLED&lt;/code&gt; 로 서버 응답을 덮어쓰는 분기를 넣어뒀다. 그리고 시뮬레이터를
이렇게 띄웠다.&lt;/p&gt;</description></item><item><title>@Observable 마이그레이션, 모르고 가면 발 헛디딘다 — 함정 5가지</title><link>https://jun7680.github.io/p/observable-migration-traps/</link><pubDate>Mon, 18 May 2026 08:20:00 +0900</pubDate><guid>https://jun7680.github.io/p/observable-migration-traps/</guid><description>&lt;p&gt;iOS 17부터 들어온 Observation 프레임워크와 &lt;code&gt;@Observable&lt;/code&gt; 매크로, 이제 슬슬 마이그레이션해도 되겠다 싶어서 손대다 보면 묘하게 헛디디는 지점이 있다. 드롭인 교체인 줄 알고 갔다가 view가 안 그려지거나 반대로 너무 자주 그려지는 걸 보고 식겁한 적이 한 번쯤 있을 거다. 비슷한 일을 두 번 겪지 않으려고 자주 만나는 함정 다섯 가지를 정리해둔다.&lt;/p&gt;</description></item><item><title>iOS 26 SwiftUI 네이티브 WebView, 드디어 한 줄이다</title><link>https://jun7680.github.io/p/swiftui-webview-webpage-ios26/</link><pubDate>Mon, 18 May 2026 08:15:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-webview-webpage-ios26/</guid><description>&lt;p&gt;SwiftUI 프로젝트에서 웹 페이지 하나 띄우려고 &lt;code&gt;UIViewRepresentable&lt;/code&gt;을 새로 짜본 경험, iOS 개발자라면 한 번쯤 있을 것이다. &lt;code&gt;WKWebView&lt;/code&gt;를 감싸는 의식 같은 코드 말이다. iOS 26부터는 그게 한 줄로 끝난다. 오늘은 새로 등장한 &lt;code&gt;WebView&lt;/code&gt;와 &lt;code&gt;WebPage&lt;/code&gt;를 빠르게 정리해본다.&lt;/p&gt;</description></item><item><title>Foundation Models, Swift 3줄로 온디바이스 LLM 호출하기</title><link>https://jun7680.github.io/p/foundation-models-3lines-on-device-llm/</link><pubDate>Mon, 18 May 2026 08:10:00 +0900</pubDate><guid>https://jun7680.github.io/p/foundation-models-3lines-on-device-llm/</guid><description>&lt;p&gt;OpenAI API 키 발급받고, 사용량 청구서 도착할 때마다 가슴이 쿵 떨어지고, 키가 깃허브에 노출됐을까봐 git history를 뒤지던 시절을 잠깐 떠올려본다. iOS 26부터는 그 모든 게 한 번에 사라진다. &lt;strong&gt;앱에서 LLM 한 번 부르는 데 진짜로 세 줄이면 끝난다.&lt;/strong&gt; 그것도 비용 0원, 네트워크 0번으로.&lt;/p&gt;</description></item><item><title>@Entry 매크로로 EnvironmentValues 보일러플레이트 날려버리기</title><link>https://jun7680.github.io/p/swiftui-entry-macro-environmentvalues/</link><pubDate>Mon, 18 May 2026 08:05:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-entry-macro-environmentvalues/</guid><description>&lt;p&gt;SwiftUI로 작업하다 보면 커스텀 &lt;code&gt;EnvironmentValues&lt;/code&gt;를 만들 일이 종종 생긴다. 그런데 값 하나 추가하려고 &lt;code&gt;EnvironmentKey&lt;/code&gt; struct 만들고, &lt;code&gt;defaultValue&lt;/code&gt; 지정하고, &lt;code&gt;EnvironmentValues&lt;/code&gt; extension에 getter/setter 두 줄까지 — 매번 같은 보일러플레이트를 반복하는 게 솔직히 좀 짜증났다. 그러다 &lt;code&gt;@Entry&lt;/code&gt; 매크로를 알게 됐는데, &amp;ldquo;이걸 왜 이제야 줬냐&amp;rdquo; 싶을 만큼 깔끔하다. 게다가 Xcode 16 이상이면 쓸 수 있고, deployment target은 iOS 13까지 내려간다.&lt;/p&gt;</description></item><item><title>iOS 26 .glassEffect() 실전 가이드 — content엔 절대 바르지 말 것</title><link>https://jun7680.github.io/p/ios-26-liquid-glass-glasseffect-guide/</link><pubDate>Mon, 18 May 2026 08:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/ios-26-liquid-glass-glasseffect-guide/</guid><description>&lt;p&gt;WWDC25 보고 제일 먼저 만져본 게 Liquid Glass다. 처음엔 그냥 블러 modifier 하나 추가된 줄 알았는데, 막상 이것저것 발라보니 &amp;ldquo;한 줄 API&amp;quot;보다 &amp;ldquo;어디에 바르냐&amp;quot;가 훨씬 중요한 기능이었다. iOS 7 이래 가장 큰 디자인 변화라는 말이 괜히 나온 게 아니다.&lt;/p&gt;</description></item><item><title>Firebase RemoteConfig 빼고 GitHub Actions + 정적 JSON으로 갈아탄 후기</title><link>https://jun7680.github.io/p/firebase-remoteconfig-to-static-json-migration/</link><pubDate>Fri, 15 May 2026 15:40:00 +0900</pubDate><guid>https://jun7680.github.io/p/firebase-remoteconfig-to-static-json-migration/</guid><description>&lt;p&gt;iOS 앱에서 버전 강제 업데이트, 공지 팝업, 외부 링크 URL, feature flag 4가지를 Firebase RemoteConfig로 관리하고 있었다. 어느 날 실험실(Lab) 기능 토글을 하나 더 얹어야 했고, 그 김에 RemoteConfig 자체를 걷어내기로 했다. 결과적으로 &lt;strong&gt;정적 서버에 JSON 한 파일을 굽고 &lt;code&gt;URLSession&lt;/code&gt;으로 GET 하는 구조&lt;/strong&gt;가 우리 케이스엔 모든 면에서 나았다.&lt;/p&gt;</description></item><item><title>SwiftUI에서 PreferenceKey가 안 먹어서 onGeometryChange로 갈아탄 디버깅</title><link>https://jun7680.github.io/p/swiftui-scroll-navigation-title-ongeometrychange/</link><pubDate>Fri, 15 May 2026 09:50:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-scroll-navigation-title-ongeometrychange/</guid><description>&lt;p&gt;SwiftUI 화면에서 본문 헤더가 스크롤로 가려질 때 같은 제목을 네비게이션 중앙 타이틀로 자연스럽게 띄우고 싶었다. 단순한 토글이 아니라 &lt;strong&gt;헤더가 가려지는 정도에 비례해서 opacity가 점점 진해지는&lt;/strong&gt; 동작이 필요했다. 별것 아닌 줄 알았는데&amp;hellip; 한참 헤맸다.&lt;/p&gt;</description></item><item><title>SwiftUI에서 NantesLabel 살리기</title><link>https://jun7680.github.io/p/swiftui-nanteslabel-uiviewrepresentable/</link><pubDate>Thu, 14 May 2026 16:26:21 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-nanteslabel-uiviewrepresentable/</guid><description>&lt;p&gt;코멘트 영역을 SwiftUI로 짜고 있었다. 요구사항은 셋이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;www 링크를 자동 탐지해서 파란색으로 표시하고 탭하면 브라우저를 연다&lt;/li&gt;
&lt;li&gt;4줄까지 노출하고 그 이상은 말줄임 + &amp;ldquo;더 보기&amp;rdquo; 토글&lt;/li&gt;
&lt;li&gt;다이나믹 타입 대응&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;SwiftUI &lt;code&gt;Text&lt;/code&gt;로는 셋 다 안 된다. &lt;code&gt;Text&lt;/code&gt; 자체에 자동 링크 탐지가 없고, 줄 수 제한과 &amp;ldquo;더 보기&amp;rdquo; 토글을 같이 처리하기도 빈약하다. 기존 UIKit 화면에서는 &lt;code&gt;NantesLabel&lt;/code&gt; 라이브러리로 다 해결돼 있었다.&lt;/p&gt;</description></item><item><title>`.ignoresSafeArea(.bottom)` 한 줄에 액션 버튼이 통째로 사라졌다</title><link>https://jun7680.github.io/p/swiftui-ignoressafearea-bottom-overlay-trap/</link><pubDate>Thu, 14 May 2026 16:26:20 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-ignoressafearea-bottom-overlay-trap/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;새 SwiftUI 화면을 만들고 있었다. 배경색이 화면 최하단(home indicator 영역)까지 깔리게 하고 싶어서 본체 View에 한 줄을 추가했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ignoresSafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;ScrollView 위에 액션 버튼을 &lt;code&gt;.overlay(alignment: .bottom)&lt;/code&gt;으로 띄워둔 화면이었다. 시뮬레이터를 띄워보니 액션 버튼이 home indicator 위까지 그려져 있었다. 탭은 되는데 디자인이 어긋났다.&lt;/p&gt;</description></item><item><title>Bool 한 개에 결정 세 개 묶었을 때</title><link>https://jun7680.github.io/p/swiftui-shared-component-bool-option-splitting/</link><pubDate>Thu, 14 May 2026 16:26:18 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-shared-component-bool-option-splitting/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;공용 SwiftUI 업로드 섹션 컴포넌트가 있다. 처음 만들 때 화면 두 곳에서 썼다. 둘 다 디바이더가 같은 자리에 있었다. &lt;code&gt;showsDividers: Bool = true&lt;/code&gt; 옵션 하나 만들고 끝났다.&lt;/p&gt;
&lt;p&gt;새 화면 두 곳이 더 들어왔다. 이제 디바이더 정책이 모두 다르다. 옵션 하나로는 표현이 안 된다.&lt;/p&gt;</description></item><item><title>Coordinator 시그니처 안 깨고 UIKit MVVM+C → SwiftUI MVI 갈아끼우기</title><link>https://jun7680.github.io/p/uikit-mvvmc-to-swiftui-mvi-keep-coordinator-signature/</link><pubDate>Thu, 14 May 2026 16:26:17 +0900</pubDate><guid>https://jun7680.github.io/p/uikit-mvvmc-to-swiftui-mvi-keep-coordinator-signature/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;UIKit으로 만들어 둔 상세 화면 하나를 SwiftUI로 통째로 옮기는 작업을 받았다. 디자인도 바뀌고, 상태 관리도 MVVM에서 MVI(Store 패턴)로 바뀐다. 화면 내부는 거의 새로 짜는 수준이다. 환경은 Xcode 26.4 / iOS 17+.&lt;/p&gt;</description></item><item><title>`final class` 상속 막혔을 때 컴포지션 써본 후기</title><link>https://jun7680.github.io/p/final-class-inheritance-vs-composition/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/final-class-inheritance-vs-composition/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;한 화면에 첨부 파일 기능을 새로 붙여야 했다. 다른 화면들에서 이미 잘 굴러가던 &lt;code&gt;BaseUploadViewModel&lt;/code&gt;이 있었다. 정책 검증, 사진/파일 선택, 중복 체크 전부 들어 있는 친구.&lt;/p&gt;</description></item><item><title>Swift 프로토콜에 옵셔널 property 넣을 뻔한 이야기</title><link>https://jun7680.github.io/p/swift-protocol-optional-property-trap/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/swift-protocol-optional-property-trap/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;프로토콜 하나 추가하다가 컴파일러가 손사래쳤다. 한 줄짜리 일이라고 생각했는데 그 아래에 설계 얘기가 깔려 있었다.&lt;/p&gt;
&lt;p&gt;폼 화면이 여러 종류 있어서 공통 프로토콜로 묶고 싶었다. 일부 폼은 첨부 섹션이 있고 일부는 없어서 그냥 옵셔널로 적어 두면 되겠지 했다.&lt;/p&gt;</description></item><item><title>SwiftUI 펼침/접힘 애니메이션에 대한 나의 생각</title><link>https://jun7680.github.io/p/swiftui-uihostingcontroller-animation-jank/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/swiftui-uihostingcontroller-animation-jank/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;UIKit 화면 안에 SwiftUI로 만든 첨부 섹션을 붙였다. &lt;code&gt;UIHostingController&lt;/code&gt;로 임베드하고, 첨부 리스트를 더보기/접기 토글로 펼쳤다 접도록 만들었다. 디자인도 평범하고 기능도 멀쩡한데, 딱 하나가 거슬렸다. 토글이 자연스럽지가 않았다.&lt;/p&gt;</description></item><item><title>공용 SwiftUI 컴포넌트의 마진, 누구 책임이지?</title><link>https://jun7680.github.io/p/swiftui-shared-component-margin-policy/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/swiftui-shared-component-margin-policy/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;디자인 검토에서 한 마디 들었다. &amp;ldquo;첨부 섹션만 안쪽으로 더 들어가 있는데요?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;다른 폼 컴포넌트는 좌우 20pt 들여쓰기인데, 첨부 섹션만 두 배쯤 들어가 보였다. 화면 캡처에 자를 대보니 정말 정확히 두 배, 40pt. 원인은 단순했는데, 단순해서 더 자주 일어나는 종류였다.&lt;/p&gt;</description></item><item><title>서버 스펙 안 나왔을 때 클라 먼저 작업하는 법</title><link>https://jun7680.github.io/p/client-work-before-server-spec/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/client-work-before-server-spec/</guid><description>&lt;h2 id="도입"&gt;도입&lt;/h2&gt;
&lt;p&gt;기획에서 새 화면에 첨부 파일을 붙이라고 했다. 백엔드 쪽은 &amp;ldquo;API는 다음 스프린트에&amp;quot;라는 답. 클라 입장에선 기다리거나, 일단 만들거나 둘 중 하나다.&lt;/p&gt;</description></item><item><title>GCD 시리얼 큐로 로그 라이터를 재작성한 이유 — actor + AsyncStream을 버린 결정</title><link>https://jun7680.github.io/p/gcd-serial-queue-over-actor-for-thread-confined/</link><pubDate>Wed, 13 May 2026 15:46:00 +0900</pubDate><guid>https://jun7680.github.io/p/gcd-serial-queue-over-actor-for-thread-confined/</guid><description>&lt;p&gt;크래시 리포트에 이런 시그니처가 떴다. 보자마자 빡쳤다 ㅋㅋ.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;EXC_BREAKPOINT in:
closure #1 in static Log.saveLogData(message:)
└─ swift_allocObject
 └─ _xzm_xzone_malloc_freelist_outlined
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;힙 corruption 패턴. 메모리 할당 단에서 깨지면서 SIGTRAP. 빈도는 낮은데 일관되게 나옴.&lt;/p&gt;
&lt;p&gt;스택 따라가서 &lt;code&gt;Log.saveLogData&lt;/code&gt;로 갔다. 호출처를 grep해보니 &lt;strong&gt;306곳&lt;/strong&gt;. 모든 곳에서 비동기로 떨어지는 진단 로그가 단일 라이터에 몰리는 구조였다.&lt;/p&gt;</description></item><item><title>47개 @Model 중 4개만 보존하기 — default-safe 정책 설계</title><link>https://jun7680.github.io/p/swiftdata-default-safe-delete-policy/</link><pubDate>Wed, 13 May 2026 15:44:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftdata-default-safe-delete-policy/</guid><description>&lt;p&gt;SwiftData 마이그하면서 풀어야 하는 흔한 문제 하나. 로그아웃 시 데이터 어떻게 정리할 거냐.&lt;/p&gt;
&lt;ul&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;/ul&gt;
&lt;p&gt;총 47개 &lt;code&gt;@Model&lt;/code&gt; 중 4개만 보존, 43개는 삭제. 이걸 어떻게 표현할 거냐? 코드에 들어가는 한 줄, 한 변수의 위치가 안전성을 바꾸는 케이스다.&lt;/p&gt;</description></item><item><title>SwiftData #Predicate에서 enum 비교가 0건을 반환하는 이유</title><link>https://jun7680.github.io/p/swiftdata-predicate-enum-silent-zero/</link><pubDate>Wed, 13 May 2026 15:42:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftdata-predicate-enum-silent-zero/</guid><description>&lt;p&gt;SwiftData 마이그 중에 디버깅 제일 빡셌던 게 &amp;ldquo;에러는 안 나는데 결과가 빈&amp;rdquo; 케이스였다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&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;let&lt;/span&gt; &lt;span class="nv"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ItemScopeType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inbox&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;let&lt;/span&gt; &lt;span class="nv"&gt;descriptor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FetchDescriptor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ItemEntity2&lt;/span&gt;&lt;span class="p"&gt;&amp;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;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;Predicate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;scope&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;let&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descriptor&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;// results.count == 0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DB엔 분명히 &lt;code&gt;scope = .inbox&lt;/code&gt;인 row가 100건 들어있다. 근데 predicate는 0건 반환. throw도 없고, 로그도 없음.&lt;/p&gt;</description></item><item><title>@Model 스캐폴드 id + .unique 제약 = 조용한 데이터 손실</title><link>https://jun7680.github.io/p/swiftdata-invalidint-unique-silent-loss/</link><pubDate>Wed, 13 May 2026 15:40:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftdata-invalidint-unique-silent-loss/</guid><description>&lt;p&gt;SwiftData 마이그 중에 본문 데이터가 한 건만 남고 나머지는 사라지는 버그를 잡고 있었다. API 응답엔 100건이 오는데 DB엔 1건. 더 황당한 건 — 에러가 단 한 줄도 안 뜸.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;try? context.save()&lt;/code&gt;가 모든 걸 삼키고 있었다 ㅋㅋ.&lt;/p&gt;</description></item><item><title>Swift String.hashValue로 DB PK를 만들면 안 되는 이유</title><link>https://jun7680.github.io/p/swift-string-hashvalue-pk-trap/</link><pubDate>Wed, 13 May 2026 15:35:00 +0900</pubDate><guid>https://jun7680.github.io/p/swift-string-hashvalue-pk-trap/</guid><description>&lt;p&gt;SwiftData 마이그 중에 상세 화면이 자꾸 &amp;ldquo;데이터 로드 실패&amp;quot;로 떨어지는 버그를 잡고 있었다. 리스트엔 아이템이 멀쩡히 보이는데 탭만 하면 바로 &lt;code&gt;dataNotFound&lt;/code&gt;. 더 빡친 건 앱 재시작할수록 실패하는 아이템이 점점 늘어남.&lt;/p&gt;</description></item><item><title>Swift 6에서 매일 만나는 변화 7가지 — strict concurrency부터 typed throws까지</title><link>https://jun7680.github.io/p/swift-6-language-changes/</link><pubDate>Wed, 13 May 2026 15:25:00 +0900</pubDate><guid>https://jun7680.github.io/p/swift-6-language-changes/</guid><description>&lt;p&gt;Swift 6 풀린 지 한참 됐는데 5.10에서 곧장 6으로 점프하면 컴파일 에러가 줄줄이 뜨는 게 정상임. 가장 큰 이유는 &lt;strong&gt;strict concurrency 검사가 기본 켜진&lt;/strong&gt; 것. 그 외에도 매일 만지는 코드에 영향 큰 변화가 꽤 있어서 한 번 정리해둔다.&lt;/p&gt;</description></item><item><title>AI가 짠 코드 PR 리뷰에서 AI한테 회신 떠넘기기</title><link>https://jun7680.github.io/p/ai-pr-review-handoff/</link><pubDate>Wed, 13 May 2026 14:25:00 +0900</pubDate><guid>https://jun7680.github.io/p/ai-pr-review-handoff/</guid><description>&lt;p&gt;PR에 코멘트를 달았다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;options&lt;/code&gt; 변수 재할당되는 곳이 없는 거 같은데, &lt;code&gt;let&lt;/code&gt;으로 둬도 될 것 같아요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;답이 왔다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;해당 메서드가 &lt;code&gt;mutating&lt;/code&gt;으로 선언돼서 &lt;code&gt;var&lt;/code&gt;가 필요합니다. &lt;code&gt;let&lt;/code&gt;으로 바꾸면 빌드가 실패합니다.&lt;/p&gt;</description></item><item><title>Swift mutating인데 self를 안 바꾸는 함수 — 컴파일 통과하는 시그니처의 함정</title><link>https://jun7680.github.io/p/swift-mutating-without-self-mutation/</link><pubDate>Wed, 13 May 2026 14:20:00 +0900</pubDate><guid>https://jun7680.github.io/p/swift-mutating-without-self-mutation/</guid><description>&lt;p&gt;PR 리뷰에서 이런 코드를 봤다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&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;public&lt;/span&gt; &lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;UserPreferences&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;public&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;isEnabledAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;isEnabledFeatureA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ... 20개 더 ...&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;public&lt;/span&gt; &lt;span class="kr"&gt;mutating&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updated&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;isEnabledAll&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 class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&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;isEnabledFeatureA&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 class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&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;// ... 동일하게 22개 ...&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="kc"&gt;Self&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;return&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;init&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;isEnabledAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isEnabledAll&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;isEnabledAll&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;isEnabledFeatureA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isEnabledFeatureA&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;isEnabledFeatureA&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;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;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;호출부.&lt;/p&gt;</description></item><item><title>isPush 플래그가 만드는 Coordinator 흐름 분기 지옥</title><link>https://jun7680.github.io/p/coordinator-ispush-flag-trap/</link><pubDate>Wed, 13 May 2026 14:15:00 +0900</pubDate><guid>https://jun7680.github.io/p/coordinator-ispush-flag-trap/</guid><description>&lt;p&gt;어느 날 푸시 알림으로 특정 화면 들어가서 작업 완료하면 본문에 그대로 머무는 버그를 받았다. 일반 진입(리스트 → 상세 → 작업)에선 정상 동작, &lt;strong&gt;푸시 진입에서만&lt;/strong&gt; 작업 후 리스트로 안 돌아옴. 한참 헤매다 보니 Coordinator 패턴에서 흔히 만들어지는 &amp;ldquo;진입 경로별 분기 트랩&amp;quot;이었음. 정리해둔다.&lt;/p&gt;</description></item><item><title>UIKit ↔ SwiftUI 브릿징 — UIViewRepresentable vs UIHostingController 언제 뭘 쓸지</title><link>https://jun7680.github.io/p/uikit-swiftui-bridging/</link><pubDate>Wed, 13 May 2026 11:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/uikit-swiftui-bridging/</guid><description>&lt;p&gt;iOS 앱에서 UIKit과 SwiftUI를 한 번에 다 들어내는 마이그레이션은 거의 없다. 새 화면은 SwiftUI로 짜지만 기존 UIKit 컴포넌트가 한참 남아 있고, 또 반대로 SwiftUI 화면 안에 PDFView 같은 UIKit 전용 컨트롤을 넣어야 하는 경우도 있다. 그래서 두 프레임워크 사이를 잇는 다리 네 가지는 거의 모든 프로젝트에서 한 번씩 만난다. 어떤 상황에 어떤 걸 골라야 하는지 한 번 정리해 둔다.&lt;/p&gt;</description></item><item><title>UICollectionView CellRegistration — 옛날 register/dequeue 그만 쓰자</title><link>https://jun7680.github.io/p/uikit-cell-registration/</link><pubDate>Tue, 12 May 2026 13:30:00 +0900</pubDate><guid>https://jun7680.github.io/p/uikit-cell-registration/</guid><description>&lt;p&gt;iOS 14에서 들어온 API인데 의외로 잘 안 알려진 쪽. 옛날 &lt;code&gt;register(_:forCellWithReuseIdentifier:)&lt;/code&gt; + &lt;code&gt;dequeueReusableCell(withReuseIdentifier:)&lt;/code&gt; 패턴을 쓰지 않게 된다. 처음 보면 살짝 낯설 수 있는데, 한 번 잡아두면 옛날 패턴으로 안 돌아감.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jun7680.github.io/p/uikit-diffable-datasource-memo/"&gt;DiffableDataSource&lt;/a&gt; 자료에 같이 묶여 나오는 경우가 많아서 &amp;ldquo;DiffableDataSource 쓸 때만 쓰는 거&amp;rdquo; 같이 보이는데, 사실 &lt;strong&gt;단독으로도 그냥 쓸 수 있다&lt;/strong&gt;. 옛날 &lt;code&gt;UICollectionViewDataSource&lt;/code&gt; 그대로 가면서 cell 등록만 갈아도 ID 관리가 사라져서 깔끔해짐.&lt;/p&gt;</description></item><item><title>UICollectionViewDiffableDataSource 쓰면서 정리한 메모</title><link>https://jun7680.github.io/p/uikit-diffable-datasource-memo/</link><pubDate>Tue, 12 May 2026 13:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/uikit-diffable-datasource-memo/</guid><description>&lt;p&gt;&lt;code&gt;UICollectionViewDiffableDataSource&lt;/code&gt;는 iOS 13에서 들어온 데이터 소스 패턴. 기존 &lt;code&gt;cellForItemAt&lt;/code&gt; + &lt;code&gt;numberOfItems&lt;/code&gt; + &lt;code&gt;performBatchUpdates&lt;/code&gt; 묶음을 &lt;strong&gt;snapshot 한 번 적용&lt;/strong&gt;으로 갈음한다. 옛날 코드에서 옮기면서 정리한 메모.&lt;/p&gt;</description></item><item><title>NavigationView에서 NavigationStack으로 옮긴 메모</title><link>https://jun7680.github.io/p/navigationview-to-navigationstack-migration/</link><pubDate>Tue, 12 May 2026 12:30:00 +0900</pubDate><guid>https://jun7680.github.io/p/navigationview-to-navigationstack-migration/</guid><description>&lt;p&gt;NavigationView가 iOS 16에서 deprecated된 지 한참 됐는데, 콘솔 경고 한 줄만 무시하면서 끌고 왔다. SwiftData 마이그하면서 어차피 화면들 다시 만질 일 생긴 김에 같이 옮겼다.&lt;/p&gt;</description></item><item><title>withAnimation, .animation, .transition 헷갈릴 때 보는 메모</title><link>https://jun7680.github.io/p/swiftui-animation-modifier-vs-withanimation/</link><pubDate>Tue, 12 May 2026 12:30:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-animation-modifier-vs-withanimation/</guid><description>&lt;p&gt;SwiftUI 애니메이션 처음 만지면 &lt;code&gt;withAnimation&lt;/code&gt;이랑 &lt;code&gt;.animation&lt;/code&gt; modifier 둘 중에 뭘 써야 하는지가 가장 헷갈린다. 한 번 잡아두면 다 비슷한 구조라 편한데, 그 전까지는 적용했는데 애니메이션이 안 먹히는 케이스가 자꾸 나옴.&lt;/p&gt;</description></item><item><title>SwiftUI List 스크롤 끊길 때 점검하는 메모</title><link>https://jun7680.github.io/p/swiftui-list-lazyvstack-performance/</link><pubDate>Tue, 12 May 2026 12:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/swiftui-list-lazyvstack-performance/</guid><description>&lt;p&gt;SwiftUI에서 List나 LazyVStack에 row 100개 넣었더니 스크롤이 뚝뚝 끊기는 게 보인다. UIKit &lt;code&gt;UITableView&lt;/code&gt;/&lt;code&gt;UICollectionView&lt;/code&gt;보다 손이 덜 가는 대신 안 보이는 함정이 좀 있다. 디버깅하면서 매번 같은 패턴이 나와서 메모로 남긴다.&lt;/p&gt;</description></item><item><title>Realm에서 SwiftData로: 42개 모델을 옮기는 전략과 함정 (Part 6 — View 다중 mount 디버깅)</title><link>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-6-view-multi-mount-debugging/</link><pubDate>Tue, 12 May 2026 11:25:00 +0900</pubDate><guid>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-6-view-multi-mount-debugging/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — 마이그레이션 후 리스트 진입 시 스켈레톤이 두 번 깜빡이는 회귀가 보였다. 가설 5개를 짜서 누적 패치를 시도했는데 진짜 원인은 그 5개에 없었다. &lt;code&gt;ObjectIdentifier(self).hashValue&lt;/code&gt; 진단 로그 한 줄로 &lt;code&gt;NoteListStore&lt;/code&gt;가 &lt;strong&gt;3번 init&lt;/strong&gt;되는 걸 5분 만에 확인. 원인은 &lt;code&gt;subscribeAccount&lt;/code&gt;가 &lt;code&gt;loadFirstFolder()&lt;/code&gt; 결과로 받아온 &lt;code&gt;.invalidInt(-1)&lt;/code&gt; sentinel을 첫 emit으로 흘렸고, 기존 가드 &lt;code&gt;1 != -1&lt;/code&gt;이 그대로 통과하면서 가짜 folder로 attach가 한 번 더 일어났던 것. &lt;code&gt;accountFirstFolder.folderID.isValid&lt;/code&gt; 가드 한 줄 + attach 책임 단일화 + &lt;code&gt;attachedFolderID&lt;/code&gt; latch 3중으로 구조적으로 막았다. 총 6편 시리즈의 &lt;strong&gt;마지막, 6편(View 다중 mount 디버깅 편)&lt;/strong&gt;.&lt;/p&gt;</description></item><item><title>iOS 17 이후 @StateObject는 거의 안 쓰게 됐다</title><link>https://jun7680.github.io/p/state-stateobject-observedobject-observable-ios17/</link><pubDate>Tue, 12 May 2026 11:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/state-stateobject-observedobject-observable-ios17/</guid><description>&lt;p&gt;iOS 17 이전에는 SwiftUI 상태 관리할 때 &lt;code&gt;@StateObject&lt;/code&gt;랑 &lt;code&gt;@ObservedObject&lt;/code&gt; 중에 뭘 써야 하는지가 진짜 헷갈렸다. 둘 다 ViewModel 들고 다닐 때 쓰는데 동작이 미묘하게 달라서, 잘못 쓰면 자식 화면이 부모 리렌더할 때마다 상태 리셋된다. 17부터 &lt;a href="https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro"&gt;Observation 매크로&lt;/a&gt;(&lt;code&gt;@Observable&lt;/code&gt;) 들어오면서 이 고민 자체가 그냥 없어짐.&lt;/p&gt;</description></item><item><title>Realm에서 SwiftData로: 42개 모델을 옮기는 전략과 함정 (Part 5 — Async/Await 통합)</title><link>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-5-async-await-integration/</link><pubDate>Tue, 12 May 2026 10:30:00 +0900</pubDate><guid>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-5-async-await-integration/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — 동기 메서드 안의 &lt;code&gt;Task { await ... }&lt;/code&gt; 는 호출자가 sync 완료를 알 수 없는 fire-and-forget이다. SwiftData 같은 비동기 저장소를 이걸로 갱신하고 바로 query를 돌리면 옛 캐시가 먼저 도착해서 skeleton 해제 직후 EmptyView가 한 프레임 깜빡이는 race가 생긴다. 해결은 &lt;code&gt;async&lt;/code&gt; 시그니처를 호출 체인 위로 정직하게 전파하고 &lt;code&gt;await&lt;/code&gt;로 완료를 보장하는 것. 그 과정에서 &lt;code&gt;async throws&lt;/code&gt; 오버로드 자동 선택 함정, &lt;code&gt;Task&lt;/code&gt; vs &lt;code&gt;Task.detached&lt;/code&gt;, Combine sink 안의 &lt;code&gt;Task&lt;/code&gt; cancellation, Combine ↔ async 다리 패턴까지 같이 정리. 총 6편 시리즈의 &lt;strong&gt;5편(Async/Await 통합 편)&lt;/strong&gt;.&lt;/p&gt;</description></item><item><title>Realm에서 SwiftData로: 42개 모델을 옮기는 전략과 함정 (Part 4 — Combine 통합)</title><link>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-4-combine-integration/</link><pubDate>Tue, 12 May 2026 09:40:00 +0900</pubDate><guid>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-4-combine-integration/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — SwiftData에는 Realm &lt;code&gt;Results&lt;/code&gt; 같은 &lt;strong&gt;자동 Combine publisher&lt;/strong&gt; 가 없다. SwiftUI &lt;code&gt;@Query&lt;/code&gt; 한 줄을 빼면 UIKit/Combine/Observation 어느 쪽에서도 컬렉션 단위 자동 이벤트는 빌트인이 아니다. &lt;code&gt;Just(snapshot) + NotificationCenter.publisher&lt;/code&gt;를 &lt;code&gt;.merge&lt;/code&gt;하는 패턴 하나로 Realm 시절 흐름이 1:1 복원된다. 다만 &lt;strong&gt;&lt;code&gt;Just&lt;/code&gt; 첫 emit이 즉시 동기로 흘러든다&lt;/strong&gt;는 차이를 모르면 hideSkeleton race가 생긴다. 이벤트 책임이 우리 쪽으로 넘어왔다는 건 누락·과다·스레드 미스매치 같은 새 함정 4종이 따라붙는다는 뜻. 각각의 가드 패턴까지 정리한다. 총 6편 시리즈의 &lt;strong&gt;4편(Combine 통합 편)&lt;/strong&gt;.&lt;/p&gt;</description></item><item><title>Realm에서 SwiftData로: 42개 모델을 옮기는 전략과 함정 (Part 3 — DAO 패턴)</title><link>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-3-dao-pattern/</link><pubDate>Tue, 12 May 2026 08:50:00 +0900</pubDate><guid>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-3-dao-pattern/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — SwiftData DAO를 &lt;code&gt;class&lt;/code&gt; 인스턴스로 만들면 &lt;code&gt;ModelContext&lt;/code&gt;를 어디서 들고 있을지 결정이 도메인 곳곳으로 분산되고, 결국 같은 row를 다른 컨텍스트로 건드리는 race가 생긴다. &lt;strong&gt;DAO는 &lt;code&gt;enum&lt;/code&gt; + &lt;code&gt;static&lt;/code&gt; 메서드, &lt;code&gt;ModelContext&lt;/code&gt; 접근은 단일 진입점&lt;/strong&gt;(&lt;code&gt;AppStore.shared.mainContext&lt;/code&gt;)으로 강제한다. &lt;code&gt;autosaveEnabled = false&lt;/code&gt;로 두고 DAO에서 명시적 &lt;code&gt;save()&lt;/code&gt; + 변경 알림을 직접 발사한다. Realm 시절 &lt;code&gt;try! Realm()&lt;/code&gt; + &lt;code&gt;.filter&lt;/code&gt; + &lt;code&gt;.add(update: .modified)&lt;/code&gt; 3종 세트가 어떻게 enum DAO로 1:1 매핑되는지까지 정리한다. 총 6편 시리즈의 &lt;strong&gt;3편(DAO 패턴 편)&lt;/strong&gt;.&lt;/p&gt;</description></item><item><title>Realm에서 SwiftData로: 42개 모델을 옮기는 전략과 함정 (Part 2 — Primary Key 함정 3가지)</title><link>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-2-primary-key-traps/</link><pubDate>Mon, 11 May 2026 11:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-2-primary-key-traps/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — SwiftData &lt;code&gt;@Attribute(.unique)&lt;/code&gt; Primary Key를 쓰다 silent로 무너지는 3가지를 만났다. ① &lt;code&gt;.invalidInt&lt;/code&gt; 같은 sentinel을 PK 기본값으로 두면 row 충돌이 난다. ② &lt;code&gt;String.hashValue&lt;/code&gt;는 프로세스 재시작마다 시드가 랜덤이라 PK가 안 맞는다. ③ &lt;code&gt;#Predicate&lt;/code&gt; 안에서 enum 비교는 silent fail 한다. &lt;strong&gt;결정적 해시(Boost &lt;code&gt;hash_combine&lt;/code&gt; 포팅) + rawValue 컬럼 분리 + lookup-then-upsert&lt;/strong&gt;로 막았다. 총 6편 시리즈의 &lt;strong&gt;2편(Primary Key 함정 편)&lt;/strong&gt;.&lt;/p&gt;</description></item><item><title>Realm에서 SwiftData로: 42개 모델을 옮기는 전략과 함정 (Part 1 — 전략편)</title><link>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-1-strategy/</link><pubDate>Mon, 11 May 2026 10:00:00 +0900</pubDate><guid>https://jun7680.github.io/p/realm-to-swiftdata-migration-part-1-strategy/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — iOS 앱(데이터 모델 42개, 수만~십만 단위 row 운영) Realm을 SwiftData로 옮기기로 했다. &lt;strong&gt;iOS 17 최소 지원 + 일괄 마이그레이션 + 사용자 데이터는 다음 sync에서 자연 복원&lt;/strong&gt; 전략. 이 글은 그 결정의 배경과, 마이그레이션 도는 동안 UX를 어떻게 처리했는지를 정리한다. 총 6편 시리즈의 &lt;strong&gt;1편(전략편)&lt;/strong&gt;.&lt;/p&gt;</description></item><item><title>Swift 5.7에서 매일 쓰는 개선 7가지</title><link>https://jun7680.github.io/p/swift-5.7%EC%97%90%EC%84%9C-%EB%A7%A4%EC%9D%BC-%EC%93%B0%EB%8A%94-%EA%B0%9C%EC%84%A0-7%EA%B0%80%EC%A7%80/</link><pubDate>Wed, 15 Jun 2022 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/swift-5.7%EC%97%90%EC%84%9C-%EB%A7%A4%EC%9D%BC-%EC%93%B0%EB%8A%94-%EA%B0%9C%EC%84%A0-7%EA%B0%80%EC%A7%80/</guid><description>&lt;p&gt;WWDC 2022에서 Swift 5.7이 같이 풀렸다. 보통은 이런 마이너 버전에 큰 변화가 없는데, 5.7은 일상 코드에 영향을 주는 변경이 꽤 많아서 한 번 정리해 둔다. 다 적으면 너무 길어지니까 매일 쓰게 되는 7가지만 추렸다.&lt;/p&gt;</description></item><item><title>UIStackView isHidden이 누적되는 동작과 안전한 토글 패턴</title><link>https://jun7680.github.io/p/uistackview-ishidden%EC%9D%B4-%EB%88%84%EC%A0%81%EB%90%98%EB%8A%94-%EB%8F%99%EC%9E%91%EA%B3%BC-%EC%95%88%EC%A0%84%ED%95%9C-%ED%86%A0%EA%B8%80-%ED%8C%A8%ED%84%B4/</link><pubDate>Wed, 13 Apr 2022 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/uistackview-ishidden%EC%9D%B4-%EB%88%84%EC%A0%81%EB%90%98%EB%8A%94-%EB%8F%99%EC%9E%91%EA%B3%BC-%EC%95%88%EC%A0%84%ED%95%9C-%ED%86%A0%EA%B8%80-%ED%8C%A8%ED%84%B4/</guid><description>&lt;p&gt;UIKit으로 토글 가능한 패널을 만들 때 가장 흔히 쓰는 건 &lt;code&gt;UIStackView&lt;/code&gt; 안의 view에 &lt;code&gt;isHidden&lt;/code&gt;을 적용하는 방식이다. 그런데 이걸 애니메이션 블록 안에서 토글하다 보면, &lt;strong&gt;버튼을 다섯 번 누르면 다섯 번 다시 눌러야 보이는&lt;/strong&gt; 이상한 현상에 부딪힌다. 코드는 분명 한 줄인데 동작은 그렇지 않다. 처음 봤을 때는 디버그로 한참을 헤맸다.&lt;/p&gt;</description></item><item><title>iOS Fastlane을 적용해보자</title><link>https://jun7680.github.io/p/ios-fastlane%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90/</link><pubDate>Thu, 03 Mar 2022 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/ios-fastlane%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90/</guid><description>&lt;h3 id="도입이유"&gt;도입이유&lt;/h3&gt;
&lt;p&gt;친구랑 얘기하다 원시인 소리를 들었다&amp;hellip;😀&lt;/p&gt;
&lt;h5 id="현-배포-절차"&gt;현 배포 절차&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;클린 -&amp;gt; Product -&amp;gt; Archive -&amp;gt; 완료 콜백 올때까지 대기 -&amp;gt; 업로드 &lt;del&gt;(대략 30분&amp;hellip;?)&lt;/del&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id="도입-후-절차"&gt;도입 후 절차&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;fastlane &amp;rsquo;lane명&amp;rsquo; 끗!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자동배포라는 말만 들을때는 실수하면 어쩌지..? 세팅이 어려우면 어쩌지?? 이러한 고민이 있었지만 막상 도입 마음을 먹고 차분한 마음을 가지고 차근차근 따라하다 보니 나도 원시인 탈출을 할 수 있었다&lt;/p&gt;</description></item><item><title>GCD에서 Swift Concurrency로 — 콜백 지옥을 async/await로 정리하기</title><link>https://jun7680.github.io/p/gcd%EC%97%90%EC%84%9C-swift-concurrency%EB%A1%9C-%EC%BD%9C%EB%B0%B1-%EC%A7%80%EC%98%A5%EC%9D%84-async/await%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0/</link><pubDate>Tue, 11 Jan 2022 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/gcd%EC%97%90%EC%84%9C-swift-concurrency%EB%A1%9C-%EC%BD%9C%EB%B0%B1-%EC%A7%80%EC%98%A5%EC%9D%84-async/await%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0/</guid><description>&lt;p&gt;iOS 개발에서 비동기 처리는 오랫동안 GCD(Grand Central Dispatch)가 도맡았다. 2021년 Swift Concurrency가 나오면서 async/await로 옮겨가는 분위기인데, 둘이 어떻게 다른지 그리고 GCD 코드를 들어내면서 만나는 함정을 한 번 정리해뒀다.&lt;/p&gt;</description></item><item><title>iOS에 FCM 붙이기 — 인증 키부터 토큰 흐름, 그리고 자주 막히는 지점</title><link>https://jun7680.github.io/p/ios%EC%97%90-fcm-%EB%B6%99%EC%9D%B4%EA%B8%B0-%EC%9D%B8%EC%A6%9D-%ED%82%A4%EB%B6%80%ED%84%B0-%ED%86%A0%ED%81%B0-%ED%9D%90%EB%A6%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%9E%90%EC%A3%BC-%EB%A7%89%ED%9E%88%EB%8A%94-%EC%A7%80%EC%A0%90/</link><pubDate>Mon, 10 Jan 2022 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/ios%EC%97%90-fcm-%EB%B6%99%EC%9D%B4%EA%B8%B0-%EC%9D%B8%EC%A6%9D-%ED%82%A4%EB%B6%80%ED%84%B0-%ED%86%A0%ED%81%B0-%ED%9D%90%EB%A6%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%9E%90%EC%A3%BC-%EB%A7%89%ED%9E%88%EB%8A%94-%EC%A7%80%EC%A0%90/</guid><description>&lt;p&gt;iOS에 푸시를 붙일 때 가장 흔히 쓰는 게 Firebase Cloud Messaging(FCM)이다. Google이 APNs 위에 한 겹 추상화를 깔아둔 형태라, 서버에서 한 가지 페이로드 포맷으로 iOS/Android/웹에 보낼 수 있어서 편하다. 다만 처음 붙일 때 인증 키, capability, 토큰 흐름 같은 게 한 번에 묶여서 어디서 막히는지 헷갈리기 쉽다. 한 번 정리해 둔다.&lt;/p&gt;</description></item><item><title>RxSwift Hot Observable vs Cold Observable — 다중 구독에서 갈리는 동작</title><link>https://jun7680.github.io/p/rxswift-hot-observable-vs-cold-observable-%EB%8B%A4%EC%A4%91-%EA%B5%AC%EB%8F%85%EC%97%90%EC%84%9C-%EA%B0%88%EB%A6%AC%EB%8A%94-%EB%8F%99%EC%9E%91/</link><pubDate>Fri, 07 Jan 2022 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/p/rxswift-hot-observable-vs-cold-observable-%EB%8B%A4%EC%A4%91-%EA%B5%AC%EB%8F%85%EC%97%90%EC%84%9C-%EA%B0%88%EB%A6%AC%EB%8A%94-%EB%8F%99%EC%9E%91/</guid><description>&lt;p&gt;RxSwift를 처음 만지면 그냥 &lt;code&gt;.subscribe { ... }&lt;/code&gt; 붙이면 흐름이 흘러서 별 문제없이 동작한다. 그런데 같은 Observable에 두 곳에서 구독을 걸면 갑자기 네트워크 요청이 두 번 나간다거나, 타이머가 한 번만 도는 게 아니라 구독자마다 따로 도는 식의 상황이 생긴다. 이 차이가 바로 Hot이냐 Cold냐의 문제다.&lt;/p&gt;</description></item><item><title>Archives</title><link>https://jun7680.github.io/archives/</link><pubDate>Tue, 28 May 2019 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/archives/</guid><description/></item><item><title>About</title><link>https://jun7680.github.io/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/about/</guid><description>&lt;p&gt;iOS 개발하면서 만진 패턴, 디버깅한 자국, 새로 배운 거를 모아두는 공간입니다.&lt;/p&gt;
&lt;h2 id="작성자"&gt;작성자&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;오깅중&lt;/strong&gt; — iOS Developer.&lt;/p&gt;
&lt;p&gt;Swift / SwiftUI / UIKit 위주로 작업하고, 최근에는 SwiftData·Swift Concurrency 마이그레이션을 굴리는 중. 글은 회고·가이드·디버깅 메모 형태로 쌓고 있어요.&lt;/p&gt;</description></item><item><title>Search</title><link>https://jun7680.github.io/search/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/search/</guid><description>&lt;p&gt;이 블로그는 헤더 우측의 검색 버튼 또는 &lt;code&gt;⌘K&lt;/code&gt;로 검색을 열 수 있습니다.&lt;/p&gt;</description></item><item><title>개인정보처리방침</title><link>https://jun7680.github.io/privacy/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://jun7680.github.io/privacy/</guid><description>&lt;p&gt;본 블로그(&lt;code&gt;jun7680.github.io&lt;/code&gt;)는 방문자의 개인정보를 안전하게 다루기 위해 다음과 같이 처리방침을 안내합니다.&lt;/p&gt;
&lt;h2 id="1-수집하는-정보"&gt;1. 수집하는 정보&lt;/h2&gt;
&lt;p&gt;본 블로그는 별도의 회원가입·로그인 기능을 제공하지 않으며, 방문자에게 개인정보를 직접 요청하지 않습니다.&lt;/p&gt;</description></item></channel></rss>