<?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>Debugging · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/debugging/</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/debugging/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Playwright &#34;Test timeout of 30000ms exceeded.&#34;: 끝나지 않는 대기부터 찾기</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-playwright-timeout-30000ms/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-playwright-timeout-30000ms/</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Playwright</category>
      <category>UI Testing</category>
      <category>Testing Best Practices</category>
      <category>Debugging</category>
      <description>Playwright의 &#34;Test timeout of 30000ms exceeded.&#34;가 실제로 무엇을 재는지, 제가 확인하는 순서대로 본 흔한 원인, 멈춘 단계를 찾는 방법, 그리고 타임아웃을 늘리는 게 오답인 경우를 정리했습니다. 제 테스트 스위트에서 실제로 난 실패, 테스트 다섯 개와 이름이 바뀐 테스트 ID 하나의 이야기도 담았습니다.</description>
      <content:encoded><![CDATA[<p><code>Test timeout of 30000ms exceeded.</code>는 Playwright 테스트 하나가 30초 제한 시간을 넘겼다는 뜻입니다. 30초는 Playwright Test의 기본값이고, 이 제한 시간에는 테스트 본문과 <code>beforeEach</code> 훅, 픽스처(fixture) 준비가 모두 들어갑니다. 메시지가 특정 단계가 아니라 테스트를 지목하는 건, 클릭이나 입력, 페이지 이동에 기본적으로 자체 타임아웃이 없기 때문입니다. 이것들은 얼마가 걸리든 기다리고, 테스트 타임아웃은 그저 가장 먼저 울리는 타이머일 뿐이죠.</p>
<p>그래서 이 에러가 정말 속도 문제인 경우는 드뭅니다. 대개는 어떤 단계가 절대 일어나지 않을 일을 기다리고 있었던 겁니다. 제가 확인하는 순서대로 원인과 해결책을 정리하면 이렇습니다.</p>
<ol>
<li>로케이터가 아무것도 찾지 못해서, <code>click()</code>이나 <code>fill()</code>이 테스트 시간이 다 될 때까지 기다립니다. 테스트 ID나 텍스트가 바뀌었거나, 대화상자가 앞을 가리고 있는 게 흔한 이유입니다. 로케이터를 고치세요.</li>
<li>페이지 이동이나 <code>waitForURL()</code>이 끝내 오지 않는 페이지를 기다립니다. 리다이렉트, 로그인 화면, 첫 실행 화면 때문에 앱이 다른 곳으로 가버린 경우죠. 기다리는 대상을 고치세요.</li>
<li>준비 과정이 정말로 느립니다. 첫 요청에서야 컴파일을 시작하는 개발 서버, 0으로 스케일 다운됐다가 깨어나는 백엔드, <code>beforeEach</code>마다 UI로 하는 로그인 같은 것들입니다. 환경을 고치거나, 준비를 한 번만 하세요.</li>
<li>테스트가 그냥 깁니다. 이때, 그리고 이때만 <code>test.slow()</code>나 <code>test.setTimeout()</code>으로 그 테스트에 시간을 더 주세요.</li>
</ol>
<p>어느 쪽이든 첫 번째 할 일은 시간이 다 됐을 때 아직 실행 중이던 단계를 찾는 것입니다. 두 번째는 <code>actionTimeout</code>을 설정해서, 다음에 실패할 때는 에러가 그 단계를 직접 알려주게 하는 것이고요.</p>

<h2 class="relative group">30초에는 무엇이 들어가나
    <div id="30초에는-무엇이-들어가나" 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="#30%ec%b4%88%ec%97%90%eb%8a%94-%eb%ac%b4%ec%97%87%ec%9d%b4-%eb%93%a4%ec%96%b4%ea%b0%80%eb%82%98" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>테스트 타임아웃은 픽스처 준비, <code>beforeEach</code> 훅, 테스트 함수가 함께 쓰는 하나의 제한 시간입니다. 테스트가 끝나면 <code>afterEach</code> 훅과 픽스처 정리(teardown)는 같은 길이의 제한 시간을 따로 받습니다. <code>beforeAll</code>과 <code>afterAll</code>도 같은 값을 받지만 어떤 테스트와도 시간을 나눠 쓰지 않고, 실패할 때도 자기 메시지를 따로 냅니다.</p>
<p>이 제한 시간 안에서 자기 타이머를 가진 건 어서션(assertion)뿐입니다. <code>await expect(locator).toBeVisible()</code>은 기본 5초가 지나면 포기하고, 무엇을 찾고 있었는지 정확히 알려줍니다. 액션과 페이지 이동은 그렇지 않습니다. <code>actionTimeout</code>과 <code>navigationTimeout</code>의 기본값은 무제한입니다. 이 에러가 이렇게 모호한 이유가 전부 여기 있습니다. 절대 나타나지 않을 요소를 클릭하는 코드는 실패하지 않습니다. 다른 무언가가 테스트를 끝낼 때까지 얌전히 기다릴 뿐이죠.</p>
<p>아래는 실행을 끝낼 수 있는 타임아웃 전부와, 각각이 출력하는 메시지입니다. 메시지는 제 프로젝트에 깔린 버전 중 가장 오래된 1.58과 가장 최신인 1.63에서 그대로 옮겼습니다.</p>
<table>
	<thead>
			<tr>
					<th>타임아웃</th>
					<th>기본값</th>
					<th>설정하는 곳</th>
					<th>출력되는 메시지</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>테스트 (본문, <code>beforeEach</code>, 픽스처 준비)</td>
					<td>30,000 ms</td>
					<td>설정 파일의 <code>timeout</code>, <code>--timeout</code>, <code>test.setTimeout()</code>, <code>test.slow()</code></td>
					<td><code>Test timeout of 30000ms exceeded.</code></td>
			</tr>
			<tr>
					<td><code>beforeAll</code> / <code>afterAll</code></td>
					<td>테스트 타임아웃과 같은 값</td>
					<td>훅 안에서 <code>test.setTimeout()</code></td>
					<td><code>&quot;beforeAll&quot; hook timeout of 30000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>어서션 (<code>expect</code>)</td>
					<td>5,000 ms</td>
					<td>설정 파일의 <code>expect: { timeout }</code>, 어서션별 <code>{ timeout }</code></td>
					<td><code>expect(locator).toBeVisible() failed</code> … <code>Timeout: 5000ms</code></td>
			</tr>
			<tr>
					<td>액션 (<code>click</code>, <code>fill</code>)</td>
					<td>없음</td>
					<td><code>use: { actionTimeout }</code>, 액션별 <code>{ timeout }</code></td>
					<td><code>TimeoutError: locator.click: Timeout 10000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>페이지 이동 (<code>goto</code>, <code>waitForURL</code>)</td>
					<td>없음</td>
					<td><code>use: { navigationTimeout }</code>, 호출별 <code>{ timeout }</code></td>
					<td><code>TimeoutError: page.waitForURL: Timeout 15000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>전체 실행</td>
					<td>없음</td>
					<td>설정 파일의 <code>globalTimeout</code></td>
					<td><code>Timed out waiting 3600s for the test suite to run</code></td>
			</tr>
			<tr>
					<td><code>webServer</code> 기동</td>
					<td>60,000 ms</td>
					<td><code>webServer: { timeout }</code></td>
					<td><code>Timed out waiting 60000ms from config.webServer.</code></td>
			</tr>
	</tbody>
</table>
<p>액션, 페이지 이동, 전체 실행에는 기본값이 없으니, 해당 행의 숫자는 직접 설정한 값이 들어가는 자리입니다.</p>

<h2 class="relative group">테스트 다섯 개와 이름이 바뀐 테스트 ID
    <div id="테스트-다섯-개와-이름이-바뀐-테스트-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%85%8c%ec%8a%a4%ed%8a%b8-%eb%8b%a4%ec%84%af-%ea%b0%9c%ec%99%80-%ec%9d%b4%eb%a6%84%ec%9d%b4-%eb%b0%94%eb%80%90-%ed%85%8c%ec%8a%a4%ed%8a%b8-id" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>2월, 제가 만드는 견적 앱의 테스트 스위트에서 테스트 다섯 개가 한 번의 실행 중에 모두 같은 식으로 실패했습니다. 이 스위트는 개발 서버가 띄운 앱의 웹 버전을 워커 하나로 돌리고, 시드 데이터를 새로 넣은 로컬 백엔드를 상대로 테스트합니다. 다섯 개 중 첫 번째를 조금 줄여서 옮기면 이렇습니다.</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">  1) [default] › tests/templates.spec.ts:50:7 › Templates › should edit a template ───
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Test timeout of 30000ms exceeded.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Error: locator.click: Target page, context or browser has been closed
</span></span><span class="line"><span class="cl">    Call log:
</span></span><span class="line"><span class="cl">      - waiting for locator(&#39;[data-testid=&#34;template-item&#34;]&#39;).filter({ hasText: &#39;E2E Edit Template&#39; }).locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      57 |       .locator(&#39;[data-testid=&#34;template-item&#34;]&#39;)
</span></span><span class="line"><span class="cl">      58 |       .filter({ hasText: &#34;E2E Edit Template&#34; });
</span></span><span class="line"><span class="cl">    &gt; 59 |     await item.locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;).click();
</span></span><span class="line"><span class="cl">         |                                                         ^
</span></span><span class="line"><span class="cl">      60 |     await page.waitForTimeout(500);</span></span></code></pre></div></div>
<p><code>Target page, context or browser has been closed</code>라는 줄만 보면 브라우저가 죽은 것 같습니다. 아닙니다. 발밑에서 페이지가 닫혔을 때 액션이 내는 메시지입니다. 30초가 다 되자 Playwright가 테스트를 정리했고, 아직 기다리던 클릭이 사라진 페이지를 보고한 것이죠. 기본 제공 <code>page</code> 픽스처를 썼다면 이 줄은 <code>Error: locator.click: Test timeout of 30000ms exceeded.</code>처럼 타임아웃을 한 번 더 말해줬을 겁니다. 이 테스트들은 <code>beforeAll</code>에서 브라우저 컨텍스트를 직접 열기 때문에, 클릭 쪽에는 일반적인 메시지가 간 겁니다.</p>
<p>쓸모 있는 건 호출 로그(call log)입니다. 클릭은 <code>[data-testid=&quot;template-item&quot;]</code>을 기다리고 있었습니다. 그보다 열 시간 전에 저는 목록 행 컴포넌트의 테스트 ID를 <code>template-item</code>에서 <code>template-item-${name}</code>으로 바꿨습니다. 네이티브 앱의 E2E 테스트가 이름으로 특정 행을 집어낼 수 있게 하려고요. <code>=</code>를 쓴 속성 선택자는 값 전체가 같아야 일치하니, 웹 테스트는 이제 페이지 어디에도 없는 요소를 찾고 있었습니다. 액션 타임아웃도 없었으니 그 클릭은 테스트 시간이 다 될 때까지 기다릴 수밖에 없었고요. 30초짜리 실패 다섯 개면 2분 30초인데, 전체 실행 시간이 3.8분이었습니다.</p>
<p>제가 먼저 넣은 건 재시도였습니다. 30분도 안 돼서 설정에 <code>retries: process.env.CI ? 2 : 1</code>이 들어갔고, 거기 달린 주석에는 지금도 &ldquo;for flaky browser crashes&rdquo;, 즉 가끔 나는 브라우저 크래시 대비용이라고 적혀 있습니다. 다음 실행에서는 같은 다섯 개 중 네 개가 재시도까지 포함해 또 실패했고, 실행 시간은 3.8분에서 6.4분으로 늘었습니다. 이 describe 블록들은 직렬(serial) 모드로 돌아갑니다. 하나가 실패하면 그룹의 나머지는 건너뛰고, 재시도는 그룹을 처음부터 다시 돌리죠. 그래서 잘못된 클릭마다 30초를 두 번씩 기다렸습니다. 매번 같은 이유로 실패하는 테스트는 재시도로 살릴 수 없습니다. 대가만 두 번 치를 뿐입니다.</p>
<p>해결책은 접두사 매칭이었습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">item</span> <span class="o">=</span> <span class="nx">page</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid^=&#34;template-item-&#34;]&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">filter</span><span class="p">({</span> <span class="nx">hasText</span><span class="o">:</span> <span class="s2">&#34;E2E Edit Template&#34;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">item</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;edit-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span></span></span></code></pre></div></div>
<p>더 넓게 보면, 테스트 ID도 일종의 계약입니다. 이 ID 하나에 기대는 테스트 스위트가 두 개였는데, 둘 다 서로의 존재를 몰랐던 거죠. Playwright 모범 사례 가이드가 제가 쓴 것 같은 CSS 선택자보다 <code>getByRole()</code>처럼 사용자에게 보이는 속성에 기반한 로케이터를 권하는 것도, 사용자는 눈치채지도 못할 DOM 변경에 테스트가 깨지지 않게 하려는 것입니다.</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%88%ec%b6%98-%eb%8b%a8%ea%b3%84-%ec%b0%be%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>먼저 호출 로그를 읽으세요. 이 로그는 시간이 다 됐을 때 아직 실행 중이던 단계의 것이고, <code>waiting for locator(...)</code> 뒤에 아무것도 이어지지 않는다면 로케이터가 끝내 아무것도 찾지 못했다는 뜻입니다.</p>
<p>그걸로 부족하면 트레이스를 엽니다. 제 앱 스위트 두 개, 견적 앱 스위트와 제가 만드는 마켓플레이스·커뮤니티 앱 스위트는 둘 다 <code>trace: &quot;on-first-retry&quot;</code>를 씁니다. 첫 재시도 때만 기록하는 옵션이라 <code>retries</code>가 1 이상이어야 합니다. 재시도를 안 쓴다면 <code>&quot;retain-on-failure&quot;</code>를, 한 번만 돌려볼 때는 <code>--trace on</code>을 쓰면 됩니다. 그다음 트레이스를 직접 열거나, HTML 리포트에서 엽니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npx playwright show-trace test-results/&lt;test-folder&gt;/trace.zip
</span></span><span class="line"><span class="cl">npx playwright show-report</span></span></code></pre></div></div>
<p>끝나지 않은 액션은 목록 끝 근처, 에러가 표시된 항목입니다. 그걸 선택하고 스냅샷을 보세요. Playwright가 기다리던 그 순간의 페이지라서, 기대했던 요소가 있는지 직접 뒤져볼 수 있습니다. 제 경우라면 테스트가 찾지 않는 ID를 단 행이 멀쩡히 거기 있었을 겁니다.</p>
<p>최근 버전은 실패 스크린샷 옆에 <code>error-context.md</code>도 남깁니다. 실패한 순간의 페이지를 텍스트 스냅샷으로 담은 파일인데, 요소가 없는 경우라면 이것만으로 충분할 때가 많습니다.</p>
<p>함정이 하나 있습니다. <code>--debug</code>는 Inspector를 열면서 <code>--max-failures=1</code>, <code>--headed</code>, <code>--workers=1</code>과 함께 <code>--timeout=0</code>까지 설정합니다. <code>--debug</code>에서 통과한 테스트는 30초에 대해서는 아무것도 말해주지 않습니다.</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%aa%a8%eb%93%a0-%ec%95%a1%ec%85%98%ec%97%90-%ec%83%81%ed%95%9c%ec%84%a0%ec%9d%84" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>제 설정 파일 중 액션 타임아웃을 정해둔 곳은 하나도 없습니다. 그래서 요소 하나가 없다는 이유로 30초를 다섯 번이나 쓴 거죠. <code>use</code>에 두 줄만 넣으면 달라집니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@playwright/test&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">timeout</span>: <span class="kt">30_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="o">:</span> <span class="p">{</span> <span class="nx">timeout</span>: <span class="kt">5_000</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">use</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">actionTimeout</span>: <span class="kt">10_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">navigationTimeout</span>: <span class="kt">15_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">trace</span><span class="o">:</span> <span class="s2">&#34;retain-on-failure&#34;</span><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>그러면 똑같이 잘못된 로케이터가, 테스트에 아직 시간이 남아 있을 때 이렇게 실패합니다.</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">    TimeoutError: locator.click: Timeout 10000ms exceeded.
</span></span><span class="line"><span class="cl">    Call log:
</span></span><span class="line"><span class="cl">      - waiting for locator(&#39;[data-testid=&#34;template-item&#34;]&#39;).filter({ hasText: &#39;E2E Edit Template&#39; }).locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;)</span></span></code></pre></div></div>
<p>어느 단계에서, 어떤 액션이, 얼마나 기다렸는지가 다 나옵니다. 두 값은 테스트 타임아웃보다 넉넉히 작게 두는 게 좋다고 봅니다. 그래야 멈춘 단계 하나가 제한 시간을 다 써버리고 모호한 메시지 뒤에 숨는 일이 없습니다. 2월의 문제는 30초가 아니었습니다. 그 클릭에 걸린 타이머가 그것 하나뿐이었으니, 그게 울렸을 뿐이죠.</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%83%80%ec%9e%84%ec%95%84%ec%9b%83%ec%9d%80-%eb%a9%88%ec%b6%98-%ed%85%8c%ec%8a%a4%ed%8a%b8%ea%b0%80-%ec%95%84%eb%8b%88%eb%9d%bc-%ea%b8%b4-%ed%85%8c%ec%8a%a4%ed%8a%b8%ec%97%90%eb%a7%8c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>타임아웃을 늘리는 건 테스트가 실제로 시간이 걸리는 일을 할 때 맞는 해결책입니다. 어떤 단계가 일어나지 않을 일을 기다리고 있을 때는 틀린 해결책이고요. 숫자를 키워봐야 실패가 더 늦게 올 뿐이니까요. 어느 쪽인지는 대개 트레이스를 보면 정리됩니다. 액션이 하나하나 끝나는데 그냥 수가 많다면 긴 테스트이고, 액션 하나가 끝까지 <code>waiting for</code>에 머물러 있다면 멈춘 테스트입니다.</p>
<p>긴 테스트라면 스위트 전체가 아니라 실제로 긴 곳에 시간을 더하세요. 마켓플레이스 앱 스위트에는 채팅 화면을 돌면서 스크린샷을 찍는 스모크 테스트가 하나 있는데, 이 테스트에는 3분을 줍니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s2">&#34;chat: open, send a message, go back&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">test</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="mi">180</span><span class="nx">_000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p><code>describe</code> 안에서 <code>test.setTimeout()</code>을 부르면 그룹 안의 모든 테스트에 적용됩니다. 견적 앱 스위트의 작업 비용(job-cost) 테스트가 1분을 받는 방식이 바로 이겁니다. <code>test.slow()</code>는 현재 타임아웃이 얼마든 세 배로 늘리고, 조건을 받을 수도 있습니다. <code>beforeEach</code> 안에서 <code>testInfo.setTimeout()</code>을 쓰면 그 훅을 거치는 모든 테스트의 시간이 늘어납니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s2">&#34;slow in WebKit&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span><span class="p">,</span> <span class="nx">browserName</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">test</span><span class="p">.</span><span class="nx">slow</span><span class="p">(</span><span class="nx">browserName</span> <span class="o">===</span> <span class="s2">&#34;webkit&#34;</span><span class="p">,</span> <span class="s2">&#34;This page is slow in WebKit&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <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></span><span class="line"><span class="cl"><span class="nx">test</span><span class="p">.</span><span class="nx">beforeEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">},</span> <span class="nx">testInfo</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">testInfo</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="nx">testInfo</span><span class="p">.</span><span class="nx">timeout</span> <span class="o">+</span> <span class="mi">30</span><span class="nx">_000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>제한 시간을 매번 조용히 갉아먹는 게 하나 더 있습니다. 고정 대기입니다. 견적 앱 스위트에는 거의 모든 스펙 파일에 걸쳐 <code>page.waitForTimeout()</code> 호출이 백 개 넘게 있습니다. 위의 편집 테스트는 클릭에 닿기도 전에 3초를 자고 시작하는데, 얼마를 더 잤어도 살아나지는 못했을 겁니다. Playwright API 문서도 이 부분은 단호합니다. &ldquo;Never wait for timeout in production. Tests that wait for time are inherently flaky.&rdquo; 프로덕션에서는 절대 시간으로 기다리지 말라는 것, 시간을 기다리는 테스트는 본질적으로 불안정하다는 것입니다. 웹 우선 어서션(web-first assertion)은 원래 필요한 만큼만, 딱 그만큼 기다립니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="c1">// before
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;save-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForTimeout</span><span class="p">(</span><span class="mi">1500</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s2">&#34;E2E Edited Template&#34;</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// after
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;save-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s2">&#34;E2E Edited Template&#34;</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span></span></span></code></pre></div></div>
<p>어서션 타임아웃도 똑같이 조심해야 합니다. 두 앱 스위트 모두 <code>expect.timeout</code>을 15초로 올려두었고, 둘 다 프로덕션 빌드가 아니라 앱의 개발 서버를 상대로 돌아갑니다. 여유는 생기지만, 느린 어서션 두 개가 연달아 오면 둘이서 30초를 다 써버릴 수도 있습니다. 그러면 테스트 타임아웃이 먼저 터지고, 다시 모호한 메시지로 돌아가게 되죠.</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%85%8c%ec%8a%a4%ed%8a%b8%ec%9d%98-%ed%83%80%ec%9d%b4%eb%a8%b8%ea%b0%80-%ec%95%84%eb%8b%90-%eb%95%8c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>메시지에 <code>config.webServer</code>가 보인다면 아직 테스트는 하나도 실행되지 않은 겁니다. Playwright는 <code>webServer</code> 명령을 실행하고, 그 <code>url</code>이 응답할 때까지 기다린 뒤에야 테스트를 시작합니다. 이 대기에는 별도의 타임아웃이 있고, 기본값은 60초입니다. 두 앱 스위트 모두 개발 서버에 2분을 줍니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">webServer</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">command</span><span class="o">:</span> <span class="s2">&#34;npm run web -- --port 8081&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">url</span><span class="o">:</span> <span class="s2">&#34;http://localhost:8081&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">reuseExistingServer</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">timeout</span>: <span class="kt">120_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span></span></span></code></pre></div></div>
<p>테스트가 로컬 서버가 아니라 배포된 환경을 상대로 돈다면, 첫 요청이 얼마나 걸리는지부터 확인해보세요. 제가 쓰는 호스팅 환경을 마침내 측정해봤더니, 0으로 스케일 다운된 상태에서 시작하는 머신이 무엇이든 응답하기까지 25~33초가 걸렸습니다. 첫 클릭도 하기 전에 테스트 제한 시간의 대부분, 혹은 전부를 쓰는 셈입니다. 그 이야기는 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-09-09-scale-to-zero-504s/" >0으로 스케일 다운, 그리고 재시작마다 30초짜리 장애</a>에 정리해두었습니다.</p>

<h2 class="relative group">&ldquo;No tests found&quot;가 뜬다면
    <div id="no-tests-found가-뜬다면" 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="#no-tests-found%ea%b0%80-%eb%9c%ac%eb%8b%a4%eb%a9%b4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p><code>No tests found</code>는 비슷해 보이지만 타임아웃이 아닙니다. Playwright가 찾아봤는데 실행할 게 없었다는 뜻입니다. 기본적으로 <code>testDir</code> 아래에서 <code>**/*.@(spec|test).?(c|m)[jt]s?(x)</code>에 맞는 파일을 모으고, <code>testDir</code>의 기본값은 설정 파일이 있는 폴더입니다. 제 설정들은 설정 파일 옆의 <code>testDir: &quot;./tests&quot;</code>를 가리키니, 다른 곳에 저장한 스펙은 보이지 않습니다. 명령줄 인자는 파일 경로와 매칭되는 정규식이라서, 에러에 &ldquo;Make sure that arguments are regular expressions matching test files.&ldquo;라는 문장이 붙습니다. 아무것도 실행하지 않고 Playwright가 무엇을 모았는지만 보려면 이렇게 합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npx playwright <span class="nb">test</span> --list</span></span></code></pre></div></div>
<p>그 목록에 스펙이 없다면, 애초에 타임아웃이 날 일도 없었던 겁니다. 그리고 Playwright를 처음 세팅하는 중이라면, 스크린샷 비교부터 시작하자는 제 주장은 <a href="https://jared.lynskey.co.nz/ko/posts/2024/2024-06-09-playwright-tests/" >소규모 팀일수록 비주얼 테스팅</a>에 있습니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-22-playwright-timeout-30000ms/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <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>
