<?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/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Xcode · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/xcode/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Tue, 27 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/xcode/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Xcode로 iOS 앱 디버깅하는 방법: 브레이크포인트, LLDB, Instruments, 시뮬레이터</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2026/2026-02-26-debugging-ios-mobile-apps/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2026/2026-02-26-debugging-ios-mobile-apps/</guid>
      <pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>iOS</category>
      <category>Xcode</category>
      <category>Debugging</category>
      <category>Swift</category>
      <category>iOS Simulator</category>
      <category>Mobile Development</category>
      <description>Xcode로 iOS 앱을 디버깅하는 방법을 정리했습니다. 조건부·심볼릭 브레이크포인트, 알아 두면 좋은 LLDB 명령어, 뷰 디버거와 메모리 디버거, Instruments, sanitizer, 크래시 로그, 그리고 시뮬레이터에서는 놓치고 실기기에서만 잡히는 버그까지 다룹니다.</description>
      <content:encoded><![CDATA[<p>Xcode에서 iOS 앱을 디버깅하려면 먼저 예외 브레이크포인트를 걸어 두세요. 크래시가 나면 예외를 throw한 바로 그 줄에서 멈춥니다. 그다음 버그를 재현하고, LLDB 콘솔에서 <code>po</code>로 상태를 확인합니다. 나머지는 증상에 맞는 도구를 고르는 일입니다. 레이아웃 문제는 뷰 디버거, 누수와 느려짐은 메모리 그래프와 Instruments, 가끔씩만 나는 크래시는 Address Sanitizer와 Thread Sanitizer. 마무리는 실제 iPhone에서 합니다. 시뮬레이터에서는 아예 드러나지 않는 버그가 한 무더기 있거든요.</p>
<p>디버깅에 쓰는 시간이 정말 많습니다. 누구나 그렇죠. 요즘은 제 앱의 버그를 쫓느라 Xcode와 시뮬레이터 안에서 보내는 시간이 대부분인데, 그럴 때마다 새삼 느낍니다. Xcode에는 거의 아무도 쓰지 않는 강력한 디버깅 기능이 정말 많다는 걸요. 이 글은 오랜 시간에 걸쳐 자리 잡은 제 디버깅 워크플로우 중에서, 실제로 시간을 아껴주는 부분들을 정리한 것입니다.</p>

<h2 class="relative group">브레이크포인트는 생각보다 강력합니다
    <div id="브레이크포인트는-생각보다-강력합니다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b8%8c%eb%a0%88%ec%9d%b4%ed%81%ac%ed%8f%ac%ec%9d%b8%ed%8a%b8%eb%8a%94-%ec%83%9d%ea%b0%81%eb%b3%b4%eb%8b%a4-%ea%b0%95%eb%a0%a5%ed%95%a9%eb%8b%88%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>대부분은 거터를 클릭해 브레이크포인트를 하나 찍고 끝냅니다. 그런데 우클릭해 보면 디버깅 방식 자체를 바꿔놓을 옵션이 잔뜩 나옵니다.</p>

<h3 class="relative group">조건부 브레이크포인트
    <div id="조건부-브레이크포인트" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a1%b0%ea%b1%b4%eb%b6%80-%eb%b8%8c%eb%a0%88%ec%9d%b4%ed%81%ac%ed%8f%ac%ec%9d%b8%ed%8a%b8" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>1000개 항목을 처리하는 루프에서 347번째만 말썽이라고 해봅시다. 브레이크포인트에 346번 걸려가며 기다릴 수는 없죠.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mf">0.</span><span class="p">.&lt;</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">processUser</span><span class="p">(</span><span class="n">users</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>  <span class="c1">// 여기에 브레이크포인트 설정, 조건: i == 347</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>브레이크포인트를 우클릭해 조건에 <code>i == 347</code>을 넣으면 Xcode는 조건이 참일 때만 멈춥니다. <code>userId == &quot;abc123&quot;</code>이나 <code>error != nil</code> 같은 조건도 똑같이 됩니다.</p>

<h3 class="relative group">심볼릭 브레이크포인트
    <div id="심볼릭-브레이크포인트" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%ac%eb%b3%bc%eb%a6%ad-%eb%b8%8c%eb%a0%88%ec%9d%b4%ed%81%ac%ed%8f%ac%ec%9d%b8%ed%8a%b8" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>메서드가 어디서 호출되는지 모를 때 씁니다. 브레이크포인트 네비게이터(Cmd+8)에서 + 버튼을 누르고 &ldquo;Symbolic Breakpoint&quot;를 선택합니다.</p>
<p>뷰 컨트롤러가 로드될 때마다 전부 보고 싶다면 심볼을 <code>viewDidLoad</code>로 설정하면 됩니다. Xcode가 모든 호출에서 멈춥니다. 너무 시끄러우면 <code>MyViewController.viewDidLoad</code>처럼 좁히면 되고요.</p>
<p>제가 상시로 걸어두는 건 이 세 가지입니다.</p>
<ul>
<li><code>UIViewAlertForUnsatisfiableConstraints</code>: Auto Layout 충돌을 그 자리에서 잡아줍니다</li>
<li><code>objc_exception_throw</code>: 크래시가 나기 전에 Objective-C 예외에서 멈춥니다</li>
<li><code>malloc_error_break</code>: 메모리 할당 문제를 찾아냅니다</li>
</ul>

<h3 class="relative group">예외 브레이크포인트
    <div id="예외-브레이크포인트" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%98%88%ec%99%b8-%eb%b8%8c%eb%a0%88%ec%9d%b4%ed%81%ac%ed%8f%ac%ec%9d%b8%ed%8a%b8" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>새 프로젝트에서 가장 먼저 해야 할 설정입니다. 예외 브레이크포인트를 추가해 두면(브레이크포인트 네비게이터에서 + → Exception Breakpoint) 앱이 이미 죽은 뒤가 아니라 뭔가 throw되는 그 순간에 Xcode가 멈춥니다.</p>
<p>콘솔에 정체불명 크래시로만 찍히던 nil 언래핑 버그를 이걸로 수도 없이 잡았습니다.</p>

<h3 class="relative group">브레이크포인트 액션
    <div id="브레이크포인트-액션" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b8%8c%eb%a0%88%ec%9d%b4%ed%81%ac%ed%8f%ac%ec%9d%b8%ed%8a%b8-%ec%95%a1%ec%85%98" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>브레이크포인트가 꼭 실행을 멈춰야 하는 건 아닙니다. 브레이크포인트를 편집해서 &ldquo;Add Action&quot;을 누르면 이런 게 됩니다.</p>
<ul>
<li>멈추지 않고 변수 출력: <code>&quot;User count: @(users.count)@&quot;</code></li>
<li>LLDB 명령 자동 실행</li>
<li>셸 스크립트 실행</li>
<li>소리 재생(저는 드물게 타는 코드 경로를 확인할 때 씁니다)</li>
</ul>
<p>&ldquo;Automatically continue after evaluating actions&quot;에 체크하면, 흐름을 끊지 않는 로거가 됩니다.</p>

<h2 class="relative group">LLDB: 실제로 쓰게 되는 콘솔 명령어
    <div id="lldb-실제로-쓰게-되는-콘솔-명령어" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#lldb-%ec%8b%a4%ec%a0%9c%eb%a1%9c-%ec%93%b0%ea%b2%8c-%eb%90%98%eb%8a%94-%ec%bd%98%ec%86%94-%eb%aa%85%eb%a0%b9%ec%96%b4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>브레이크포인트에서 멈췄을 때 아래쪽 콘솔은 크래시 로그나 들여다보는 곳이 아닙니다. 그게 LLDB고, 정말 유용합니다.</p>

<h3 class="relative group">기본
    <div id="기본" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b8%b0%eb%b3%b8" aria-label="앵커">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po user
▿ User
  - id: &#34;123&#34;
  - name: &#34;John Doe&#34;
  - email: &#34;john@example.com&#34;</code></pre></div>
<p><code>po</code>는 객체를 읽기 좋은 형태로 출력합니다. 제 LLDB 사용의 90%가 이겁니다.</p>
<p>더 자세히 보고 싶으면 <code>p</code>를 씁니다.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) p user.name
(String) $R0 = &#34;John Doe&#34;</code></pre></div>
<p>로컬 변수를 한 번에 다 보려면 이렇게 하고요.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) frame variable</code></pre></div>

<h3 class="relative group">런타임에 값 바꾸기
    <div id="런타임에-값-바꾸기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%9f%b0%ed%83%80%ec%9e%84%ec%97%90-%ea%b0%92-%eb%b0%94%ea%be%b8%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>LLDB가 진짜 밥값을 하는 지점입니다. 재컴파일 없이 변수를 바꿀 수 있거든요.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) expr user.name = &#34;Jane Doe&#34;
(lldb) expr index = 0</code></pre></div>
<p>버그를 찾았는데 리빌드 없이 수정을 시험해 보고 싶다면? 변수를 바꾸고 계속 실행하면 됩니다. 엣지 케이스를 좁혀갈 때 늘 이렇게 값을 바꿔가며 확인합니다.</p>
<p>메서드 호출도 됩니다.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po self.refreshUI()
(lldb) expr navigationController?.popViewController(animated: true)</code></pre></div>

<h3 class="relative group">네비게이션
    <div id="네비게이션" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%84%a4%eb%b9%84%ea%b2%8c%ec%9d%b4%ec%85%98" aria-label="앵커">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) bt              # 콜 스택 표시
(lldb) frame select 3  # 다른 스택 프레임으로 이동
(lldb) continue        # 실행 계속 (또는 단순히 &#39;c&#39;)
(lldb) n               # 스텝 오버 (다음 줄)
(lldb) s               # 함수로 스텝 인</code></pre></div>

<h3 class="relative group">워치포인트
    <div id="워치포인트" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9b%8c%ec%b9%98%ed%8f%ac%ec%9d%b8%ed%8a%b8" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>변수가 언제 바뀌는지 알고 싶으면 워치포인트를 겁니다.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) watchpoint set variable user.isLoggedIn</code></pre></div>
<p>이제 코드 어디에서 값이 바뀌든 그 순간 실행이 멈춥니다. 예상 밖의 상태 변경을 추적할 때 이만한 게 없습니다.</p>

<h2 class="relative group">Console.app: 숨은 디버깅 도구
    <div id="consoleapp-숨은-디버깅-도구" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consoleapp-%ec%88%a8%ec%9d%80-%eb%94%94%eb%b2%84%ea%b9%85-%eb%8f%84%ea%b5%ac" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Xcode 콘솔로 충분할 때가 많지만, 시스템 로그며 크래시 리포트며 전부 봐야 할 때는 Console.app을 엽니다.</p>
<p>OSLog를 쓰고 있다면(써야 합니다) Console.app에서 로그를 검색하고 필터링할 수 있습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;networking&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Starting API request&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">&#34;Request URL: </span><span class="si">\(</span><span class="n">url</span><span class="p">.</span><span class="n">absoluteString</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">error</span><span class="p">(</span><span class="s">&#34;Failed to decode: </span><span class="si">\(</span><span class="n">error</span><span class="p">.</span><span class="n">localizedDescription</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Console.app에서는 이렇게 합니다.</p>
<ol>
<li>시뮬레이터나 연결된 기기를 선택</li>
<li>서브시스템으로 필터: <code>subsystem:com.example.app</code></li>
<li>레벨로 필터: <code>subsystem:com.example.app AND level:error</code></li>
</ol>
<p>자주 쓰는 필터 술어는 저장해둘 수 있습니다. 저는 네트워크 요청용, 데이터베이스 작업용, 오류와 경고만 보여주는 것, 이렇게 세 개를 갖고 있습니다.</p>

<h3 class="relative group">print() 대신 OSLog를 쓰는 이유
    <div id="print-대신-oslog를-쓰는-이유" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#print-%eb%8c%80%ec%8b%a0-oslog%eb%a5%bc-%ec%93%b0%eb%8a%94-%ec%9d%b4%ec%9c%a0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>OSLog는 <code>print()</code>를 여기저기 뿌리는 것보다 훨씬 낫습니다. 구조화되어 있어서 레벨과 카테고리로 필터링할 수 있고, 로깅 자체가 최적화되어 있어 앱을 느리게 만들지도 않습니다. 프로덕션에서는 민감한 데이터를 자동으로 가려주고, 앱이 죽은 뒤에도 로그가 남습니다.</p>
<p>급하게 확인할 때는 <code>print()</code>도 괜찮습니다. 나중에 다시 볼 가능성이 있는 건 OSLog로 남기세요.</p>

<h3 class="relative group">Console.app 필터 고급 활용
    <div id="consoleapp-필터-고급-활용" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consoleapp-%ed%95%84%ed%84%b0-%ea%b3%a0%ea%b8%89-%ed%99%9c%ec%9a%a9" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Console.app은 꽤 강력한 술어 쿼리를 지원합니다. 제가 자주 쓰는 것들입니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># 앱의 모든 오류와 결함
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND (level:error OR level:fault)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 실패한 네트워크 요청
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND category:networking AND eventMessage CONTAINS &#34;failed&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 지난 5분간의 모든 것
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND timestamp &gt;= now(-5m)</span></span></code></pre></div></div>
<p>로그를 내보내서 버그 리포트에 첨부할 수도 있습니다. Xcode 콘솔에서 복사해 붙여넣는 것보다 훨씬 낫죠.</p>

<h2 class="relative group">뷰 디버깅
    <div id="뷰-디버깅" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b7%b0-%eb%94%94%eb%b2%84%ea%b9%85" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>UI가 깨졌는데 이유를 모르겠을 때 가장 빠른 길은 뷰 디버거입니다.</p>
<p>앱을 실행하고 문제의 화면으로 이동한 다음, Xcode 디버그 바의 &ldquo;Debug View Hierarchy&rdquo; 버튼을 누릅니다(또는 Cmd+Shift+D). 뷰 계층 전체가 3D로 분해되어 나타납니다.</p>
<p>돌려보면 이런 게 눈에 들어옵니다.</p>
<ul>
<li>다른 뷰 뒤에 가려진 채 렌더링되는 뷰</li>
<li>화면 저 바깥에 나가 있는 뷰</li>
<li>크기가 0인 뷰</li>
<li>선택한 뷰의 전체 제약 체인</li>
</ul>
<p>저는 이걸로 탭 이벤트를 삼키던 투명 버튼, 0x0으로 렌더링되던 레이블, 어쩌다 폭이 10,000픽셀이 된 이미지를 찾아냈습니다.</p>

<h3 class="relative group">Auto Layout 디버깅
    <div id="auto-layout-디버깅" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#auto-layout-%eb%94%94%eb%b2%84%ea%b9%85" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>뷰 계층에 보라색 경고 아이콘이 보이면 제약 충돌입니다. 클릭하면 어떤 제약끼리 싸우고 있는지 Xcode가 정확히 보여줍니다.</p>
<p>제약에는 식별자를 붙여두세요.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">heightConstraint</span><span class="p">.</span><span class="n">identifier</span> <span class="p">=</span> <span class="s">&#34;ProfileImageHeight&#34;</span></span></span></code></pre></div></div>
<p>제약이 깨졌을 때 오류에 메모리 주소 대신 <code>&quot;ProfileImageHeight&quot;</code>가 찍힙니다. 로그 읽기가 완전히 달라집니다.</p>
<p>LLDB에서 제약 트리를 출력할 수도 있습니다.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po view.hasAmbiguousLayout
(lldb) po view._autolayoutTrace()</code></pre></div>

<h2 class="relative group">시간을 아껴주는 시뮬레이터 기능
    <div id="시간을-아껴주는-시뮬레이터-기능" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%9c%ea%b0%84%ec%9d%84-%ec%95%84%ea%bb%b4%ec%a3%bc%eb%8a%94-%ec%8b%9c%eb%ae%ac%eb%a0%88%ec%9d%b4%ed%84%b0-%ea%b8%b0%eb%8a%a5" aria-label="앵커">#</a>
    </span>
    
</h2>

<h3 class="relative group">느린 애니메이션
    <div id="느린-애니메이션" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%8a%90%eb%a6%b0-%ec%95%a0%eb%8b%88%eb%a9%94%ec%9d%b4%ec%85%98" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Debug → Slow Animations를 켜면 모든 게 1/10 속도로 움직입니다. 전환 중에 무슨 일이 벌어지는지, 애니메이션이 왜 이상해 보이는지 확인하기 딱 좋습니다.</p>

<h3 class="relative group">메모리 경고 시뮬레이션
    <div id="메모리-경고-시뮬레이션" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%94%eb%aa%a8%eb%a6%ac-%ea%b2%bd%ea%b3%a0-%ec%8b%9c%eb%ae%ac%eb%a0%88%ec%9d%b4%ec%85%98" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Debug → Simulate Memory Warning. 메모리가 부족할 때 앱이 어떻게 버티는지 테스트합니다. 이걸로 이미지 캐싱 버그를 꽤 찾았습니다. 경고가 오기 전까지는 멀쩡하다가, 오는 순간 이미지가 전부 사라지는 종류죠.</p>

<h3 class="relative group">Network Link Conditioner
    <div id="network-link-conditioner" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#network-link-conditioner" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Xcode → Open Developer Tools → Network Link Conditioner</p>
<p>3G, LTE, 심한 패킷 손실, 완전 오프라인까지 시뮬레이션할 수 있습니다. WiFi에서는 잘 되던 API 요청이 셀룰러에서 타임아웃된 적이 있다면, 바로 이걸로 재현할 수 있습니다.</p>
<p>저는 지연 500ms에 패킷 손실 10%짜리 &ldquo;Bad Network&rdquo; 프로필을 상비해 둡니다. 빠른 사무실 WiFi에서는 절대 안 보이는 타임아웃 버그와 로딩 상태 문제가 여기서 걸려 나옵니다.</p>

<h3 class="relative group">상태 표시줄 오버라이드
    <div id="상태-표시줄-오버라이드" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%83%81%ed%83%9c-%ed%91%9c%ec%8b%9c%ec%a4%84-%ec%98%a4%eb%b2%84%eb%9d%bc%ec%9d%b4%eb%93%9c" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>시뮬레이터 상태 표시줄을 우클릭하면 시간, 배터리 잔량, 신호 세기, 통신사를 바꿀 수 있습니다. 스크린샷을 통일하거나 배터리 잔량별 UI를 확인할 때 유용합니다.</p>

<h3 class="relative group">위치 시뮬레이션
    <div id="위치-시뮬레이션" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9c%84%ec%b9%98-%ec%8b%9c%eb%ae%ac%eb%a0%88%ec%9d%b4%ec%85%98" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Debug → Location으로 책상에 앉은 채 온갖 위치를 재현할 수 있습니다. 커스텀 좌표, 도시 산책, 고속도로 주행까지 다 있습니다. 위치 기반 기능을 테스트하거나 특정 지역에서만 나는 문제를 잡을 때 아주 요긴합니다.</p>

<h2 class="relative group">메모리 디버깅
    <div id="메모리-디버깅" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%94%eb%aa%a8%eb%a6%ac-%eb%94%94%eb%b2%84%ea%b9%85" aria-label="앵커">#</a>
    </span>
    
</h2>

<h3 class="relative group">Debug Memory Graph
    <div id="debug-memory-graph" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#debug-memory-graph" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Debug Memory Graph 버튼(Cmd+Shift+M)을 누르면 메모리에 있는 모든 객체와 참조 관계, 그리고 정작 궁금한 것, 즉 어떤 게 누수되고 있는지를 Xcode가 보여줍니다.</p>
<p>보라색 느낌표가 누수입니다. 클릭하면 retain cycle이 보입니다.</p>
<p>제가 가장 자주 보는 누수는 이겁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">ViewController</span><span class="p">:</span> <span class="n">UIViewController</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nv">onComplete</span><span class="p">:</span> <span class="p">(()</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">setupHandler</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kc">self</span><span class="p">.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ❌ self를 강하게 캡처</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>weak self로 고칩니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">?.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ✓ retain cycle 없음</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>delegate도 weak이어야 합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">weak</span> <span class="kd">var</span> <span class="nv">delegate</span><span class="p">:</span> <span class="n">ManagerDelegate</span><span class="p">?</span>  <span class="c1">// 단순한 &#39;var&#39;가 아님</span></span></span></code></pre></div></div>

<h3 class="relative group">Instruments
    <div id="instruments" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#instruments" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>메모리를 제대로 파고들 때는 Instruments를 씁니다(Product → Profile, 또는 Cmd+I).</p>
<p>Allocations 인스트루먼트로는 모든 객체 할당, 시간에 따른 메모리 증가, 메모리를 가장 많이 쓰는 클래스, 할당이 일어난 위치의 스택 트레이스까지 볼 수 있습니다.</p>
<p>제가 주로 찾는 건 시간에 비례해 계속 늘어나는 메모리(거의 누수입니다), 예상 밖의 큰 할당(이미지 1000장을 한 번에 다 읽어버린 건 아닌지?), 해제됐어야 하는데 남아 있는 객체, 이 세 가지입니다.</p>
<p>동작 전후로 세대를 표시해 두면(작은 깃발 버튼) 남아 있으면 안 되는 게 뭐가 남았는지 확인할 수 있습니다.</p>

<h2 class="relative group">성능 프로파일링
    <div id="성능-프로파일링" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%84%b1%eb%8a%a5-%ed%94%84%eb%a1%9c%ed%8c%8c%ec%9d%bc%eb%a7%81" aria-label="앵커">#</a>
    </span>
    
</h2>

<h3 class="relative group">Time Profiler
    <div id="time-profiler" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#time-profiler" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Product → Profile → Time Profiler. 앱에서 느린 동작을 하면서 기록한 뒤 호출 트리를 읽습니다.</p>
<p>&ldquo;Self Weight&quot;로 정렬하면 병목이 보입니다. 어떤 함수가 self weight 40%를 차지한다면, 시간이 거기로 새고 있는 겁니다.</p>
<p>예전에 JSON 파싱 함수가 초당 1000번 호출되는 걸 발견한 적이 있습니다. 백그라운드 큐로 옮겼더니 UI 버벅임이 사라졌죠.</p>

<h3 class="relative group">Main Thread Checker
    <div id="main-thread-checker" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#main-thread-checker" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>백그라운드 스레드에서의 UI 업데이트는 Xcode가 알아서 잡아줍니다. 이런 메시지가 보이면,</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Main Thread Checker: UI API called on a background thread: -[UILabel setText:]</span></span></code></pre></div></div>
<p>이런 짓을 한 겁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ❌ 크래시!</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>고치면 이렇게 됩니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ✓</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h2 class="relative group">런타임 진단
    <div id="런타임-진단" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%9f%b0%ed%83%80%ec%9e%84-%ec%a7%84%eb%8b%a8" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Edit Scheme → Run → Diagnostics. 여기 늘어선 체크박스들이 손으로는 절대 못 찾을 버그를 잡아줍니다.</p>

<h3 class="relative group">Address Sanitizer
    <div id="address-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#address-sanitizer" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>메모리 손상을 찾아냅니다. use-after-free, 버퍼 오버플로, 메모리 누수. 앱이 2~3배 느려지지만, 다른 방법으로는 사실상 추적이 불가능한 버그를 잡아줍니다. 가끔씩만 나는 크래시를 쫓을 때 켭니다.</p>

<h3 class="relative group">Thread Sanitizer
    <div id="thread-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thread-sanitizer" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>데이터 레이스와 스레딩 문제를 잡습니다. 켜놓고 앱을 평소처럼 쓰기만 하면 됩니다. 경쟁 상태가 있다면 Thread Sanitizer가 찾아냅니다.</p>
<p>Address Sanitizer와 Thread Sanitizer는 동시에 못 켜기 때문에, 이상한 크래시를 잡을 때는 둘을 번갈아 씁니다.</p>

<h3 class="relative group">Zombie Objects
    <div id="zombie-objects" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zombie-objects" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>해제된 객체로 보낸 메시지를 잡습니다. 켜두면 해제된 메모리를 건드리는 순간 Xcode가 정확히 알려줍니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">*** -[MyViewController viewDidLoad]: message sent to deallocated instance 0x600001234000</span></span></code></pre></div></div>
<p>계속 켜두면 안 됩니다. 객체가 해제되지 않아서 메모리가 계속 불어나거든요. 하지만 특정 use-after-free 버그를 집어낼 때는 최고입니다.</p>

<h2 class="relative group">흔한 디버깅 시나리오
    <div id="흔한-디버깅-시나리오" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%9d%94%ed%95%9c-%eb%94%94%eb%b2%84%ea%b9%85-%ec%8b%9c%eb%82%98%eb%a6%ac%ec%98%a4" aria-label="앵커">#</a>
    </span>
    
</h2>

<h3 class="relative group">실행하자마자 크래시
    <div id="실행하자마자-크래시" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%a4%ed%96%89%ed%95%98%ec%9e%90%eb%a7%88%ec%9e%90-%ed%81%ac%eb%9e%98%ec%8b%9c" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>일단 예외 브레이크포인트부터 추가합니다. 보통 그걸로 잡힙니다.</p>
<p>안 잡히면 Info.plist에 빠진 키, 초기화 코드의 강제 언래핑, 의존성 주입 실패 순으로 의심해 봅니다.</p>

<h3 class="relative group">UI가 업데이트되지 않음
    <div id="ui가-업데이트되지-않음" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ui%ea%b0%80-%ec%97%85%eb%8d%b0%ec%9d%b4%ed%8a%b8%eb%90%98%ec%a7%80-%ec%95%8a%ec%9d%8c" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>매번. 매번 이 중 하나입니다.</p>
<ol>
<li>업데이트가 메인 스레드에서 일어나지 않음</li>
<li>outlet이 연결되지 않음</li>
<li>뷰가 실제로는 화면에 없음(LLDB에서 <code>po view.window</code>로 확인, nil이면 계층에 없는 겁니다)</li>
</ol>
<p>테이블 뷰나 컬렉션 뷰라면 <code>reloadData()</code> 호출을 잊은 거고요.</p>

<h3 class="relative group">메모리 경고로 앱이 죽음
    <div id="메모리-경고로-앱이-죽음" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%94%eb%aa%a8%eb%a6%ac-%ea%b2%bd%ea%b3%a0%eb%a1%9c-%ec%95%b1%ec%9d%b4-%ec%a3%bd%ec%9d%8c" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Instruments Allocations로 뭐가 메모리를 붙잡고 있는지 봅니다. 보통은 캐시에서 해제되지 않는 이미지, 해제 안 되는 뷰 컨트롤러(retain cycle), 끝없이 자라는 배열이나 딕셔너리 중 하나입니다.</p>
<p>시뮬레이터에서 메모리 경고를 시뮬레이션해서 정리 코드를 테스트해 두세요.</p>

<h3 class="relative group">원인 모를 크래시
    <div id="원인-모를-크래시" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9b%90%ec%9d%b8-%eb%aa%a8%eb%a5%bc-%ed%81%ac%eb%9e%98%ec%8b%9c" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>sanitizer를 전부 켭니다. 간헐적이면 대개 스레딩 문제니 Thread Sanitizer로 돌리고, 시스템 프레임워크 안에서 죽는다면 대개 메모리 손상이니 Address Sanitizer로 돌립니다.</p>

<h3 class="relative group">재현되지 않는 크래시
    <div id="재현되지-않는-크래시" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9e%ac%ed%98%84%eb%90%98%ec%a7%80-%ec%95%8a%eb%8a%94-%ed%81%ac%eb%9e%98%ec%8b%9c" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>크래시가 남의 폰에서 났더라도 리포트는 대개 어딘가에 남아 있습니다. TestFlight와 App Store 빌드라면 Xcode의 Organizer(Window → Organizer → Crashes)에 분석 데이터 공유에 동의한 사용자들의 크래시 리포트가 모입니다. 빌드와 함께 dSYM을 올렸다면 심볼리케이션까지 된 상태로요. 테스터의 폰이라면 리포트가 기기 안에 그대로 있습니다. 설정 → 개인정보 보호 및 보안 → 분석 및 향상 → 분석 데이터에 앱 이름으로 시작하는 <code>.ips</code> 파일이 있으니, AirDrop으로 하나 보내 달라고 하면 됩니다. 그다음은 크래시가 난 스레드의 스택 트레이스에서 출발해 위의 시나리오를 차례로 짚어 보세요.</p>

<h3 class="relative group">시뮬레이터에서 OTA 업데이트 디버깅
    <div id="시뮬레이터에서-ota-업데이트-디버깅" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%9c%eb%ae%ac%eb%a0%88%ec%9d%b4%ed%84%b0%ec%97%90%ec%84%9c-ota-%ec%97%85%eb%8d%b0%ec%9d%b4%ed%8a%b8-%eb%94%94%eb%b2%84%ea%b9%85" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>제 앱은 OTA(over-the-air) 업데이트를 내보내는데, 이 플로우 전체를 디버깅하기에는 시뮬레이터가 제일 편합니다.</p>
<p>먼저 네트워크 트래픽부터 봅니다. 앱 서브시스템으로 필터링한 Console.app에 매니페스트 요청과 응답이 실시간으로 흘러가니, 어떤 헤더가 나가고 서버가 뭘 돌려주는지 정확히 보입니다.</p>
<p>다음엔 일부러 네트워크를 나쁘게 만듭니다. Network Link Conditioner로 느린 연결이나 패킷 손실에서 업데이트 플로우가 어떻게 버티는지 확인합니다. 앱이 멈추는지, 로딩 인디케이터는 뜨는지, 제대로 폴백하는지.</p>
<p>업데이트 체크 로직 주변에는 로그를 심어둡니다. 체크 시작, 업데이트 발견, 다운로드 시작, 새 번들 로드, 각 시점마다 남깁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;updates&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Checking for updates...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">update</span> <span class="p">=</span> <span class="k">try</span> <span class="n">await</span> <span class="n">Updates</span><span class="p">.</span><span class="n">checkForUpdateAsync</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Update available: </span><span class="si">\(</span><span class="n">update</span><span class="p">.</span><span class="n">isAvailable</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>마지막으로 매니페스트 엔드포인트 자체도 따로 테스트해 봅니다. 앱이 보내는 것과 같은 헤더로 직접 curl 하면 됩니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -H <span class="s2">&#34;expo-protocol-version: 1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-platform: ios&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-runtime-version: 1.0.0&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     https://updates.example.com/manifest</span></span></code></pre></div></div>
<p>클라이언트 쪽을 파기 전에 서버가 올바른 데이터를 주고 있는지 여기서 먼저 확인할 수 있습니다.</p>

<h2 class="relative group">서드파티 도구
    <div id="서드파티-도구" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%84%9c%eb%93%9c%ed%8c%8c%ed%8b%b0-%eb%8f%84%ea%b5%ac" aria-label="앵커">#</a>
    </span>
    
</h2>

<h3 class="relative group">Charles Proxy / Proxyman
    <div id="charles-proxy--proxyman" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#charles-proxy--proxyman" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>네트워크 트래픽 전체 보기, 요청과 응답 수정, 오류 조건 테스트. API 디버깅에는 필수입니다.</p>
<p>Proxyman이 Charles보다 UI가 낫고 macOS에서 더 자연스럽습니다. 작년에 갈아탄 뒤로 돌아갈 생각이 없습니다.</p>

<h3 class="relative group">Reveal
    <div id="reveal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#reveal" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Xcode 뷰 디버거와 비슷하지만 더 강력합니다. Xcode에 연결하지 않아도 실기기에서 동작해서, 테스터 기기의 레이아웃 문제를 잡을 때 좋습니다.</p>

<h2 class="relative group">시뮬레이터와 실기기의 차이
    <div id="시뮬레이터와-실기기의-차이" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%9c%eb%ae%ac%eb%a0%88%ec%9d%b4%ed%84%b0%ec%99%80-%ec%8b%a4%ea%b8%b0%ea%b8%b0%ec%9d%98-%ec%b0%a8%ec%9d%b4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>디버깅은 대부분 시뮬레이터에서 합니다. 빠르거든요. 케이블도, 서명도, 설치 대기도 없습니다. 하지만 시뮬레이터는 Mac의 프로세서 위에서 Mac의 메모리와 Mac의 네트워크로 돌아가는 iOS 빌드일 뿐이고, 어떤 버그는 폰에만 있습니다.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>시뮬레이터</th>
					<th>실제 iPhone</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>속도</td>
					<td>Mac CPU로 돌아서 대개 폰보다 빠름</td>
					<td>실제 수치. 성능 프로파일링은 여기서</td>
			</tr>
			<tr>
					<td>메모리</td>
					<td>사실상 한도가 없어서 메모리 경고를 시뮬레이션해야 함</td>
					<td>너무 많이 쓰면 시스템이 앱을 종료</td>
			</tr>
			<tr>
					<td>네트워크</td>
					<td>Mac의 연결 그대로</td>
					<td>셀룰러, 약한 신호, Wi-Fi와 모바일 데이터 사이의 전환</td>
			</tr>
			<tr>
					<td>카메라, 블루투스, NFC</td>
					<td>쓸 수 없음</td>
					<td>쓸 수 있음</td>
			</tr>
			<tr>
					<td>푸시 알림</td>
					<td><code>xcrun simctl push</code>로 흉내 낼 수 있음</td>
					<td>사용자가 받는 그대로 실제 전달</td>
			</tr>
			<tr>
					<td>반복 속도</td>
					<td>몇 초</td>
					<td>느림: 서명, 설치, 케이블이나 Wi-Fi</td>
			</tr>
	</tbody>
</table>
<p>그래서 로직은 시뮬레이터에서 만들고 디버깅하고, 릴리스 전에는 아직 지원하는 가장 오래된 iPhone에서 앱을 돌려 봅니다. 성능, 메모리 압박, 하드웨어를 건드리는 기능은 기기에서 돌려 보기 전까지는 제대로 테스트했다고 할 수 없습니다.</p>

<h3 class="relative group">실기기에서 실행하기
    <div id="실기기에서-실행하기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%a4%ea%b8%b0%ea%b8%b0%ec%97%90%ec%84%9c-%ec%8b%a4%ed%96%89%ed%95%98%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>iPhone을 연결해 실행 대상으로 고르고, Signing &amp; Capabilities에서 팀을 지정합니다(내 폰에 설치하는 정도면 무료 Apple ID로 충분합니다). iOS 16부터는 폰에서도 설정 → 개인정보 보호 및 보안에서 개발자 모드를 켜야 하고, 처음 켤 때 한 번 재시동됩니다. 개발자 모드가 켜지면 폰의 설정 → 개발자 메뉴에 Network Link Conditioner가 생기는데, Mac 버전과 똑같은 나쁜 네트워크 프로필이 들어 있습니다.</p>

<h3 class="relative group">무선 디버깅
    <div id="무선-디버깅" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%ac%b4%ec%84%a0-%eb%94%94%eb%b2%84%ea%b9%85" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>USB로 기기를 한 번만 연결하고, Window → Devices and Simulators에서 &ldquo;Connect via network&quot;에 체크한 뒤 선을 뽑습니다. 같은 WiFi에 있는 한 기기는 Xcode에 계속 잡혀 있습니다.</p>

<h3 class="relative group">Device Console
    <div id="device-console" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#device-console" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Window → Devices and Simulators → Open Console. 시스템 로그, 앱 로그, 크래시 리포트까지 전부 보입니다. Xcode 콘솔보다 훨씬 상세합니다.</p>
<p>테스터가 &ldquo;앱이 죽었어요&quot;라고 하면 저는 꼭 기기를 연결해서 콘솔 로그를 받아봅니다. 모든 걸 설명해 주는 시스템 레벨 오류가 남아 있는 경우가 많거든요.</p>

<h2 class="relative group">실제 디버깅 순서
    <div id="실제-디버깅-순서" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%a4%ec%a0%9c-%eb%94%94%eb%b2%84%ea%b9%85-%ec%88%9c%ec%84%9c" aria-label="앵커">#</a>
    </span>
    
</h2>
<ol>
<li>예외 브레이크포인트가 없으면 추가</li>
<li>버그 재현</li>
<li>문제로 의심되는 곳 근처에 브레이크포인트 설정</li>
<li>LLDB에서 <code>po</code>로 값 확인</li>
<li>변수를 바꿔가며 재컴파일 없이 수정 시험</li>
<li>UI 문제면 뷰 계층 확인</li>
<li>성능 문제면 Instruments로 프로파일</li>
<li>메모리나 스레딩 문제면 sanitizer 켜기</li>
</ol>
<p>요령은 증상에서 거꾸로 짚어가는 겁니다. 크래시? 예외 브레이크포인트. 느림? Time Profiler. 메모리? Instruments. UI가 이상함? 뷰 디버거.</p>

<h2 class="relative group">더 일찍 알았으면 좋았을 것들
    <div id="더-일찍-알았으면-좋았을-것들" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%8d%94-%ec%9d%bc%ec%b0%8d-%ec%95%8c%ec%95%98%ec%9c%bc%eb%a9%b4-%ec%a2%8b%ec%95%98%ec%9d%84-%ea%b2%83%eb%93%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>assertion을 쓰세요. 로직 오류가 버그가 되기 전에 잡힙니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="bp">assert</span><span class="p">(</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#34;Users array should never be empty here&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>디버깅을 시작하기 전에 커밋해 두세요. 가설을 시험한다고 이것저것 고치다 보면 반드시 되돌리고 싶어집니다.</p>
<p>끝나면 print 문은 지우세요. 최소한 <code>#if DEBUG</code>로 감싸두기라도 하고요. 묵은 디버그 로그는 다음 버그를 가립니다.</p>
<p>LLDB는 제대로 익혀두세요. 앱을 스무 번 리빌드하는 것보다 빠릅니다.</p>
<p>그리고 시뮬레이터는 진짜 폰이 아닙니다. 기기에서만 나는 버그는 거의 항상 스레딩 아니면 메모리 문제입니다. 아니면 성능 문제거나요. 시뮬레이터는 진짜 iPhone 칩이 아니라 Mac CPU로 돌아가니까요.</p>

<h2 class="relative group">늘 열어두는 도구
    <div id="늘-열어두는-도구" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%8a%98-%ec%97%b4%ec%96%b4%eb%91%90%eb%8a%94-%eb%8f%84%ea%b5%ac" aria-label="앵커">#</a>
    </span>
    
</h2>
<ul>
<li>Xcode(당연히)</li>
<li>앱 서브시스템으로 필터링한 Console.app</li>
<li>네트워크 디버깅용 Proxyman</li>
<li>상황이 심각해지면 Instruments</li>
</ul>
<hr>
<p>어느 도구도 만능은 아닙니다. 뷰 디버거는 메모리 누수를 못 찾고, Instruments는 Auto Layout 충돌을 풀어주지 못합니다. 결국 실력은 증상에 맞는 도구를 고르는 데서 갈리고, 그건 횟수를 쌓아야 늡니다. retain cycle을 몇 번 밟다 보면 스택 트레이스만 봐도 냄새가 나기 시작하죠. 디버깅이 재미있어지는 날은 아마 안 오겠지만, 적어도 고역은 아니게 됩니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-02-26-debugging-ios-mobile-apps/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
