<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>asher_joon.log</title>
        <link>https://velog.io/</link>
        <description>Asher입니다. 하지만 Joon이라고도 불리는</description>
        <lastBuildDate>Tue, 01 Sep 2026 12:52:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>asher_joon.log</title>
            <url>https://velog.velcdn.com/images/asher_joon/profile/a20019a7-676f-422c-a702-209e826fe5bb/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. asher_joon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/asher_joon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[FastAPI 매개변수 순서, 신경 안 써도 되는 이유 (Path와 * 트릭)]]></title>
            <link>https://velog.io/@asher_joon/FastAPI-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%EC%88%9C%EC%84%9C-%EC%8B%A0%EA%B2%BD-%EC%95%88-%EC%8D%A8%EB%8F%84-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-Path%EC%99%80-%ED%8A%B8%EB%A6%AD</link>
            <guid>https://velog.io/@asher_joon/FastAPI-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%EC%88%9C%EC%84%9C-%EC%8B%A0%EA%B2%BD-%EC%95%88-%EC%8D%A8%EB%8F%84-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-Path%EC%99%80-%ED%8A%B8%EB%A6%AD</guid>
            <pubDate>Tue, 01 Sep 2026 12:52:49 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>FastAPI 튜토리얼의 <a href="https://fastapi.tiangolo.com/ko/tutorial/path-params-numeric-validations/#order-the-parameters-as-you-need">경로 매개변수와 숫자 검증</a> 문서를 보다가 눈에 띄는 섹션이 있었습니다. 바로 &quot;필요한 대로 매개변수 정렬하기&quot;입니다. 파이썬 함수 정의에서 매개변수 순서 때문에 골치 아팠던 경험이 있다면, FastAPI가 이 문제를 어떻게 풀어내는지 흥미롭게 볼 만한 내용입니다.</p>
<h2 id="문제-상황-기본값-없는-매개변수는-앞에-와야-한다">문제 상황: 기본값 없는 매개변수는 앞에 와야 한다</h2>
<p>파이썬 함수는 기본값이 없는 매개변수를 기본값이 있는 매개변수보다 앞에 둬야 합니다. 그렇지 않으면 문법 오류가 납니다.</p>
<p>그런데 FastAPI에서 경로 매개변수를 검증하려면 보통 이렇게 씁니다.</p>
<pre><code class="language-python">item_id: int = Path(title=&quot;The ID of the item to get&quot;)</code></pre>
<p>여기서 <code>Path(...)</code>는 함수 매개변수의 &quot;기본값&quot; 자리에 들어갑니다. 그래서 만약 뒤에 기본값 없는 쿼리 매개변수 <code>q: str</code>가 온다면, 파이썬 입장에서는 &quot;기본값 있는 매개변수 뒤에 기본값 없는 매개변수가 왔다&quot;는 문법 오류가 발생할 수 있는 상황이 됩니다.</p>
<h2 id="fastapi의-답-순서는-신경-쓰지-않는다">FastAPI의 답: 순서는 신경 쓰지 않는다</h2>
<p>문서에서 가장 핵심적인 문장은 이거였습니다.</p>
<blockquote>
<p>FastAPI에서는 중요하지 않습니다. 이름, 타입 그리고 기본값 선언(Query, Path 등)으로 매개변수를 감지하며 순서는 신경 쓰지 않습니다.</p>
</blockquote>
<p>즉 FastAPI는 함수 시그니처를 위치 순서가 아니라 &quot;이름 + 타입 + Query/Path 같은 선언&quot;으로 분석합니다. 그래서 파이썬 문법만 통과하면 다음과 같이 기본값 없는 쿼리 매개변수 <code>q</code>를 앞에, 기본값 있어 보이는 <code>item_id: int = Path(...)</code>를 뒤에 두는 식으로 재정렬해서 문제를 피할 수 있습니다.</p>
<pre><code class="language-python">from fastapi import FastAPI, Path

app = FastAPI()


@app.get(&quot;/items/{item_id}&quot;)
async def read_items(q: str, item_id: int = Path(title=&quot;The ID of the item to get&quot;)):
    results = {&quot;item_id&quot;: item_id}
    if q:
        results.update({&quot;q&quot;: q})
    return results</code></pre>
<p>파이썬 문법 규칙은 지키면서, FastAPI 입장에서 어떤 게 경로 매개변수이고 어떤 게 쿼리 매개변수인지는 순서가 아니라 선언 자체로 판단하는 겁니다.</p>
<h2 id="더-깔끔한-방법-annotated를-쓰면-애초에-문제가-안-생긴다">더 깔끔한 방법: Annotated를 쓰면 애초에 문제가 안 생긴다</h2>
<p>사실 이 순서 문제는 <code>Query()</code>나 <code>Path()</code>를 함수 매개변수의 기본값 자리에 넣는 스타일 때문에 생깁니다. <code>Annotated</code>를 쓰면 애초에 기본값 자리를 쓰지 않기 때문에 이런 제약이 사라집니다.</p>
<pre><code class="language-python">from typing import Annotated

from fastapi import FastAPI, Path

app = FastAPI()


@app.get(&quot;/items/{item_id}&quot;)
async def read_items(
    q: str, item_id: Annotated[int, Path(title=&quot;The ID of the item to get&quot;)]
):
    results = {&quot;item_id&quot;: item_id}
    if q:
        results.update({&quot;q&quot;: q})
    return results</code></pre>
<p><code>Path(title=...)</code>가 타입 힌트 안(<code>Annotated[int, Path(...)]</code>)으로 들어가면서 더 이상 &quot;기본값&quot;으로 취급되지 않기 때문에, 원래 순서(<code>q</code> 먼저, <code>item_id</code> 나중)로 두든 바꿔서 두든 아무 문제가 없습니다. 지금 FastAPI 최신 버전 기준으로 권장되는 방식이기도 합니다.</p>
<h2 id="몰라도-되지만-알아두면-재밌는-트릭-">몰라도 되지만 알아두면 재밌는 트릭: <code>*</code></h2>
<p>문서에는 <code>Annotated</code>를 쓰지 않는 경우를 위한 작은 트릭도 소개되어 있습니다. 함수의 첫 매개변수로 <code>*</code>를 두는 겁니다.</p>
<pre><code class="language-python">from fastapi import FastAPI, Path

app = FastAPI()


@app.get(&quot;/items/{item_id}&quot;)
async def read_items(*, item_id: int = Path(title=&quot;The ID of the item to get&quot;), q: str):
    results = {&quot;item_id&quot;: item_id}
    if q:
        results.update({&quot;q&quot;: q})
    return results</code></pre>
<p>파이썬에서 <code>*</code>는 그 자체로는 아무 동작도 하지 않지만, 그 뒤에 오는 모든 매개변수를 반드시 키워드 인자로만 호출하도록 강제합니다(이른바 <code>kwargs</code>). 이렇게 하면 기본값이 있는 매개변수가 먼저 오든 나중에 오든 파이썬 문법 오류에서 자유로워집니다. 자주 쓸 일은 없지만, <code>Annotated</code>를 쓰지 않는 레거시 코드를 다룰 때는 알아두면 쓸모 있는 트릭입니다.</p>
<h2 id="왜-이게-중요한가">왜 이게 중요한가</h2>
<p>사실 이 섹션 자체는 아주 작은 이야기지만, FastAPI의 설계 철학을 잘 보여줍니다. 함수 시그니처의 &quot;위치&quot;라는 파이썬의 제약과, API 스펙을 표현해야 하는 &quot;선언&quot;이라는 요구사항이 부딪힐 수 있는데, FastAPI는 타입 힌트와 <code>Query</code>/<code>Path</code> 같은 명시적 선언을 분석해서 이 둘을 분리해줍니다. 개발자는 파이썬 문법만 신경 쓰면 되고, &quot;이게 경로 매개변수인지 쿼리 매개변수인지&quot;는 이름과 선언으로 알아서 판단해주는 거죠.</p>
<h2 id="요약-query-path-하나로-메타데이터부터-숫자-검증까지">요약: Query, Path 하나로 메타데이터부터 숫자 검증까지</h2>
<p>문서의 요약 부분에서 짚고 넘어갈 만한 포인트가 하나 더 있습니다.</p>
<blockquote>
<p>Query, Path(아직 보지 못한 다른 것들도)를 사용하면 쿼리 매개변수와 문자열 검증에서와 마찬가지로 메타데이터와 문자열 검증을 선언할 수 있습니다. 그리고 숫자 검증 또한 선언할 수 있습니다.</p>
</blockquote>
<p>즉 <code>Query</code>와 <code>Path</code>는 단순히 &quot;이건 쿼리 매개변수, 이건 경로 매개변수&quot;라는 위치 정보만 알려주는 게 아니라, <code>title</code>/<code>description</code> 같은 메타데이터, <code>min_length</code>/<code>max_length</code>/정규식 같은 문자열 검증, 그리고 숫자 검증까지 한 번에 선언할 수 있는 창구입니다. 숫자 검증에 쓰이는 키워드는 다음 4가지입니다.</p>
<ul>
<li><code>gt</code>: greater than (초과, ~보다 커야 함)</li>
<li><code>ge</code>: greater than or equal (이상, ~보다 크거나 같아야 함)</li>
<li><code>lt</code>: less than (미만, ~보다 작아야 함)</li>
<li><code>le</code>: less than or equal (이하, ~보다 작거나 같아야 함)</li>
</ul>
<p>예를 들어 <code>item_id</code>가 0 이상 1000 이하여야 하고, 쿼리 매개변수 <code>size</code>는 0보다 크고 10.5보다 작은 실수여야 한다면 다음과 같이 선언할 수 있습니다.</p>
<pre><code class="language-python">from typing import Annotated

from fastapi import FastAPI, Path, Query

app = FastAPI()


@app.get(&quot;/items/{item_id}&quot;)
async def read_items(
    *,
    item_id: Annotated[int, Path(title=&quot;The ID of the item to get&quot;, ge=0, le=1000)],
    q: str,
    size: Annotated[float, Query(gt=0, lt=10.5)],
):
    results = {&quot;item_id&quot;: item_id}
    if q:
        results.update({&quot;q&quot;: q})
    if size:
        results.update({&quot;size&quot;: size})
    return results</code></pre>
<p>여기서 중요한 건 이 네 키워드가 <code>int</code>뿐 아니라 <code>float</code> 같은 실수 타입에도 그대로 적용된다는 점입니다. <code>gt=0</code>을 걸면 <code>0.5</code>는 유효하지만 <code>0.0</code>이나 <code>0</code>은 거부됩니다. 즉 &quot;1보다 작아도 되지만 반드시 0보다는 커야 한다&quot;처럼, 정수 검증만으로는 표현하기 애매한 조건도 자연스럽게 표현할 수 있습니다.</p>
<p>그리고 문서는 <code>Query</code>, <code>Path</code>가 사실 공통 <code>Param</code> 클래스를 상속하는 서브클래스이기 때문에 이런 검증 옵션들을 동일하게 공유한다는 기술적인 사실도 짚어줍니다. 심지어 <code>Query</code>, <code>Path</code> 같은 이름은 실제로는 클래스가 아니라 함수이고, 호출하면 해당 이름의 클래스 인스턴스를 반환하는 구조라고 합니다. 클래스를 직접 쓰는 대신 함수 형태로 감싸둔 이유는 편집기가 타입 오류를 잘못 표시하지 않게 하기 위해서라는데, 사용자 경험(정확히는 개발자 경험)을 이런 세세한 부분까지 신경 쓴다는 인상을 받았습니다.</p>
<p>결국 <code>Query</code>/<code>Path</code> 하나로 &quot;이 값이 어디서 오는지 + 어떤 메타데이터를 가지는지 + 문자열/숫자로서 어떤 조건을 만족해야 하는지&quot;까지 한 번에 선언할 수 있다는 게 이 요약 섹션의 핵심입니다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>FastAPI는 매개변수를 위치 순서가 아니라 이름, 타입, <code>Query</code>/<code>Path</code> 같은 선언으로 인식한다.</li>
<li>기본값 없는 매개변수 뒤에 기본값 있는 매개변수를 자유롭게 배치해도 동작한다 (문법만 지키면).</li>
<li><code>Annotated</code>를 쓰면 애초에 기본값 자리를 쓰지 않으므로 순서 문제 자체가 사라진다. 특별한 이유가 없다면 이 방식이 가장 깔끔하다.</li>
<li><code>Annotated</code>를 쓰지 않아야 하는 상황이라면, 함수 첫 매개변수로 <code>*</code>를 둬서 강제로 키워드 인자화하는 트릭을 쓸 수 있다.</li>
<li><code>Query</code>/<code>Path</code>는 메타데이터, 문자열 검증뿐 아니라 <code>gt</code>/<code>ge</code>/<code>lt</code>/<code>le</code>로 숫자 검증까지 함께 선언할 수 있고, <code>int</code>와 <code>float</code> 모두에 적용된다.</li>
</ul>
<p>작은 디테일이지만, 이런 부분에서도 &quot;개발자가 파이썬 문법에 억지로 맞추게 하지 않는다&quot;는 FastAPI의 태도가 느껴집니다.</p>
<p>참고: <a href="https://fastapi.tiangolo.com/ko/tutorial/path-params-numeric-validations/#order-the-parameters-as-you-need">FastAPI 공식 문서 - 경로 매개변수와 숫자 검증: 필요한 대로 매개변수 정렬하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI는 동기/비동기를 어떻게 구분할까? async def와 def 제대로 이해하기]]></title>
            <link>https://velog.io/@asher_joon/FastAPI%EB%8A%94-%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%EB%B6%84%ED%95%A0%EA%B9%8C-async-def%EC%99%80-def-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@asher_joon/FastAPI%EB%8A%94-%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%EB%B6%84%ED%95%A0%EA%B9%8C-async-def%EC%99%80-def-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 01 Sep 2026 12:21:15 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>FastAPI로 API를 만들다 보면 항상 마주치는 질문이 있습니다. &quot;이 경로 처리 함수, <code>def</code>로 써야 할까 <code>async def</code>로 써야 할까?&quot; 언뜻 사소해 보이지만 이 선택이 서버 성능에 꽤 큰 영향을 줍니다. 공식 문서의 <a href="https://fastapi.tiangolo.com/ko/async/">Concurrency and async / await</a> 페이지를 기반으로, FastAPI가 동기와 비동기를 어떻게 구분하고 처리하는지, 그리고 그게 왜 이렇게까지 편하게 느껴지는지 정리해봤습니다.</p>
<h2 id="결론부터-fastapi는-알아서-처리해준다">결론부터: FastAPI는 알아서 처리해준다</h2>
<p>가장 인상 깊었던 부분은 이거였습니다.</p>
<blockquote>
<p>어쨌든 위의 어떤 경우에서도 FastAPI는 여전히 비동기적으로 동작하며 매우 빠릅니다.</p>
</blockquote>
<p>즉 <code>def</code>로 짜든 <code>async def</code>로 짜든, 하나의 앱 안에서 두 스타일을 섞어 쓰든 FastAPI가 알아서 올바르게 처리합니다. 개발자가 굳이 이벤트 루프를 직접 관리하거나 콜백 지옥에 빠질 필요가 없다는 뜻입니다. 다만 상황에 맞게 고르면 성능을 더 끌어올릴 수 있죠.</p>
<h2 id="판단-기준은-딱-하나-await로-호출해야-하는가">판단 기준은 딱 하나: &quot;await로 호출해야 하는가?&quot;</h2>
<p>FastAPI 문서가 제시하는 기준은 놀라울 만큼 단순합니다.</p>
<ol>
<li>사용하는 라이브러리가 <code>await</code>로 호출하라고 안내한다면 → 경로 처리 함수를 <code>async def</code>로 선언한다.</li>
</ol>
<pre><code class="language-python">@app.get(&quot;/&quot;)
async def read_results():
    results = await some_library()
    return results</code></pre>
<ol start="2">
<li>라이브러리가 <code>await</code>를 지원하지 않는다면(대부분의 DB 드라이버가 아직 그렇습니다) → 그냥 일반 <code>def</code>로 선언한다.</li>
</ol>
<pre><code class="language-python">@app.get(&quot;/&quot;)
def results():
    results = some_library()
    return results</code></pre>
<ol start="3">
<li>정말 잘 모르겠으면 → 그냥 <code>def</code>를 쓰면 된다.</li>
</ol>
<p>이게 전부입니다. 코루틴이 뭔지, 이벤트 루프가 어떻게 도는지 몰라도 이 세 줄만 기억하면 실무에서 막힐 일이 거의 없습니다.</p>
<h2 id="왜-이런-구분이-필요할까-io-바운드-작업의-정체">왜 이런 구분이 필요할까: I/O 바운드 작업의 정체</h2>
<p>비동기가 의미 있는 이유는 결국 &quot;기다림&quot; 때문입니다. 네트워크로 요청을 받고, DB 쿼리 결과를 기다리고, 외부 API 응답을 기다리는 작업들은 CPU 입장에서 보면 대부분 멍하니 대기하는 시간입니다. 이런 작업을 I/O 바운드 작업이라고 부릅니다.</p>
<p>동기 코드는 이 대기 시간 동안 아무것도 못 하고 멈춰 있습니다. 반면 비동기 코드는 &quot;이 작업은 끝날 때까지 시간이 걸리니, 그동안 다른 요청을 처리하고 있다가 결과가 오면 알려줘&quot;라는 식으로 동작합니다. 웹 API는 수많은 요청이 동시에 들어오고 각 요청마다 크고 작은 대기가 끼어 있는 전형적인 I/O 바운드 워크로드라서, 비동기 처리의 이점을 가장 크게 보는 영역입니다.</p>
<p>문서에서는 이걸 햄버거 가게 비유로 설명합니다. 점원 한 명이 주문만 받고 손님은 번호표를 들고 자리에 가서 대화하다가 번호가 뜨면 찾으러 가는 방식(동시성)이, 점원이 요리까지 끝낼 때까지 카운터 앞에서 멀뚱히 서서 기다리는 방식(동기)보다 훨씬 효율적이라는 거죠. 재미있는 비유지만 핵심을 정확히 찌릅니다.</p>
<h2 id="async--await-문법이-편한-이유">async / await 문법이 편한 이유</h2>
<p>파이썬의 <code>async</code>/<code>await</code> 문법이 좋은 건, 비동기 코드를 마치 순서대로 실행되는 일반 코드처럼 읽고 쓸 수 있게 해준다는 점입니다.</p>
<pre><code class="language-python">async def get_burgers(number: int):
    # 햄버거를 만들기 위한 비동기 처리
    return burgers</code></pre>
<p><code>await get_burgers(2)</code>라고 쓰면 &quot;이 작업이 끝날 때까지 기다렸다가 결과를 받아라, 그동안 파이썬은 다른 일을 하러 가도 된다&quot;는 뜻이 됩니다. 예전처럼 콜백을 겹겹이 쌓거나 스레드를 직접 관리하지 않아도, 코드는 위에서 아래로 읽히는 동기 코드와 똑같이 생겼는데 실제로는 논블로킹으로 동작합니다. 이게 콜백 지옥이나 스레드 동기화 문제를 겪어본 사람이라면 특히 크게 와닿는 편의성입니다.</p>
<h2 id="fastapi-내부에서는-무슨-일이-일어날까">FastAPI 내부에서는 무슨 일이 일어날까</h2>
<p>기술적으로 좀 더 들어가면, FastAPI(정확히는 그 기반인 Starlette)는 경로 처리 함수가 어떻게 선언됐는지에 따라 실행 방식을 다르게 가져갑니다.</p>
<ul>
<li><code>async def</code>로 선언한 함수는 이벤트 루프 위에서 직접 실행됩니다.</li>
<li>일반 <code>def</code>로 선언한 함수는 서버가 블로킹되지 않도록 별도의 외부 스레드풀에서 실행된 뒤 그 결과를 await 합니다.</li>
</ul>
<p>의존성(Dependency)에도 동일한 규칙이 적용되고, 서로 다른 스타일의 의존성이 얽혀 있어도 FastAPI가 알아서 조율해줍니다. 즉 개발자는 &quot;이 작업이 I/O를 기다리는가, 아닌가&quot;만 판단하면 되고, 실행 위치를 어디로 보낼지는 프레임워크가 대신 결정해줍니다.</p>
<h2 id="동시성과-병렬성을-헷갈리지-않기">동시성과 병렬성을 헷갈리지 않기</h2>
<p>문서는 동시성(concurrency)과 병렬성(parallelism)을 구분하는 데도 공을 들입니다. 둘 다 &quot;여러 일이 비슷한 시간에 일어난다&quot;는 점은 같지만,</p>
<ul>
<li>동시성은 대기 시간이 많은 작업(I/O 바운드)에서 대기하는 동안 다른 일을 처리하는 전략이고,</li>
<li>병렬성은 여러 작업을 실제로 동시에 처리할 자원(CPU 코어 등)을 늘려서 처리하는 전략입니다(CPU 바운드 작업에 유리).</li>
</ul>
<p>FastAPI는 웹 API처럼 대기가 많은 상황에서는 비동기(동시성)로, 머신러닝 추론처럼 순수 연산이 많은 CPU 바운드 상황에서는 멀티프로세싱(병렬성)으로 대응할 수 있어서 두 마리 토끼를 다 잡을 수 있다는 점도 인상적이었습니다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>판단 기준은 단순하다: 라이브러리가 <code>await</code>를 지원하면 <code>async def</code>, 아니면 그냥 <code>def</code>.</li>
<li>확신이 없으면 <code>def</code>를 써도 FastAPI는 여전히 빠르게 동작한다.</li>
<li><code>def</code>와 <code>async def</code>는 한 프로젝트, 심지어 한 라우터 안에서도 섞어 쓸 수 있고 FastAPI가 알아서 처리한다.</li>
<li>내부적으로 <code>def</code>는 스레드풀에서, <code>async def</code>는 이벤트 루프에서 실행되도록 FastAPI가 자동으로 라우팅해준다.</li>
<li>이 모든 게 개발자에게 &quot;복잡한 비동기 프로그래밍 지식&quot;을 강요하지 않으면서도 높은 성능을 뽑아낼 수 있게 해주는, FastAPI가 편하다고 느껴지는 이유다.</li>
</ul>
<p>결국 FastAPI의 async 처리 방식이 매력적인 이유는 어렵게 설계된 게 아니라, &quot;몰라도 동작하고, 알면 더 잘 동작한다&quot;는 점에 있는 것 같습니다.</p>
<p>참고: <a href="https://fastapi.tiangolo.com/ko/async/">FastAPI 공식 문서 - Concurrency and async / await</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI Cloud 훑어보기: fastapi deploy 한 줄로 배포까지]]></title>
            <link>https://velog.io/@asher_joon/FastAPI-Cloud-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0-fastapi-deploy-%ED%95%9C-%EC%A4%84%EB%A1%9C-%EB%B0%B0%ED%8F%AC%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@asher_joon/FastAPI-Cloud-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0-fastapi-deploy-%ED%95%9C-%EC%A4%84%EB%A1%9C-%EB%B0%B0%ED%8F%AC%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 31 Aug 2026 14:06:51 GMT</pubDate>
            <description><![CDATA[<p>FastAPI 공식 튜토리얼을 따라가다 보면 &quot;앱 배포하기(선택 사항)&quot; 섹션에서 낯선 명령어를 하나 만난다.</p>
<pre><code class="language-bash">$ uv run fastapi deploy

Deploying to FastAPI Cloud...

✅ Deployment successful!

🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev</code></pre>
<p><code>uv run fastapi dev</code>까지는 익숙한데 <code>fastapi deploy</code>는 처음 보여서 찾아봤다. <strong>FastAPI Cloud</strong>(<a href="https://fastapicloud.com/">fastapicloud.com</a>)에 대한 정리.</p>
<h2 id="fastapi-cloud란">FastAPI Cloud란</h2>
<p>FastAPI Cloud는 FastAPI를 만든 저자와 팀이 직접 만든 배포 플랫폼이다. FastAPI로 앱을 만들 때 느꼈던 개발자 경험(DX)을 그대로 배포 단계까지 이어가겠다는 게 목표다.</p>
<blockquote>
<p>FastAPI Cloud는 <em>FastAPI and friends</em> 오픈소스 프로젝트의 주요 스폰서이자 자금 제공자이기도 하다.</p>
</blockquote>
<p>핵심 문장은 공식 문서에 있는 이 한 줄이다.</p>
<blockquote>
<p>선택적으로 FastAPI 앱을 한 번의 명령어로 FastAPI Cloud에 배포할 수 있다.</p>
</blockquote>
<p>즉 Docker 이미지를 빌드하고, 레지스트리에 올리고, 인프라를 설정하는 과정 없이 <code>fastapi deploy</code> 한 번으로 끝낸다는 컨셉이다.</p>
<h2 id="왜-필요한가">왜 필요한가</h2>
<p>FastAPI 자체는 오픈소스고 표준(ASGI, OpenAPI)을 따르기 때문에 원래도 아무 클라우드에나 배포할 수 있다. 다만 그 과정에서 컨테이너화, 오토스케일링 설정, HTTPS 인증서, 환경 변수 관리 같은 반복 작업이 매번 들어간다.</p>
<p>FastAPI Cloud는 이 반복 작업을 CLI 한 줄 뒤로 감춘다. 랜딩 페이지에서 강조하는 지점도 결국 &quot;설정 없는 배포&quot;다.</p>
<ul>
<li><strong>Effortless deploys</strong>: 설정 파일 없이 배포</li>
<li><strong>On-demand scaling</strong>: 요청량에 따른 오토스케일링</li>
<li><strong>Scale to zero</strong>: 트래픽이 없으면 0으로 줄여서 비용 절감</li>
<li><strong>Secure by default</strong>: 기본 HTTPS 적용</li>
</ul>
<h2 id="어떻게-동작하는가">어떻게 동작하는가</h2>
<p>과정은 세 단계로 요약된다.</p>
<ol>
<li><p>FastAPI 앱을 평소처럼 작성한다.</p>
<pre><code class="language-python">from fastapi import FastAPI

app = FastAPI()

@app.get(&quot;/&quot;)
def main():
    return {&quot;message&quot;: &quot;Hello World&quot;}</code></pre>
</li>
<li><p>터미널에서 명령 하나를 실행한다.</p>
<pre><code class="language-bash">uv run fastapi deploy</code></pre>
</li>
<li><p>프로덕션에 올라간다.</p>
<pre><code class="language-json">{&quot;message&quot;: &quot;Hello World&quot;}</code></pre>
</li>
</ol>
<p>CLI가 프로젝트 구조를 자동으로 감지해서 배포하고, 로그인이 안 되어 있으면 인증을 위해 브라우저가 자동으로 열린다. <code>pyproject.toml</code>, <code>uv.lock</code>, <code>requirements.txt</code> 어떤 조합이든 표준 파이썬 프로젝트라면 그대로 인식한다.</p>
<h2 id="fastapi-cloud-cli는-이미-딸려온다">fastapi-cloud-cli는 이미 딸려온다</h2>
<p>여기가 개인적으로 제일 흥미로웠던 부분이다. <code>uv add &quot;fastapi[standard]&quot;</code>로 설치하면 <code>fastapi-cli[standard]</code>가 같이 깔리는데, 여기에 <code>fastapi-cloud-cli</code>가 기본 포함되어 있다. 즉 별도 설치 없이 <code>fastapi deploy</code> 명령을 바로 쓸 수 있는 이유가 여기 있다.</p>
<p>굳이 빼고 싶다면 이렇게 설치하면 된다.</p>
<pre><code class="language-bash">uv add &quot;fastapi[standard-no-fastapi-cloud-cli]&quot;</code></pre>
<h2 id="요금제-public-beta-기준">요금제 (Public Beta 기준)</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Hobby</th>
<th>Pro</th>
</tr>
</thead>
<tbody><tr>
<td>가격</td>
<td>$0/월</td>
<td>$20/좌석/월</td>
</tr>
<tr>
<td>앱 개수</td>
<td>3개</td>
<td>25개</td>
</tr>
<tr>
<td>커스텀 도메인</td>
<td>1개</td>
<td>20개</td>
</tr>
<tr>
<td>리소스</td>
<td>0.1 vCPU / 512MB (공유)</td>
<td>0.1 vCPU / 512MB</td>
</tr>
<tr>
<td>오토스케일</td>
<td>최대 3 replica</td>
<td>최대 10 replica</td>
</tr>
<tr>
<td>로그·메트릭 보관</td>
<td>1일</td>
<td>14일</td>
</tr>
<tr>
<td>팀원</td>
<td>1명</td>
<td>최대 10명</td>
</tr>
<tr>
<td>지원</td>
<td>커뮤니티 Slack</td>
<td>우선 Slack 지원</td>
</tr>
</tbody></table>
<p>Hobby는 신용카드 없이 바로 시작 가능하고, 개인 프로젝트나 학습용으로 충분한 스펙이다. 팀 단위 협업(RBAC, 우선 지원)이 필요해지면 Pro로 넘어가는 구조.</p>
<h2 id="다른-클라우드에-배포하고-싶다면">다른 클라우드에 배포하고 싶다면</h2>
<p>FastAPI Cloud를 꼭 써야 하는 건 아니다. FastAPI는 오픈소스고 표준 기반이라 원하는 클라우드 제공자 가이드를 따라 어디에든 배포할 수 있다. 공식 문서도 이 부분을 명확히 하고 있다. FastAPI Cloud는 &quot;가장 빠른 기본 경로&quot; 정도로 이해하면 될 것 같다.</p>
<h2 id="정리">정리</h2>
<ul>
<li><code>fastapi deploy</code>는 FastAPI 공식 튜토리얼 마지막에 등장하는 선택 사항이다.</li>
<li>FastAPI Cloud는 FastAPI 저자 팀이 만든, FastAPI 전용 배포 플랫폼이다.</li>
<li>설정 파일 없이 CLI 한 줄로 배포, 오토스케일링, scale-to-zero, 기본 HTTPS를 제공한다.</li>
<li><code>fastapi[standard]</code>를 설치하면 <code>fastapi-cloud-cli</code>가 기본 포함되어 있어 바로 <code>fastapi deploy</code>를 쓸 수 있다.</li>
<li>Public Beta 기준 Hobby(무료)/Pro($20/좌석) 두 플랜이 있고, 다른 클라우드로 배포하는 것도 여전히 자유롭다.</li>
</ul>
<p>참고: <a href="https://fastapicloud.com/">fastapicloud.com</a>, <a href="https://fastapi.tiangolo.com/ko/#deploy-your-app-optional">FastAPI 공식 문서 - 앱 배포하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[uv add가 "No pyproject.toml found"로 죽을 때]]></title>
            <link>https://velog.io/@asher_joon/uv-add%EA%B0%80-No-pyproject.toml-found%EB%A1%9C-%EC%A3%BD%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@asher_joon/uv-add%EA%B0%80-No-pyproject.toml-found%EB%A1%9C-%EC%A3%BD%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Mon, 31 Aug 2026 13:34:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/asher_joon/post/12f585fd-d1ae-4647-9f02-090aae21f7d5/image.png" alt=""></p>
<p>FastAPI 튜토리얼을 따라 하다가 첫 줄에서 막혔다.</p>
<pre><code class="language-bash">$ uv add &quot;fastapi[standard]&quot;
error: No `pyproject.toml` found in current directory or any parent directory</code></pre>
<p>패키지 이름을 잘못 쓴 것도, uv가 안 깔린 것도 아니었다. 원인은 훨씬 단순했다.</p>
<h2 id="uv에는-두-개의-세계가-있다">uv에는 두 개의 세계가 있다</h2>
<p>uv는 명령어가 두 갈래로 나뉜다.</p>
<table>
<thead>
<tr>
<th>명령</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td><strong>프로젝트 인터페이스</strong><br><code>uv add</code>, <code>uv remove</code>, <code>uv run</code>, <code>uv sync</code></td>
<td><code>pyproject.toml</code>을 읽고 고쳐 쓴다</td>
</tr>
<tr>
<td><strong>pip 인터페이스</strong><br><code>uv pip install</code>, <code>uv pip list</code></td>
<td>활성화된 환경에 그냥 설치한다</td>
</tr>
</tbody></table>
<p><code>uv add</code>는 후자가 아니라 전자다. 단순히 &quot;패키지를 설치해줘&quot;가 아니라 <strong>&quot;이 프로젝트의 의존성 목록에 이 패키지를 추가하고, 잠금 파일을 갱신하고, 환경을 동기화해줘&quot;</strong>라는 뜻이다.</p>
<p>그러니 의존성을 적어둘 <code>pyproject.toml</code>이 없으면 uv 입장에서는 할 말이 없다. 어디에 적으라는 건지 모르니까.</p>
<h2 id="에러-메시지를-다시-읽어보면">에러 메시지를 다시 읽어보면</h2>
<blockquote>
<p>No pyproject.toml found in current directory <strong>or any parent directory</strong></p>
</blockquote>
<p>뒷부분이 핵심이다. uv는 현재 디렉터리만 보고 포기하지 않는다. 루트에 닿을 때까지 상위 디렉터리를 거슬러 올라가며 <code>pyproject.toml</code>을 찾는다. 모노레포나 워크스페이스 하위에서 명령을 실행해도 동작하게 하기 위한 설계다.</p>
<pre><code>/                          ← pyproject.toml 없음
└── Users/joon/            ← pyproject.toml 없음
    └── ADA_Challenge/     ← pyproject.toml 없음
        └── bamti-backend/ ← 여기서 시작, 없음</code></pre><p>전부 훑고도 못 찾았다는 얘기다. 즉 <strong>&quot;오타가 났다&quot;가 아니라 &quot;아직 프로젝트가 아니다&quot;</strong>라는 신호다.</p>
<h2 id="해결-프로젝트부터-만든다">해결: 프로젝트부터 만든다</h2>
<h3 id="1-uv-init">1. uv init</h3>
<pre><code class="language-bash">uv init --name bamti-backend --python 3.13
# Initialized project `bamti-backend`</code></pre>
<h3 id="2-uv-add">2. uv add</h3>
<pre><code class="language-bash">uv add &quot;fastapi[standard]&quot;</code></pre>
<p>이제는 조용히 잘 된다. <code>fastapi</code>, <code>uvicorn</code>, <code>starlette</code>, <code>pydantic</code>, <code>typer</code>, <code>watchfiles</code>까지 30여 개 패키지가 한 번에 들어온다. (<code>[standard]</code>는 개발 서버와 CLI에 필요한 것들을 묶어놓은 extras다.)</p>
<h3 id="3-실행">3. 실행</h3>
<p><code>main.py</code>를 만들고,</p>
<pre><code class="language-python">from fastapi import FastAPI

app = FastAPI()

@app.get(&quot;/&quot;)
def read_root():
    return {&quot;Hello&quot;: &quot;World&quot;}</code></pre>
<p>띄운다.</p>
<pre><code class="language-bash">uv run fastapi dev main.py</code></pre>
<pre><code>⚡️ Starting FastAPI in development mode
🐍 Using import string: main:app
🌐 Server started at http://127.0.0.1:8000
   Documentation at http://127.0.0.1:8000/docs</code></pre><p><code>curl http://127.0.0.1:8000/</code> → <code>{&quot;Hello&quot;:&quot;World&quot;}</code>. 끝.</p>
<h2 id="uv-init이-만들어준-것들">uv init이 만들어준 것들</h2>
<table>
<thead>
<tr>
<th>파일</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>pyproject.toml</code></td>
<td>의존성 목록. <code>uv add</code>가 고쳐 쓰는 대상</td>
</tr>
<tr>
<td><code>uv.lock</code></td>
<td>정확한 버전 잠금. 커밋할 것</td>
</tr>
<tr>
<td><code>.python-version</code></td>
<td>파이썬 버전 고정 (3.13)</td>
</tr>
<tr>
<td><code>.venv/</code></td>
<td>가상환경. uv가 알아서 만들고 관리</td>
</tr>
<tr>
<td><code>.gitignore</code>, <code>README.md</code>, <code>src/</code></td>
<td>기본 스캐폴딩</td>
</tr>
<tr>
<td><code>.git/</code></td>
<td>git 저장소도 같이 초기화된다</td>
</tr>
</tbody></table>
<p>시스템 파이썬은 3.14.7이었지만 <code>--python 3.13</code>으로 고정하자 uv가 3.13을 알아서 받아왔다. 이게 <code>.python-version</code>에 박혀서 협업자도 같은 버전을 쓰게 된다.</p>
<h2 id="덤-source-venvbinactivate는-필요-없다">덤: source .venv/bin/activate는 필요 없다</h2>
<p><code>uv run</code>을 앞에 붙이면 uv가 알아서 프로젝트 환경에서 실행한다. 심지어 실행 직전에 <code>uv.lock</code>과 환경이 어긋났는지 확인하고 동기화까지 해준다.</p>
<pre><code class="language-bash">uv run fastapi dev main.py    # activate 없이 바로
uv run python -c &quot;import fastapi; print(fastapi.__version__)&quot;</code></pre>
<p>activate를 깜빡해서 전역 파이썬에 설치하는 사고가 구조적으로 사라진다.</p>
<h2 id="덤-2-정체불명의-빈-파일">덤 2: 정체불명의 빈 파일</h2>
<p>정리하다 보니 디렉터리에 <code>uv</code>라는 이름의 0바이트 파일이 하나 있었다. 아마 <code>uv 뭐시기</code>를 치다가 리다이렉션 오타(<code>&gt;</code>)를 냈던 것 같다. 눈에 안 띄는 쓰레기 파일이니 지우고 넘어갔다.</p>
<h2 id="정리">정리</h2>
<p><code>uv add</code>가 <code>pyproject.toml</code>을 못 찾겠다고 하면,</p>
<ul>
<li>잘못 친 게 아니라 아직 프로젝트가 없는 것이다 → <code>uv init</code></li>
<li>프로젝트를 만들기 싫고 그냥 설치만 하고 싶다면 → <code>uv venv</code> 후 <code>uv pip install</code></li>
<li>파일 하나짜리 스크립트라면 → <code>uv add --script script.py fastapi</code> (PEP 723 인라인 메타데이터)</li>
<li><code>pyproject.toml</code>만 딱 필요하다면 → <code>uv init --bare</code></li>
</ul>
<p>튜토리얼이 <code>uv init</code>으로 시작하는 데는 이유가 있었다.</p>
<hr>
<p>환경: uv 0.12.7 / macOS(Apple Silicon) / Python 3.13 / FastAPI 0.141.1</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Challenge 2 회고] 디자인을 처음부터 다시 배우며 지나온 나의 두 번째 애플 디벨로퍼 아카데미 챌린지]]></title>
            <link>https://velog.io/@asher_joon/Challenge-2-%ED%9A%8C%EA%B3%A0-%EB%94%94%EC%9E%90%EC%9D%B8%EC%9D%84-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EB%B0%B0%EC%9A%B0%EB%A9%B0-%EC%A7%80%EB%82%98%EC%98%A8-%EB%82%98%EC%9D%98-%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%95%A0%ED%94%8C-%EB%94%94%EB%B2%A8%EB%A1%9C%ED%8D%BC-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EC%B1%8C%EB%A6%B0%EC%A7%80</link>
            <guid>https://velog.io/@asher_joon/Challenge-2-%ED%9A%8C%EA%B3%A0-%EB%94%94%EC%9E%90%EC%9D%B8%EC%9D%84-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EB%B0%B0%EC%9A%B0%EB%A9%B0-%EC%A7%80%EB%82%98%EC%98%A8-%EB%82%98%EC%9D%98-%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%95%A0%ED%94%8C-%EB%94%94%EB%B2%A8%EB%A1%9C%ED%8D%BC-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EC%B1%8C%EB%A6%B0%EC%A7%80</guid>
            <pubDate>Thu, 30 Apr 2026 04:49:46 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, 애플 디벨로퍼 아카데미에서 끊임없이 &#39;본질&#39;을 탐구하는 러너 <strong>Asher(애셔)</strong>입니다.</p>
<p>Challenge 1이 &#39;적응과 탐색&#39;의 시간이었다면, 이번 <strong>Challenge 2</strong>의 미션은 &#39;Improve My Skills&#39;였습니다. 단순히 앱을 완성하는 것을 넘어, &quot;나는 어떤 개발자가 되고 싶은가?&quot;라는 근본적인 질문에 답을 찾아가는 과정이었죠. </p>
<p>C1의 프로토타입을 Hi-Fi 앱으로 발전시키며, 기술적 욕심보다는 <strong>&#39;의도된 디자인&#39;</strong>의 힘을 깨달았던 저의 치열했던 여정을 기록합니다.
<img src="https://velog.velcdn.com/images/asher_joon/post/589a1c5c-2350-4f85-83ff-cddd7111a843/image.png" alt=""></p>
<hr>
<h2 id="1-정말로-배우고-싶은-게-뭐예요-아이작의-한마디">1. &quot;정말로 배우고 싶은 게 뭐예요?&quot; 아이작의 한마디</h2>
<p>C2를 시작할 때 제 머릿속은 &#39;무엇을 구현할까&#39;로 가득했습니다. 
하지만 <strong>아이작(Isaac) 멘토</strong>와의 면담은 제 관점을 완전히 뒤흔들어 놓았습니다.</p>
<blockquote>
<p>*&quot;결과물에 연연하지 말고, 밖에서도 충분히 할 수 있는 것 말고, 아카데미에서만 도전해 볼 수 있는 &#39;진짜 배우고 싶은 것&#39;에 집중하세요.&quot;*</p>
</blockquote>
<p>이 한마디가 제 이정표가 되었습니다. 
저는 그동안 &#39;개발자니까&#39;라며 외면해왔던 <strong>디자인의 깊은 설계</strong>를 핵심 목표로 재설정했습니다. 예쁜 그림을 그리는 게 아니라, <strong>사용자의 경험을 설계하는 법</strong>을 배우기로 다짐한 것입니다.</p>
<hr>
<h2 id="2-디자인의-본질-어떻게가-아닌-왜에서-시작하다">2. 디자인의 본질: &quot;어떻게&quot;가 아닌 &quot;왜&quot;에서 시작하다</h2>
<p>디자인 컨셉을 잡기 위해 <strong>프라이데이(Friday) 멘토</strong>을 찾았을 때, 저는 충격적인 경험을 했습니다. 
저는 &quot;어떤 디자인 컨셉이 좋을까요?&quot;라고 물었지만, 
프라이데이는 서비스의 <strong>&#39;가장 뿌리가 되는 가치&#39;</strong>를 되물으셨습니다.</p>
<p>단순히 MVP를 강조할 디자인을 찾는 것이 아니라, 최초의 질문으로 돌아가는 활동을 시작했습니다.</p>
<ul>
<li><strong>집단의 특징 분석:</strong> 우리 앱을 사용할 사람들은 어떤 특징을 가졌는가?</li>
<li><strong>감각의 구체화:</strong> 그 집단을 보면 어떤 느낌이 드는가? 그 느낌에 맞는 폰트는 무엇인가?</li>
<li><strong>가치와 느낌의 연결:</strong> 어떤 앱을 만드는가보다, 어떤 &#39;해소&#39;를 제공하고 싶은가가 디자인의 시작이었습니다.</li>
</ul>
<p>결국 디자인 컨셉은 <strong>&quot;서비스가 해결하려는 문제를 어떻게 하면 가장 잘 해소해줄 수 있을까?&quot;</strong>에 대한 고민 끝에 자연스럽게 도출되는 것임을 배웠습니다.</p>
<hr>
<h2 id="3-꼬리에-꼬리를-무는-설계-프로젝트-계획의-재구성">3. 꼬리에 꼬리를 무는 설계: 프로젝트 계획의 재구성</h2>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/cfbd3da2-4f76-4163-acf1-06ff39d53baa/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/98c91dc2-5fc2-419e-b892-e414f531bc4d/image.png" alt=""></p>
<p>저는 프로젝트 계획부터 완전히 다시 짰습니다. 단순히 기능을 나열하는 대신, 스스로에게 끊임없이 질문을 던졌습니다.</p>
<ol>
<li><strong>문제의식:</strong> 우리가 해결하려는 진짜 문제는 무엇인가?</li>
<li><strong>연상과 감정:</strong> 문제를 볼 때 어떤 색깔이나 사물이 떠오르는가?</li>
<li><strong>유저 경험:</strong> 유저가 앱을 열고 사용할 때 어떤 감정을 느껴야 하는가?</li>
</ol>
<p>이 질문들에 답하며 저는 <strong>&#39;가벼움&#39;, &#39;직관적&#39;, &#39;신선함&#39;</strong>이라는 키워드를 뽑아냈고, 이를 시각적으로 구현하기 위해 <strong>5번의 페이퍼 프로토타이핑</strong>을 진행했습니다. </p>
<p>디자인 지식이 거의 &#39;Zero&#39;에 가까웠기에 더 치열했습니다. 수많은 자료를 찾아보고, 피드백을 받고, 다시 깎아내는 과정을 반복한 끝에야 마지막 페이퍼 프로토타입에서 <strong>&quot;내가 전달하고 싶었던 느낌&quot;</strong>을 동료들과 멘토들에게 인정받을 수 있었습니다.</p>
<hr>
<h2 id="4-시련을-넘어-hi-fi로-종이에서-픽셀로">4. 시련을 넘어 Hi-Fi로: 종이에서 픽셀로</h2>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/4c1a70a5-236d-4d54-9aca-b69ddb28321d/image.png" alt=""></p>
<p>하지만 &#39;느낌&#39;을 실체화하는 것은 또 다른 벽이었습니다. Sketch와 Figma를 통해 앱 전체를 디자인해 본 것은 이번이 처음이었기 때문입니다.</p>
<ul>
<li><strong>장벽:</strong> 툴 사용법이 익숙지 않아 남들보다 몇 배의 시간을 쏟아야 했습니다. </li>
<li><strong>사건:</strong> 작업 중 파일을 잃어버려 하루치 수정 사항이 통째로 날아가는 아찔한 순간도 있었습니다. 하지만 포기하지 않고 다시 그리며, 오히려 이전보다 더 정교한 레이아웃을 잡을 수 있었습니다.</li>
<li><strong>피드백의 힘:</strong> <ul>
<li><strong>스타크:</strong> &quot;버튼 사이즈(44x44)와 간격 등 HIG를 준수했는가?&quot;</li>
<li><strong>쉐인:</strong> &quot;불필요한 그림자를 줄이고 정보의 위계질서를 명확히 하라.&quot;</li>
</ul>
</li>
</ul>
<p>이러한 피드백을 거치며 단순히 &#39;이쁜 앱&#39;이 아니라, <strong>사용자의 경험과 서비스의 의도를 연결하는 디자인</strong>이 무엇인지 체득했습니다.</p>
<hr>
<h2 id="5-외부-데이터-수호자로서의-기술적-성장">5. &#39;외부 데이터 수호자&#39;로서의 기술적 성장</h2>
<p>학습 커뮤니티 <strong>&#39;무야호&#39;</strong>에서 저는 <strong>&#39;외부 데이터 수호자(노홍철 캐릭터)&#39;</strong>로서 팀원들의 성장을 지원했습니다.</p>
<ul>
<li><strong>공식 문서라는 나침반:</strong> 타인의 코드를 함께 고치는 것은 내 코드를 수정하는 것보다 훨씬 어려웠습니다. 설계 배경을 이해하기 위해 <strong>Apple 공식 문서와 WWDC 영상</strong>을 파고들었습니다.</li>
<li><strong>설명 가능한 코드:</strong> 막연한 검색 대신 공식 자료를 기준으로 &#39;왜 이렇게 작동해야 하는지&#39; 설명하려 노력했고, 이 과정에서 저 또한 SwiftUI의 구조를 더 깊게 이해하게 되었습니다.</li>
</ul>
<hr>
<h2 id="6-회고를-마치며-나는-결과보다-과정에서-더-많이-배우는-사람이-되었다">6. 회고를 마치며: &quot;나는 결과보다 과정에서 더 많이 배우는 사람이 되었다&quot;</h2>
<p>이번 Challenge 2는 저에게 기술적인 성장 그 이상의 의미를 주었습니다. 좋은 결과물은 갑자기 만들어지는 것이 아니라, <strong>문제를 바라보는 태도와 그것을 풀어가는 지루한 반복 속에서 나온다</strong>는 진리를 배웠기 때문입니다.</p>
<p>이제 저는 무엇을 만들었는지뿐만 아니라, <strong>&quot;왜 그렇게 만들었는지&quot;</strong>를 설명할 수 있는 개발자로 한 걸음 더 나아갔습니다. 이 뜨거운 마음을 가지고 다음 챌린지에서도 본질에 집중하는 배움을 이어가겠습니다.</p>
<p>함께 머리를 맞대준 퍼스널 팀원들 팀원들, 그리고 아이작, 프라이데이 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Challenge 1 회고] 성과보다 성장에 집중했던, 나의 첫 번째 애플 디벨로퍼 아카데미 챌린지]]></title>
            <link>https://velog.io/@asher_joon/Challenge-1-%ED%9A%8C%EA%B3%A0-%EC%84%B1%EA%B3%BC%EB%B3%B4%EB%8B%A4-%EC%84%B1%EC%9E%A5%EC%97%90-%EC%A7%91%EC%A4%91%ED%96%88%EB%8D%98-%EB%82%98%EC%9D%98-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EC%95%A0%ED%94%8C-%EB%94%94%EB%B2%A8%EB%A1%9C%ED%8D%BC-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EC%B1%8C%EB%A6%B0%EC%A7%80</link>
            <guid>https://velog.io/@asher_joon/Challenge-1-%ED%9A%8C%EA%B3%A0-%EC%84%B1%EA%B3%BC%EB%B3%B4%EB%8B%A4-%EC%84%B1%EC%9E%A5%EC%97%90-%EC%A7%91%EC%A4%91%ED%96%88%EB%8D%98-%EB%82%98%EC%9D%98-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EC%95%A0%ED%94%8C-%EB%94%94%EB%B2%A8%EB%A1%9C%ED%8D%BC-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EC%B1%8C%EB%A6%B0%EC%A7%80</guid>
            <pubDate>Sun, 19 Apr 2026 07:59:12 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, 애플 디벨로퍼 아카데미에서 열정적으로 항해 중인 러너 <strong>Asher(애셔)</strong>입니다. </p>
<p>아카데미에 들어와서 처음으로 맞이한 &#39;Challenge 1&#39;이 드디어 마무리되었습니다. 단순히 앱 하나를 뚝딱 만들어내는 과정이 아니라, Apple이 추구하는 디자인 철학(HIG)을 체득하고, CBL(Challenge Based Learning)이라는 생소하지만 매력적인 학습 방식에 푹 빠져들었던 시간이었습니다. </p>
<p>중구난방으로 적어두었던 생각의 파편들을 모아, 저의 첫 번째 CBL을 기록해 보려 합니다. </p>
<hr>
<h2 id="1-애플은-왜-이럴까-hig와-디자인에-대한-고찰">1. &quot;애플은 왜 이럴까?&quot; HIG와 디자인에 대한 고찰</h2>
<p>처음에는 그냥 &quot;목록 보여주는 거 아닌가?&quot; 싶었던 것들이 막상 <strong>HIG(Human Interface Guidelines)</strong>를 파고드니 용도가 확실히 다르다는 것을 알게 되었습니다.</p>
<h3 id="캐러셀carousel-vs-리스트list">캐러셀(Carousel) vs 리스트(List)</h3>
<ul>
<li><strong>캐러셀:</strong> 주목시키고 싶은 콘텐츠를 하나씩 강조하며 이미지에 좀 더 집중하게 만듭니다.</li>
<li><strong>리스트:</strong> 전체를 쭉 훑어볼 때 유리하며, 텍스트 위주로 정보를 전달할 때 핵심적인 역할을 합니다.
&quot;무엇을 보여주고 싶냐&quot;에 따라 선택이 달라져야 한다는, 아주 기본적이지만 중요한 포인트를 배웠습니다.</li>
</ul>
<h3 id="애플이-둥둥-떠다니는-버튼을-싫어하는-이유">애플이 &#39;둥둥 떠다니는 버튼&#39;을 싫어하는 이유</h3>
<p>안드로이드에서는 흔한 FAB(Floating Action Button)를 왜 iOS에서는 잘 쓰지 않을까? 처음엔 의아했지만, iOS의 네비게이션 구조와 맞지 않을뿐더러 좁은 화면에서 사용자의 시야를 계속 방해하기 때문이라는 걸 이해하게 되었습니다. 제한된 영역 내에서 최적의 정보 경험을 제공하려는 애플의 철학을 엿볼 수 있었습니다.</p>
<h3 id="구조의-명확성-탭--네비게이션--모달">구조의 명확성: 탭 / 네비게이션 / 모달</h3>
<p>이 세 가지를 헷갈리면 UX가 꼬이기 시작합니다.</p>
<ul>
<li><strong>탭:</strong> 앱의 최상위 영역을 이동할 때</li>
<li><strong>네비게이션:</strong> 같은 흐름 안에서 깊이 들어갈 때</li>
<li><strong>모달:</strong> 현재 흐름을 잠시 끊고 집중시킬 때 (Sheet 등)
애플 앱들이 왜 다 비슷하게 생겼는지 이제야 이해가 갔습니다. 그 규칙 안에서 디자인하는 게 결국 &#39;가장 iOS다운 앱&#39;을 만드는 지름길이었습니다.</li>
</ul>
<hr>
<h2 id="2-swiftui-데이터의-흐름을-지배하다">2. SwiftUI, 데이터의 흐름을 지배하다</h2>
<p>머릿속에 있는 디자인을 코드로 옮기는 게 생각보다 더 어려웠습니다. 억지로 절대값을 쓰기보다 SwiftUI의 레이아웃 시스템을 이해해야 원하는 대로 나오더라고요.</p>
<ul>
<li><strong>@State &amp; @Binding:</strong> 부모가 데이터를 갖고 자식에게 내려주며 양방향 싱크를 맞추는 구조. 이게 SwiftUI 데이터 흐름의 정수라는 걸 체감했습니다.</li>
<li><strong>AppStorage &amp; EnvironmentObject:</strong> 앱이 꺼져도 유지되어야 하는 데이터는 <code>AppStorage</code>로, 앱 전체에서 공유되는 메모리 데이터는 <code>@EnvironmentObject</code>로 관리했습니다. </li>
<li><strong>디스크 저장(Save/Load):</strong> 메모리는 앱을 끄면 사라지니, 디스크에서 데이터를 읽어 메모리로 올리고 바뀔 때마다 다시 저장하는 구조를 설계하며 데이터의 영속성에 대해 깊이 고민해 보았습니다.</li>
</ul>
<hr>
<h2 id="3-새천년-팀과-함께한-cbl의-여정-engage">3. &#39;새천년&#39; 팀과 함께한 CBL의 여정 (Engage)</h2>
<p>멘토 하워드, 프라이데이와 함께 팀 9, <strong>&#39;새천년&#39;</strong>이라는 이름으로 시작했습니다. &quot;앞으로의 새로운 천년을 이끄는 사람들이 되자&quot;는 다짐과 함께요.</p>
<h3 id="💡-big-idea의-변화">💡 Big Idea의 변화</h3>
<p>처음엔 &#39;웰빙&#39;을 키워드로 잡았지만, 우리 팀원들이 진짜 하고 싶은 건 결국 포항과 아카데미에서의 <strong>&#39;활동(Activity)&#39;</strong>이었습니다. </p>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/2f0492c9-3f31-473e-a851-a30c9ee91d60/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/96e689b9-8d13-49e0-bb06-3368976bd7ce/image.png" alt=""></p>
<p>PM이 없다보니 정말 다양한 의견들이 나와서 때로는 어려움을 겪었지만, 서로의 관점에 대해 자유롭게 이야기하며 &quot;취향이 다양한 러너들이 포항 내 여가활동을 함께 할 수 있는 환경을 만들자&quot;는 마일스톤(CS)에 도달했습니다.</p>
<p><strong>우리의 Essential Question (EQ):</strong> </p>
<blockquote>
<p>&quot;어떻게 하면 다양한 사람들과 다양한 활동을 놓치지 않고 찾을 수 있을까?&quot;</p>
</blockquote>
<hr>
<h2 id="4-안-힘들어요---진실을-캐내는-스몰토크-리서치-investigate">4. &quot;안 힘들어요?&quot; - 진실을 캐내는 &#39;스몰토크&#39; 리서치 (Investigate)</h2>
<p>리서치 과정이 특히 기억에 남습니다. 대놓고 &quot;설문해 주세요&quot; 하면 진실되지 않은 답변이 온다는 걸 알았거든요. 모든 사람들이 인터뷰, 설문조사를 하게되면 바른 생활 어린이가 되는 것을 느꼈습니다😂. 그래서 우리는 <strong>&#39;잠입 리서치&#39;</strong>를 했습니다.</p>
<p>다른 팀 리서치를 도와준 뒤 &quot;안 힘드세요?&quot;, &quot;오늘 끝나고 뭐 하세요?&quot; 같은 스몰토크로 시작해, 대상자가 리서치임을 인지하지 못한 상태에서 진짜 속마음을 들었습니다. </p>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/a9f4f2ab-deb8-46d6-af07-818dda56c06c/image.png" alt=""></p>
<p>그렇게 모은 데이터는 상당했습니다. 시각화도 진행해 보았죠.</p>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/cb20b00c-22a0-4e7e-b3fe-a7301108df45/image.png" alt=""></p>
<p><strong>리서치를 통해 얻은 핵심 인사이트:</strong></p>
<ol>
<li>줌챗과 컨플루언스는 익숙지 않아 정보가 파편화되어 있다.</li>
<li>오후 세션이 끝나면 다들 방전된다. 그래서 &#39;주말&#39;이나 &#39;단기성 이벤트&#39;가 더 필요하다.</li>
<li>호스트를 하고 싶지만 사람이 안 모일까 봐, 혹은 참여 확정에 대한 심리적 부담감 때문에 망설인다.</li>
</ol>
<hr>
<h2 id="5-아이디어의-확장과-구체화">5. 아이디어의 확장과 구체화</h2>
<p>우리는 수집한 데이터를 바탕으로 네 가지 방향을 치열하게 검토했습니다.</p>
<ul>
<li>번개 및 이벤트성 모임 포커스</li>
<li>&#39;입소문&#39;을 시스템화하는 리뷰/추천 시스템</li>
<li>시각 자료(영상, 포스터)로 흥미를 끄는 릴스 스타일의 UI</li>
<li>호스트의 부담(참여 확정의 거부감)을 줄여주는 사전 조사 시스템</li>
</ul>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/f9153375-8c20-472d-bf41-48e8327b704e/image.png" alt=""></p>
<p>결국 우리의 결론은 <strong>&quot;정기 모임이 부담스러운 러너를 위해 맞춤형 추천으로 허들을 낮춰주는 앱&quot;</strong>이었습니다.</p>
<hr>
<h2 id="6-프로토타이핑-종이에서-코드로">6. 프로토타이핑: 종이에서 코드로</h2>
<p>본격적으로 코드를 짜기 전, 페이퍼 프로토타입을 통해 흐름을 점검했습니다.</p>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/d5485e86-3a02-4914-a3d3-4d5ad1042ee2/image.png" alt=""></p>
<p>이 과정에서 멘토님들께 소중한 피드백을 받았습니다. &quot;홈의 커뮤니티 기능은 과감히 빼고, 검색과 상세 페이지에 집중하라&quot;는 조언 덕분에 핵심 가치에 더 집중할 수 있었습니다.</p>
<p>그 결과, 아래와 같은 앱을 완성할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/0e6ce755-7120-4ce6-a0c9-5c558b0a9acd/image.png" alt=""></p>
<hr>
<h2 id="7-회고를-마치며-나는-swiftui와-조금-더-친해졌다">7. 회고를 마치며: &quot;나는 SwiftUI와 조금 더 친해졌다&quot;</h2>
<p>이번 Challenge 1은 성과에만 집착하지 않고, 순전히 내가 배우고 싶은 것을 해나간 시간이라 만족도가 매우 높습니다. </p>
<p>처음 기획할 때 욕심이 많아 기능을 잔뜩 넣고 싶었지만, 결국 <strong>&quot;이 기능이 진짜 필요한가?&quot;</strong>를 자문하며 덜어내는 과정 자체가 실력이라는 걸 배웠습니다. 억지로 절대값을 쓰지 않고 프레임워크가 원하는 방식대로 따라가는 게 훨씬 자연스럽다는 것도요.</p>
<p>오랜만에 새로운 언어를 배우며 결과물에 집중하지 않고 학습 과정 그 자체를 즐겼던, 매우 의미 있는 시간이었습니다. 이제 이 자신감을 가지고 다음 챌린지로 나아갈 준비가 된 것 같습니다. </p>
<p>함께해 준 &#39;새천년&#39; 팀원들 (링쿠,유메,비비,쿼티,한), 그리고 멘토 하워드, 프라이데이 너무 감사합니다! 함께해 준 모든 분들 덕분에 아쉬움이 남지 않는 Challenge 1 이였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI Day 7 - ScoreKeeper (Identifiable, Stepper, ForEach 바인딩)]]></title>
            <link>https://velog.io/@asher_joon/SwiftUI-Day-7-ScoreKeeper-Identifiable-Stepper-ForEach-%EB%B0%94%EC%9D%B8%EB%94%A9</link>
            <guid>https://velog.io/@asher_joon/SwiftUI-Day-7-ScoreKeeper-Identifiable-Stepper-ForEach-%EB%B0%94%EC%9D%B8%EB%94%A9</guid>
            <pubDate>Wed, 18 Mar 2026 04:29:15 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-파일-구조">프로젝트 파일 구조</h2>
<pre><code>ScoreKeeper/
├── ScoreKeeperApp.swift   # @main 진입점
├── Player.swift           # Identifiable 모델 구조체
└── ContentView.swift      # 플레이어 목록 + 점수 관리 UI</code></pre><p><img src="https://velog.velcdn.com/images/asher_joon/post/c6e7846e-490f-4d69-8bc7-55303a488803/image.png" alt=""></p>
<hr>
<h2 id="identifiable-프로토콜--고유-식별-가능한-모델"><code>Identifiable</code> 프로토콜 — 고유 식별 가능한 모델</h2>
<pre><code class="language-swift">struct Player: Identifiable {
    let id = UUID()
    var name: String
    var score: Int = 0
}</code></pre>
<blockquote>
<p><code>Identifiable</code>을 채택하면 <code>ForEach</code>에서 <code>id:</code> 파라미터 없이 사용할 수 있다.
<code>UUID()</code>는 앱 실행마다 전 세계에서 유일한 식별자를 자동 생성한다.
<code>id</code>는 변하지 않으므로 <code>let</code>, 나머지는 수정 가능하므로 <code>var</code>.</p>
</blockquote>
<h3 id="identifiable-채택-전후-비교">Identifiable 채택 전후 비교</h3>
<pre><code class="language-swift">// Identifiable 없을 때 — id: 명시 필요
ForEach(players, id: \.name) { player in ... }

// Identifiable 있을 때 — 생략 가능
ForEach(players) { player in ... }</code></pre>
<hr>
<h2 id="foreach배열--배열-요소에-바인딩으로-접근"><code>ForEach($배열)</code> — 배열 요소에 바인딩으로 접근</h2>
<pre><code class="language-swift">ForEach($players) { $player in
    TextField(&quot;Name&quot;, text: $player.name)
    Stepper(&quot;\(player.score)&quot;, value: $player.score)
}</code></pre>
<blockquote>
<p><code>ForEach($players)</code> 처럼 <code>$</code>를 붙이면 각 요소를 <strong>바인딩</strong>으로 받을 수 있다.
<code>$player.name</code>, <code>$player.score</code> 처럼 하위 프로퍼티까지 직접 수정 가능하다.
이렇게 하면 부모 뷰의 <code>@State</code> 배열이 자동으로 업데이트된다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>형태</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>ForEach(players)</code></td>
<td>읽기 전용 (값 표시)</td>
</tr>
<tr>
<td><code>ForEach($players)</code></td>
<td>읽기 + 쓰기 (값 수정)</td>
</tr>
</tbody></table>
<hr>
<h2 id="stepper--증감-버튼으로-숫자-조절"><code>Stepper</code> — 증감 버튼으로 숫자 조절</h2>
<pre><code class="language-swift">Stepper(&quot;\(player.score)&quot;, value: $player.score)</code></pre>
<blockquote>
<p><code>Stepper</code>는 <code>+</code> / <code>-</code> 버튼으로 숫자를 1씩 올리고 내리는 UI 컴포넌트다.
첫 번째 파라미터는 레이블(표시 텍스트), <code>value:</code>는 바인딩된 숫자 값이다.</p>
</blockquote>
<h3 id="범위-제한도-가능">범위 제한도 가능</h3>
<pre><code class="language-swift">Stepper(&quot;\(player.score)&quot;, value: $player.score, in: 0...100)</code></pre>
<hr>
<h2 id="버튼으로-배열에-새-항목-추가">버튼으로 배열에 새 항목 추가</h2>
<pre><code class="language-swift">@State private var newPlayer: String = &quot;&quot;
@State private var players: [Player] = [
    Player(name: &quot;Asher&quot;, score: 0),
    Player(name: &quot;Joon&quot;, score: 0),
    Player(name: &quot;Nessa&quot;, score: 0)
]

Button(&quot;Add Player&quot;, systemImage: &quot;plus.circle.fill&quot;) {
    players.append(Player(name: newPlayer, score: 0))
}</code></pre>
<blockquote>
<p><code>players.append()</code>로 새 <code>Player</code>를 추가하면 <code>ForEach</code>가 자동으로 UI를 갱신한다.</p>
</blockquote>
<hr>
<h2 id="전체-코드-요약">전체 코드 요약</h2>
<pre><code class="language-swift">// Player.swift
struct Player: Identifiable {
    let id = UUID()
    var name: String
    var score: Int = 0
}

// ContentView.swift
struct ContentView: View {
    @State private var newPlayer: String = &quot;&quot;
    @State private var players: [Player] = [
        Player(name: &quot;Asher&quot;, score: 0),
        Player(name: &quot;Joon&quot;, score: 0),
        Player(name: &quot;Nessa&quot;, score: 0)
    ]

    var body: some View {
        VStack {
            ForEach($players) { $player in
                TextField(&quot;Name&quot;, text: $player.name)
                Stepper(&quot;\(player.score)&quot;, value: $player.score)
            }

            Button(&quot;Add Player&quot;, systemImage: &quot;plus.circle.fill&quot;) {
                players.append(Player(name: newPlayer, score: 0))
            }
            .padding()
            Spacer()
        }
        .padding()
    }
}</code></pre>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><code>Identifiable</code></td>
<td>고유 <code>id</code>를 보장하는 프로토콜 — <code>ForEach</code>에서 <code>id:</code> 생략 가능</td>
</tr>
<tr>
<td><code>UUID()</code></td>
<td>전 세계 유일한 식별자 자동 생성</td>
</tr>
<tr>
<td><code>ForEach($배열)</code></td>
<td>배열 요소를 바인딩으로 받아 직접 수정</td>
</tr>
<tr>
<td><code>Stepper</code></td>
<td><code>+</code> / <code>-</code> 버튼으로 숫자 값 증감</td>
</tr>
<tr>
<td><code>players.append()</code></td>
<td>배열에 새 요소 추가 → UI 자동 갱신</td>
</tr>
</tbody></table>
<hr>
<h2 id="다음에-배울-것">다음에 배울 것</h2>
<ul>
<li><code>.onDelete</code> 로 리스트 항목 스와이프 삭제</li>
<li><code>@Binding</code>으로 자식 뷰에서 부모 배열 수정</li>
<li><code>NavigationStack</code> + <code>NavigationLink</code>로 화면 전환</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 26 & macOS Tahoe: 더 유연하고 투명해진 SwiftUI 디자인 시스템 살펴보기]]></title>
            <link>https://velog.io/@asher_joon/iOS-26-macOS-Tahoe-%EB%8D%94-%EC%9C%A0%EC%97%B0%ED%95%98%EA%B3%A0-%ED%88%AC%EB%AA%85%ED%95%B4%EC%A7%84-SwiftUI-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@asher_joon/iOS-26-macOS-Tahoe-%EB%8D%94-%EC%9C%A0%EC%97%B0%ED%95%98%EA%B3%A0-%ED%88%AC%EB%AA%85%ED%95%B4%EC%A7%84-SwiftUI-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 18 Mar 2026 04:24:56 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 이번 WWDC25 세션 중 가장 시각적으로 즐거웠던 <strong>&quot;Build a SwiftUI app with the new design&quot;</strong> 내용을 정리했습니다. Apple이 새롭게 제시한 디자인 언어인 <strong>Liquid Glass</strong>를 어떻게 SwiftUI 프로젝트에 녹여낼 수 있을지 핵심만 짚어보겠습니다. 🎨</p>
<hr>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/03e19dca-dc5f-4293-b087-5524aadc5cd5/image.png" alt=""></p>
<h2 id="1-새로운-디자인의-심장-liquid-glass">1. 새로운 디자인의 심장: Liquid Glass</h2>
<p>이번 업데이트의 주인공은 단연 <strong>Liquid Glass(리퀴드 글래스)</strong>입니다. 기존의 Glassmorphism보다 한 단계 진화하여, 유리의 광학적 특성과 액체의 역동적인 움직임을 결합한 재질입니다. 새로운 디자인의 방식으로서 좀 더 편안한 디자인을 사용자에게 제공하고 있다고 생각합니다.</p>
<ul>
<li><strong>동적 적응:</strong> 스크롤되는 배경의 색상과 명암에 따라 실시간으로 빛을 굴절시키며 대비를 조절합니다.</li>
<li><strong>살아있는 인터랙션:</strong> 버튼을 누르거나 슬라이더를 옮길 때, 단순한 강조가 아니라 재질 자체가 반응하며 사용자에게 피드백을 줍니다.</li>
</ul>
<hr>
<h2 id="2-앱-구조app-structure의-진화">2. 앱 구조(App Structure)의 진화</h2>
<p>사용자가 앱을 탐색하는 방식에 맞춰 기본 컴포넌트들이 더 영리해졌습니다.</p>
<h3 id="📌-navigationsplitview--sidebars">📌 NavigationSplitView &amp; Sidebars</h3>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/883cce6c-e24a-4380-9b92-a6f2e6126dc6/image.png" alt=""></p>
<p>사이드바가 이제 콘텐츠 위에 떠 있는 <strong>Floating</strong> 형태로 변했습니다.</p>
<ul>
<li><strong><code>.backgroundExtensionEffect</code></strong>: 사이드바 뒤쪽으로 배경 이미지를 확장하여 콘텐츠가 잘리지 않고 자연스럽게 이어지게 만듭니다.</li>
</ul>
<h3 id="📌-tabview의-최소화-동작">📌 TabView의 최소화 동작</h3>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/34e89ab6-dc7e-46f1-83b9-67e52e1fc909/image.png" alt=""></p>
<p>아이폰의 한정된 화면을 더 넓게 쓸 수 있는 기능이 추가되었습니다.</p>
<pre><code class="language-swift">TabView {
    // 탭 콘텐츠
}
.tabBarMinimizeBehavior(.onScroll) // 스크롤 시 탭 바가 자동으로 축소됨</code></pre>
<hr>
<h2 id="3-주요-api-업데이트-및-코드-포인트">3. 주요 API 업데이트 및 코드 포인트</h2>
<h3 id="🔘-더-강력해진-buttons--controls">🔘 더 강력해진 Buttons &amp; Controls</h3>
<p>이제 표준 컨트롤에 Liquid Glass를 입히는 것이 매우 간단해졌습니다.</p>
<ul>
<li><strong>새로운 스타일</strong>: <code>.buttonStyle(.glass)</code> 또는 <code>.glassProminent</code>를 사용하여 즉시 적용 가능합니다.</li>
<li><strong>Concentricity(동심성)</strong>: 컨테이너의 곡률과 내부 버튼의 곡률을 완벽하게 맞추는 설정이 추가되었습니다.</li>
</ul>
<!-- end list -->

<pre><code class="language-swift">Rectangle()
    .fill(.blue)
    .clipShape(.concentricRectangle(corner: .container)) // 부모 뷰 곡률에 자동 맞춤</code></pre>
<h3 id="🔍-search-experience">🔍 Search Experience</h3>
<p>검색 인터페이스가 플랫폼별로 최적화되었습니다.</p>
<ul>
<li><strong>iOS</strong>: 검색 필드가 하단으로 이동하여 한 손 조작이 훨씬 쉬워졌습니다.</li>
<li><strong><code>.searchToolbarBehavior(.minimized)</code></strong>: 검색이 주 기능이 아닌 경우 아이콘으로 최소화해 두었다가 필요할 때 확장할 수 있습니다.</li>
</ul>
<h3 id="🏷️-toolbar-enhancements">🏷️ Toolbar Enhancements</h3>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/74463832-eb88-4167-9531-f3d3e1d291f8/image.png" alt=""></p>
<ul>
<li><code>ToolbarSpacer</code>: 버튼 간의 논리적 그룹화를 위해 고정 간격이나 유연한 간격을 줄 수 있습니다.</li>
<li><code>.badge()</code>: 이제 탭 바뿐만 아니라 툴바 아이템에도 배지를 직접 달 수 있습니다.</li>
</ul>
<hr>
<h2 id="4-커스텀-뷰에-liquid-glass-입히기">4. 커스텀 뷰에 Liquid Glass 입히기</h2>
<p>시스템 UI 외에 내가 만든 커스텀 컴포넌트에도 이 아름다운 효과를 적용할 수 있습니다.</p>
<pre><code class="language-swift">struct CustomBadge: View {
    var body: some View {
        Text(&quot;New&quot;)
            .glossEffect(.capsule) // 리퀴드 글래스 효과 적용
            .interactive(true)     // 터치 시 시각적 반응 활성화
    }
}</code></pre>
<blockquote>
<p><strong>💡 핵심 팁: GlassEffectContainer</strong>
여러 개의 Glass 요소가 겹칠 때 서로의 굴절 값을 공유하려면 <code>GlassEffectContainer</code>로 묶어줘야 합니다. 그렇지 않으면 경계선이 어색하게 보일 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="마치며-업데이트를-위한-체크리스트">마치며: 업데이트를 위한 체크리스트</h2>
<p>WWDC25 디자인 시스템을 앱에 적용하기 위한 3단계 요약입니다.</p>
<ol>
<li><strong>Xcode 26 SDK 빌드</strong>: 표준 컨트롤은 빌드만으로도 새로운 디자인이 상당 부분 자동 적용됩니다.</li>
<li><strong>커스텀 배경 제거</strong>: 기존에 <code>.presentationBackground</code> 등으로 직접 구현했던 어두운 배경이나 불투명한 레이어들을 제거하고 시스템의 Liquid Glass가 빛나게 하세요.</li>
<li><strong>아이콘 미세 조정</strong>: 새로운 시스템은 모노크롬(Monochrome) 렌더링을 선호합니다. 화려한 색상보다는 계층 구조를 통한 의미 전달에 집중하세요.</li>
</ol>
<hr>
<h3 id="🔗-참고-링크">🔗 참고 링크</h3>
<ul>
<li><a href="https://www.google.com/search?q=https://developer.apple.com/videos/play/wwdc2025/3MugGCtm26A">Apple Developer - Build a SwiftUI app with the new design</a></li>
</ul>
<hr>
<p><strong>도움이 되셨다면 공유와 댓글 부탁드립니다! 궁금한 점은 언제든 남겨주세요.</strong> 😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swift에서 var인데도 값 변경이 안 되는 이유 (Struct 내부에서 발생한 에러 해결)]]></title>
            <link>https://velog.io/@asher_joon/Swift%EC%97%90%EC%84%9C-var%EC%9D%B8%EB%8D%B0%EB%8F%84-%EA%B0%92-%EB%B3%80%EA%B2%BD%EC%9D%B4-%EC%95%88-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-Struct-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@asher_joon/Swift%EC%97%90%EC%84%9C-var%EC%9D%B8%EB%8D%B0%EB%8F%84-%EA%B0%92-%EB%B3%80%EA%B2%BD%EC%9D%B4-%EC%95%88-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-Struct-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 17 Mar 2026 01:44:45 GMT</pubDate>
            <description><![CDATA[<h1 id="swift에서-var인데도-값-변경이-안-되는-이유">Swift에서 <code>var</code>인데도 값 변경이 안 되는 이유</h1>
<p>SwiftUI로 간단한 <code>DiceView</code>를 만들다가 예상하지 못한 에러를 만났습니다.</p>
<p>분명 <code>var</code>로 선언한 변수인데도 값 변경이 되지 않았습니다.</p>
<p>처음에는 <strong>&quot;var인데 왜 변경이 안 되지?&quot;</strong> 라고 생각했지만
알고 보니 <strong>변수 문제가 아니라 코드 위치 문제</strong>였습니다.</p>
<p>이번 글에서는 그 이유를 정리해보려고 합니다.</p>
<hr>
<h1 id="문제-상황">문제 상황</h1>
<p><img src="https://velog.velcdn.com/images/asher_joon/post/ddc47094-9bdf-4752-95d3-fc084d49754d/image.png" alt=""></p>
<p>다음과 같은 코드를 작성했습니다.</p>
<pre><code class="language-swift">struct DiceView: View {
    let diceNumber: Int = 1
    var canChangeNumber: Int = 1

    canChangeNumber = 2

    init(canChageNumber: Int) {
        self.canChangeNumber = canChageNumber
    }

    var body: some View {
        Image(systemName: &quot;die.face.1&quot;)
            .font(.largeTitle)
    }
}</code></pre>
<p>여기서 다음 코드에서 에러가 발생합니다.</p>
<pre><code class="language-swift">canChangeNumber = 2</code></pre>
<hr>
<h1 id="let과-var의-차이"><code>let</code>과 <code>var</code>의 차이</h1>
<p>Swift에서 값을 선언하는 방법은 크게 두 가지가 있습니다.</p>
<h2 id="let"><code>let</code></h2>
<p>변경할 수 없는 <strong>상수</strong></p>
<pre><code class="language-swift">let number = 1
number = 2 // ❌ 에러</code></pre>
<h2 id="var"><code>var</code></h2>
<p>변경할 수 있는 <strong>변수</strong></p>
<pre><code class="language-swift">var number = 1
number = 2 // ✅ 가능</code></pre>
<p>그래서 처음에는 <code>var</code>인데도 값 변경이 안 되는 것이 이해되지 않았습니다.</p>
<p>하지만 문제는 <strong>var 자체가 아니라 코드 위치</strong>에 있었습니다.</p>
<hr>
<h1 id="에러가-발생한-진짜-이유">에러가 발생한 진짜 이유</h1>
<p>Swift의 <code>struct</code> 내부에는 다음 요소만 올 수 있습니다.</p>
<ul>
<li>Property (프로퍼티)</li>
<li>Method (함수)</li>
<li>Initializer (<code>init</code>)</li>
</ul>
<p>하지만 아래 코드는 <strong>실행문(statement)</strong> 입니다.</p>
<pre><code class="language-swift">canChangeNumber = 2</code></pre>
<p>이 코드는 <strong>어떤 함수나 <code>init</code> 안에도 포함되어 있지 않습니다.</strong></p>
<p>그래서 Swift는</p>
<blockquote>
<p>&quot;이 코드는 언제 실행해야 하는 코드지?&quot;</p>
</blockquote>
<p>라고 판단하게 되고 <strong>컴파일 에러가 발생합니다.</strong></p>
<p>즉, <strong>struct 내부에는 실행 코드를 직접 작성할 수 없습니다.</strong></p>
<hr>
<h1 id="해결-방법">해결 방법</h1>
<h2 id="1️⃣-초기값을-직접-변경">1️⃣ 초기값을 직접 변경</h2>
<p>가장 간단한 방법입니다.</p>
<pre><code class="language-swift">var canChangeNumber: Int = 2</code></pre>
<hr>
<h2 id="2️⃣-init에서-변경-가장-일반적인-방법">2️⃣ <code>init</code>에서 변경 (가장 일반적인 방법)</h2>
<pre><code class="language-swift">struct DiceView: View {
    let diceNumber: Int = 1
    var canChangeNumber: Int = 1

    init(canChageNumber: Int) {
        self.canChangeNumber = canChageNumber
    }

    var body: some View {
        Image(systemName: &quot;die.face.1&quot;)
            .font(.largeTitle)
    }
}</code></pre>
<hr>
<h2 id="3️⃣-함수에서-변경">3️⃣ 함수에서 변경</h2>
<pre><code class="language-swift">func changeNumber() {
    canChangeNumber = 2
}</code></pre>
<hr>
<h1 id="swiftui에서-상태를-바꿀-때">SwiftUI에서 상태를 바꿀 때</h1>
<p>SwiftUI에서는 값이 바뀌면 <strong>UI도 함께 업데이트</strong>되어야 합니다.</p>
<p>그래서 보통 다음과 같이 <code>@State</code>를 사용합니다.</p>
<pre><code class="language-swift">@State var diceNumber = 1</code></pre>
<p>이렇게 하면 값이 바뀔 때 <strong>화면도 자동으로 다시 렌더링</strong>됩니다.</p>
<hr>
<h1 id="정리">정리</h1>
<p>이번 에러의 핵심은 다음과 같습니다.</p>
<ul>
<li><code>var</code>라서 문제가 발생한 것이 아니다</li>
<li><code>struct</code> 내부에는 <strong>실행문을 직접 작성할 수 없다</strong></li>
<li>값 변경은 <strong>init 또는 함수 내부에서 해야 한다</strong></li>
</ul>
<p>SwiftUI를 처음 공부할 때 <strong>struct 구조 때문에 발생하는 에러가 꽤 많습니다.</strong></p>
<p>이번 문제를 통해 <strong>Swift struct의 기본 구조를 이해하는 데 도움이 되었습니다.</strong></p>
<hr>
<p>💡 SwiftUI 공부하면서 겪은 문제들을 계속 정리해보려고 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI Day 4 - 온보딩 플로우 (TabView Page Style, 파라미터 컴포넌트)]]></title>
            <link>https://velog.io/@asher_joon/SwiftUI-Day-4-%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%8C%EB%A1%9C%EC%9A%B0-TabView-Page-Style-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
            <guid>https://velog.io/@asher_joon/SwiftUI-Day-4-%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%8C%EB%A1%9C%EC%9A%B0-TabView-Page-Style-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</guid>
            <pubDate>Mon, 16 Mar 2026 17:45:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/asher_joon/post/adeb1180-4703-4022-b7c1-161d2109cf75/image.png" alt="">
<img src="https://velog.velcdn.com/images/asher_joon/post/6073c3aa-e076-449e-96cb-49495bd4d6da/image.png" alt=""></p>
<hr>
<h2 id="📁-프로젝트-파일-구조">📁 프로젝트 파일 구조</h2>
<pre><code>OnboardingFlow/
├── OnboardingFlowApp.swift      # @main 진입점
├── ContentView.swift            # TabView (page 스타일) + 그라디언트 배경
├── WelcomePage.swift            # 첫 번째 온보딩 페이지
├── FeaturePage.swift            # 두 번째 온보딩 페이지
└── FeatureCard.swift            # 재사용 가능한 기능 카드 컴포넌트</code></pre><hr>
<h2 id="📱-tabview--페이지-스타일-온보딩">📱 TabView — 페이지 스타일 (온보딩)</h2>
<p><code>TabView</code>에 <code>.tabViewStyle(.page)</code>를 적용하면 바텀탭바 대신 <strong>좌우 스와이프 페이지</strong> 형태가 된다.</p>
<pre><code class="language-swift">struct ContentView: View {
    let gradientColors: [Color] = [.grdientTop, .gradientBottom]

    var body: some View {
        TabView {
            WelcomePage()
            FeaturePage()
        }
        .background(Gradient(colors: gradientColors))
        .tabViewStyle(.page)   // 핵심: 페이지 스타일 적용
    }
}</code></pre>
<h3 id="tabview-스타일-비교">TabView 스타일 비교</h3>
<table>
<thead>
<tr>
<th>스타일</th>
<th>코드</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>바텀 탭바</td>
<td><code>.tabViewStyle(.automatic)</code> 또는 기본값</td>
<td>하단 탭 UI</td>
</tr>
<tr>
<td>페이지 스와이프</td>
<td><code>.tabViewStyle(.page)</code></td>
<td>좌우 스와이프 + 점(dot) 인디케이터</td>
</tr>
</tbody></table>
<blockquote>
<p>온보딩 화면처럼 &quot;순서대로 넘기는&quot; UX에는 <code>.page</code> 스타일이 적합하다.</p>
</blockquote>
<hr>
<h2 id="🌈-gradient-배경">🌈 Gradient 배경</h2>
<pre><code class="language-swift">let gradientColors: [Color] = [.grdientTop, .gradientBottom]

.background(Gradient(colors: gradientColors))</code></pre>
<blockquote>
<p><code>Gradient(colors:)</code>는 위→아래 방향의 선형 그라디언트를 생성한다.
색상 배열의 순서가 그라디언트 방향을 결정한다.</p>
</blockquote>
<hr>
<h2 id="🧩-파라미터-있는-컴포넌트-featurecard">🧩 파라미터 있는 컴포넌트 (FeatureCard)</h2>
<p>컴포넌트에 <code>let</code> 프로퍼티를 선언하면 <strong>외부에서 값을 주입</strong>할 수 있다.</p>
<pre><code class="language-swift">struct FeatureCard: View {
    let iconName: String      // 외부에서 받는 파라미터
    let description: String

    var body: some View {
        HStack {
            Image(systemName: iconName)   // 파라미터 사용
                .font(.largeTitle)
                .frame(width: 50)
                .padding(.trailing, 10)

            Text(description)

            Spacer()
        }
        .padding()
        .background(.tint, in: RoundedRectangle(cornerRadius: 12))
        .foregroundStyle(Color(.white))
    }
}

// 사용법 — 각각 다른 아이콘과 텍스트 전달
FeatureCard(iconName: &quot;person.2.crop.square.stack.fill&quot;, description: &quot;소셜 기능 설명&quot;)
FeatureCard(iconName: &quot;quote.bubble.fill&quot;, description: &quot;메시지 기능 설명&quot;)</code></pre>
<blockquote>
<p>파라미터 컴포넌트를 쓰면 <strong>같은 UI 구조, 다른 데이터</strong>를 반복 없이 표현할 수 있다.</p>
</blockquote>
<hr>
<h2 id="🎨-backgroundtint-in-문법">🎨 <code>.background(.tint, in:)</code> 문법</h2>
<pre><code class="language-swift">.background(.tint, in: RoundedRectangle(cornerRadius: 12))</code></pre>
<table>
<thead>
<tr>
<th>파라미터</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>.tint</code></td>
<td>앱의 액센트 컬러 (자동 적용)</td>
</tr>
<tr>
<td><code>in: RoundedRectangle(...)</code></td>
<td>배경의 모양 지정</td>
</tr>
</tbody></table>
<blockquote>
<p><code>.tint</code>는 앱 전체의 포인트 컬러를 자동으로 가져오므로, 특정 색상을 하드코딩하지 않아도 된다.</p>
</blockquote>
<hr>
<h2 id="🏠-welcomepage--첫-화면">🏠 WelcomePage — 첫 화면</h2>
<pre><code class="language-swift">struct WelcomePage: View {
    var body: some View {
        VStack {
            ZStack {
                RoundedRectangle(cornerRadius: 30)
                    .frame(width: 150, height: 150)
                    .foregroundColor(Color.blue)

                Image(systemName: &quot;figure.2.and.child.holdinghands&quot;)
                    .font(.system(size: 70))
                    .foregroundColor(.white)
            }
            Text(&quot;Welcome to My App&quot;)
                .font(.largeTitle)
                .fontWeight(.semibold)
                .padding(.top)

            Text(&quot;Description text&quot;)
                .font(.title2)
        }
        .padding()
    }
}</code></pre>
<blockquote>
<p>Day 1에서 배운 ZStack(배경 도형 + 아이콘) 패턴을 온보딩에 그대로 활용.</p>
</blockquote>
<hr>
<h2 id="📄-featurepage--기능-소개-화면">📄 FeaturePage — 기능 소개 화면</h2>
<pre><code class="language-swift">struct FeaturePage: View {
    var body: some View {
        VStack {
            Text(&quot;Feature Page&quot;)
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding(.bottom)

            FeatureCard(iconName: &quot;person.2.crop.square.stack.fill&quot;,
                        description: &quot;A multiline description about a feature&quot;)
            FeatureCard(iconName: &quot;quote.bubble.fill&quot;,
                        description: &quot;Short description about a feature&quot;)
        }
        .padding()
    }
}</code></pre>
<blockquote>
<p>파라미터 컴포넌트 덕분에 화면은 단순히 카드를 나열하는 역할만 한다.</p>
</blockquote>
<hr>
<h2 id="✅-핵심-정리">✅ 핵심 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><code>TabView</code> + <code>.page</code></td>
<td>좌우 스와이프 온보딩 UI</td>
</tr>
<tr>
<td><code>Gradient(colors:)</code></td>
<td>배열 순서대로 그라디언트 배경</td>
</tr>
<tr>
<td><code>let</code> 프로퍼티</td>
<td>컴포넌트에 외부에서 값 주입</td>
</tr>
<tr>
<td><code>.background(.tint, in:)</code></td>
<td>모양 + 색상을 한 번에 배경 지정</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-다음에-배울-것">✅ 다음에 배울 것</h2>
<ul>
<li><code>@State</code>, <code>@Binding</code>으로 온보딩 완료 여부 관리</li>
<li><code>AppStorage</code>로 온보딩 다시 안 보이게 처리</li>
<li>온보딩 완료 후 메인 화면으로 전환 (<code>fullScreenCover</code>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI Day 3 - Apple 공식 튜토리얼 따라하기 (구조체, 연산 프로퍼티, 조건 표현식)]]></title>
            <link>https://velog.io/@asher_joon/SwiftUI-Day-3-Apple-%EA%B3%B5%EC%8B%9D-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-%EA%B5%AC%EC%A1%B0%EC%B2%B4-%EC%97%B0%EC%82%B0-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EC%A1%B0%EA%B1%B4-%ED%91%9C%ED%98%84%EC%8B%9D</link>
            <guid>https://velog.io/@asher_joon/SwiftUI-Day-3-Apple-%EA%B3%B5%EC%8B%9D-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-%EA%B5%AC%EC%A1%B0%EC%B2%B4-%EC%97%B0%EC%82%B0-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EC%A1%B0%EA%B1%B4-%ED%91%9C%ED%98%84%EC%8B%9D</guid>
            <pubDate>Mon, 16 Mar 2026 17:43:12 GMT</pubDate>
            <description><![CDATA[<h1 id="업로드중"><img src="blob:https://velog.io/dfc3f24e-8223-48a2-b4c8-48e14786bbd7" alt="업로드중.."></h1>
<blockquote>
<p>이 내용은 Apple 공식 SwiftUI 튜토리얼을 따라 실습한 내용입니다.</p>
</blockquote>
<hr>
<h2 id="📁-프로젝트-파일-구조">📁 프로젝트 파일 구조</h2>
<pre><code>Tutorial/
├── TutorialApp.swift        # @main 진입점
└── ContentView.swift        # DateInfo 구조체 + 날씨 UI</code></pre><hr>
<h2 id="🧱-데이터를-담는-구조체-struct">🧱 데이터를 담는 구조체 (Struct)</h2>
<p>뷰에서 사용하는 <strong>데이터를 구조체로 정의</strong>하면 재사용성과 가독성이 높아진다.</p>
<pre><code class="language-swift">struct DateInfo: View {
    let days: String    // 요일 (Mon, Tue, ...)
    let isRainy: Bool   // 비 여부
    let high: Int       // 최고 기온
    let low: Int        // 최저 기온

    var body: some View { ... }
}</code></pre>
<blockquote>
<p><code>View</code> 프로토콜을 채택한 구조체는 <code>body</code>를 통해 UI를 표현한다.
<code>let</code> 프로퍼티로 선언하면 외부에서 초기화 시 값을 주입받는다.</p>
</blockquote>
<hr>
<h2 id="🔁-연산-프로퍼티-computed-property">🔁 연산 프로퍼티 (Computed Property)</h2>
<p>저장된 값을 직접 리턴하는 게 아니라, <strong>조건에 따라 계산해서 반환</strong>하는 프로퍼티.</p>
<pre><code class="language-swift">var iconName: String {
    isRainy ? &quot;sun.max.fill&quot; : &quot;cloud.rain.fill&quot;
}

var iconColor: Color {
    isRainy ? Color.yellow : Color.blue
}</code></pre>
<blockquote>
<p>연산 프로퍼티를 쓰면 <code>body</code> 안에 분기 로직을 넣지 않아도 된다 — 코드가 깔끔해진다.</p>
</blockquote>
<hr>
<h2 id="❓-삼항-연산자-ternary-operator">❓ 삼항 연산자 (Ternary Operator)</h2>
<pre><code class="language-swift">조건 ? 참일 때 값 : 거짓일 때 값</code></pre>
<pre><code class="language-swift">isRainy ? &quot;sun.max.fill&quot; : &quot;cloud.rain.fill&quot;
// isRainy가 true면 &quot;sun.max.fill&quot;
// isRainy가 false면 &quot;cloud.rain.fill&quot;</code></pre>
<blockquote>
<p>Swift의 삼항 연산자는 <code>if/else</code> 한 줄 버전. 값을 반환해야 할 때 자주 쓰인다.</p>
</blockquote>
<hr>
<h2 id="🖊-문자열-보간-string-interpolation">🖊 문자열 보간 (String Interpolation)</h2>
<p>변수 값을 문자열 안에 직접 삽입하는 문법.</p>
<pre><code class="language-swift">Text(&quot;High: \(high)&quot;)   // &quot;High: 80&quot;
Text(&quot;Low: \(low)&quot;)     // &quot;Low: 30&quot;</code></pre>
<blockquote>
<p><code>\(변수명)</code> 형태로 사용. Swift에서 가장 기본적인 문자열 합성 방법.</p>
</blockquote>
<hr>
<h2 id="🎨-fontweight로-글자-굵기-조절">🎨 Font.Weight로 글자 굵기 조절</h2>
<pre><code class="language-swift">Text(&quot;High: \(high)&quot;)
    .fontWeight(Font.Weight.semibold)   // 반굵게

Text(&quot;Low: \(low)&quot;)
    .fontWeight(Font.Weight.medium)     // 중간 굵기
    .foregroundStyle(Color.secondary)   // 회색 계열 보조 색상</code></pre>
<h3 id="fontweight-종류-가벼운-순">Font.Weight 종류 (가벼운 순)</h3>
<table>
<thead>
<tr>
<th>Weight</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>.ultraLight</code></td>
<td>가장 얇음</td>
</tr>
<tr>
<td><code>.thin</code></td>
<td>얇음</td>
</tr>
<tr>
<td><code>.light</code></td>
<td>가벼움</td>
</tr>
<tr>
<td><code>.regular</code></td>
<td>기본</td>
</tr>
<tr>
<td><code>.medium</code></td>
<td>중간</td>
</tr>
<tr>
<td><code>.semibold</code></td>
<td>반굵게</td>
</tr>
<tr>
<td><code>.bold</code></td>
<td>굵게</td>
</tr>
<tr>
<td><code>.heavy</code></td>
<td>두껍게</td>
</tr>
<tr>
<td><code>.black</code></td>
<td>가장 두껍게</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎨-colorsecondary">🎨 Color.secondary</h2>
<pre><code class="language-swift">.foregroundStyle(Color.secondary)</code></pre>
<blockquote>
<p><code>Color.secondary</code>는 시스템이 자동으로 정하는 <strong>보조 색상</strong> (보통 회색).
다크모드/라이트모드 대응이 자동으로 된다 — 하드코딩 색상보다 권장.</p>
</blockquote>
<hr>
<h2 id="📋-전체-코드">📋 전체 코드</h2>
<pre><code class="language-swift">struct ContentView: View {
    var body: some View {
        HStack {
            DateInfo(days: &quot;Mon&quot;, isRainy: true,  high: 80, low: 30)
            DateInfo(days: &quot;Tue&quot;, isRainy: false, high: 30, low: 20)
        }
    }
}

struct DateInfo: View {
    let days: String
    let isRainy: Bool
    let high: Int
    let low: Int

    var iconName: String {
        isRainy ? &quot;sun.max.fill&quot; : &quot;cloud.rain.fill&quot;
    }
    var iconColor: Color {
        isRainy ? Color.yellow : Color.blue
    }

    var body: some View {
        VStack {
            Text(days)
                .font(Font.headline)
            Image(systemName: iconName)
                .foregroundStyle(iconColor)
                .font(Font.largeTitle)
                .padding(5)
            Text(&quot;High: \(high)&quot;)
                .fontWeight(Font.Weight.semibold)
            Text(&quot;Low: \(low)&quot;)
                .fontWeight(Font.Weight.medium)
                .foregroundStyle(Color.secondary)
        }
        .padding(10)
    }
}</code></pre>
<hr>
<h2 id="✅-핵심-정리">✅ 핵심 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>구조체 + <code>View</code></td>
<td>UI와 데이터를 하나의 단위로 묶기</td>
</tr>
<tr>
<td>연산 프로퍼티</td>
<td>조건 로직을 <code>body</code> 밖으로 분리</td>
</tr>
<tr>
<td>삼항 연산자</td>
<td>조건에 따른 값 반환 한 줄로 표현</td>
</tr>
<tr>
<td>문자열 보간 <code>\()</code></td>
<td>변수를 문자열 안에 직접 삽입</td>
</tr>
<tr>
<td><code>Color.secondary</code></td>
<td>다크모드 자동 대응 보조 색상</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-다음에-배울-것">✅ 다음에 배울 것</h2>
<ul>
<li><code>ForEach</code>로 데이터 배열을 리스트로 렌더링</li>
<li><code>@State</code>로 뷰 내부 상태 관리</li>
<li><code>ObservableObject</code>로 뷰 외부 데이터 바인딩</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] HStack, VStack, ZStack으로 헤더 컴포넌트 만들기]]></title>
            <link>https://velog.io/@asher_joon/SwiftUI-HStack-VStack-ZStack%EC%9C%BC%EB%A1%9C-%ED%97%A4%EB%8D%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@asher_joon/SwiftUI-HStack-VStack-ZStack%EC%9C%BC%EB%A1%9C-%ED%97%A4%EB%8D%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 11 Mar 2026 14:39:05 GMT</pubDate>
            <description><![CDATA[<h1 id="swiftui-입문--f1-앱-헤더-만들기">SwiftUI 입문 — F1 앱 헤더 만들기</h1>
<blockquote>
<p>Apple Developer Academy @ POSTECH 입과 후 첫 번째 SwiftUI 학습 기록입니다.
제가 스스로 연습과 학습을 위한 학습입니다. 애플아카데미에서 제공하는 학습자료가 아닌 점 알려드립니다. 전혀 무관함을 알려드립니다. 개인프로젝트입니다.</p>
</blockquote>
<hr>
<h2 id="들어가며">들어가며</h2>
<p>SwiftUI를 처음 접하면서 가장 먼저 만들어본 것은 F1 레이싱 앱의 헤더 컴포넌트입니다. 단순해 보이지만 레이아웃의 핵심 개념인 <code>HStack</code>, <code>VStack</code>, <code>ZStack</code>, <code>Spacer</code>를 모두 활용할 수 있는 좋은 출발점이었습니다.</p>
<p>완성된 헤더는 아래와 같습니다.</p>
<pre><code>☰  MONACO GRAND PRIX                    ⚙
   🔴 Live • Lap 42/78</code></pre><hr>
<h2 id="프로젝트-구조">프로젝트 구조</h2>
<p>파일을 역할별로 분리하는 것부터 시작했습니다.</p>
<pre><code>practiceApp/
├── Assets.xcassets
├── Components/
│   └── Header.swift       # 재사용 가능한 UI 부품
├── practiceApp.swift      # @main 앱 진입점
└── Screens/
    └── HomeView.swift     # 실제 화면</code></pre><ul>
<li><code>Components/</code> — 여러 화면에서 재사용할 수 있는 작은 UI 단위</li>
<li><code>Screens/</code> — 하나의 완성된 화면 단위</li>
</ul>
<blockquote>
<p><code>ContentView.swift</code>는 필수가 아닙니다. <code>@main</code> 파일에서 원하는 View를 바로 진입점으로 지정할 수 있습니다.</p>
</blockquote>
<pre><code class="language-swift">@main
struct practiceApp: App {
    var body: some Scene {
        WindowGroup {
            HomeView() // ContentView 없이 바로 연결
        }
    }
}</code></pre>
<hr>
<h2 id="레이아웃-기초--hstack--vstack--zstack">레이아웃 기초 — HStack / VStack / ZStack</h2>
<p>SwiftUI의 레이아웃은 세 가지 Stack으로 구성됩니다.</p>
<table>
<thead>
<tr>
<th>Stack</th>
<th>방향</th>
<th>주요 용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>HStack</code></td>
<td>가로 (←→)</td>
<td>아이콘, 버튼 나열</td>
</tr>
<tr>
<td><code>VStack</code></td>
<td>세로 (↑↓)</td>
<td>제목 + 부제목 배치</td>
</tr>
<tr>
<td><code>ZStack</code></td>
<td>앞뒤 겹치기</td>
<td>배경 위에 컨텐츠 올리기</td>
</tr>
</tbody></table>
<p>각 Stack은 <code>alignment</code> 파라미터로 정렬 방향을 지정할 수 있습니다.</p>
<pre><code class="language-swift">VStack(alignment: .leading) { }  // 왼쪽 정렬
ZStack(alignment: .top) { }      // 위쪽 정렬</code></pre>
<hr>
<h2 id="spacer--공간을-차지하는-투명한-뷰">Spacer() — 공간을 차지하는 투명한 뷰</h2>
<p><code>Spacer()</code>는 <strong>남은 공간을 전부 차지하는 투명한 뷰</strong>입니다. 직접 무언가를 밀어내는 것이 아니라, 공간을 먼저 차지함으로써 다른 뷰들이 상대적으로 밀려나는 효과를 만듭니다.</p>
<pre><code class="language-swift">// HStack — 양쪽 끝으로 분리
HStack {
    Text(&quot;왼쪽&quot;)
    Spacer()
    Text(&quot;오른쪽&quot;)
}

// VStack — 헤더를 상단에 고정
VStack {
    Header()
    Spacer()
}</code></pre>
<hr>
<h2 id="전체-화면-검은-배경-적용">전체 화면 검은 배경 적용</h2>
<p><code>ZStack</code>으로 <code>Color.black</code>을 배경으로 먼저 깔고, 그 위에 컨텐츠를 올립니다. <code>.ignoresSafeArea()</code>를 적용하면 노치/홈바 영역까지 색상이 채워집니다.</p>
<pre><code class="language-swift">struct HomeView: View {
    var body: some View {
        ZStack(alignment: .top) {
            Color.black
                .ignoresSafeArea()  // Safe Area 영역까지 검은색

            VStack(alignment: .leading) {
                Header()
                Spacer()
            }
        }
    }
}</code></pre>
<hr>
<h2 id="sf-symbols">SF Symbols</h2>
<p>Apple이 제공하는 <strong>6,000개 이상의 무료 아이콘 라이브러리</strong>입니다. 별도 설치 없이 <code>Image(systemName:)</code>으로 바로 사용할 수 있습니다.</p>
<pre><code class="language-swift">Image(systemName: &quot;line.3.horizontal&quot;)  // 햄버거 메뉴
Image(systemName: &quot;gearshape&quot;)          // 설정
Image(systemName: &quot;magnifyingglass&quot;)    // 검색</code></pre>
<blockquote>
<p>Mac App Store에서 <strong>SF Symbols</strong> 앱을 설치하면 아이콘을 카테고리별로 검색하고 이름을 바로 복사할 수 있어 개발 속도가 훨씬 빨라집니다.</p>
</blockquote>
<hr>
<h2 id="커스텀-컬러--color-extension">커스텀 컬러 — Color Extension</h2>
<p><code>extension</code>으로 Color를 확장하면 프로젝트 전반에서 <code>.f1Red</code>처럼 기본 색상처럼 사용할 수 있습니다.</p>
<pre><code class="language-swift">extension Color {
    static let f1Red = Color(red: 225/255, green: 6/255, blue: 0/255)
}

// 사용
.foregroundColor(.f1Red)
.foregroundColor(.f1Red.opacity(0.3))  // 투명도 조절</code></pre>
<hr>
<h2 id="배경-있는-버튼--zstack-활용">배경 있는 버튼 — ZStack 활용</h2>
<p><code>ZStack</code>으로 도형과 버튼을 겹쳐서 배경이 있는 버튼을 만들 수 있습니다.</p>
<pre><code class="language-swift">ZStack {
    RoundedRectangle(cornerRadius: 10)
        .frame(width: 50, height: 50)
        .foregroundColor(.f1Red.opacity(0.3))
    Button(action: {}) {
        Image(systemName: &quot;gearshape&quot;)
            .foregroundColor(.red)
            .font(.system(size: 30))
    }
}</code></pre>
<hr>
<h2 id="최종-코드">최종 코드</h2>
<pre><code class="language-swift">// Header.swift
import SwiftUI

extension Color {
    static let f1Red = Color(red: 225/255, green: 6/255, blue: 0/255)
}

struct Header: View {
    var body: some View {
        HStack {
            Button(action: {}) {
                Image(systemName: &quot;line.3.horizontal&quot;)
                    .foregroundColor(.red)
                    .font(.system(size: 30))
            }

            VStack(alignment: .leading) {
                Text(&quot;Monaco Grand Prix&quot;)
                    .font(.title3)
                    .bold()
                    .foregroundColor(.white)
                HStack {
                    Circle()
                        .frame(width: 8, height: 8)
                        .foregroundColor(.red)
                    Text(&quot;Live • Lap 42/78&quot;)
                        .font(.title3)
                        .bold()
                        .foregroundColor(.red)
                }
            }
            .padding()

            Spacer()

            ZStack {
                RoundedRectangle(cornerRadius: 10)
                    .frame(width: 50, height: 50)
                    .foregroundColor(.f1Red.opacity(0.3))
                Button(action: {}) {
                    Image(systemName: &quot;gearshape&quot;)
                        .foregroundColor(.red)
                        .font(.system(size: 30))
                }
            }
        }
        .padding(.horizontal)
    }
}</code></pre>
<hr>
<h2 id="마치며">마치며</h2>
<p>첫 SwiftUI 컴포넌트를 만들면서 느낀 것은, UIKit과 달리 레이아웃을 <strong>선언적으로 구성</strong>한다는 점이 매우 직관적이라는 것입니다. Stack을 조합하고 Spacer로 공간을 배분하는 방식에 익숙해지면 복잡한 레이아웃도 명확하게 구조화할 수 있을 것 같습니다.</p>
<p>다음에는 <code>@State</code>와 <code>@Binding</code>을 활용해 버튼에 실제 동작을 연결하고, 바텀 네비게이션바를 통한 화면 전환을 구현할 예정입니다.</p>
<hr>
<blockquote>
<p>학습 기록은 GitHub <a href="https://github.com/TheSoftBelly/AppleDeveloperAcademy_TIL">AppleDeveloperAcademy_TIL</a>에서 확인할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>