<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kyu_ios_dev.log</title>
        <link>https://velog.io/</link>
        <description>iOS 앱 개발자</description>
        <lastBuildDate>Tue, 05 May 2026 02:31:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. kyu_ios_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kyu_ios_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[🔥 Claude Code 토큰을 아끼는 세팅: context-mode, Bun, CLAUDE.md/AGENTS.md]]></title>
            <link>https://velog.io/@kyu_ios_dev/how-to-save-on-claude-tokens</link>
            <guid>https://velog.io/@kyu_ios_dev/how-to-save-on-claude-tokens</guid>
            <pubDate>Tue, 05 May 2026 02:31:26 GMT</pubDate>
            <description><![CDATA[<p>Claude Code를 쓰다 보면 처음에는 정말 편합니다. 파일을 열어 달라고 하면 열어 주고, 로그를 붙여 넣으면 원인을 찾아 주고, 프로젝트 구조도 금방 파악합니다.</p>
<p>그런데 프로젝트가 조금만 커져도 금방 이런 문제가 생깁니다.</p>
<ul>
<li>긴 로그를 한 번 붙였을 뿐인데 컨텍스트가 크게 줄어듭니다.</li>
<li>큰 파일 몇 개를 읽히면 대화가 무거워집니다.</li>
<li>같은 프로젝트 규칙을 매번 다시 설명하게 됩니다.</li>
<li>패키지 매니저, 빌드 명령어, 테스트 방식이 계속 흔들립니다.</li>
</ul>
<p>이 글은 그런 상황을 줄이기 위한 세팅 가이드입니다.</p>
<p>핵심은 세 가지입니다.</p>
<ol>
<li><code>context-mode</code>로 큰 파일과 로그를 그대로 넘기지 않기</li>
<li><code>Bun</code>으로 빠른 패키지 매니저와 실행 환경을 맞추기</li>
<li><code>CLAUDE.md</code> 또는 <code>AGENTS.md</code>로 프로젝트 규칙을 고정하기</li>
</ol>
<p>제대로 맞춰 두면 Claude가 매번 불필요하게 많은 내용을 읽지 않아도 됩니다. 작업 방식에 따라 토큰 사용량을 크게 줄일 수 있고, 큰 로그나 긴 파일을 다룰 때는 체감 차이가 꽤 큽니다.</p>
<p>다만 먼저 현실적인 부분부터 짚고 가겠습니다. 이 세팅이 모든 상황에서 무조건 토큰을 90% 줄여 주는 마법은 아닙니다. 효과는 프로젝트 크기, 사용 습관, Claude에게 어떤 방식으로 요청하는지에 따라 달라집니다. 그래도 “큰 파일을 통째로 읽히는 습관”을 줄이는 것만으로도 충분히 의미 있는 개선을 만들 수 있습니다.</p>
<hr>
<h2 id="왜-claude-code에서-토큰이-빨리-줄어들까">왜 Claude Code에서 토큰이 빨리 줄어들까</h2>
<p>Claude 같은 AI 도구는 대화 내용과 파일 내용, 로그, 코드 일부를 컨텍스트 안에 넣고 작업합니다. 쉽게 말하면 AI가 참고할 수 있는 작업 책상이 있는데, 그 책상 위에 너무 많은 자료를 올려 두면 공간이 빠르게 줄어드는 구조입니다.</p>
<p>예를 들어 이런 요청을 자주 한다고 해 보겠습니다.</p>
<pre><code class="language-text">이 로그 전체 보고 원인 찾아줘.</code></pre>
<p>또는 이렇게 요청할 수도 있습니다.</p>
<pre><code class="language-text">이 프로젝트 전체를 보고 구조 파악해줘.</code></pre>
<p>작은 프로젝트라면 괜찮습니다. 하지만 실제 앱 프로젝트, 서버 프로젝트, iOS 프로젝트, 프론트엔드 프로젝트처럼 파일이 많아지면 이야기가 달라집니다.</p>
<p>AI가 꼭 읽지 않아도 되는 파일까지 많이 읽게 되고, 로그도 원인과 관련 없는 부분까지 컨텍스트에 들어갑니다. 그러면 정작 중요한 코드 수정이나 분석에 쓸 공간이 줄어듭니다.</p>
<p>그래서 중요한 원칙은 이것입니다.</p>
<blockquote>
<p>AI에게 원본 자료를 전부 넘기기보다, 필요한 결과만 계산해서 넘긴다.</p>
</blockquote>
<p>이 원칙을 도와주는 도구가 바로 <code>context-mode</code>입니다.</p>
<hr>
<h2 id="context-mode는-무엇을-해-주나">context-mode는 무엇을 해 주나</h2>
<p><code>context-mode</code>는 Claude에게 큰 파일이나 긴 로그를 그대로 먹이는 대신, 로컬에서 명령을 실행하고 필요한 결과만 요약해서 넘기는 방식의 도구입니다.</p>
<p>예를 들어 Claude가 프로젝트 안에서 특정 문자열을 찾고 싶다고 해 보겠습니다.</p>
<p>기존 방식은 이런 식이 되기 쉽습니다.</p>
<pre><code class="language-text">프로젝트 파일들을 열어 보고 FIXME가 있는 곳을 찾아줘.</code></pre>
<p>그러면 Claude가 여러 파일을 직접 읽으면서 컨텍스트를 많이 사용합니다.</p>
<p>반면 <code>context-mode</code>를 쓰면 이런 식으로 바뀝니다.</p>
<pre><code class="language-text">ctx_search로 FIXME가 있는 파일과 줄만 찾아줘.</code></pre>
<p>또는 내부적으로 <code>rg</code>, <code>find</code>, <code>sed</code>, <code>git diff</code> 같은 명령 결과만 가져오게 할 수 있습니다. 그러면 Claude는 파일 전체가 아니라 “필요한 줄”, “검색 결과”, “요약된 통계”만 보게 됩니다.</p>
<p>결과적으로 컨텍스트 사용량이 줄어들고, 작업도 더 선명해집니다.</p>
<hr>
<h2 id="context-mode에서-자주-쓰는-도구들">context-mode에서 자주 쓰는 도구들</h2>
<p>설치 후 Claude에서 사용할 수 있는 대표 도구는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>도구</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>ctx_execute</code></td>
<td>셸 명령 하나를 실행하고 결과를 요약해서 가져옵니다.</td>
</tr>
<tr>
<td><code>ctx_batch_execute</code></td>
<td>여러 명령을 한 번에 실행합니다. 프로젝트 점검에 유용합니다.</td>
</tr>
<tr>
<td><code>ctx_fetch_and_index</code></td>
<td>URL이나 문서를 가져와 인덱싱합니다. 긴 문서를 다룰 때 좋습니다.</td>
</tr>
<tr>
<td><code>ctx_search</code></td>
<td>인덱싱된 자료나 프로젝트에서 필요한 내용만 검색합니다.</td>
</tr>
<tr>
<td><code>ctx_stats</code></td>
<td>context-mode 사용량과 상태를 확인합니다.</td>
</tr>
</tbody></table>
<p>초보자 입장에서는 처음부터 모든 도구를 외울 필요가 없습니다. 우선은 아래 세 가지만 기억해도 충분합니다.</p>
<pre><code class="language-text">ctx_execute: 명령 실행
ctx_search: 필요한 내용 검색
ctx_stats: 상태 확인</code></pre>
<hr>
<h2 id="준비물">준비물</h2>
<p>이 글은 macOS 또는 Linux 계열 터미널을 기준으로 설명합니다. macOS에서 Claude Code를 쓰는 경우가 많기 때문에 zsh 기준 명령어도 함께 적었습니다.</p>
<p>준비물은 다음과 같습니다.</p>
<ul>
<li>Claude Code가 설치되어 있어야 합니다.</li>
<li>터미널을 사용할 수 있어야 합니다.</li>
<li>Node.js 또는 npm이 설치되어 있으면 좋습니다.</li>
<li>Bun을 설치할 예정입니다.</li>
<li>프로젝트 루트에 <code>CLAUDE.md</code> 또는 <code>AGENTS.md</code>를 둘 수 있어야 합니다.</li>
</ul>
<p>설치가 처음이라면 명령어를 한 줄씩 실행하고, 중간에 에러가 나는지 확인하면서 진행하는 것을 추천합니다.</p>
<hr>
<h2 id="1단계-bun-설치하기">1단계: Bun 설치하기</h2>
<p>Bun은 JavaScript 런타임이자 패키지 매니저입니다. Node.js 생태계와 비슷하게 쓸 수 있지만, 설치와 실행 속도가 빠른 편입니다.</p>
<p>터미널에서 아래 명령어를 실행합니다.</p>
<pre><code class="language-bash">curl -fsSL https://bun.sh/install | bash</code></pre>
<p>설치가 끝났다면 zsh 설정 파일에 Bun 경로를 추가합니다.</p>
<pre><code class="language-bash">echo &#39;export PATH=&quot;$HOME/.bun/bin:$PATH&quot;&#39; &gt;&gt; ~/.zshrc
source ~/.zshrc</code></pre>
<p>설치가 잘 되었는지 확인합니다.</p>
<pre><code class="language-bash">bun --version</code></pre>
<p>버전 숫자가 나오면 정상입니다.</p>
<p>만약 <code>command not found: bun</code>이 나온다면 터미널을 완전히 껐다가 다시 열어 보세요. 그래도 안 된다면 아래 명령어로 경로가 들어갔는지 확인합니다.</p>
<pre><code class="language-bash">echo $PATH</code></pre>
<p><code>$HOME/.bun/bin</code> 또는 실제 홈 경로의 <code>.bun/bin</code>이 보이면 됩니다.</p>
<hr>
<h2 id="2단계-context-mode-설치하기">2단계: context-mode 설치하기</h2>
<p>이제 <code>context-mode</code>를 설치합니다.</p>
<pre><code class="language-bash">npm install -g context-mode</code></pre>
<p>설치 후 도움말이나 상태 명령을 실행해 봅니다.</p>
<pre><code class="language-bash">ctx stats</code></pre>
<p>환경에 따라 명령어가 <code>ctx_stats</code>처럼 노출될 수도 있습니다. 설치한 패키지 버전에 따라 이름이 다를 수 있으니, 명령이 먹지 않으면 아래처럼 도움말을 먼저 확인해 보세요.</p>
<pre><code class="language-bash">context-mode --help</code></pre>
<p>또는 설치 위치를 확인합니다.</p>
<pre><code class="language-bash">which context-mode</code></pre>
<p>여기까지 성공했다면 로컬에서 context-mode를 쓸 준비는 된 것입니다.</p>
<hr>
<h2 id="3단계-claude-mcp-설정에-context-mode-등록하기">3단계: Claude MCP 설정에 context-mode 등록하기</h2>
<p>Claude가 <code>context-mode</code>를 사용하려면 MCP 서버로 등록해야 합니다.</p>
<p>Claude Code 환경마다 설정 파일 위치가 조금 다를 수 있습니다. 이미 MCP 설정을 쓰고 있다면 기존 설정에 <code>context-mode</code> 항목만 추가하면 됩니다.</p>
<p>예시는 다음과 같습니다.</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;context-mode&quot;: {
      &quot;command&quot;: &quot;context-mode&quot;,
      &quot;args&quot;: [&quot;mcp&quot;]
    }
  }
}</code></pre>
<p>이미 다른 MCP 서버가 있다면 <code>mcpServers</code> 안에 함께 넣어 주세요.</p>
<p>예를 들어 기존 설정이 이런 형태라면,</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;github&quot;: {
      &quot;command&quot;: &quot;github-mcp-server&quot;
    }
  }
}</code></pre>
<p>아래처럼 하나를 더 추가합니다.</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;github&quot;: {
      &quot;command&quot;: &quot;github-mcp-server&quot;
    },
    &quot;context-mode&quot;: {
      &quot;command&quot;: &quot;context-mode&quot;,
      &quot;args&quot;: [&quot;mcp&quot;]
    }
  }
}</code></pre>
<p>설정을 바꾼 뒤에는 Claude Code를 재시작하는 것이 좋습니다. MCP 설정은 실행 중 자동 반영되지 않는 경우가 많습니다.</p>
<hr>
<h2 id="4단계-hooks-설정하기">4단계: Hooks 설정하기</h2>
<p>Hooks는 Claude가 도구를 사용하기 전후에 특정 작업을 걸어 둘 수 있는 설정입니다. 쉽게 말하면 “Claude가 작업할 때 지켜야 할 자동 규칙”을 붙이는 느낌입니다.</p>
<p>예시는 다음과 같습니다.</p>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;PreToolUse&quot;: [
      {
        &quot;matcher&quot;: &quot;*&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;context-mode hook pre&quot;
          }
        ]
      }
    ],
    &quot;PostToolUse&quot;: [
      {
        &quot;matcher&quot;: &quot;*&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;context-mode hook post&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<p>이 설정은 프로젝트나 Claude Code 설정 방식에 따라 넣는 위치가 달라질 수 있습니다. 중요한 것은 <code>PreToolUse</code>, <code>PostToolUse</code>가 실제로 Claude의 도구 호출 전후에 실행되도록 연결하는 것입니다.</p>
<p>처음부터 Hooks까지 복잡하게 느껴진다면, 우선 MCP 등록만 해 두고 사용해도 됩니다. 이후에 컨텍스트 관리가 더 필요해졌을 때 Hooks를 추가해도 늦지 않습니다.</p>
<hr>
<h2 id="5단계-claudemd-또는-agentsmd-만들기">5단계: CLAUDE.md 또는 AGENTS.md 만들기</h2>
<p>Claude에게 매번 같은 설명을 반복하지 않으려면 프로젝트 루트에 규칙 파일을 만들어 두는 것이 좋습니다.</p>
<p>Claude 전용으로는 <code>CLAUDE.md</code>를 많이 사용합니다. 여러 AI 코딩 도구와 함께 쓸 예정이라면 <code>AGENTS.md</code>도 좋습니다.</p>
<p>예를 들어 프로젝트 루트에 아래 파일을 만듭니다.</p>
<pre><code class="language-bash">touch CLAUDE.md</code></pre>
<p>또는</p>
<pre><code class="language-bash">touch AGENTS.md</code></pre>
<p>그리고 다음처럼 프로젝트 규칙을 적습니다.</p>
<pre><code class="language-markdown"># Project Rules

## Package Manager
- Use Bun when possible.
- Use `bun install` instead of `npm install`.
- Use `bun run` for scripts.

## Context Usage
- Prefer context-mode tools for large files, logs, and broad searches.
- Do not paste full logs unless necessary.
- Use search commands before opening many files.

## Code Style
- Keep changes small and focused.
- Do not rewrite unrelated files.
- Run tests or build checks after meaningful changes when possible.

## Project Notes
- Write important implementation notes here.
- Add build commands and test commands here.</code></pre>
<p>너무 길게 쓰는 것보다, 프로젝트에서 자주 틀리는 부분을 짧고 명확하게 적는 것이 좋습니다.</p>
<p>예를 들어 iOS 프로젝트라면 이런 내용을 추가할 수 있습니다.</p>
<pre><code class="language-markdown">## iOS Build
- Use Xcode project settings as the source of truth.
- Do not modify signing settings unless asked.
- Prefer small UIKit/SwiftUI changes that match the existing design system.</code></pre>
<p>프론트엔드 프로젝트라면 이런 식도 좋습니다.</p>
<pre><code class="language-markdown">## Frontend
- Preserve the existing design language.
- Avoid adding new dependencies unless necessary.
- Check mobile layout when touching UI.</code></pre>
<p>이 파일의 목적은 Claude를 묶어 두는 것이 아니라, 프로젝트의 기본 습관을 알려 주는 것입니다.</p>
<hr>
<h2 id="6단계-bun-명령어로-바꾸기">6단계: Bun 명령어로 바꾸기</h2>
<p>Bun을 설치했다면 자주 쓰는 npm 명령어를 Bun 기준으로 바꿔 두면 좋습니다.</p>
<table>
<thead>
<tr>
<th>기존 명령어</th>
<th>Bun 명령어</th>
</tr>
</thead>
<tbody><tr>
<td><code>npm install</code></td>
<td><code>bun install</code></td>
</tr>
<tr>
<td><code>npm install 패키지</code></td>
<td><code>bun add 패키지</code></td>
</tr>
<tr>
<td><code>npm uninstall 패키지</code></td>
<td><code>bun remove 패키지</code></td>
</tr>
<tr>
<td><code>npm run dev</code></td>
<td><code>bun run dev</code></td>
</tr>
<tr>
<td><code>npm run build</code></td>
<td><code>bun run build</code></td>
</tr>
<tr>
<td><code>npx some-tool</code></td>
<td><code>bunx some-tool</code></td>
</tr>
</tbody></table>
<p>다만 모든 프로젝트에서 무조건 Bun으로 바꾸는 것은 조심해야 합니다.</p>
<p>이미 팀에서 <code>package-lock.json</code>을 기준으로 npm을 쓰고 있다면 그대로 유지하는 편이 안전할 수 있습니다. 반대로 개인 프로젝트이거나 Bun을 쓰기로 결정한 프로젝트라면 <code>CLAUDE.md</code>나 <code>AGENTS.md</code>에 명확히 적어 두는 것이 좋습니다.</p>
<pre><code class="language-markdown">## Package Manager
- This project uses Bun.
- Do not create `package-lock.json`.
- Use `bun.lock` as the lockfile.</code></pre>
<p>이렇게 적어 두면 Claude가 실수로 npm 명령을 쓰는 일을 줄일 수 있습니다.</p>
<hr>
<h2 id="7단계-제대로-적용됐는지-확인하기">7단계: 제대로 적용됐는지 확인하기</h2>
<p>세팅 후에는 바로 큰 작업을 맡기기보다 작은 요청으로 테스트해 보는 것이 좋습니다.</p>
<p>예를 들어 Claude에게 이렇게 요청해 볼 수 있습니다.</p>
<pre><code class="language-text">프로젝트에서 FIXME 주석이 있는 파일과 줄 번호만 찾아줘. 파일 전체를 열지 말고 검색 결과만 요약해줘.</code></pre>
<p>또는 이렇게 요청해 볼 수 있습니다.</p>
<pre><code class="language-text">최근 git diff를 요약해줘. 변경된 파일 전체를 읽기보다 핵심 변경점만 정리해줘.</code></pre>
<p>Claude가 <code>ctx_search</code>, <code>ctx_execute</code>, <code>rg</code>, <code>git diff --stat</code> 같은 방식으로 필요한 결과만 가져온다면 방향이 잘 잡힌 것입니다.</p>
<p>상태 확인 명령도 실행해 봅니다.</p>
<pre><code class="language-bash">ctx stats</code></pre>
<p>여기서 중요한 것은 숫자 하나에 집착하지 않는 것입니다. 컨텍스트 절감 효과는 작업 방식에 따라 달라집니다. 대신 다음과 같은 변화가 있는지 보세요.</p>
<ul>
<li>Claude가 큰 파일을 무작정 열지 않는다.</li>
<li>검색 결과 중심으로 판단한다.</li>
<li>긴 로그를 통째로 읽기보다 필요한 줄만 본다.</li>
<li>같은 프로젝트 규칙을 반복해서 설명하지 않아도 된다.</li>
</ul>
<p>이 네 가지가 잡히면 세팅 효과는 충분히 나오고 있는 것입니다.</p>
<hr>
<h2 id="실제로-써-보면-좋은-요청-방식">실제로 써 보면 좋은 요청 방식</h2>
<p>세팅만큼 중요한 것이 요청 방식입니다. 같은 일을 시켜도 어떻게 말하느냐에 따라 컨텍스트 사용량이 크게 달라집니다.</p>
<p>좋은 예시는 다음과 같습니다.</p>
<pre><code class="language-text">전체 파일을 다 읽지 말고, 먼저 관련 파일을 검색해서 후보를 좁힌 다음 수정해줘.</code></pre>
<pre><code class="language-text">이 로그에서 에러 원인으로 보이는 부분만 추려서 설명해줘.</code></pre>
<pre><code class="language-text">변경 전에 관련 함수가 어디서 호출되는지 검색으로 확인해줘.</code></pre>
<pre><code class="language-text">큰 파일은 필요한 함수 주변만 읽어줘.</code></pre>
<p>반대로 아래 요청은 컨텍스트를 많이 쓰기 쉽습니다.</p>
<pre><code class="language-text">프로젝트 전체를 다 보고 문제점 알려줘.</code></pre>
<pre><code class="language-text">이 로그 전체 분석해줘.</code></pre>
<pre><code class="language-text">모든 파일 열어서 구조 파악해줘.</code></pre>
<p>물론 정말 전체 분석이 필요한 순간도 있습니다. 하지만 매번 그렇게 시작하면 컨텍스트가 빠르게 줄어듭니다. 먼저 검색으로 범위를 좁히고, 필요한 파일만 읽히는 습관이 좋습니다.</p>
<hr>
<h2 id="claudemdagentsmd에-넣으면-좋은-내용">CLAUDE.md/AGENTS.md에 넣으면 좋은 내용</h2>
<p>규칙 파일에는 모든 것을 적을 필요가 없습니다. Claude가 작업할 때 자주 헷갈릴 만한 것만 넣으면 됩니다.</p>
<p>추천 항목은 다음과 같습니다.</p>
<ul>
<li>프로젝트 실행 명령어</li>
<li>테스트 명령어</li>
<li>빌드 명령어</li>
<li>패키지 매니저 기준</li>
<li>코드 스타일</li>
<li>수정하면 안 되는 파일</li>
<li>자주 발생하는 빌드 이슈</li>
<li>배포 전 확인 항목</li>
<li>UI 작업 시 지켜야 할 디자인 방향</li>
</ul>
<p>예시는 다음과 같습니다.</p>
<pre><code class="language-markdown"># Project Guide

## Commands
- Install: `bun install`
- Dev: `bun run dev`
- Build: `bun run build`
- Test: `bun test`

## Rules
- Keep edits focused.
- Do not change generated files manually.
- Do not add dependencies without explaining why.
- Prefer searching before opening large files.

## Before Finishing
- Run the smallest relevant verification command.
- Summarize changed files and why they changed.</code></pre>
<p>초보자라면 처음에는 이 정도만 적어도 충분합니다. 프로젝트를 진행하면서 필요한 규칙을 조금씩 추가하면 됩니다.</p>
<hr>
<h2 id="흔한-실수와-해결-방법">흔한 실수와 해결 방법</h2>
<h3 id="1-mcp-설정-후-claude-code를-재시작하지-않음">1. MCP 설정 후 Claude Code를 재시작하지 않음</h3>
<p>MCP 설정을 바꿨는데 도구가 보이지 않는다면 Claude Code를 재시작해 보세요. 설정 파일은 저장했지만 실행 중인 세션에는 반영되지 않았을 수 있습니다.</p>
<h3 id="2-claudemd를-너무-길게-작성함">2. CLAUDE.md를 너무 길게 작성함</h3>
<p>규칙 파일이 너무 길면 오히려 매번 읽어야 할 정보가 많아집니다. 프로젝트에 꼭 필요한 규칙만 남기는 것이 좋습니다.</p>
<h3 id="3-npm과-bun을-섞어서-사용함">3. npm과 Bun을 섞어서 사용함</h3>
<p>한 프로젝트에서 <code>package-lock.json</code>, <code>bun.lock</code>, <code>yarn.lock</code>이 뒤섞이면 의존성 관리가 꼬일 수 있습니다. 팀 프로젝트라면 기존 방식을 따르고, 개인 프로젝트라면 하나로 정해 두세요.</p>
<h3 id="4-로그를-그대로-붙여-넣는-습관을-유지함">4. 로그를 그대로 붙여 넣는 습관을 유지함</h3>
<p>context-mode를 설치해도 사용 습관이 그대로라면 효과가 줄어듭니다. 긴 로그는 파일로 두고, Claude에게 필요한 부분만 검색하게 하는 편이 좋습니다.</p>
<h3 id="5-프로젝트-전체를-봐줘로-시작함">5. “프로젝트 전체를 봐줘”로 시작함</h3>
<p>처음부터 전체 분석을 시키기보다, 증상과 관련된 키워드, 파일명, 에러 메시지를 기준으로 좁혀 가는 편이 좋습니다.</p>
<hr>
<h2 id="개인적으로-추천하는-기본-세팅">개인적으로 추천하는 기본 세팅</h2>
<p>처음 시작한다면 너무 복잡하게 가지 말고 아래 순서로 적용하는 것을 추천합니다.</p>
<ol>
<li>Bun 설치</li>
<li>context-mode 설치</li>
<li>Claude MCP에 context-mode 등록</li>
<li>프로젝트 루트에 <code>CLAUDE.md</code> 작성</li>
<li>큰 파일을 열기 전에 검색부터 하도록 요청 습관 바꾸기</li>
<li>필요해지면 Hooks 추가</li>
</ol>
<p>처음부터 Hooks와 고급 설정까지 완벽하게 맞추려고 하면 오히려 중간에 지칠 수 있습니다. 먼저 기본 흐름을 만들고, 실제 프로젝트에서 불편한 지점을 하나씩 보완하는 방식이 가장 오래 갑니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>Claude Code를 잘 쓰는 핵심은 “AI에게 많이 보여 주는 것”이 아니라 “AI가 필요한 것을 정확히 보게 하는 것”입니다.</p>
<p><code>context-mode</code>는 큰 파일과 긴 로그를 다룰 때 컨텍스트 낭비를 줄여 줍니다. <code>Bun</code>은 빠른 실행 환경과 패키지 관리 흐름을 만들어 줍니다. <code>CLAUDE.md</code>와 <code>AGENTS.md</code>는 프로젝트마다 반복되는 규칙 설명을 줄여 줍니다.</p>
<p>이 세 가지를 함께 쓰면 Claude Code가 더 가볍고 일관되게 움직입니다. 특히 프로젝트가 커질수록 차이가 커집니다.</p>
<p>처음에는 작은 프로젝트 하나에만 적용해 보세요. 그리고 Claude에게 이렇게 요청해 보면 됩니다.</p>
<pre><code class="language-text">큰 파일을 바로 열지 말고, 먼저 검색으로 관련 위치를 좁힌 다음 필요한 부분만 읽어서 작업해줘.</code></pre>
<p>이 한 문장만 습관이 되어도 Claude Code를 쓰는 방식이 꽤 달라집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 레이어 최적화: async/await + HTTP/2/3 관점에서 (Swift 6 · iOS 18 · Xcode 26)]]></title>
            <link>https://velog.io/@kyu_ios_dev/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%B5%9C%EC%A0%81%ED%99%94-asyncawait-HTTP23-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C-Swift-6-iOS-18-Xcode-26</link>
            <guid>https://velog.io/@kyu_ios_dev/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%B5%9C%EC%A0%81%ED%99%94-asyncawait-HTTP23-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C-Swift-6-iOS-18-Xcode-26</guid>
            <pubDate>Fri, 19 Sep 2025 10:35:29 GMT</pubDate>
            <description><![CDATA[<h2 id="네트워크-레이어-최적화-asyncawait--http23-관점에서-swift-6-·-ios-18-·-xcode-26">네트워크 레이어 최적화: async/await + HTTP/2/3 관점에서 (Swift 6 · iOS 18 · Xcode 26)</h2>
<p>요약
앱의 네트워크 레이어는 단순한 API 호출 집합이 아니라 앱 성능과 사용자 경험을 좌우하는 핵심 인프라입니다. Swift의 async/await를 사용해 깔끔하게 비동기 흐름을 설계하면서, URLSession 설정(커넥션 풀·HTTP/2·HTTP/3 친화적), 요청 중복 제거(in-flight dedupe), 요청 우선순위·취소 처리, 로컬/네트워크 캐시, 안정적인 재시도(backoff) 전략을 조합하면 대역폭·CPU·메모리 사용을 줄이고 응답성을 크게 향상시킬 수 있습니다. 아래는 개념 설명과 실전 코드(복사해서 바로 붙여넣어 쓸 수 있음)입니다.</p>
<p>⸻</p>
<p>1) 설계 목표(무엇을 개선하려는가)
    •    불필요한 동시 연결 수를 줄여 서버/클라이언트 부하를 완화.
    •    동일한 리소스에 대해 중복 요청을 막아 대역폭 절약.
    •    HTTP/2의 멀티플렉싱과 HTTP/3(QUIC)의 장점을 살리되, 서버·네트워크 조건에 유연하게 대응.
    •    네트워크 상태(셀룰러/와이파이/제한 모드)에 따른 정책(해상도·동시도) 적용.
    •    async/await 기반으로 취소·에러·재시도를 명확하게 처리.</p>
<p>⸻</p>
<p>2) URLSession 구성 포인트 (핵심 속성과 이유)
    •    URLSessionConfiguration.httpMaximumConnectionsPerHost
한 호스트당 동시 연결 수 제한. HTTP/2의 경우 멀티플렉싱으로 많은 스트림을 하나의 연결에서 처리하므로 너무 높게 설정할 필요 없음(대개 6~10 권장, 앱·서버 특성에 따라 조정).
    •    URLSessionConfiguration.requestCachePolicy 및 URLCache 설정
적절한 메모리/디스크 캐시를 두면 동일 요청 재시작 시 네트워크 호출을 줄일 수 있음. (특히 이미지·정적 리소스)
    •    configuration.waitsForConnectivity = true
네트워크가 없을 때 자동 재시도/대기 로직을 URLSession에게 위임해 UX를 개선.
    •    configuration.allowsExpensiveNetworkAccess / allowsConstrainedNetworkAccess
사용자의 네트워크 조건이나 전원 정책에 따라 요청을 제한하거나 허용할 수 있음(iOS에서 제공).
    •    TLS 및 보안 관련 설정
ATS(앱 전송 보안)를 지키되, 서버 측에서 HTTP/3(QUIC) 지원 시 프로토콜 우선순위를 확인. iOS는 네이티브로 QUIC을 지원하면 HTTP/3를 사용함(서버·OS 지원 필요).</p>
<p>⸻</p>
<p>3) 요청 중복 방지(in-flight dedupe)와 응답 재사용</p>
<p>동일 URL(또는 동일한 리소스 식별자)에 대해 여러 UI 요소가 동시에 요청을 발생시키면 네트워크 낭비가 발생한다. 이를 방지하려면 “in-flight map”을 두어 이미 진행 중인 Task를 재사용한다.</p>
<p>아래 NetworkClient 예제는 async/await와 Task를 사용해 in-flight dedupe, 기본 재시도 로직, 취소 처리를 포함합니다.</p>
<pre><code>import Foundation

enum NetworkError: Error {
    case invalidResponse
    case httpError(status: Int, data: Data?)
}

final class NetworkClient {
    static let shared = NetworkClient()

    private let session: URLSession
    // in-flight dedupe: URLRequest key -&gt; Task&lt;Data, Error&gt;
    private var inFlight = [String: Task&lt;Data, Error&gt;]()
    private let lock = NSLock() // 간단 안전성 보장 (actor 대체 가능)

    init() {
        let config = URLSessionConfiguration.ephemeral
        config.httpMaximumConnectionsPerHost = 8
        config.waitsForConnectivity = true
        config.requestCachePolicy = .useProtocolCachePolicy
        // URLCache 커스터마이징(메모리/디스크)
        config.urlCache = URLCache(memoryCapacity: 50 * 1024 * 1024,
                                  diskCapacity: 200 * 1024 * 1024,
                                  diskPath: &quot;network-cache&quot;)

        session = URLSession(configuration: config)
    }

    // requestKey: 요청을 고유하게 식별하는 문자열 (URL + method + bodyHash 등)
    private func requestKey(for request: URLRequest) -&gt; String {
        var s = request.url?.absoluteString ?? &quot;&quot;
        s += &quot;|\(request.httpMethod ?? &quot;GET&quot;)&quot;
        if let body = request.httpBody {
            s += &quot;|\(body.hashValue)&quot;
        }
        return s
    }

    func data(for request: URLRequest, retries: Int = 2) async throws -&gt; Data {
        let key = requestKey(for: request)

        lock.lock()
        if let t = inFlight[key] {
            lock.unlock()
            return try await t.value
        }

        let task = Task&lt;Data, Error&gt; {
            defer {
                self.lock.lock()
                self.inFlight.removeValue(forKey: key)
                self.lock.unlock()
            }
            return try await self.performRequestWithRetry(request: request, retries: retries)
        }

        inFlight[key] = task
        lock.unlock()

        return try await task.value
    }

    private func performRequestWithRetry(request: URLRequest, retries: Int) async throws -&gt; Data {
        var attempt = 0
        var delay: TimeInterval = 0.2

        while true {
            if Task.isCancelled { throw CancellationError() }
            do {
                let (data, response) = try await session.data(for: request)
                guard let http = response as? HTTPURLResponse else { throw NetworkError.invalidResponse }
                if 200..&lt;300 ~= http.statusCode {
                    return data
                } else {
                    throw NetworkError.httpError(status: http.statusCode, data: data)
                }
            } catch {
                attempt += 1
                // 특정 에러 타입은 재시도하지 않음(예: 4xx)
                if attempt &gt; retries || isNonRetriable(error: error) {
                    throw error
                }
                // 지수 백오프 + jitter
                let jitter = Double.random(in: 0...(delay * 0.1))
                try await Task.sleep(nanoseconds: UInt64((delay + jitter) * 1_000_000_000))
                delay = min(delay * 2, 5.0)
            }
        }
    }

    private func isNonRetriable(error: Error) -&gt; Bool {
        if let netErr = error as? URLError {
            // 타임아웃이나 네트워크 연결 불가 등은 재시도 가능
            // 인증 실패, bad request 등은 재시도 불가
            switch netErr.code {
            case .userAuthenticationRequired, .userCancelledAuthentication, .cannotFindHost:
                return true
            default:
                return false
            }
        }
        if case NetworkError.httpError(let status, _) = error {
            return (400..&lt;500).contains(status)
        }
        return false
    }
}</code></pre><p>포인트 설명
    •    inFlight 맵은 동일한 요청키에 대해 이미 생성된 Task를 다른 호출자들이 재사용하도록 한다.
    •    NSLock으로 간단한 스레드 안전을 보장했지만, 대규모 동시성 코드에서는 actor로 대체하는 편이 더 안전하다.
    •    performRequestWithRetry는 재시도 횟수와 지수적 백오프(jitter 포함)를 적용한다. 4xx 계열의 클라이언트 에러는 재시도하지 않도록 판단한다.</p>
<p>⸻</p>
<p>4) HTTP/2와 HTTP/3 고려사항
    •    HTTP/2 장점: 단일 TCP 연결 위에서 스트림을 멀티플렉싱하므로 연결 수를 줄이고 레이턴시를 개선한다. 하지만 서버가 연결당 허용하는 동시 스트림 수(limit)를 두므로, 과도한 동시 스트림 생성은 서버에서 거부되거나 지연을 초래할 수 있다. 따라서 httpMaximumConnectionsPerHost를 무작정 크게 하지 말고 서버 한도와 실측을 기준으로 튜닝한다.
    •    HTTP/3(QUIC) 장점: UDP 기반의 QUIC는 연결/핸드쉐이크 지연이 작고 패킷 손실에 덜 민감해 모바일 환경에서 유리할 수 있다. iOS 네트워크 스택이 서버와 협의해 자동으로 HTTP/3를 사용할 수 있으니 클라이언트에서 별도 처리 없이 이점을 누릴 수 있다. 다만 서버(및 CDN)가 HTTP/3을 지원해야 한다.
    •    멀티플렉싱 주의: 이미지 같은 큰 스트리밍 리소스는 멀티플렉싱으로 인해 작은 요청의 응답이 지연될 수 있다. 이런 경우에는 큰 파일은 별도 전용 연결(또는 범위를 나눠 다운샘플링)로 처리하는 전략을 고려한다.</p>
<p>⸻</p>
<p>5) 우선순위·취소·부하 제어(광고·미리 로드 등)
    •    UI가 즉시 필요로 하는 요청(예: 현재 화면의 프로필 이미지)과 백그라운드 사전 페칭을 구분한다. 우선순위 높은 요청은 in-flight 우선순위 큐에 넣거나 별도 URLRequest의 networkServiceType/priority 속성을 사용해 OS에게 힌트를 준다.
    •    Task 취소를 적극적으로 사용해 사용자가 화면을 벗어나면 관련 요청을 중단하고 리소스를 해제한다. URLSession의 data(for:) 호출은 Task 취소와 연동되어 요청을 취소한다.
    •    과도한 동시 요청은 클라이언트에서 페이싱(pacing)으로 제어(예: per-host 세마포어)한다. per-host 세마포어는 httpMaximumConnectionsPerHost와는 별개로 클라이언트가 조정 가능한 안전장치다.</p>
<p>⸻</p>
<p>6) 캐시 전략(요청/응답 캐시 + 로컬 최적화)
    •    서버가 적절한 Cache-Control, ETag 헤더를 제공하면 클라이언트는 URLCache를 통해 네트워크 호출을 줄일 수 있다. 중요한 것은 서버와 협업해 TTL과 revalidation 정책을 설계하는 것.
    •    정적 리소스(이미지, 폰트 등)는 디스크 캐시를 넉넉히 잡아두고 만료·정리 정책을 서버 정책과 맞춘다.
    •    민감 데이터(인증이 필요한 리소스)는 캐시하지 않거나, 캐시 보안(암호화) 정책을 별도 둔다.</p>
<p>⸻</p>
<p>7) 예외 처리와 관찰(모니터링)
    •    각 요청의 시작/종료/에러 타입(타임아웃, DNS 실패, TLS 실패 등)을 로깅해 지표로 수집한다. P95, P99 레이턴시, 재시도율, 취소율 같은 메트릭은 네트워크 정책을 튜닝하는 근거가 된다.
    •    네트워크 레이어에서 중요한 이벤트(예: 서버의 5xx 비율 상승)는 상위 레이어(서버 팀)로 알리고, 클라이언트에서는 자동 페이싱/기능 축소(저해상도 모드 등)를 고려한다.</p>
<p>⸻</p>
<p>8) SwiftUI/앱 코드와의 통합(사용 예)</p>
<p>SwiftUI 뷰에서 NetworkClient를 쉽게 쓰는 방법 예시:</p>
<pre><code>@MainActor
final class ProfileViewModel: ObservableObject {
    @Published var avatar: UIImage?

    func loadAvatar(url: URL) {
        Task {
            var req = URLRequest(url: url)
            req.httpMethod = &quot;GET&quot;
            // 필요 시 priority/headers 설정
            do {
                let data = try await NetworkClient.shared.data(for: req)
                if Task.isCancelled { return }
                avatar = UIImage(data: data)
            } catch {
                // 에러 처리(placeholder 등)
                print(&quot;avatar load failed:&quot;, error)
            }
        }
    }
}</code></pre><p>뷰가 사라질 때 Task를 취소하면 URLSession 호출도 즉시 취소되어 불필요한 네트워크 사용을 막는다.</p>
<p>⸻</p>
<p>마무리(권장 단계)
    1.    기본 URLSessionConfiguration을 적절히 세팅(connectionPerHost, cache 등).
    2.    in-flight dedupe와 재시도 정책을 적용해 동일 요청·비정상 네트워크 상황에서의 낭비를 줄인다.
    3.    HTTP/2·HTTP/3 특성을 이해하고 서버·CDN과 협의해 최적화(썸네일 서비스, Accept 헤더 등)를 진행한다.
    4.    실제 기기에서 다양한 네트워크(와이파이/4G/3G/로우파워) 시나리오로 측정하고 동시도·재시도 정책을 조정한다.
    5.    메트릭을 수집해 동작을 관찰하고, 필요한 경우 적응형(policies) 동시도 조절을 도입한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TaskGroup 성능 튜닝과 스로틀링 패턴 — 동시성 한계 관리( Swift 6 · iOS 18 · Xcode 26 )]]></title>
            <link>https://velog.io/@kyu_ios_dev/TaskGroup-%EC%84%B1%EB%8A%A5-%ED%8A%9C%EB%8B%9D%EA%B3%BC-%EC%8A%A4%EB%A1%9C%ED%8B%80%EB%A7%81-%ED%8C%A8%ED%84%B4-Swift-6-iOS-18-Xcode-26</link>
            <guid>https://velog.io/@kyu_ios_dev/TaskGroup-%EC%84%B1%EB%8A%A5-%ED%8A%9C%EB%8B%9D%EA%B3%BC-%EC%8A%A4%EB%A1%9C%ED%8B%80%EB%A7%81-%ED%8C%A8%ED%84%B4-Swift-6-iOS-18-Xcode-26</guid>
            <pubDate>Fri, 19 Sep 2025 01:48:17 GMT</pubDate>
            <description><![CDATA[<h2 id="taskgroup-성능-튜닝과-스로틀링-패턴--동시성-한계-관리-swift-6-·-ios-18-·-xcode-26-">TaskGroup 성능 튜닝과 스로틀링 패턴 — 동시성 한계 관리( Swift 6 · iOS 18 · Xcode 26 )</h2>
<p>요약
무제한 병렬화는 직관적으로 성능을 올릴 것 같지만, 실제로는 네트워크 커넥션 제한, 디코딩 CPU, 메모리 점유, 디스크 I/O 등 실자원(physical resource)을 포화시켜 전체 처리량·응답성을 떨어뜨립니다. TaskGroup으로 작업을 묶되, 동시 실행 수(동시도, concurrency)를 제어하는 경량 동기화(예: AsyncSemaphore), 우선순위 분리, 리트라이·백오프, 취소 처리 전략을 결합하면 안정적이고 예측 가능한 병렬 처리 시스템을 만들 수 있습니다. 아래는 개념 설명, 설계 고려사항, 구체적 코드, 튜닝·테스트 방법, 그리고 운영에서 관찰해야 할 지표를 포함한 실무 가이드입니다.</p>
<p>⸻</p>
<p>문제의 본질 — 왜 무제한 병렬화가 역효과를 내는가?</p>
<p>동시에 더 많은 작업을 실행하면 이상적으로 처리량이 늘어납니다. 하지만 현실은 다음과 같은 병목이 존재합니다.
    •    네트워크 제한: 모바일 기기와 서버는 동시 연결/스레드 제한을 가지고 있습니다. 동시 요청이 많아지면 TCP 연결 재사용 비용, 큐잉, 패킷 손실로 전체 지연이 커집니다.
    •    CPU 바운드 작업: 이미지 디코딩, 압축 해제, 암호화/복호화 같은 작업은 CPU를 집중적으로 사용합니다. 디코딩을 병렬로 너무 많이 돌리면 컨텍스트 스위칭과 캐시 미스가 증가합니다.
    •    메모리 압박: 각 작업이 큰 버퍼(예: 이미지 바이트)를 할당하면 OOM(Out Of Memory) 위험이 커집니다.
    •    디스크 I/O 병목: 로컬에 쓰기/읽기를 많이 하면 디스크 큐가 늘어나고 응답성이 떨어집니다.</p>
<p>즉, 병렬 작업 수를 제어하지 않으면 자원 경쟁이 발생해 처리량과 응답성 모두 악화됩니다. 따라서 동시도 제한(스로틀링)은 안정성 향상과 전체 처리량 최적화에 필수입니다.</p>
<p>⸻</p>
<p>설계 아이디어 요약
    1.    경량 동시성 제어: actor 기반 AsyncSemaphore 등으로 동시 슬롯을 관리해 작업 시작 수를 제한한다.
    2.    구조화된 실행: withThrowingTaskGroup 또는 withTaskGroup을 사용하되, 실제 자원 사용 시점(예: 네트워크 호출, 디코딩 시작)에 세마포어를 획득한다.
    3.    우선순위 분리: 긴급(로딩 화면) vs 백그라운드(사전 페칭) 작업을 다른 세마포어나 풀로 분리한다.
    4.    취소 일관성: 부모 Task 취소 시 자식 Task가 빠르게 반응하도록 Task.checkCancellation()과 defer 리소스 정리를 활용한다.
    5.    리트라이와 백오프: 네트워크 오류에 대해 지수 백오프 + jitter를 사용해 재시도한다.
    6.    적응형 동시도: 실패율/지연을 관측해 동시도를 동적으로 조정(증가/감소)하는 적응형 알고리즘을 적용 가능.</p>
<p>⸻</p>
<p>AsyncSemaphore (actor 기반) — 구현과 설명</p>
<p>actor를 이용하면 동시성 안전하게 대기자 목록을 관리할 수 있습니다. 아래 코드는 간단명료하며 Task 취소도 자연스럽게 작동합니다.</p>
<pre><code>import Foundation

actor AsyncSemaphore {
    private var value: Int
    private var waiters: [CheckedContinuation&lt;Void, Never&gt;] = []

    init(value: Int) {
        precondition(value &gt;= 0)
        self.value = value
    }

    func wait() async {
        if value &gt; 0 {
            value -= 1
            return
        }
        await withCheckedContinuation { (continuation: CheckedContinuation&lt;Void, Never&gt;) in
            waiters.append(continuation)
        }
    }

    func signal() {
        if let first = waiters.first {
            waiters.removeFirst()
            first.resume()
        } else {
            value += 1
        }
    }

    func availablePermits() -&gt; Int { value }
}</code></pre><ul>
<li>wait()는 사용 가능한 슬롯이 있으면 즉시 반환하고, 없으면 비동기적으로 대기열에 들어갑니다.</li>
<li>signal()은 대기자가 있으면 하나를 깨우고, 없으면 가용 슬롯을 증가시킵니다.</li>
<li>actor 내부에 상태가 있으므로 별도의 락 없이 안전합니다.</li>
</ul>
<p>⸻</p>
<p>TaskGroup + Semaphore 조합 예제</p>
<p>TaskGroup 안에서 무작정 addTask만 하고 내부에서 기다리지 않으면 생성만 되고 자원 사용 시점은 통제되지 않습니다. 그래서 네트워크 호출 직전에 await semaphore.wait()를 호출하도록 설계합니다.</p>
<pre><code>import Foundation

func fetchURLsLimited(_ urls: [URL], concurrency: Int) async -&gt; [Result&lt;Data, Error&gt;] {
    let semaphore = AsyncSemaphore(value: concurrency)
    var results = Array&lt;Result&lt;Data, Error&gt;?&gt;(repeating: nil, count: urls.count)

    await withTaskGroup(of: Void.self) { group in
        for (index, url) in urls.enumerated() {
            if Task.isCancelled { break }

            group.addTask {
                await semaphore.wait()
                defer { semaphore.signal() }

                if Task.isCancelled {
                    results[index] = .failure(CancellationError())
                    return
                }

                do {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    results[index] = .success(data)
                } catch {
                    results[index] = .failure(error)
                }
            }
        }
        // 그룹이 끝날 때까지 대기
        await group.waitForAll()
    }
    return results.compactMap { $0 }
}</code></pre><p>포인트
    •    세마포어 획득은 네트워크 시작 직전에 하므로, 그룹이 많이 생성되어도 실제 자원 사용 수는 제한됩니다.
    •    defer로 항상 signal()을 호출해 슬롯 반환을 보장합니다.
    •    결과는 인덱스 기반 배열에 기록해 원래 순서를 유지하거나 필요하면 순서 무관으로 처리할 수 있습니다.</p>
<p>⸻</p>
<p>우선순위 처리 — 현실적인 전략</p>
<p>우선순위를 단순히 Task 우선순위(priority:)로만 맡기기보다는, 카테고리별 세마포어 풀을 두는 것이 효과적입니다.
    •    highPrioritySemaphore (ex: value = 2) — 화면 표시용, 사용자 눈앞 작업
    •    defaultSemaphore (ex: value = 4) — 일반 페칭, 리스트 항목
    •    backgroundSemaphore (ex: value = 1~2) — 사전 페칭, 백그라운드 작업</p>
<p>요청을 분류하여 적절한 세마포어를 선택하면 급한 작업이 느릿느릿 처리되는 상황을 피할 수 있습니다. 더 정교하게는 가중치 기반 할당(dynamic weight)이나 토큰 버킷(token bucket) 방식으로 조절할 수 있습니다.</p>
<p>⸻</p>
<p>리트라이와 백오프 — 안정적인 네트워크 복구</p>
<p>지수 백오프 + jitter(무작위 소량 추가)는 네트워크 폭주 시 재시도를 완화하는 표준 패턴입니다.</p>
<pre><code>func retrying&lt;T&gt;(
    attempts: Int = 3,
    initialDelay: TimeInterval = 0.2,
    maxDelay: TimeInterval = 5.0,
    operation: @escaping () async throws -&gt; T
) async throws -&gt; T {
    var currentDelay = initialDelay
    var lastError: Error?
    for i in 0..&lt;attempts {
        if Task.isCancelled { throw CancellationError() }
        do {
            return try await operation()
        } catch {
            lastError = error
            if i == attempts - 1 { break }
            // jitter 추가: 랜덤 ±10% 정도
            let jitter = Double.random(in: -0.1...0.1) * currentDelay
            let wait = max(0, currentDelay + jitter)
            try await Task.sleep(nanoseconds: UInt64(wait * 1_000_000_000))
            currentDelay = min(currentDelay * 2, maxDelay)
        }
    }
    throw lastError!
}</code></pre><ul>
<li>중요한 점: 취소 가능하도록 구현해야 하며, 재시도 루프 안에서도 Task.isCancelled 체크를 고려합니다.</li>
<li>서버가 과부하 상태일 때는 재시도보다 실패를 상위 레이어에 전파해 사용자에게 즉시 피드백을 주는 것이 UX상 더 나을 수 있습니다.</li>
</ul>
<p>⸻</p>
<p>CPU 바운드 작업(디코딩 등)과 I/O 바운드 작업(네트워크/디스크)의 분리</p>
<p>동일한 동시도 설정으로 CPU 바운드와 I/O 바운드를 동시에 제어하면 최적화가 어렵습니다. 권장 설계:
    •    네트워크 다운로드: 비교적 경량 작업. 동시도는 네트워크 상황과 서버 제한에 맞춰 4~8 범위를 자주 실험.
    •    이미지 디코딩 / 변환: CPU 바운드. 디코더 풀(decoder pool)이나 DispatchQueue.concurrentPerform 대신 Task.detached + 세마포어로 CPU 슬롯을 제한(예: 기기 코어 수 기반: max(1, physicalCores - 1)).
    •    디스크 쓰기: 디스크 쓰기는 I/O 큐잉이 발생하므로 디스크 전용 쓰기 큐를 두고 일괄 쓰기(batch) 또는 백그라운드 스로틀링 적용.</p>
<p>구현 예: 다운로드는 downloadSemaphore(value: 6), 디코딩은 decodeSemaphore(value: 2)처럼 분리해서 사용.</p>
<p>⸻</p>
<p>적응형(Adaptive) 동시도 전략</p>
<p>정적 숫자 대신 런타임 관찰값을 보고 동시도를 조정:
    •    성공기반 증감: 최근 N건의 평균 응답시간이 낮으면 동시도 증가, 높으면 감소.
    •    실패율 기반: 실패율이 높아지면 동시도 감소.
    •    기기 상태 고려: 배터리 Saver 모드 / low power mode / 네트워크 유형(4G vs Wi-Fi)에 따라 동시도 조정.
    •    스타트업 단계: 앱 시작 시 동시도를 낮게 시작해 점진적으로 올리는 방식(warm-up).</p>
<p>이런 로직은 actor로 상태를 관리하고, 주기적으로(예: 1초 단위) 지표를 집계해 결정합니다.</p>
<p>⸻</p>
<p>취소 전파와 리소스 정리</p>
<p>부모 Task가 취소되면 자식들은 Task.isCancelled 또는 try Task.checkCancellation()으로 응답해야 합니다. 중요한 점:
    •    네트워크 요청 중 취소: URLSession의 dataTask는 Task 취소 시 자동으로 취소되므로 try await 호출 직후 취소 예외가 발생할 수 있습니다.
    •    디코딩 중 취소: 디코딩 함수 내부에서 Task.checkCancellation()을 적절히 호출하거나, 긴 루프(예: 스트리밍 디코딩) 도중 취소를 확인한다.
    •    리소스 정리: 파일 핸들, 임시 버퍼 등은 defer로 반드시 닫고 제거한다.</p>
<p>⸻</p>
<p>모니터링·프로파일링: 무엇을 측정해야 하는가?
    •    응답 시간(P95, P99): 단순 평균이 아닌 퍼센타일을 본다.
    •    동시 다운로드/디코딩 수: 실제 동시 자원 사용량.
    •    메모리 사용량(peak): 각 동시도 별 peak 메모리 관찰.
    •    오류율 &amp; 재시도 횟수: 네트워크/서버 문제인지 클라이언트 스로틀링 문제인지 구분.
    •    스크롤 프레임 드랍: UI 경험을 직접적으로 해치는 지표.</p>
<p>툴: Xcode Instruments(Allocations, Time Profiler, Network)와 자체 메트릭(로그, Firebase/Datadog 등) 결합.</p>
<p>⸻</p>
<p>실제 튜닝 워크플로
    1.    수치 가설 설정: 예: 이미지 다운로드 concurrency = 6, decode concurrency = 2.
    2.    시나리오 테스트: 와이파이/셀룰러/저전력 기기에서 스크롤 스트레스 테스트(대량 이미지).
    3.    측정: P95 응답시간, 메모리 peak, 재시도 비율, 프레임 드랍 수집.
    4.    조정: 동시도 감소 → 메모리/CPU 안정성 개선? 증가 → 처리량 향상? 의사결정.
    5.    적응형 적용: 단일 수치로 고정하지 말고 런타임 지표에 따라 조정하는 로직 추가.
    6.    회귀 테스트: 정책 변경 시 기존 시나리오 재검증.</p>
<p>⸻</p>
<p>운영 고려사항
    •    서버 협력: API에서 썸네일 제공 또는 Accept 헤더를 통한 최적 해상도 전달을 협의하면 클라이언트 부담이 크게 줄어든다.
    •    계측 포인트: 각 작업의 시작·종료 시각, 실패 원인 분류를 로그로 남겨 문제 원인을 빠르게 파악한다.
    •    피드백 루프: 실사용 지표로 동시도 정책을 개선(예: 특정 지역·네트워크에서 낮은 동시도를 권장)한다.</p>
<p>⸻</p>
<p>마무리
    •    동시성은 무조건 많이 돌린다고 좋은 것이 아니라, 시스템 자원과 작업 특성에 맞춰 제어해야 효율적입니다.
    •    TaskGroup + AsyncSemaphore(또는 토큰 버킷) 조합은 실무에서 직관적이고 안전하게 동시도를 제어하는 방법입니다.
    •    우선순위 분리, CPU/I/O 작업 분리, 리트라이/백오프, 취소 일관성, 측정과 적응형 정책이 함께할 때 진정으로 견고한 병렬 처리 파이프라인이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swift Concurrency 정리 (Swift 6 · iOS 18 · Xcode 26)]]></title>
            <link>https://velog.io/@kyu_ios_dev/Swift-Concurrency-%EC%A0%95%EB%A6%AC-Swift-6-iOS-18-Xcode-26</link>
            <guid>https://velog.io/@kyu_ios_dev/Swift-Concurrency-%EC%A0%95%EB%A6%AC-Swift-6-iOS-18-Xcode-26</guid>
            <pubDate>Wed, 17 Sep 2025 05:30:17 GMT</pubDate>
            <description><![CDATA[<p>Swift Concurrency 정리 (Swift 6 · iOS 18 · Xcode 26)</p>
<p>Swift 6의 actor, Task, TaskGroup, 취소(cancellation), @MainActor 등 동시성 도구를 단계별로 정리합니다. 코드 예제와 함께 실제로 어떻게 쓰는지 바로 복사해서 붙여넣을 수 있도록 구성했습니다.</p>
<p>⸻</p>
<p>전제(환경)
    •    Swift 6 이상
    •    iOS 18 이상
    •    Xcode 26 이상</p>
<p>⸻</p>
<p>들어가기 전 — 비유로 빠르게 이해하기</p>
<p>앱을 한 명의 바텐더라고 생각하면 이해하기 쉽습니다. 여러 손님 주문을 동시에 처리해야 하고, 전화도 오고, 새 음료도 만들어야 합니다. 동시성은 앱이 여러 작업을 ‘겉보기상 동시에’ 효율적으로 처리하게 해주지만, 손님끼리 음료가 뒤섞이면 안 되므로 데이터 소유권과 동기화가 중요합니다.</p>
<p>⸻</p>
<ol>
<li>핵심 개념 정리
 •    Task: 비동기 작업 단위. async/await와 함께 사용.
 •    TaskGroup: 여러 Task를 묶어 병렬 실행하고 결과를 모으는 구조.
 •    actor: 내부 상태를 순차적으로 보호하는 격리(격리된 상태 소유자).
 •    @MainActor: UI 업데이트를 메인 스레드에서 안전하게 수행하도록 보장.
 •    취소(cancellation): Task를 중간에 멈추는 메커니즘. 긴 작업은 중간중간 취소 신호를 확인해야 한다.</li>
</ol>
<p>⸻</p>
<ol start="2">
<li>actor — 상태 격리</li>
</ol>
<pre><code>actor Counter {
    private var value: Int = 0

    func increment() {
        value += 1
    }

    func get() -&gt; Int {
        return value
    }
}</code></pre><p>사용 예:</p>
<pre><code>let counter = Counter()

Task {
    await counter.increment()
    let v = await counter.get()
    print(&quot;counter = \(v)&quot;)
}</code></pre><p>설명: actor는 내부의 가변 상태에 대한 동시 접근을 자동으로 직렬화한다. 외부에서 바로 상태를 변경할 수 없으므로 데이터 경쟁을 방지한다.</p>
<p>⸻</p>
<ol start="3">
<li>Task와 async/await — 네트워크 등 비동기 기본</li>
</ol>
<pre><code>func fetchData(from url: URL) async throws -&gt; Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

Task {
    do {
        let data = try await fetchData(from: URL(string: &quot;https://example.com&quot;)!)
        print(&quot;data size: \(data.count)&quot;)
    } catch {
        print(&quot;fetch error:&quot;, error)
    }
}</code></pre><p>설명: await는 비동기 작업이 끝날 때까지 현재 Task를 일시 중단한다. Task {} 내부에서 await를 사용할 수 있다.</p>
<p>⸻</p>
<ol start="4">
<li>TaskGroup — 병렬 작업과 결과 집계</li>
</ol>
<pre><code>func fetchAll(_ urls: [URL]) async throws -&gt; [Data] {
    return try await withThrowingTaskGroup(of: Data?.self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                return data
            }
        }

        var results = [Data]()
        for try await data in group {
            if let d = data { results.append(d) }
        }
        return results
    }
}</code></pre><p>설명: withThrowingTaskGroup를 사용하면 자식 Task의 생명주기를 부모가 관리하므로 에러/취소 전파가 일관된다. 작업이 완료되는 순서대로 결과를 처리한다.</p>
<p>⸻</p>
<ol start="5">
<li>취소 처리 — 중간에 멈추기</li>
</ol>
<pre><code>func performLongOperation() async throws {
    try Task.checkCancellation()    // 시작 즉시 취소 확인

    let handle = openSomeResource()
    defer { handle.close() }        // 취소되어도 리소스 정리

    for i in 0..&lt;1000 {
        try Task.checkCancellation() // 루프 중간중간 취소 확인
        // 긴 작업 수행...
    }
}</code></pre><p>설명: Task.checkCancellation()은 취소 시 CancellationError를 던져 빠르게 종료한다. defer로 리소스 정리를 보장한다.</p>
<p>⸻</p>
<ol start="6">
<li>UI 업데이트: @MainActor 사용</li>
</ol>
<pre><code>@MainActor
class ViewModel: ObservableObject {
    @Published private(set) var items: [String] = []

    func load(urls: [URL]) {
        Task {
            do {
                let dataList = try await fetchAll(urls)
                items = dataList.map { String(data: $0, encoding: .utf8) ?? &quot;&quot; }
            } catch {
                // 에러 처리
            }
        }
    }
}</code></pre><p>설명: @MainActor를 적용하면 ViewModel의 상태 변경이 메인 스레드에서 안전하게 일어난다. UI 바인딩이 안전해진다.</p>
<p>⸻</p>
<ol start="7">
<li>중복 요청 방지 (in-flight dedupe)</li>
</ol>
<pre><code>@MainActor
final class ImageService {
    private var inFlight = [URL: Task&lt;UIImage?, Error&gt;]()

    func image(for url: URL) async throws -&gt; UIImage? {
        if let task = inFlight[url] {
            return try await task.value
        }

        let task = Task {
            defer { Task { await removeInFlight(url: url) } }
            let (data, _) = try await URLSession.shared.data(from: url)
            return UIImage(data: data)
        }

        inFlight[url] = task
        return try await task.value
    }

    private func removeInFlight(url: URL) {
        inFlight.removeValue(forKey: url)
    }
}</code></pre><p>설명: 동일 URL에 대한 기존 Task가 있으면 재사용해 네트워크/CPU 낭비를 줄인다.</p>
<p>⸻</p>
<ol start="8">
<li>실제로 해볼 연습 과제<ol>
<li>버튼을 누르면 5개의 URL을 fetchAll로 동시에 받아오고, 결과 크기 합계를 화면에 보여주는 앱을 만들어 보세요.</li>
<li>취소 버튼을 만들어 긴 루프 작업을 중간에 멈추도록 구현해 보세요.</li>
<li>같은 이미지를 여러 셀에서 동시에 요청하는 목록을 만들어 요청 수가 중복되지 않는지 로깅해 보세요.</li>
</ol>
</li>
</ol>
<p>⸻</p>
<ol start="9">
<li>흔한 실수와 해결법
 •    actor 내부에서 오래 걸리는 작업을 실행하면 actor가 막혀 다른 요청을 지연시킬 수 있다. → actor는 상태 관리만 하고, 네트워크 호출은 actor 밖에서 수행하거나 별도 Task로 분리한다.
 •    UI 업데이트를 백그라운드에서 수행하면 불안정해진다. → @MainActor로 ViewModel을 지정하거나 await MainActor.run { ... }을 사용한다.
 •    취소를 확인하지 않고 루프를 계속 실행하면 불필요한 연산과 리소스 낭비가 발생한다. → 루프 내부에 try Task.checkCancellation()를 넣어 취소에 즉시 응답하도록 한다.</li>
</ol>
<p>⸻</p>
<ol start="10">
<li>코드 리뷰 체크리스트 (복사해서 사용 가능)
•    공유하는 값(mutable state)은 actor 또는 @MainActor로 보호되어 있는가?
•    장기 작업(파일 처리, 대량 연산 등)에 취소 체크가 있는가?
•    동일 리소스에 대한 중복 네트워크 요청을 방지하는가(in-flight dedupe)?
•    UI 업데이트는 MainActor 내에서 실행되는가?
•    TaskGroup / async let 등 구조화된 동시성을 사용해 작업 라이프사이클을 관리하는가?</li>
</ol>
<p>⸻</p>
<p>결론</p>
<p>동시성 도구를 적절히 사용하면 여러 작업을 안전하고 효율적으로 처리할 수 있습니다. actor로 상태 소유권을 명확히 하고, Task/TaskGroup으로 작업 라이프사이클을 구조화하며, 취소와 메인 스레드 접근을 일관되게 처리하면 안정성과 성능이 개선됩니다. 위 코드를 직접 타이핑해 실행해 보면 개념이 더 빨리 체화됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Xcode 26에서 재사용 가능한 Preview 모듈 만들기]]></title>
            <link>https://velog.io/@kyu_ios_dev/Xcode-26-%EA%B8%B0%EC%A4%80-SwiftUI-%ED%94%84%EB%A6%AC%EB%B7%B0-%EB%AA%A9%EC%97%85-%EB%AA%A8%EB%93%88%ED%99%94-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@kyu_ios_dev/Xcode-26-%EA%B8%B0%EC%A4%80-SwiftUI-%ED%94%84%EB%A6%AC%EB%B7%B0-%EB%AA%A9%EC%97%85-%EB%AA%A8%EB%93%88%ED%99%94-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Wed, 17 Sep 2025 01:38:24 GMT</pubDate>
            <description><![CDATA[<h2 id="xcode-26에서-재사용-가능한-preview-모듈-만들기">Xcode 26에서 재사용 가능한 Preview 모듈 만들기</h2>
<p>요약: Swift 패키지(또는 프로젝트 내 타깃)를 이용해 “Preview 전용” 리소스·더미 데이터·헬퍼를 분리하면 SwiftUI 미리보기가 빨라지고, 재사용성·테스트 편의성이 올라갑니다. 이 글은 실무에서 바로 적용 가능한 패턴, 코드 템플릿, 주의사항을 단계별로 정리합니다.</p>
<p>⸻</p>
<p>왜 Preview 전용 모듈을 만들까?
    •    뷰를 빠르게 확인하려면 네트워크·DB 의존성이 제거된 더미 데이터가 필요합니다.
    •    여러 화면(또는 여러 프로젝트)에서 동일한 샘플 이미지/모델·헬퍼를 공유하면 유지보수가 쉬워집니다.
    •    #if DEBUG로 릴리즈 빌드에 영향 주지 않고 프리뷰 전용 코드를 안전하게 유지할 수 있습니다.
    •    Swift Package로 만들면 다른 앱/라이브러리에서도 동일한 Preview 리소스를 재사용 가능.</p>
<p>⸻</p>
<p>핵심 아이디어 (한 문장)</p>
<p>AppPreviews라는 Swift Package(또는 앱 내부의 Preview 타깃)를 만들어 PreviewData, 샘플 이미지(리소스), Preview 전용 헬퍼를 제공하고, 뷰의 PreviewProvider에서 이를 사용하면 끝.</p>
<p>⸻</p>
<p>단계별 구현 가이드</p>
<p>1) Swift Package 생성 (권장)</p>
<p>Xcode → File &gt; New &gt; Package... 로 새 패키지 생성
    •    이름: AppPreviews
    •    Platforms: iOS (최소버전은 앱 상황에 맞게)
    •    Targets: AppPreviews (라이브러리)</p>
<p>Package.swift 예제:</p>
<pre><code>import PackageDescription

let package = Package(
    name: &quot;AppPreviews&quot;,
    platforms: [.iOS(.v16)],
    products: [
        .library(name: &quot;AppPreviews&quot;, targets: [&quot;AppPreviews&quot;])
    ],
    targets: [
        .target(
            name: &quot;AppPreviews&quot;,
            resources: [
                .process(&quot;PreviewAssets&quot;) // sample images, json 등
            ]
        )
    ]
)</code></pre><p>PreviewAssets 폴더를 패키지 루트에 만들고 샘플 이미지(예: sample1.png) 넣기.</p>
<p>⸻</p>
<p>2) Preview 데이터와 헬퍼 만들기</p>
<p>패키지 내부에 PreviewData.swift 파일 추가:</p>
<pre><code>#if DEBUG
import Foundation
import UIKit
import SwiftUI

public enum PreviewData {
    // 모델 더미 예시
    public struct Photo {
        public let id: String
        public let title: String
        public let date: Date
        public let imageName: String
    }

    public static var samplePhoto: Photo {
        Photo(id: &quot;p1&quot;, title: &quot;Sample Photo&quot;, date: Date(), imageName: &quot;sample1&quot;)
    }

    // UIImage 로드 (Bundle.module 사용)
    public static var sampleUIImage: UIImage {
        let bundle = Bundle.module
        return UIImage(named: &quot;sample1&quot;, in: bundle, with: nil) ?? UIImage()
    }

    // 샘플 ViewModel (간단 mock)
    public static var cameraViewModelMock: CameraViewModel {
        CameraViewModel(isPreview: true)
    }
}
#endif</code></pre><ul>
<li>Bundle.module은 Swift Package의 리소스에 접근하는 안전한 방법입니다.</li>
<li>#if DEBUG로 감싸면 릴리즈 빌드에 해당 코드가 포함되지 않음.</li>
</ul>
<p>추가로 Image 편의 확장:</p>
<pre><code>#if DEBUG
import SwiftUI

public extension Image {
    static func preview(_ name: String) -&gt; Image {
        Image(name, bundle: .module)
    }
}
#endif</code></pre><p>⸻</p>
<p>3) 앱에 패키지 연결하기
    •    프로젝트 → Package Dependencies → + → 로컬 패키지(또는 Git URL) 추가
    •    앱 타깃에서 AppPreviews를 임포트하여 사용</p>
<p>⸻</p>
<p>4) 뷰에서 PreviewData 사용 (예시)</p>
<p>CameraView.swift의 Preview 부분:</p>
<pre><code>import SwiftUI
import AppPreviews

struct CameraView: View {
    var vm: CameraViewModel

    var body: some View {
        VStack {
            Text(vm.title)
            Image.preview(&quot;sample1&quot;)
                .resizable()
                .scaledToFit()
        }
    }
}

struct CameraView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            CameraView(vm: PreviewData.cameraViewModelMock)
                .previewDevice(&quot;iPhone 15 Pro&quot;)
            CameraView(vm: PreviewData.cameraViewModelMock)
                .preferredColorScheme(.dark)
        }
    }
}</code></pre><p>⸻</p>
<p>실무 팁 &amp; 모범 사례</p>
<p>리소스 관리
    •    벡터 PDF는 Scales = Single Vector PDF로 넣어두면 다양한 해상도에서 선명.
    •    이미지 이름 충돌 방지: Preview 패키지의 이미지 이름은 접두사(preview_)를 붙여 구분.</p>
<p>API 설계
    •    Preview 패키지는 공개 API만 제공하세요. (public 타입/메서드)
    •    앱 내부에서만 쓰일 mock 타입은 #if DEBUG로 감싸 내부 구현을 숨기면 안전.</p>
<p>환경 기반 분기</p>
<p>EnvironmentKey로 preview 모드 신호를 전달하면 런타임 로직을 분기하기 쉬움:</p>
<pre><code>private struct IsPreviewKey: EnvironmentKey {
    static let defaultValue: Bool = false
}
extension EnvironmentValues {
    var isPreview: Bool {
        get { self[IsPreviewKey.self] }
        set { self[IsPreviewKey.self] = newValue }
    }
}</code></pre><p>미리보기에서 environment(.isPreview, true) 설정.</p>
<p>Mock Network
    •    Preview 패키지에 MockNetwork를 포함시켜 API 레이어를 무력화하고, 미리보기 전용 데이터를 반환하게 하세요.
    •    실제 앱에서는 의존성 주입으로 런타임 네트워크와 교체.</p>
<p>Preview 다양성
    •    여러 기기, 라이트/다크, Dynamic Type, Accessibility 옵션을 Previews에 넣어 UI를 폭넓게 점검하세요:</p>
<pre><code>.preferredColorScheme(.dark)
.environment(\.sizeCategory, .accessibilityExtraLarge)</code></pre><p>성능 팁
    •    Preview에 heavy 연산(예: 이미지 디코딩, 모델 로딩)을 직접 실행하지 마세요. 미리 생성한 가벼운 샘플을 사용하세요.
    •    Preview 패키지의 리소스는 작게(수십 KB~수백 KB) 유지.</p>
<p>⸻</p>
<p>자주 겪는 문제 &amp; 해결
    •    Preview에서 이미지가 보이지 않음
-&gt; Swift Package에 이미지를 넣었다면 Image(&quot;name&quot;, bundle: .module)로 불러야 함. 앱의 Asset과 이름이 겹쳐도 충돌 가능성이 있으니 확인.
    •    패키지 변경 후 Preview가 갱신되지 않음
-&gt; Xcode Clean (Cmd+Shift+K) + Rebuild 또는 Xcode 재시작 권장.
    •    릴리즈에 Debug 코드가 포함됨
-&gt; #if DEBUG를 빠뜨리지 않았는지 확인.</p>
<p>⸻</p>
<p>정리 — 왜 이 패턴이 좋은가?
    •    빠른 UI iteration: 더미 데이터로 즉시 시각 검증 가능
    •    재사용성: 단일 패키지로 여러 프로젝트/타깃에서 공유
    •    안전성: #if DEBUG + 공개 API 설계로 릴리즈 영향 최소화
    •    협업: 디자이너/PM도 동일 샘플로 UI 확인 가능</p>
<p>⸻</p>
<p>코드 템플릿(요약)
    •    Package.swift — resources 포함
    •    PreviewAssets/ — 샘플 이미지들
    •    PreviewData.swift — mock 모델·ViewModel·이미지 로더
    •    Image+Preview.swift — Image(name, bundle: .module) 편의 확장</p>
<p>⸻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI에서 프리뷰 모듈화하기: 실전 가이드]]></title>
            <link>https://velog.io/@kyu_ios_dev/SwiftUI%EC%97%90%EC%84%9C-%ED%94%84%EB%A6%AC%EB%B7%B0-%EB%AA%A8%EB%93%88%ED%99%94%ED%95%98%EA%B8%B0-%EC%8B%A4%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@kyu_ios_dev/SwiftUI%EC%97%90%EC%84%9C-%ED%94%84%EB%A6%AC%EB%B7%B0-%EB%AA%A8%EB%93%88%ED%99%94%ED%95%98%EA%B8%B0-%EC%8B%A4%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Wed, 17 Sep 2025 01:25:50 GMT</pubDate>
            <description><![CDATA[<h2 id="swiftui에서-프리뷰-모듈화하기-실전-가이드">SwiftUI에서 프리뷰 모듈화하기: 실전 가이드</h2>
<p>SwiftUI의 가장 강력한 기능 중 하나는 Xcode Preview입니다.
UI를 빌드할 때마다 시뮬레이터를 돌리지 않고도 즉시 결과를 확인할 수 있죠. 하지만 규모가 커질수록 프리뷰 코드가 뷰 파일을 어지럽히고, 시나리오별 테스트가 복잡해지는 문제가 생깁니다. 이번 글에서는 프리뷰 코드를 모듈화해 관리성과 속도를 개선하는 방법을 정리합니다.</p>
<p>⸻</p>
<ol>
<li>왜 프리뷰 모듈화가 필요할까?
 •    파일 가독성: 뷰 코드와 프리뷰 코드가 한 파일에 섞이면 관리가 어렵습니다.
 •    재사용성 부족: 동일한 샘플 데이터를 여러 뷰에서 중복 정의하는 경우가 많습니다.
 •    테스트 범위 축소: 다크 모드, 접근성, 로케일 등 다양한 상황을 쉽게 누락합니다.</li>
</ol>
<p>👉 해결책은 뷰 로직과 프리뷰 로직을 분리하고, Mock 서비스와 샘플 데이터를 활용하는 것입니다.</p>
<p>⸻</p>
<ol start="2">
<li>구조 설계</li>
</ol>
<p>프로젝트 내에서 다음과 같이 나눕니다:</p>
<pre><code>📂 Feature/
 ┣ 📄 UsersView.swift        // 실제 뷰 + 뷰모델
 ┗ 📄 UsersView+Preview.swift // 프리뷰 전용 코드</code></pre><p>⸻</p>
<ol start="3">
<li>실제 뷰: 최소한으로 유지하기</li>
</ol>
<p>UsersView.swift에는 뷰 로직과 뷰모델만 둡니다.</p>
<pre><code>struct UsersView: View {
    @StateObject private var vm: UsersViewModel

    init(service: UserService) {
        _vm = StateObject(wrappedValue: UsersViewModel(service: service))
    }

    var body: some View {
        NavigationStack {
            Group {
                if vm.isLoading {
                    ProgressView(&quot;Loading…&quot;)
                } else if let error = vm.error {
                    Text(error).foregroundStyle(.red)
                } else if vm.users.isEmpty {
                    ContentUnavailableView(&quot;No Users&quot;)
                } else {
                    List(vm.users) { user in
                        Text(user.name)
                    }
                }
            }
            .navigationTitle(&quot;Users&quot;)
        }
        .task { vm.load() }
    }
}</code></pre><p>👉 여기에는 실제 서비스 주입과 UI 로직만 남습니다.
프리뷰용 목업은 모두 따로 분리합니다.</p>
<p>⸻</p>
<ol start="4">
<li>프리뷰 전용 코드 모듈화</li>
</ol>
<p>UsersView+Preview.swift에는 Mock 서비스와 샘플 데이터를 정의합니다.</p>
<pre><code>extension User {
    static let samples: [User] = [
        .init(name: &quot;Lee&quot;),
        .init(name: &quot;Kim&quot;),
        .init(name: &quot;Park&quot;)
    ]
}

struct MockUserService: UserService {
    var users: [User] = .samples
    func fetch() async throws -&gt; [User] { users }
}

extension UsersView {
    static var mockList: UsersView { .init(service: MockUserService()) }
}

#Preview(&quot;List • Light&quot;) {
    UsersView.mockList
        .preferredColorScheme(.light)
}

#Preview(&quot;List • Dark&quot;) {
    UsersView.mockList
        .preferredColorScheme(.dark)
}</code></pre><p>⸻</p>
<ol start="5">
<li>다양한 시나리오 지원</li>
</ol>
<p>프리뷰를 활용하면 실제 상황을 다양하게 재현할 수 있습니다.
    •    ✅ 다크 모드 / 라이트 모드
    •    ✅ Empty State / Error State
    •    ✅ 접근성 글자 크기
    •    ✅ RTL 레이아웃 (아랍어, 히브리어 등)</p>
<p>이렇게 여러 시나리오를 정의해두면, QA 단계에서 발견할 문제를 미리 캐치할 수 있습니다.</p>
<p>⸻</p>
<ol start="6">
<li>성능 최적화 팁
 •    프리뷰용 서비스는 async/await 지연을 최소화합니다.
 •    무거운 데이터를 피하고, 가벼운 샘플 모델만 사용합니다.
 •    #Preview 매크로를 활용해 필요할 때만 빌드합니다.</li>
</ol>
<p>⸻</p>
<ol start="7">
<li>결론</li>
</ol>
<p>SwiftUI 프리뷰는 단순히 “UI 미리보기”가 아니라,
실제 앱의 다양한 상태를 테스트하는 강력한 도구입니다.</p>
<p>👉 핵심은 뷰 로직과 프리뷰 로직을 분리하고,
👉 Mock 데이터와 서비스로 시나리오를 풍부하게 구성하는 것입니다.</p>
<p>이렇게 모듈화하면:
    •    파일이 깔끔해지고,
    •    프리뷰 속도가 빨라지며,
    •    QA 품질도 높아집니다.</p>
<p>⸻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UX 책 2종 리뷰 (UX/UI의 10가지 심리학 법칙, UX 디자인의 모든 것)]]></title>
            <link>https://velog.io/@kyu_ios_dev/detentionbloggroup2st</link>
            <guid>https://velog.io/@kyu_ios_dev/detentionbloggroup2st</guid>
            <pubDate>Mon, 29 May 2023 06:16:55 GMT</pubDate>
            <description><![CDATA[<h3 id="감금-블로그단-2회차">감금 블로그단 2회차</h3>
<p>이번에는 UI/UX 책 2종의 독서 후기를 시작 해 보겠습니다.</p>
<p>사실 두 책의 성격은 다르기에 어느 책이 낫다고 할 수는 없지만
UX 디자인의 모든 것 (이하 디자인의 모든 것 으로 표기) 은 기본적인 UI/UX 를 잘 설명 하고 있으며 한국 작가 분이 쓴 책이기에 발행 당시 21년도의 한국 앱들의 예시를 들며 풍부한 스크린샷으로 비교 설명을 잘 해주었습니다. 후반부에는 과감하게 두종의 앱을 서로 장단점을 비교하는 부분도 흥미롭게 볼 수 있습니다.</p>
<p>반면에 UX/UI의 10가지 심리학 법칙 (이하 심리학 법칙 으로 표기) 은 UI의 트렌드 보다는 통용적으로 적용되고 사용 될 수 있는 심리학적인 관점에서 UX를 설명 하고 있습니다. 참고로 심리학 법칙은 20년도에 발행 되었습니다.</p>
<p>디자인의 모든 것 부터 얘기를 해 보자면
서두에는 UI의 설계단계 즉 기획부터 주 사용자를 가정한 페르소나 기획법을 소개하며 가상의 사용자를 상정한 캐릭터를 통해 부정적 vs 긍정적, IT 사용성 높은 이해도 vs 낮은 이해도, 연령별 사용자, 부정적인 사용자, 극단적인 사용자 까지 아우르는 모든 사항에 대한 그러면서도 주 사용층이 될 사용자들을 위한 예상 UI를 어떻게 뽑아내며 고민 할지를 설명 해 주고 있습니다. 하지만 이 모든걸 잘 예상 하면서도 결정권자의 인사이트, 즉 클라이언트의 요구에 의해 &#39;난 이런걸 보여주고 싶어&#39; 로 만들어진 컨텐츠 화면의 위험성을 본인의 경험하에 얘기해 주는 부분에서는 공감을 할 수 밖에 없었습니다.
극단적인 예로 필요한 버튼들만 잘 들어오게 만들어진 증권앱과 불필요한 커뮤니티 요소가 겹치며 소셜앱처럼 보여지게 만들어진 증권앱을 비교하며 보여주는데 흥미로웠습니다.</p>
<p>그 다음 챕터들에서는 기본적인 UI 구성 요소들의 설명과 어떻게 UX 적으로 배치 되어야 하는지와 앱이나 웹에서의 기본적인 화면 흐름을 따라갈때 공통적으로 나오는 페이이지들 그 속에서 이뤄지는 UX의 방향과 고려해야 할 예외사항들에 대해 설명하고 있습니다.
사실 이 부분들을 어떻다 하고 나열하기 보다 넘어갈 수 밖에 없는게 UI 글들을 볼때 많이들 봤던 부분들이고 공감 할 수 있는 내용들입니다. 그러면서도 필요할때 찾아보거나 처음 보는 분들은 쉽게 이해 할 수 있도록 한국 최신 앱들의 스크린샷 들을 보여 주며 작가 분께서 잘 설명하고 있습니다.</p>
<p>4 챕터에서는 본인과 회사의 경험에 의한 개발된 UI 의 여러가지 도구들을 사용한 테스트 방법을 설명하고 있으니 주니어 이상의 디자이너 분들과 개발자 분들이 참고하기 좋을겁니다.</p>
<p>5 챕터는 작가 분의 UX에 대한 생각에 기반하여 국내와 국외 유슈의 앱들을 예로 들며 잘 적용된 UX의 예시와 잘못 적용된 예시를 신랄하게 얘기 해 주시는데 이 부분이야 말로 이 책의 핵심이 아닐까 합니다.
가장 재미있게 읽었던 부분입니다.
특히 그 당시에 화재가 되었던 틱톡의 초창기 모습이나 유튜브의 유저들을 계속 머물게 하는 UX들, 그리고 이제 시작했던 당근마켓의 부족했던 부분들과 배민의 잘 만들어진 화면들을 비교하며 보여주는 단락들은 아무래도 한국분들이라면 많이들 사용했던 앱이라 공감하며 또는 난 아니던데 하며 생각해보며 읽을 수 있는 부분입니다.</p>
<p>6 챕터에서는 최근 트렌드를 간략하게 설명하며 마무리 하시는데 아무래도 21년도라 그 당시 대응하느라 화제였던 다크모드와 지금은 식어버린 뉴모피즘 등을 볼 수 있습니다.</p>
<p>UI/UX 설명으로는 기본이 아주 잘 씌여진 책이라 생각되며 한국앱을 예시로 들기에 사용한 입장에서는 이해가 잘 되고 정말 제목 그대로 디자인의 모든 것이기에 그 모든걸 볼 수 있으며 나중에도 필요한 부분의 글들을 찾아보기 좋다고 생각됩니다.</p>
<p>그 다음 책인 심리학 법칙,
책 전체를 관통하는 부분을 얘기하자면 단순히 몇가지 UI의 패턴과 컴포넌트를 암기하고 공부해서 사용하기 보다는 심리학적인 법칙을 이해하고 UX에 적용하는데 촛점을 맞추고 있다고 생각하시면 됩니다.</p>
<p>작가는 기획과 디자인에는 여러 방향이 있고 여러 의견이 있을 수도 있는데 그 의견에는 근거를 제시해야 하고 그 근거는 사용자들의 데이터가 될 수도 있지만 이러한 심리학적인 요소들이 될 수도 있다고 서두를 떼고 있습니다.
그러한 근거들이 결국 회사 앱의 비즈니스 UX 방향을 결정하게 된다는 거죠.</p>
<p>그러면서 이러한 심리학적 관점이 실무에도 사용 될 수 있을까? 하며 이미 적용되어 있는 앱들의 예시를 들며 설명 해 주고 있습니다.</p>
<p>1 챕터에서는 사용자들이 이미 경험에서 느껴봤던 익숙한 UX 들이 심리적인 안정감을 주며 높은 사용법 적응력을 가져온다고 얘기하고 있습니다.
또 애플과 안드로이드 OS 적인 차이도 고려하며 각 환경에 맞는 UX를 사용 해 주어야 한다고 합니다.</p>
<p>2 챕터에서는 사용자의 터치가 가능한 영역들의 위치와 크기를 고려한 UX를 설명 하고 있으며,</p>
<p>3 챕터에서는 사용자가 선택을 해야 되는 부분에서 너무 복잡하지 않게 하여 선택의 시간이 길지 않게 하면서도 사용자의 선택을 강요하는 느낌을 주지 않게 하는 방법을 심리적인 측면에서 설명 해 주고 있습니다.</p>
<p>4 챕터에서는 너무 많은 정보가 한페이지에 있거나 너무 많은 화면 단계를 거치며 들어가는 부분은 결국 사용자의 기억의 한계에 의해 목적을 잃어 버릴 수 있음을 경고하고 있습니다.</p>
<p>5 챕터에서는 사용자의 예외적인 행동 예측에 대해 얘기하며 개발적으도 예외처리를 하겠지만 UX에서도 사용자가 어떤 행동을 하든 긍정적인 메시지로 사용자가 다시 UX의 의도대로 할 수 있게 좋은 느낌으로 가이드 하는 방법의 중요성을 설명 하고 있습니다.</p>
<p>6 챕터에서는 많은 내용이 있거나 단계가 있을때는 엔드 포인트 즉 마무리 시점에서 확실하게 사용자가 목적을 알 수 있어야 하며
선택의 순간 느낀 감정이 영향을 끼쳐 얼마나 많은 사용자들이 어떤 선택을 할수 있는지 얘기 하고 있습니다. 이 부분에서는 많은 앱들이 의도적으로 UX를 적용하고 있다는 것을 현업자 분들은 아실 겁니다.</p>
<p>7 챕터에서는 시각적으로 보기 좋은 디자인이 사용성이나 퀄리티가 높다고 사용자가 인식함을 얘기하고 있으며,</p>
<p>8 챕터에서는 시각적으로 비슷한것들이 한 화면에 모여 있으면 어떤 부분이 중요한지 보여주어야 하며 사용자 역시 다 같은것 보다는 한부분이 부각되거나 차이가 나는 부분에 시선이 갈 수 없음을 설명하고 있습니다.</p>
<p>9 챕터에서는 디자인적으로 직관적이고 심플하게 하는게 중요할 수도 있지만 접근성이 낮은 사용자도 쉽게 알수있는 아이콘이나 UX는 최소한의 설명이나 익숙함이 필요함을 얘기하고 있습니다.</p>
<p>10 챕터에서는 사용자가 화면이나 통신이 필요한 UX를 하였을때 사용자가 심리적으로 기다릴수 있는 최소의 피드백 반응속도를 맞추어 주어야 하고 만약 그럴 수 없다면 UX적으로 사용자가 기다릴 시간을 수긍할 수 있게 그려주어야 한다고 하고 있습니다.</p>
<p>작가는 이러한 심리학적인 요소들이 몇개는 심리학적으로 엄청난 영향을 이미 끼쳤고 윤리적으로 문제가 되었던 요소들도 있기에 윤리적인 책임이 중요하다며 그 예로 오프라인에서도 볼 수 있는 도박장의 슬롯머신의 UX들이 심리학적으로 어떻게 구성되어 있나 설명하고 그러한 요소들이 온라인에서도 마구잡이로 적용되어서는 안된다고 말 하고 있습니다. 그러면서 SNS의 중독성 역시 좋아요 버튼과 같은 몇 UX들의 요소가 많은 영향을 끼치고 있음을 얘기합니다.</p>
<p>그러한 부분들과 디자이너 본인 회사들의 비즈니스 적인 요구도를 어떻게 잘 아우르며 심리적 UX를 결정할 지, 팀원들과 어떻게 얘기하고 협의된 UX가 나오는지를 마지막에 설명하며 책을 마무리 하고 있습니다.</p>
<p>책은 확실히 심리학의 법칙이 더 얇고 술술 읽힙니다. 하지만 그 내용의 부분 부분들은 결코 가볍지 않으며 많은 생각을 하게 해주었습니다.
UX를 고민하는 분들이라면 몇번이라도 읽으며 곱씹을만 하며 적용할 여지가 많은 부분들입니다.</p>
<p>UX는 용어 그 자체로 보면 사용자의 경험입니다.
하지만 UX 관련 말 중에 제가 가장 공감했던 거는</p>
<blockquote>
<p>UX는 사용자의 불편함을 다 들어주고 해결 해 주는게 아니다.
UX는 사용자가 불편함을 느끼지 못 하게 하는 것이다.
이 차이는 크다 결국 회사의 이익을 위한 비즈니스적인 UX는 사용자가 불편하지 않게 잘 녹여내는 것이다.</p>
</blockquote>
<p>이 말이었습니다.
비슷한 예로 설명을 많이 하는것이 어떤 공항의 예시인데
승객이 비행기에서 내려 수화물 픽업장에 도착했을때 수화물이 안 나와 있다면 불편함을 느낄겁니다. 하지만 수화물 픽업장 까지 가는 거리가 있어 걸어가다가 보면 수화물이 그때 쯤 나오기 시작했다면 승객의 불편함 정도는 달라지겠죠.</p>
<p>그리고 이 글을 쓸때쯤 티비 예능 프로그램인 &#39;장사천재 백사장&#39; 에서 가게 오픈 시간에 맞춰 이미 많은 사람들이 대기를 하고 있지만 방송에서 백종원은 손님들을 한팀 한팀씩 들어오게 합니다. 한팀마다 테이블 셋팅이 끝난 후 다음 손님이 들어오게끔 통제하죠. 자리에 들어와 앉아서 기다리는 것과 이미 밖에서 기다리고 있어 몇분을 더 기다리는 것의 차이는 큼을 설명하고 있습니다.</p>
<p>이렇게 심리학은 사회 전반 모든 분야에서 사용되고 있으며 우리가 만드는 앱들에서도 사용되어 이익에 관여 할 수밖에 없습니다.
그러함을 고민하고 생각할때 이 책은 방향을 제시 할 수도 있고 다양한 관점을 제공할겁니다.</p>
<p>그럼 20000</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[소프트웨어 장인 정신 이야기 책 리뷰]]></title>
            <link>https://velog.io/@kyu_ios_dev/detentionbloggroup1st</link>
            <guid>https://velog.io/@kyu_ios_dev/detentionbloggroup1st</guid>
            <pubDate>Sun, 07 May 2023 07:56:05 GMT</pubDate>
            <description><![CDATA[<h3 id="감금-블로그단-1회차">감금 블로그단 1회차</h3>
<p>소프트웨어 장인 정신 이야기 독서 후기를 시작 해 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/kyu_ios_dev/post/b81020df-36a1-4cc1-83b9-fd969da469af/image.png" alt=""></p>
<p>클린코드, 클린아키텍쳐 서적으로 유명한 로버트 마틴, 일명 밥 아저씨께서 새 책을 내셔서 한번 읽어 보았습니다.
분량은 꽤 되지만 이미 알고 있던 내용도 있고 관심 있는 부분들은 술술 읽혀서 다 읽는데는 일주일 정도 소요 되었습니다.</p>
<p>애자일 쪽에서 유명한 분들 마틴 파울러와 로버트 마틴의 글을 자주 접하셨던 분들은 그 분들의 애자일에 대한 생각이 바뀌어 오는 것을 지켜보던 것도 흥미로웠을겁니다.</p>
<p>이번에도 좀더 현대적으로 바뀐 이야기들이 새롭게 다가오는 부분이었습니다.</p>
<p>애자일에서도 익스트림 프로그래밍(XP) 입장에서 많은 글을 써 오던 분이었는데요 그 중에서도 원칙과 품질을 중요시하며 클린코드에 입각한 테스트 주도 프로그래밍에 대해서 많은 얘기를 하였고 이 책에서도 초반 많은 부분 심지어 절반 가까이를 테스트 주도 관련 얘기가 들어있고 충실한 예제 (JAVA) 로 되어 있어 초심자들도 보기에 편하고 테스트 주도 개발에 대한 공부용으로 쓰셔도 될듯 합니다.</p>
<p>이후에는 리팩터링에도 챕터를 할애하여 마틴 파울러의 리팩터링 최근에 현대적으로 바뀐 리팩터링 2판에 대해서도 소개를 해 주었습니다.
그 중에서 밥 아저씨가 뽑은</p>
<pre><code>&#39;컴퓨터가 이해할 수 있는 코드는 바보도 작성할 수 있다.
사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.&#39;</code></pre><p>문구는 많은 공감을 합니다.</p>
<p>뒷부분에 얘기한 무자비한, 지속적인, 테스트 주도의 리팩터링의 필요성도 마음에 새겨보았습니다.</p>
<p>이후 개발자의 윤리에 대한 부분에 대해서도 많은 부분을 할애하며 코드의 설계에서부터 다른 개발자들과 같이 일할 것을 생각하며 혼자가 아닌 모두를 위하여 품질, 테스트 편의성, 가독성, 생산성 등을 생각하며 프로그래밍 할 것을 강조하였고
코드 작성에서 끝나는 것이 아니라 본인의 코딩이 사회에 끼치는 영향과 영향력이 큰 경우 사회의 전반적인 부분에서도 코드의 통상적 스타일을 바꿀 수 있음을 명시하며 기본적인 원칙과 윤리를 유념하라고 하고 있습니다.</p>
<p>사실 이 부분에서는 나에게는 직접적으로 와 닿지는 않으나 큰 오픈소스들에서 그 영향을 끼치는 부분 큰 대형 프로젝트들에서 경험했던 중구난방 이해충돌들을 생각하면 어느정도 예상은 되는 부분이었습니다.</p>
<p>이 책은 그렇게 개발자가 평소 생각 못했던 책임의식을 강조하다가 마지막 부분에 이르러 팀워크를 설명하며 좀더 팀 단위에서 본인의 개발 생활에 어떻게 개발할지와 말미에 배우기를 멈추지말라는 본인과 독자와에게 하는 얘기로 마무리를 짓고 있습니다.</p>
<p>이책은 굳이 모든 부분을 이해하고 공감하지 않아도 특정 부분에서 공감하고 본인의 개발 생활을 되돌아보며 자신의 원칙과 목표를 세울 수 있음에도 유익한 책이라 생각됩니다.</p>
<p>이제 사회생활과 팀 개발 생활을 시작하는 주니어에게도 앞으로 어떤 팀을 만나야 하는가 내가 주도적으로 어떤 개발 환경을 요구해야 하는가</p>
<p>또는 시니어나 팀이나 파트 관리 개발자 입장에서도 어떻게 내가 팀원들에게 개발 원칙을 세워줘야 하는가 같이 만들어나가는 부분은 어떤것인가를 고민할때 이 책을 한번 읽어보기를 권합니다.</p>
<p>그럼 20000</p>
]]></description>
        </item>
    </channel>
</rss>