<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>일상 코딩</title>
    <link>https://polarcompass.tistory.com/</link>
    <description>일상적인 코딩 공부 공유 블로그</description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 00:56:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>polarcompass</managingEditor>
    <image>
      <title>일상 코딩</title>
      <url>https://tistory1.daumcdn.net/tistory/4280825/attach/6ae79abc64fe44a3978bdcebba2dbef2</url>
      <link>https://polarcompass.tistory.com</link>
    </image>
    <item>
      <title>[윈도우/리눅스/맥] OpenClaw가 열어준 시대, Anthropic은 어떻게 대응했나 &amp;mdash; Claude Code의 Channels &amp;middot; Remote Control &amp;middot; Dispatch 완전 가이드</title>
      <link>https://polarcompass.tistory.com/321</link>
      <description>&lt;p&gt;Now I have all the information I need. Let me write the complete blog post.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;OpenClaw가 열어준 시대, Anthropic은 어떻게 대응했나 — Claude Code의 Channels · Remote Control · Dispatch 완전 가이드&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — 2026년 초 OpenClaw가 &amp;quot;메신저로 AI 에이전트를 24시간 부린다&amp;quot;는 컨셉을 대중화했다. Anthropic은 Claude Code에 Channels(텔레그램·디스코드·iMessage 연동), Remote Control(웹·모바일에서 로컬 세션 이어하기), Dispatch(폰에서 데스크톱에 작업 던지기) 세 가지를 공식 탑재해 정면 대응했다. 이 글에서는 세 기능을 한눈에 비교하고, Channels 설정법을 단계별로 따라 해 보며, OpenClaw와의 차이까지 정리한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;1. 서론 — &amp;quot;메신저 → AI 에이전트&amp;quot; 시대가 열리다&lt;/h2&gt;
&lt;p&gt;2025년 11월, 오스트리아 개발자 Peter Steinberger가 만든 오픈소스 AI 에이전트 런타임 OpenClaw(구 Clawdbot → Moltbot)가 공개됐다. OpenClaw의 핵심 가치는 단순했다. &lt;strong&gt;&amp;quot;내가 쓰는 메신저에서 AI 에이전트에게 DM을 보내면, 그 에이전트가 내 로컬 머신에서 실제 작업을 수행하고 결과를 다시 메신저로 돌려준다.&amp;quot;&lt;/strong&gt; WhatsApp, Telegram, Discord, Signal 등 다양한 채널을 지원하고, 장기 세션 유지와 영구 메모리 기능까지 갖추면서 개발자 커뮤니티를 빠르게 장악했다. &amp;quot;이제 Mac Mini 하나만 있으면 24시간 코딩 에이전트를 돌릴 수 있다&amp;quot;는 밈이 퍼질 정도였다.&lt;/p&gt;
&lt;p&gt;2026년 2~3월, Anthropic이 이에 정면 대응했다. Claude Code에 &lt;strong&gt;세 가지 원격 기능&lt;/strong&gt;을 연달아 탑재한 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Remote Control&lt;/strong&gt; (2026년 2월) — 로컬에서 돌고 있는 Claude Code 세션을 claude.ai/code 웹 또는 Claude 모바일 앱에서 이어서 조작&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Channels&lt;/strong&gt; (2026년 3월 리서치 프리뷰) — Telegram, Discord, iMessage 메시지를 MCP 서버 기반 플러그인으로 실행 중인 Claude Code 세션에 밀어넣는 기능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dispatch&lt;/strong&gt; (2026년 3월) — Claude 모바일 앱에서 데스크톱(Cowork/Code)에 작업을 원격 지시하고, Computer Use까지 연계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI YouTuber Matthew Berman은 이를 두고 *&amp;quot;They&amp;#39;ve BUILT OpenClaw&amp;quot;*라고 요약했고, 커뮤니티에서는 &amp;quot;더 이상 Mac Mini를 따로 살 필요 없다&amp;quot;는 반응이 쏟아졌다. 물론 OpenClaw만의 강점도 여전히 있다. 이 글에서는 세 기능을 상세히 비교하고, 특히 Channels의 Telegram 연동을 처음부터 끝까지 따라 해 본 뒤, OpenClaw와의 객관적 비교로 마무리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 세 기능 한눈에 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Channels&lt;/th&gt;
&lt;th&gt;Remote Control&lt;/th&gt;
&lt;th&gt;Dispatch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;트리거&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;외부 메신저에서 메시지 push&lt;/td&gt;
&lt;td&gt;사용자가 웹/앱에서 세션 접속&lt;/td&gt;
&lt;td&gt;모바일 앱에서 작업 지시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;실행 환경&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;로컬 머신 (CLI)&lt;/td&gt;
&lt;td&gt;로컬 머신 (CLI 또는 VS Code)&lt;/td&gt;
&lt;td&gt;로컬 머신 (Desktop 앱)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;지원 플랫폼&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Telegram, Discord, iMessage&lt;/td&gt;
&lt;td&gt;claude.ai/code, Claude iOS/Android&lt;/td&gt;
&lt;td&gt;Claude iOS/Android + Desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;필요 구독&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pro, Max (Team/Enterprise는 관리자 활성화)&lt;/td&gt;
&lt;td&gt;Pro, Max, Team, Enterprise&lt;/td&gt;
&lt;td&gt;Pro, Max (Desktop 앱 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;핵심 용도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;외부 이벤트(CI 실패, 채팅 메시지 등)에 반응&lt;/td&gt;
&lt;td&gt;진행 중인 작업을 다른 기기에서 이어하기&lt;/td&gt;
&lt;td&gt;외출 중 새 작업을 데스크톱에 던지기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;세션 특성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;기존 열린 세션에 이벤트 주입&lt;/td&gt;
&lt;td&gt;기존 열린 세션에 원격 접속&lt;/td&gt;
&lt;td&gt;새 세션 자동 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;양방향 통신&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O (reply 도구로 메신저에 응답)&lt;/td&gt;
&lt;td&gt;O (실시간 대화)&lt;/td&gt;
&lt;td&gt;O (완료 알림 + 결과물 전달)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;핵심 차이를 한 마디로 정리하면 이렇다. Channels는 &lt;strong&gt;&amp;quot;외부에서 이벤트가 들어와서 Claude가 반응&amp;quot;&lt;/strong&gt;하는 push 모델이고, Remote Control은 &lt;strong&gt;&amp;quot;내가 원할 때 세션에 접속해서 조종&amp;quot;&lt;/strong&gt;하는 pull 모델이며, Dispatch는 &lt;strong&gt;&amp;quot;새 작업을 던지면 Desktop이 알아서 실행&amp;quot;&lt;/strong&gt;하는 delegation 모델이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Channels — 텔레그램으로 Claude Code 조종하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;⚠️ &lt;strong&gt;Channels는 2026년 3월 현재 Research Preview 상태입니다.&lt;/strong&gt; &lt;code&gt;--channels&lt;/code&gt; 플래그 문법과 프로토콜 규약이 피드백에 따라 변경될 수 있습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Channels는 이 글의 메인 주제다. MCP(Model Context Protocol) 서버가 외부 메신저의 메시지를 실행 중인 Claude Code 세션에 &lt;strong&gt;push&lt;/strong&gt;하는 구조로, Claude가 터미널 바깥에서 일어나는 일에 실시간으로 반응할 수 있게 해 준다. 채팅 브리지(Telegram, Discord)뿐 아니라 CI 실패 알림, 모니터링 웹훅 같은 이벤트도 주입할 수 있다.&lt;/p&gt;
&lt;p&gt;공식 문서: &lt;a href=&quot;https://code.claude.com/docs/en/channels&quot;&gt;https://code.claude.com/docs/en/channels&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;3-1. 사전 준비&lt;/h3&gt;
&lt;p&gt;Channels 설정에 앞서 다음 두 가지가 완료되어 있어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① Claude Code 설치 &amp;amp; 인증&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS / Linux
curl -fsSL https://claude.ai/install.sh | bash

# Windows PowerShell
irm https://claude.ai/install.ps1 | iex&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 후 프로젝트 디렉토리에서 &lt;code&gt;claude&lt;/code&gt;를 실행하면 최초 로그인 프롬프트가 나온다. claude.ai OAuth로 인증해야 한다 (API 키 인증은 Channels에서 지원하지 않는다).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② Bun 런타임 설치&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;채널 플러그인은 Bun 스크립트로 구동된다. &lt;code&gt;bun --version&lt;/code&gt;이 실패하면 아래 명령으로 설치한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS / Linux
curl -fsSL https://bun.sh/install | bash

# Windows PowerShell
powershell -c &amp;quot;irm bun.sh/install.ps1 | iex&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;Team/Enterprise 사용자:&lt;/strong&gt; 조직 관리자가 &lt;a href=&quot;https://claude.ai/admin-settings/claude-code&quot;&gt;claude.ai Admin settings → Claude Code → Channels&lt;/a&gt;에서 &lt;code&gt;channelsEnabled&lt;/code&gt;를 &lt;code&gt;true&lt;/code&gt;로 설정해야 한다. 기본값이 &lt;strong&gt;off&lt;/strong&gt;이므로 관리자 활성화 없이는 채널 메시지가 도달하지 않는다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;3-2. Telegram 봇 생성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Telegram에서 &lt;a href=&quot;https://t.me/BotFather&quot;&gt;@BotFather&lt;/a&gt;를 검색하여 대화를 연다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/newbot&lt;/code&gt; 명령을 보낸다.&lt;/li&gt;
&lt;li&gt;BotFather가 두 가지를 물어본다:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt; — 채팅 헤더에 표시될 이름 (자유 형식, 공백 가능)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Username&lt;/strong&gt; — &lt;code&gt;bot&lt;/code&gt;으로 끝나는 고유 핸들 (예: &lt;code&gt;my_claude_bot&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;완료되면 BotFather가 &lt;code&gt;123456789:AAHfiqksKZ8...&lt;/code&gt; 형태의 토큰을 발급한다. &lt;strong&gt;앞의 숫자와 콜론까지 포함해서&lt;/strong&gt; 전체를 복사한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3-3. 플러그인 설치 &amp;amp; 설정&lt;/h3&gt;
&lt;p&gt;터미널에서 &lt;code&gt;claude&lt;/code&gt;를 실행하여 Claude Code 세션에 진입한 뒤, 아래 명령을 순서대로 입력한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/plugin install telegram@claude-plugins-official&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;플러그인이 설치되면, 방금 복사한 BotFather 토큰을 등록한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/telegram:configure 123456789:AAHfiqksKZ8...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;토큰은 &lt;code&gt;~/.claude/channels/telegram/.env&lt;/code&gt; 파일에 &lt;code&gt;TELEGRAM_BOT_TOKEN=...&lt;/code&gt; 형태로 저장된다. 직접 해당 파일을 수동으로 작성해도 되고, 셸 환경변수로 설정해도 된다 (셸 환경변수가 우선).&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;여러 봇을 한 머신에서 운영하려면&lt;/strong&gt; &lt;code&gt;TELEGRAM_STATE_DIR&lt;/code&gt; 환경변수를 인스턴스별로 다른 디렉토리로 지정하면 된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;플러그인 소스 코드: &lt;a href=&quot;https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram&quot;&gt;https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;3-4. 채널 플래그로 실행&lt;/h3&gt;
&lt;p&gt;현재 Claude Code 세션을 종료하고, &lt;code&gt;--channels&lt;/code&gt; 플래그를 붙여 새 세션을 시작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude --channels plugin:telegram@claude-plugins-official&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 플래그가 없으면 MCP 서버는 연결되지만 채널 메시지가 도달하지 않는다. &lt;strong&gt;반드시 &lt;code&gt;--channels&lt;/code&gt;로 명시적으로 활성화&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;h3&gt;3-5. 페어링 &amp;amp; 접근 제어&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Telegram에서 방금 만든 봇에 &lt;strong&gt;DM&lt;/strong&gt;을 보낸다 (아무 메시지나 OK).&lt;/li&gt;
&lt;li&gt;봇이 &lt;strong&gt;6자리 페어링 코드&lt;/strong&gt;를 회신한다.&lt;/li&gt;
&lt;li&gt;Claude Code 터미널에서 해당 코드를 입력한다:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/telegram:access pair &amp;lt;6자리_코드&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;페어링이 완료되면, 이후 DM은 Claude Code 세션으로 직접 전달된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;보안 강화 — allowlist 정책 전환:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;페어링 모드가 기본값(&lt;code&gt;pairing&lt;/code&gt;)이면, 모르는 사람이 봇에 DM을 보내도 페어링 코드를 받을 수 있다. 자신의 페어링이 끝났으면 아래 명령으로 정책을 잠근다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/telegram:access policy allowlist&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이렇게 하면 allowlist에 등록된 사용자의 메시지만 통과하고, 나머지는 조용히 무시된다. 이것은 prompt injection을 방지하는 중요한 보안 조치다. allowlist는 &lt;strong&gt;발신자 ID&lt;/strong&gt;(user ID) 기준으로 게이팅된다. 그룹 채팅에서도 room ID가 아닌 개별 사용자 ID를 확인하므로, 허용되지 않은 그룹 구성원의 메시지는 통과하지 않는다.&lt;/p&gt;
&lt;h3&gt;3-6. 실제 사용 흐름 예시&lt;/h3&gt;
&lt;p&gt;설정이 모두 끝났다. 실제로 어떻게 동작하는지 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;시나리오:&lt;/strong&gt; 출퇴근 지하철 안에서 핸드폰 Telegram으로 DB 마이그레이션을 지시한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;핸드폰 Telegram에서 봇에게 DM: *&amp;quot;users 테이블에 email 컬럼 유니크 인덱스 추가해줘&amp;quot;*&lt;/li&gt;
&lt;li&gt;메시지는 Telegram Bot API → 로컬 MCP 서버 → Claude Code 세션 순으로 전달된다. 터미널에는 &lt;code&gt;&amp;lt;channel source=&amp;quot;telegram&amp;quot; chat_id=&amp;quot;...&amp;quot; sender=&amp;quot;...&amp;quot;&amp;gt;&lt;/code&gt; 태그로 이벤트가 표시된다.&lt;/li&gt;
&lt;li&gt;Claude Code가 프로젝트 컨텍스트를 읽고, 마이그레이션 파일을 생성한 뒤 실행한다.&lt;/li&gt;
&lt;li&gt;작업 완료 후 Claude가 &lt;code&gt;reply&lt;/code&gt; 도구를 호출하면, 결과 메시지가 &lt;strong&gt;Telegram 채팅으로 돌아온다.&lt;/strong&gt; 터미널에는 &lt;code&gt;sent&lt;/code&gt; 확인만 표시되고, 실제 응답 텍스트는 Telegram 쪽에서 확인할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;권한 승인이 필요한 경우:&lt;/strong&gt; Claude가 &lt;code&gt;Bash&lt;/code&gt;나 &lt;code&gt;Write&lt;/code&gt; 같은 도구를 실행하려 할 때 권한 프롬프트가 뜰 수 있다. 채널 플러그인이 Permission Relay를 지원하므로 이 프롬프트가 Telegram으로 전달된다. &lt;code&gt;yes &amp;lt;request_id&amp;gt;&lt;/code&gt; 또는 &lt;code&gt;no &amp;lt;request_id&amp;gt;&lt;/code&gt;를 답하면 원격에서 승인/거부할 수 있다. 물론 터미널에서 직접 승인해도 되고, 먼저 도착한 응답이 적용된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;완전 무인 운영:&lt;/strong&gt; &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; 플래그를 쓰면 모든 권한 프롬프트를 생략할 수 있지만, &lt;strong&gt;신뢰할 수 있는 환경(샌드박스, VM 등)에서만 사용&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;3-7. OS별 주의사항&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bun 설치 후에는 &lt;strong&gt;반드시 새 터미널 창을 열어야&lt;/strong&gt; PATH가 반영된다. 기존 PowerShell/CMD 창에서는 &lt;code&gt;bun&lt;/code&gt; 명령을 인식하지 못할 수 있다. 또한 Windows에서는 Claude Code 자체가 &lt;a href=&quot;https://git-scm.com/downloads/win&quot;&gt;Git for Windows&lt;/a&gt; 설치를 필요로 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Telegram, Discord에 더해 &lt;strong&gt;iMessage 채널&lt;/strong&gt;도 사용할 수 있다. iMessage 채널은 AppleScript 기반으로, 봇 토큰이 불필요하고 Messages 데이터베이스를 직접 읽는 방식이다. 자기 자신에게 문자를 보내면 자동으로 게이트를 통과하고, 다른 연락처는 &lt;code&gt;/imessage:access allow &amp;lt;핸들&amp;gt;&lt;/code&gt; 로 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# iMessage 채널 설치 &amp;amp; 실행 (macOS 전용)
/plugin install imessage@claude-plugins-official
claude --channels plugin:imessage@claude-plugins-official&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Telegram과 Discord만 지원된다. iMessage는 macOS 전용이므로 Linux에서는 사용할 수 없다.&lt;/p&gt;
&lt;h3&gt;3-8. Discord 채널 간략 소개&lt;/h3&gt;
&lt;p&gt;Discord 연동은 Telegram과 전체 흐름이 유사하지만, Discord 특유의 &lt;strong&gt;서버 초대&lt;/strong&gt; 단계가 추가된다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;Telegram&lt;/th&gt;
&lt;th&gt;Discord&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;봇 생성&lt;/td&gt;
&lt;td&gt;@BotFather &lt;code&gt;/newbot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Discord Developer Portal → New Application → Bot 탭에서 토큰 발급&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추가 설정&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Message Content Intent&lt;/strong&gt; 활성화 필수 (Privileged Gateway Intents)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;플러그인 설치&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/plugin install telegram@claude-plugins-official&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/plugin install discord@claude-plugins-official&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;토큰 등록&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/telegram:configure &amp;lt;토큰&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/discord:configure &amp;lt;토큰&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 초대&lt;/td&gt;
&lt;td&gt;불필요 (봇 DM 즉시 가능)&lt;/td&gt;
&lt;td&gt;봇을 서버에 초대하는 OAuth2 URL 생성 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;페어링&lt;/td&gt;
&lt;td&gt;봇 DM → 코드 → &lt;code&gt;/telegram:access pair &amp;lt;코드&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;봇 DM → 코드 → &lt;code&gt;/discord:access pair &amp;lt;코드&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude --channels plugin:telegram@claude-plugins-official&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude --channels plugin:discord@claude-plugins-official&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Discord는 Message Content Intent를 활성화하지 않으면 봇이 메시지 내용을 읽지 못하므로 반드시 Developer Portal에서 설정해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Remote Control — 모바일/웹에서 로컬 세션 이어하기&lt;/h2&gt;
&lt;p&gt;Remote Control은 로컬에서 돌고 있는 Claude Code 세션을 claude.ai/code 또는 Claude 모바일 앱에서 &lt;strong&gt;그대로 이어서&lt;/strong&gt; 조작하는 기능이다. Channels가 &amp;quot;외부 이벤트가 들어오는 것&amp;quot;이라면, Remote Control은 &amp;quot;내가 직접 접속해서 조종하는 것&amp;quot;이다. 세션은 여전히 로컬에서 실행되므로, 로컬 파일시스템, MCP 서버, 프로젝트 설정이 모두 그대로 유지된다.&lt;/p&gt;
&lt;p&gt;공식 문서: &lt;a href=&quot;https://code.claude.com/docs/en/remote-control&quot;&gt;https://code.claude.com/docs/en/remote-control&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;4-1. 시작 방법 3가지&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;① Server 모드 — 원격 접속 전용 대기&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude remote-control&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;터미널이 서버 모드로 대기하며, 세션 URL과 QR 코드를 표시한다. &lt;code&gt;--name &amp;quot;My Project&amp;quot;&lt;/code&gt;로 세션 이름을 지정할 수 있고, &lt;code&gt;--spawn worktree&lt;/code&gt;로 동시 세션을 git worktree로 격리할 수도 있다. &lt;code&gt;--capacity &amp;lt;N&amp;gt;&lt;/code&gt;으로 최대 동시 세션 수를 조절한다 (기본 32개).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 인터랙티브 + 원격 병행&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude --remote-control
# 또는 줄여서
claude --rc&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;터미널에서도 직접 입력하면서, 동시에 웹/앱에서도 접속할 수 있다. 대화는 모든 연결된 기기에서 실시간으로 동기화된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 기존 세션에서 활성화&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이미 Claude Code 세션이 열려 있는 상태에서:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/remote-control
# 또는
/rc&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;현재 대화 히스토리를 그대로 유지하면서 원격 접속을 활성화한다.&lt;/p&gt;
&lt;h3&gt;4-2. 접속 방법&lt;/h3&gt;
&lt;p&gt;Remote Control 세션이 시작되면 세 가지 방법으로 다른 기기에서 접속할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;URL 직접 열기:&lt;/strong&gt; 터미널에 표시된 URL을 브라우저에서 열면 claude.ai/code로 이동하여 세션에 접속된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;QR 코드 스캔:&lt;/strong&gt; &lt;code&gt;claude remote-control&lt;/code&gt; 모드에서 스페이스바를 누르면 QR 코드가 토글된다. Claude 모바일 앱으로 스캔하면 바로 접속된다. 아직 앱이 없다면 Claude Code 내에서 &lt;code&gt;/mobile&lt;/code&gt;을 입력하면 다운로드 QR 코드를 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;세션 목록에서 선택:&lt;/strong&gt; claude.ai/code를 열거나 Claude 앱을 열면 세션 목록에 Remote Control 세션이 표시된다. 온라인 상태인 세션은 컴퓨터 아이콘에 초록색 상태 표시가 붙는다.&lt;/p&gt;
&lt;h3&gt;4-3. 모든 세션에 자동 활성화&lt;/h3&gt;
&lt;p&gt;매번 플래그를 붙이기 귀찮다면, Claude Code 내에서 &lt;code&gt;/config&lt;/code&gt;를 실행하고 &lt;strong&gt;&amp;quot;Enable Remote Control for all sessions&amp;quot;&lt;/strong&gt;를 &lt;code&gt;true&lt;/code&gt;로 설정하면 된다. 이후 모든 인터랙티브 세션에서 자동으로 Remote Control이 활성화된다.&lt;/p&gt;
&lt;h3&gt;4-4. 제약사항&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;터미널이 열려 있어야 한다.&lt;/strong&gt; Remote Control은 로컬 프로세스로 실행된다. 터미널을 닫거나 &lt;code&gt;claude&lt;/code&gt; 프로세스를 종료하면 세션이 끝난다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;네트워크 10분 타임아웃.&lt;/strong&gt; 머신이 깨어 있지만 네트워크에 10분 이상 연결되지 못하면 세션이 타임아웃되고 프로세스가 종료된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;API 키 인증 불가.&lt;/strong&gt; claude.ai OAuth 인증만 지원한다. &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;, Bedrock, Vertex, Foundry 등 서드파티 인증으로는 사용할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;구독 요건.&lt;/strong&gt; Pro, Max, Team, Enterprise 모든 플랜에서 사용 가능하지만, Team/Enterprise에서는 관리자가 &lt;a href=&quot;https://claude.ai/admin-settings/claude-code&quot;&gt;Admin settings&lt;/a&gt;에서 &lt;strong&gt;Remote Control&lt;/strong&gt; 토글을 켜야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Dispatch — 폰에서 데스크톱에 작업 던지기&lt;/h2&gt;
&lt;p&gt;Dispatch는 Claude 모바일 앱(iOS/Android)에서 데스크톱(Cowork/Code)에 새 작업을 원격으로 지시하는 기능이다. Channels나 Remote Control과 달리 기존 세션에 접속하는 것이 아니라, &lt;strong&gt;새로운 세션을 자동 생성&lt;/strong&gt;하는 delegation 모델이다. Computer Use와 결합하면, 폰에서 데스크톱의 앱까지 원격 조종할 수 있다.&lt;/p&gt;
&lt;p&gt;공식 블로그: &lt;a href=&quot;https://claude.com/blog/dispatch-and-computer-use&quot;&gt;https://claude.com/blog/dispatch-and-computer-use&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;5-1. 설정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;필요 조건:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Desktop 앱 (최신 버전) — macOS 또는 Windows&lt;/li&gt;
&lt;li&gt;Claude 모바일 앱 (iOS 또는 Android, 최신 버전)&lt;/li&gt;
&lt;li&gt;Claude Pro 또는 Max 구독 (Team/Enterprise에서는 현재 미지원)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;페어링 절차:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;데스크톱과 모바일 양쪽 모두 앱을 최신 버전으로 업데이트한다.&lt;/li&gt;
&lt;li&gt;모바일 앱에서 &lt;strong&gt;Cowork&lt;/strong&gt; 탭을 연다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dispatch&lt;/strong&gt; → &lt;strong&gt;&amp;quot;Pair with your desktop&amp;quot;&lt;/strong&gt;을 탭한다.&lt;/li&gt;
&lt;li&gt;데스크톱 화면에 표시된 QR 코드를 모바일로 스캔한다.&lt;/li&gt;
&lt;li&gt;페어링이 완료되면 하나의 지속적 대화(persistent thread)가 생긴다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5-2. 사용 흐름&lt;/h3&gt;
&lt;p&gt;Dispatch는 단순한 &amp;quot;메시지 전달&amp;quot;이 아니라, Claude가 작업 성격을 판단하여 적절한 세션 유형을 선택하는 &lt;strong&gt;지능형 라우팅&lt;/strong&gt; 모델이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;개발 작업을 지시하는 경우:&lt;/strong&gt; 모바일에서 &amp;quot;로그인 버그 수정해줘&amp;quot;라고 보내면, Desktop의 Code 탭에 &lt;strong&gt;Dispatch 배지&lt;/strong&gt;가 붙은 새 세션이 생성된다. Claude Code가 버그를 분석하고, 코드를 수정하고, 테스트를 돌리고, PR까지 올릴 수 있다. 완료되면 모바일에 푸시 알림이 온다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;지식 작업을 지시하는 경우:&lt;/strong&gt; &amp;quot;이번 달 마케팅 리포트 작성해줘&amp;quot;처럼 문서 작업이면 Code가 아닌 Cowork 세션으로 라우팅된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Computer Use 연계:&lt;/strong&gt; Desktop 앱에서 Computer Use가 활성화되어 있으면, Dispatch로 시작된 세션도 데스크톱의 앱을 직접 제어할 수 있다. 예를 들어 &amp;quot;Excel에서 이번 분기 매출 스프레드시트 업데이트해줘&amp;quot;라고 지시하면 Claude가 실제로 Excel을 열고 작업한다. 단, Dispatch 세션의 앱 승인은 &lt;strong&gt;30분 후 만료&lt;/strong&gt;되어 재승인을 요구하므로, 일반 Code 세션보다 보안이 한 단계 강화되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;스케줄링:&lt;/strong&gt; &amp;quot;매일 아침 9시에 이메일 확인하고 요약해줘&amp;quot;처럼 반복 작업을 설정하면 별도 지시 없이 자동 실행된다.&lt;/p&gt;
&lt;h3&gt;5-3. 주의사항&lt;/h3&gt;
&lt;p&gt;Dispatch 문서에서는 안전에 대해 분명히 경고한다. 모바일 AI 에이전트가 데스크톱 AI 에이전트를 원격 제어하는 체인이 만들어지면, 폰에서 보낸 지시가 로컬 파일 읽기·삭제, 연결된 서비스 조작, 브라우저/데스크톱 앱 제어로 이어질 수 있다. 피싱 링크나 예상치 못한 명령이 연쇄적으로 실행될 수 있으므로, 활성화 전에 어떤 커넥터·플러그인이 설치되어 있는지 점검하고, 민감한 데이터는 접근 범위 밖에 두는 것이 좋다.&lt;/p&gt;
&lt;p&gt;현재 리서치 프리뷰 상태이며, macOS와 Windows만 지원하고 Linux Desktop은 아직 제공되지 않는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. OpenClaw와의 비교&lt;/h2&gt;
&lt;p&gt;OpenClaw와 Claude Code의 원격 기능은 궁극적으로 같은 문제 — &amp;quot;터미널 앞에 없어도 AI 에이전트를 부릴 수 있어야 한다&amp;quot; — 를 풀지만, 접근 방식이 상당히 다르다. 아래 표로 주요 차이를 정리한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;OpenClaw&lt;/th&gt;
&lt;th&gt;Claude Code (Channels + RC + Dispatch)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;오픈소스&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O (MIT 라이선스)&lt;/td&gt;
&lt;td&gt;X (플러그인 소스는 GitHub 공개)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;영구 메모리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O (로컬 메모리, 세션 간 유지)&lt;/td&gt;
&lt;td&gt;제한적 (CLAUDE.md 프로젝트 메모리는 있으나 세션 리셋 시 대화 컨텍스트 초기화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;지원 메신저&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WhatsApp, Telegram, Discord, Signal 등&lt;/td&gt;
&lt;td&gt;Telegram, Discord, iMessage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI 모델&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude, GPT, Gemini 등 (직접 API 키 필요)&lt;/td&gt;
&lt;td&gt;Claude 전용 (구독 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자체 호스팅&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;필수 (로컬 또는 VPS)&lt;/td&gt;
&lt;td&gt;로컬 실행 (Anthropic 인프라 경유)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;설정 난이도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;중~상 (Node.js 22+, 환경변수 다수, 직접 인프라 운영)&lt;/td&gt;
&lt;td&gt;하~중 (플러그인 설치 6단계)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;비용&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API 사용량만큼 과금&lt;/td&gt;
&lt;td&gt;Claude Pro($20/월) 또는 Max($100·$200/월) 구독료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;에코시스템&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Skills, Jobs, MAS(Multi-Agent System) 등 활발한 커뮤니티 생태계&lt;/td&gt;
&lt;td&gt;MCP 기반 플러그인, 공식 커넥터, Agent SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;보안/브랜드&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;사용자 책임 (오픈소스 특성상 보안은 직접 관리)&lt;/td&gt;
&lt;td&gt;Anthropic의 안전 철학 + allowlist 기반 접근 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Computer Use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;서드파티 연동 필요&lt;/td&gt;
&lt;td&gt;Dispatch + Desktop에서 공식 지원 (macOS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;OpenClaw는 &amp;quot;원하는 모델, 원하는 메신저, 완전한 자유도&amp;quot;를 추구하는 파워 유저와 자체 호스팅을 선호하는 개발자에게 여전히 강력한 선택이다. WhatsApp, Signal 같은 채널은 현재 Claude Code에서 공식 지원하지 않으며, 영구 메모리와 세션 간 상태 유지는 OpenClaw의 확실한 차별점이다. 반면, Claude Code 진영은 Anthropic의 안전성 브랜드, 별도 인프라 없이 플러그인 설치 몇 줄로 끝나는 편의성, 그리고 Dispatch + Computer Use 같은 데스크톱 연계에서 우위를 보인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 정리 &amp;amp; 추천 시나리오&lt;/h2&gt;
&lt;p&gt;세 가지 원격 기능과 OpenClaw를 상황별로 정리하면 이렇다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;메신저로 가볍게 지시만 하고 결과 받기&amp;quot;&lt;/strong&gt; → &lt;strong&gt;Channels (Telegram)&lt;/strong&gt; — Telegram 봇 하나 만들고 플러그인 설치하면 끝. CI 실패 알림, 빠른 코드 수정 요청 등 비동기 작업에 적합하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;진행 중이던 코딩을 이동 중에 이어서&amp;quot;&lt;/strong&gt; → &lt;strong&gt;Remote Control&lt;/strong&gt; — 회사에서 시작한 리팩토링을 퇴근길에 claude.ai/code나 모바일 앱에서 이어할 수 있다. 로컬 환경이 그대로 유지되므로 맥락이 끊기지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;집 밖에서 데스크톱에 새 작업을 던지기&amp;quot;&lt;/strong&gt; → &lt;strong&gt;Dispatch&lt;/strong&gt; — Desktop이 켜져 있기만 하면 모바일에서 작업을 지시하고, Computer Use까지 활용할 수 있다. 개발 작업은 Code 세션으로, 문서 작업은 Cowork 세션으로 자동 라우팅된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;완전 자유도 + 자체 호스팅 + 다양한 메신저 + 영구 메모리&amp;quot;&lt;/strong&gt; → &lt;strong&gt;OpenClaw&lt;/strong&gt; — WhatsApp이나 Signal을 써야 하거나, 멀티 모델 전환이 필요하거나, 세션 간 상태가 반드시 유지되어야 한다면 여전히 OpenClaw가 답이다.&lt;/p&gt;
&lt;p&gt;물론 이것들을 조합할 수도 있다. 낮에는 Remote Control로 작업하다가, 퇴근 후에는 Channels로 Telegram에서 간단한 수정을 지시하고, 주말에는 Dispatch로 데스크톱에 큰 작업을 던지는 워크플로우도 가능하다. 세 기능 모두 같은 Claude Code 엔진 위에서 돌아가므로 CLAUDE.md, 설정, MCP 서버가 공유된다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;참고 링크:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Channels 공식 문서: &lt;a href=&quot;https://code.claude.com/docs/en/channels&quot;&gt;https://code.claude.com/docs/en/channels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Channels 레퍼런스 (커스텀 채널 빌드): &lt;a href=&quot;https://code.claude.com/docs/en/channels-reference&quot;&gt;https://code.claude.com/docs/en/channels-reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Remote Control 공식 문서: &lt;a href=&quot;https://code.claude.com/docs/en/remote-control&quot;&gt;https://code.claude.com/docs/en/remote-control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Dispatch &amp;amp; Computer Use 블로그: &lt;a href=&quot;https://claude.com/blog/dispatch-and-computer-use&quot;&gt;https://claude.com/blog/dispatch-and-computer-use&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Telegram 플러그인 GitHub: &lt;a href=&quot;https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram&quot;&gt;https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Claude Code 전체 문서: &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;https://docs.anthropic.com/en/docs/claude-code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/321</guid>
      <comments>https://polarcompass.tistory.com/321#entry321comment</comments>
      <pubDate>Wed, 1 Apr 2026 01:00:00 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 14편: 통합 Docker Compose &amp;amp; 최종 점검 (시리즈 완결)</title>
      <link>https://polarcompass.tistory.com/320</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Now I have all the information needed. Let me write the comprehensive 14편.&lt;/p&gt;
&lt;h1&gt;14편: 통합 Docker Compose &amp;amp; 최종 점검 (시리즈 완결)&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈: 윈도우 네이티브 개발 환경에서 리눅스 서버 배포까지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 시리즈의 마지막 편입니다. 1편에서 윈도우 터미널을 설치하고, 12편까지 개발 도구와 컨테이너를 하나씩 세팅해 왔습니다. 13편에서 클라우드를 선택했으니, 이제 모든 서비스를 &lt;b&gt;하나의 &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/b&gt; 로 통합하고 &lt;b&gt;리눅스 프로덕션 서버&lt;/b&gt;에서 전체 스택을 기동하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 최종 프로젝트 폴더 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 리눅스 서버에 배포할 프로젝트의 전체 디렉터리 구조를 확인합니다. 로컬(윈도우)에서 작업한 소스를 Git으로 서버에 클론한 뒤, 아래 구조를 기준으로 운영합니다.&lt;/p&gt;
&lt;pre class=&quot;powershell&quot;&gt;&lt;code&gt;my-project/
├── docker-compose.yml          # 통합 컴포즈 파일
├── .env                        # 환경변수 (Git에 커밋하지 않음)
├── .env.example                # 환경변수 템플릿 (Git에 커밋)
├── Caddyfile                   # Caddy 설정 파일
│
├── frontend/                   # React SPA 소스
│   ├── src/
│   ├── public/
│   ├── package.json
│   ├── vite.config.ts
│   └── Dockerfile              # 프론트엔드 빌드용 Dockerfile
│
├── backend-go/                 # Go + Gin 소스
│   ├── main.go
│   ├── go.mod
│   ├── go.sum
│   └── Dockerfile              # Go 빌드용 Dockerfile
│
├── backend-python/             # Python + FastAPI 소스
│   ├── main.py
│   ├── requirements.txt
│   └── Dockerfile              # FastAPI 빌드용 Dockerfile
│
├── caddy/
│   └── data/                   # Caddy TLS 인증서 저장 (볼륨 마운트)
│
├── postgres/
│   └── data/                   # PostgreSQL 데이터 (볼륨 마운트)
│
├── minio/
│   └── data/                   # MinIO 오브젝트 데이터 (볼륨 마운트)
│
└── n8n/
    └── data/                   # n8n 워크플로우 데이터 (볼륨 마운트)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 원칙은 다음과 같습니다. 컨테이너의 영속 데이터는 모두 프로젝트 하위의 named volume 또는 bind mount로 관리합니다. &lt;code&gt;.env&lt;/code&gt; 파일에 민감 정보를 모아두고 Git에는 &lt;code&gt;.env.example&lt;/code&gt;만 커밋합니다. 각 백엔드 서비스는 자체 Dockerfile로 이미지를 빌드합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 환경변수 관리 (.env)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env.example&lt;/code&gt; 파일을 먼저 작성하고, 서버에서 복사하여 &lt;code&gt;.env&lt;/code&gt;로 실제 값을 채웁니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# .env.example &amp;mdash; 이 파일을 복사하여 .env로 만들고 실제 값을 채우세요.

# ─── 도메인 ───
DOMAIN=example.com

# ─── PostgreSQL ───
POSTGRES_USER=myuser
POSTGRES_PASSWORD=CHANGE_ME_postgres_password
POSTGRES_DB=mydb

# ─── MinIO ───
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=CHANGE_ME_minio_password

# ─── n8n ───
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=CHANGE_ME_n8n_password
N8N_ENCRYPTION_KEY=CHANGE_ME_random_32_char_string
WEBHOOK_URL=https://n8n.example.com/

# ─── Go Gin ───
GIN_MODE=release
DATABASE_URL=postgres://myuser:CHANGE_ME_postgres_password@postgres:5432/mydb?sslmode=disable

# ─── FastAPI ───
FASTAPI_DATABASE_URL=postgresql://myuser:CHANGE_ME_postgres_password@postgres:5432/mydb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 &lt;code&gt;.env&lt;/code&gt;를 생성하는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;cp .env.example .env
nano .env   # 실제 비밀번호와 도메인 입력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;에 &lt;code&gt;.env&lt;/code&gt;를 반드시 추가해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# .gitignore
.env
postgres/data/
minio/data/
n8n/data/
caddy/data/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 각 서비스 Dockerfile&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 프론트엔드 (React SPA 빌드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React SPA는 빌드 결과물(정적 파일)만 Caddy에 넘기면 됩니다. 멀티스테이지 빌드로 Node.js에서 빌드하고, 최종 이미지에는 빌드 결과물만 남깁니다. 이 이미지 자체를 서빙하지 않고, 빌드된 파일을 Caddy 컨테이너와 공유 볼륨으로 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# frontend/Dockerfile
FROM node:22-alpine AS builder

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# 빌드 결과물만 가벼운 이미지에 복사
FROM alpine:3.21
COPY --from=builder /app/dist /frontend-dist

# 이 컨테이너는 실행할 프로세스가 없으므로,
# docker compose에서 볼륨 복사 용도로만 사용
CMD [&quot;echo&quot;, &quot;Frontend build complete&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Go + Gin 백엔드&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# backend-go/Dockerfile
FROM golang:1.24-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM alpine:3.21
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD [&quot;./server&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. Python + FastAPI 백엔드&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# backend-python/Dockerfile
FROM python:3.13-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Caddyfile (프로덕션)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caddy는 도메인을 지정하면 Let's Encrypt 인증서를 자동으로 발급하고 갱신합니다. 별도의 &lt;code&gt;certbot&lt;/code&gt; 설정이 필요 없습니다. 아래 Caddyfile은 하나의 도메인에서 React SPA 정적 서빙 + Go Gin API + FastAPI API + n8n + MinIO Console을 모두 서브도메인 또는 경로로 라우팅하는 프로덕션 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# Caddyfile

# ─── 메인 도메인: React SPA + API 라우팅 ───
{$DOMAIN} {
    # React SPA 정적 파일 서빙
    root * /srv/frontend
    encode gzip

    # /api/go/* &amp;rarr; Go Gin 백엔드
    handle_path /api/go/* {
        reverse_proxy backend-go:8080
    }

    # /api/py/* &amp;rarr; FastAPI 백엔드
    handle_path /api/py/* {
        reverse_proxy backend-python:8000
    }

    # 나머지 모든 요청 &amp;rarr; React SPA (클라이언트 라우팅 지원)
    handle {
        try_files {path} /index.html
        file_server
    }
}

# ─── n8n 서브도메인 ───
n8n.{$DOMAIN} {
    reverse_proxy n8n:5678
}

# ─── MinIO API (S3 호환) ───
s3.{$DOMAIN} {
    reverse_proxy minio:9000
}

# ─── MinIO Console ───
minio.{$DOMAIN} {
    reverse_proxy minio:9001
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Caddyfile에서 &lt;code&gt;{$DOMAIN}&lt;/code&gt;은 환경변수를 참조합니다. Docker Compose에서 &lt;code&gt;.env&lt;/code&gt;의 &lt;code&gt;DOMAIN&lt;/code&gt; 값이 자동으로 주입됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caddy의 자동 HTTPS가 정상 작동하려면 서버의 &lt;b&gt;80번과 443번 포트&lt;/b&gt;가 외부에서 접근 가능해야 하고, DNS에 &lt;code&gt;example.com&lt;/code&gt;, &lt;code&gt;n8n.example.com&lt;/code&gt;, &lt;code&gt;s3.example.com&lt;/code&gt;, &lt;code&gt;minio.example.com&lt;/code&gt; 모두 서버 IP를 가리키는 A 레코드가 설정되어 있어야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 통합 docker-compose.yml&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 서비스를 하나로 합칩니다. 이 파일은 &lt;b&gt;리눅스 프로덕션 서버&lt;/b&gt;에서 실행하는 것을 전제로 합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.yml

services:
  # ──────────────────────────────────────────────
  #  PostgreSQL
  # ──────────────────────────────────────────────
  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}&quot;]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - internal

  # ──────────────────────────────────────────────
  #  MinIO (오브젝트 스토리지)
  # ──────────────────────────────────────────────
  minio:
    image: minio/minio:latest
    restart: unless-stopped
    command: server /data --console-address &quot;:9001&quot;
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - minio_data:/data
    healthcheck:
      test: [&quot;CMD&quot;, &quot;mc&quot;, &quot;ready&quot;, &quot;local&quot;]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - internal

  # ──────────────────────────────────────────────
  #  n8n (워크플로우 자동화)
  # ──────────────────────────────────────────────
  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    restart: unless-stopped
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
      - DB_POSTGRESDB_USER=${POSTGRES_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
      - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - WEBHOOK_URL=${WEBHOOK_URL}
      - N8N_HOST=n8n.${DOMAIN}
      - N8N_PROTOCOL=https
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - internal

  # ──────────────────────────────────────────────
  #  프론트엔드 빌드 (초기 1회만 실행)
  # ──────────────────────────────────────────────
  frontend-builder:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    volumes:
      - frontend_dist:/frontend-dist
    # 빌드 완료 후 종료되는 일회성 컨테이너
    restart: &quot;no&quot;

  # ──────────────────────────────────────────────
  #  Go + Gin 백엔드
  # ──────────────────────────────────────────────
  backend-go:
    build:
      context: ./backend-go
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      GIN_MODE: ${GIN_MODE}
      DATABASE_URL: ${DATABASE_URL}
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: [&quot;CMD&quot;, &quot;wget&quot;, &quot;--spider&quot;, &quot;-q&quot;, &quot;http://localhost:8080/health&quot;]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - internal

  # ──────────────────────────────────────────────
  #  Python + FastAPI 백엔드
  # ──────────────────────────────────────────────
  backend-python:
    build:
      context: ./backend-python
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      DATABASE_URL: ${FASTAPI_DATABASE_URL}
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: [&quot;CMD&quot;, &quot;python&quot;, &quot;-c&quot;, &quot;import urllib.request; urllib.request.urlopen('http://localhost:8000/health')&quot;]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - internal

  # ──────────────────────────────────────────────
  #  Caddy (웹 서버 / 리버스 프록시 / 자동 HTTPS)
  # ──────────────────────────────────────────────
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
      - &quot;443:443/udp&quot;   # HTTP/3 (QUIC)
    environment:
      DOMAIN: ${DOMAIN}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data          # TLS 인증서 저장
      - caddy_config:/config
      - frontend_dist:/srv/frontend:ro   # React 빌드 결과물
    depends_on:
      - frontend-builder
      - backend-go
      - backend-python
      - n8n
      - minio
    networks:
      - internal

# ──────────────────────────────────────────────
#  네트워크
# ──────────────────────────────────────────────
networks:
  internal:
    driver: bridge

# ──────────────────────────────────────────────
#  볼륨
# ──────────────────────────────────────────────
volumes:
  postgres_data:
  minio_data:
  n8n_data:
  frontend_dist:
  caddy_data:
  caddy_config:&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. 구성 해설&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네트워크&lt;/b&gt;: 모든 컨테이너가 &lt;code&gt;internal&lt;/code&gt; 브리지 네트워크를 공유합니다. 컨테이너 간 통신은 서비스 이름(예: &lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;backend-go&lt;/code&gt;)으로 이루어집니다. 외부에 노출되는 포트는 Caddy의 80/443뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;볼륨&lt;/b&gt;: PostgreSQL, MinIO, n8n의 데이터와 Caddy의 TLS 인증서는 named volume으로 영속화합니다. &lt;code&gt;frontend_dist&lt;/code&gt; 볼륨은 빌더 컨테이너가 React 빌드 결과물을 넣으면 Caddy 컨테이너가 읽기 전용으로 마운트하는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;헬스체크&lt;/b&gt;: PostgreSQL은 &lt;code&gt;pg_isready&lt;/code&gt;, MinIO는 &lt;code&gt;mc ready local&lt;/code&gt;, Go/FastAPI는 &lt;code&gt;/health&lt;/code&gt; 엔드포인트를 체크합니다. n8n과 백엔드 서비스는 &lt;code&gt;depends_on&lt;/code&gt;에서 PostgreSQL의 &lt;code&gt;service_healthy&lt;/code&gt; 조건을 사용해 DB가 준비된 후에야 기동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;frontend-builder&lt;/b&gt;: &lt;code&gt;restart: &quot;no&quot;&lt;/code&gt;로 설정되어 있어 빌드가 끝나면 종료됩니다. 프론트엔드 소스를 수정한 후에는 &lt;code&gt;docker compose build frontend-builder &amp;amp;&amp;amp; docker compose up frontend-builder&lt;/code&gt; 로 다시 빌드하면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 서버 초기 세팅 &amp;amp; 배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 서버(Ubuntu 22.04 이상 기준)에서 최초 배포하는 과정입니다. 13편에서 선택한 클라우드에 서버를 생성하고 SSH로 접속한 상태를 가정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. Docker &amp;amp; Docker Compose 설치&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 패키지 업데이트
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y

# Docker 공식 설치 스크립트
curl -fsSL https://get.docker.com | sudo sh

# 현재 사용자를 docker 그룹에 추가 (sudo 없이 docker 사용)
sudo usermod -aG docker $USER

# 재로그인 후 확인
docker --version
docker compose version&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 방화벽 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# UFW로 필요한 포트만 허용
sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # HTTP (Caddy &amp;rarr; HTTPS 리디렉트)
sudo ufw allow 443/tcp   # HTTPS
sudo ufw allow 443/udp   # HTTP/3 (QUIC)
sudo ufw enable
sudo ufw status&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. 프로젝트 클론 &amp;amp; 환경변수 설정&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 프로젝트 클론
git clone https://github.com/your-username/my-project.git
cd my-project

# 환경변수 파일 생성
cp .env.example .env
nano .env
# &amp;rarr; DOMAIN, 각 비밀번호, ENCRYPTION_KEY 등 실제 값 입력&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-4. DNS 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 관리 패널(Cloudflare, Route 53 등)에서 아래 A 레코드를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;A    example.com         &amp;rarr; 서버 IP
A    n8n.example.com     &amp;rarr; 서버 IP
A    s3.example.com      &amp;rarr; 서버 IP
A    minio.example.com   &amp;rarr; 서버 IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 전파에 수 분에서 수십 분이 걸릴 수 있습니다. &lt;code&gt;ping example.com&lt;/code&gt;으로 IP가 올바르게 응답하는지 확인한 후 다음 단계로 진행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-5. 전체 스택 기동&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 이미지 빌드 &amp;amp; 컨테이너 기동
docker compose up -d --build

# 로그 확인 (전체)
docker compose logs -f

# 특정 서비스 로그만 보기
docker compose logs -f caddy
docker compose logs -f backend-go&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 기동 시 Caddy가 Let's Encrypt에서 TLS 인증서를 발급받는 과정이 있으므로, 1~2분 정도 기다린 후 브라우저에서 &lt;code&gt;https://example.com&lt;/code&gt;에 접속합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 전체 스택 헬스체크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서비스가 정상인지 한 번에 확인합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. 컨테이너 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NAME                  STATUS                    PORTS
my-project-caddy-1          Up (healthy)   0.0.0.0:80-&amp;gt;80/tcp, 0.0.0.0:443-&amp;gt;443/tcp
my-project-postgres-1       Up (healthy)   5432/tcp
my-project-minio-1          Up (healthy)   9000/tcp, 9001/tcp
my-project-n8n-1            Up             5678/tcp
my-project-backend-go-1     Up (healthy)   8080/tcp
my-project-backend-python-1 Up (healthy)   8000/tcp
my-project-frontend-builder-1  Exited (0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;frontend-builder&lt;/code&gt;가 &lt;code&gt;Exited (0)&lt;/code&gt;인 것은 정상입니다. 빌드가 성공적으로 완료되었다는 뜻입니다. 나머지 서비스는 모두 &lt;code&gt;Up&lt;/code&gt; 또는 &lt;code&gt;Up (healthy)&lt;/code&gt; 상태여야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. 각 엔드포인트 접속 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 터미널에서 &lt;code&gt;curl&lt;/code&gt;로 각 엔드포인트를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# React SPA (메인 페이지)
curl -s -o /dev/null -w &quot;%{http_code}&quot; https://example.com
# 기대값: 200

# Go Gin API 헬스체크
curl -s https://example.com/api/go/health
# 기대값: {&quot;status&quot;:&quot;ok&quot;}

# FastAPI 헬스체크
curl -s https://example.com/api/py/health
# 기대값: {&quot;status&quot;:&quot;ok&quot;}

# n8n 대시보드
curl -s -o /dev/null -w &quot;%{http_code}&quot; https://n8n.example.com
# 기대값: 200 또는 302 (로그인 리디렉트)

# MinIO Console
curl -s -o /dev/null -w &quot;%{http_code}&quot; https://minio.example.com
# 기대값: 200 또는 302

# MinIO S3 API
curl -s -o /dev/null -w &quot;%{http_code}&quot; https://s3.example.com/minio/health/live
# 기대값: 200&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-3. PostgreSQL 접속 테스트&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;docker compose exec postgres psql -U myuser -d mydb -c &quot;SELECT 1;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;1&lt;/code&gt;이 반환되면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-4. TLS 인증서 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;curl -vI https://example.com 2&amp;gt;&amp;amp;1 | grep -E &quot;subject:|issuer:|expire&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;issuer: ... Let's Encrypt&lt;/code&gt;가 표시되면 자동 HTTPS가 정상 작동하는 것입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 운영 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-1. 프론트엔드 업데이트 시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 React 소스를 수정하고 Git push한 뒤, 서버에서 다음 명령을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;git pull
docker compose build frontend-builder
docker compose up frontend-builder
# Caddy가 자동으로 새 빌드 결과물을 서빙 (볼륨 공유)
# Caddy 재시작이 필요하면:
docker compose restart caddy&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-2. 백엔드 업데이트 시&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;git pull
docker compose up -d --build backend-go backend-python&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--build&lt;/code&gt; 플래그가 있으면 Dockerfile을 다시 빌드하고, &lt;code&gt;-d&lt;/code&gt;로 백그라운드 실행합니다. Caddy가 리버스 프록시하고 있으므로 별도 설정 변경 없이 새 컨테이너로 트래픽이 전환됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-3. 데이터 백업&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# PostgreSQL 덤프
docker compose exec postgres pg_dump -U myuser mydb &amp;gt; backup_$(date +%Y%m%d).sql

# MinIO 데이터는 볼륨 또는 mc mirror 명령으로 백업
docker run --rm -v my-project_minio_data:/data -v $(pwd):/backup \
  alpine tar czf /backup/minio_backup_$(date +%Y%m%d).tar.gz /data&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-4. 전체 스택 중지 &amp;amp; 재시작&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 중지 (데이터 유지)
docker compose down

# 재시작
docker compose up -d

# 볼륨까지 삭제 (⚠️ 데이터 완전 삭제)
docker compose down -v&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-5. 로그 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션에서는 로그가 디스크를 가득 채우지 않도록 Docker 로그 드라이버를 설정합니다. &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;에 다음을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;log-driver&quot;: &quot;json-file&quot;,
  &quot;log-opts&quot;: {
    &quot;max-size&quot;: &quot;10m&quot;,
    &quot;max-file&quot;: &quot;3&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 후 Docker를 재시작합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl restart docker&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 최종 아키텍처 다이어그램&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 스택의 트래픽 흐름을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                        ┌─── 인터넷 ───┐
                        │   사용자 브라우저  │
                        └──────┬───────┘
                               │  HTTPS (443)
                        ┌──────▼───────┐
                        │    Caddy     │  &amp;larr; 자동 TLS, HTTP/3
                        │  (리버스 프록시) │
                        └──┬───┬───┬───┘
                ┌──────────┤   │   ├──────────┐
                │          │   │   │          │
         ┌──────▼──┐  ┌───▼───▼┐  ┌▼────────┐  ┌──────────┐
         │ React   │  │ Go Gin │  │ FastAPI  │  │  n8n     │
         │ SPA     │  │ :8080  │  │ :8000    │  │  :5678   │
         │(정적파일)│  └───┬────┘  └──┬──────┘  └────┬─────┘
         └─────────┘      │          │               │
                    ┌─────▼──────────▼───────────────▼─────┐
                    │           PostgreSQL :5432            │
                    └──────────────────────────────────────┘
                    ┌──────────────────────────────────────┐
                    │           MinIO :9000 / :9001         │
                    └──────────────────────────────────────┘

  ※ 모든 컨테이너는 internal 브리지 네트워크로 연결
  ※ 외부 노출 포트는 Caddy의 80/443만 해당&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 최종 확인 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 후 아래 항목을 하나씩 체크합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;docker compose ps&lt;/code&gt;에서 모든 서비스가 &lt;code&gt;Up&lt;/code&gt; 또는 &lt;code&gt;Up (healthy)&lt;/code&gt; 상태인가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;https://example.com&lt;/code&gt;에서 React SPA가 정상 로드되는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; React Router의 서브 경로(예: &lt;code&gt;https://example.com/about&lt;/code&gt;)에 직접 접속해도 404가 아닌 SPA가 로드되는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;https://example.com/api/go/health&lt;/code&gt;가 정상 응답하는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;https://example.com/api/py/health&lt;/code&gt;가 정상 응답하는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;https://n8n.example.com&lt;/code&gt;에서 n8n 대시보드에 접속 가능한가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;https://minio.example.com&lt;/code&gt;에서 MinIO Console에 접속 가능한가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; PostgreSQL에 &lt;code&gt;docker compose exec&lt;/code&gt; 로 쿼리가 실행되는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; TLS 인증서가 Let's Encrypt로 정상 발급되었는가 (브라우저 자물쇠 아이콘 확인)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;.env&lt;/code&gt; 파일이 Git에 커밋되지 않았는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 방화벽에서 80, 443 외의 포트는 차단되어 있는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Docker 로그 로테이션이 설정되어 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 시리즈 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;14편에 걸쳐 &lt;b&gt;새 윈도우 PC에서 클라우드 배포까지&lt;/b&gt;의 전체 여정을 함께 했습니다. 시리즈 전체를 간략히 돌아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1~3편 (기반 환경)&lt;/b&gt; 에서는 Windows Terminal과 PowerShell 7을 설치하고, Winget/Scoop으로 패키지 매니저를 구성했습니다. Git과 GitHub을 연동하고, Google Antigravity(VS Code 포크)를 IDE로 세팅했습니다. WSL 없이 윈도우 네이티브만으로 충분히 쾌적한 개발 환경을 만들 수 있다는 것을 확인한 구간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4~6편 (런타임 &amp;amp; 프레임워크)&lt;/b&gt; 에서는 Node.js로 React SPA(TypeScript + Tailwind + Vite + React Router)를, Go로 Gin 웹 서버를, Python으로 FastAPI를 각각 세팅했습니다. 세 가지 언어&amp;middot;프레임워크를 하나의 프로젝트에서 조합하는 풀스택 구조를 잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7~10편 (Docker &amp;amp; 인프라 서비스)&lt;/b&gt; 에서는 Docker Desktop을 설치하고, PostgreSQL&amp;middot;MinIO&amp;middot;n8n을 각각 컨테이너로 띄웠습니다. 로컬에서 &lt;code&gt;docker compose&lt;/code&gt;로 인프라를 관리하는 워크플로우에 익숙해진 구간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;11~12편 (서버 &amp;amp; AI 도구)&lt;/b&gt; 에서는 리눅스 서버에 Caddy를 올려 리버스 프록시와 자동 HTTPS를 구성하고, Claude Code CLI를 로컬에 설치해 AI 기반 코딩 워크플로우를 경험했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;13~14편 (배포 &amp;amp; 통합)&lt;/b&gt; 에서는 클라우드 선택지를 비교하고, 최종적으로 모든 서비스를 하나의 Docker Compose로 통합하여 프로덕션 서버에 배포했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈의 핵심 메시지는 세 가지입니다. 첫째, &lt;b&gt;WSL 없이도 윈도우에서 충분히 생산적인 개발 환경을 구축할 수 있습니다.&lt;/b&gt; PowerShell 7 + Windows Terminal + Docker Desktop 조합은 대부분의 웹 개발 시나리오를 커버합니다. 둘째, &lt;b&gt;Docker Compose 하나로 로컬 개발 환경과 프로덕션 배포를 통일할 수 있습니다.&lt;/b&gt; 로컬에서 동작하는 컴포즈 파일을 서버에 그대로 올리면 됩니다. 셋째, &lt;b&gt;Caddy는 소규모 배포에서 Nginx의 훌륭한 대안입니다.&lt;/b&gt; 자동 HTTPS, 간결한 설정 파일, 리버스 프록시 기능만으로도 1인 개발자의 프로덕션 요구사항을 충족합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈에서 다루지 못한 주제(CI/CD 파이프라인, 모니터링/알림, 로드 밸런싱, Kubernetes 전환 등)는 프로젝트가 성장하면서 자연스럽게 다음 단계로 이어질 것입니다. 이 시리즈가 그 출발점이 되었기를 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;14편에 걸친 긴 여정을 끝까지 읽어주셔서 감사합니다. 즐거운 개발 되세요.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/320</guid>
      <comments>https://polarcompass.tistory.com/320#entry320comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:31:20 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 13편: 클라우드 서버 비교 &amp;amp; 선택 가이드</title>
      <link>https://polarcompass.tistory.com/319</link>
      <description>&lt;p&gt;Now I have all the data I need. Let me compile the 13편 article.&lt;/p&gt;
&lt;h1&gt;13편: 클라우드 서버 비교 &amp;amp; 선택 가이드&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;시리즈: 윈도우 네이티브 개발 환경에서 리눅스 서버 배포까지&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이전 편(1~12편)에서 윈도우 네이티브 환경의 개발 세팅, Docker 컨테이너 구성, Caddy 리버스 프록시, Claude Code까지 모두 마쳤습니다. 이제 로컬에서 만든 프로젝트를 실제로 올릴 &lt;strong&gt;클라우드 서버&lt;/strong&gt;를 골라야 합니다.&lt;/p&gt;
&lt;p&gt;이번 편에서는 1인 개발자 또는 소규모 팀이 &lt;strong&gt;VM 인스턴스 하나&lt;/strong&gt;로 배포한다는 전제 아래, 주요 클라우드를 객관적으로 비교하고 실용적인 추천을 드립니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;1. 비교 전제: 어떤 서버가 필요한가&lt;/h2&gt;
&lt;p&gt;이 시리즈의 배포 대상은 React SPA 정적 파일 + Go Gin API + FastAPI + PostgreSQL + MinIO + n8n을 Docker Compose로 묶어 Caddy가 앞단에서 리버스 프록시를 하는 구성입니다. 이 정도 스택을 안정적으로 돌리려면 최소한 vCPU 2개, RAM 4GB 수준이 필요합니다. 디스크는 SSD 50~80GB면 초기 운영에 충분하고, 트래픽이 크지 않은 개인 프로젝트나 MVP 단계를 가정합니다.&lt;/p&gt;
&lt;p&gt;따라서 모든 클라우드를 &lt;strong&gt;&amp;quot;vCPU 2 / RAM 2~4GB / SSD 50GB 이상&amp;quot;&lt;/strong&gt; 온디맨드(종량제) 기준으로 비교합니다. 한국 서비스 대상이라면 &lt;strong&gt;한국 리전(또는 서울 근접 리전)&lt;/strong&gt; 가격을 기준으로 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 클라우드별 상세 비교&lt;/h2&gt;
&lt;h3&gt;2-1. AWS EC2 (Amazon Web Services)&lt;/h3&gt;
&lt;p&gt;AWS는 가장 보편적인 퍼블릭 클라우드이며 서울 리전(ap-northeast-2)을 운영합니다. 범용 인스턴스 중 1인 개발자가 가장 많이 선택하는 것은 T3 시리즈입니다. T3는 버스터블(burstable) 인스턴스로, 평소에는 기준 CPU 성능을 사용하다가 크레딧이 쌓이면 일시적으로 풀 코어를 사용할 수 있는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;서울 리전 온디맨드 가격 (Linux)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;인스턴스&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;시간당&lt;/th&gt;
&lt;th&gt;월 환산(730h)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;t3.micro&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;$0.0130&lt;/td&gt;
&lt;td&gt;약 $9.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.small&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;$0.0260&lt;/td&gt;
&lt;td&gt;약 $18.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.medium&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;$0.0520&lt;/td&gt;
&lt;td&gt;약 $37.96&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;EBS(디스크)는 별도 과금입니다. gp3 SSD 50GB 기준 약 $4/월이 추가되므로, t3.small + 50GB SSD를 쓰면 월 약 &lt;strong&gt;$23(약 31,000원)&lt;/strong&gt; 수준입니다. t3.medium(4GB RAM)으로 가면 월 약 &lt;strong&gt;$42(약 57,000원)&lt;/strong&gt; 정도가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무료 티어:&lt;/strong&gt; 신규 가입 후 12개월간 t3.micro(2 vCPU, 1GB RAM) 월 750시간 무료. RAM 1GB라 Docker Compose 풀 스택을 돌리기엔 빠듯하지만, 테스트 용도로는 사용 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 압도적인 서비스 생태계(RDS, S3, CloudFront, Route 53 등), 레퍼런스와 커뮤니티 자료가 가장 풍부, 서울 리전 안정성 검증 완료, Savings Plan이나 Reserved Instance로 장기 사용 시 30~60% 할인 가능.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt; 가격 구조가 복잡(인스턴스 + EBS + 데이터 전송 + IP 등 각각 과금), 프리 티어 이후 비용이 빅3 중 상대적으로 높은 편, 콘솔 UI가 방대해서 초보자에게 압도적.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-2. Microsoft Azure&lt;/h3&gt;
&lt;p&gt;Azure는 한국 중부(Korea Central, 서울) 리전을 운영합니다. 버스터블 VM인 B 시리즈가 AWS T3와 비슷한 포지션입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;한국 중부 리전 온디맨드 가격 (Linux)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;인스턴스&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;시간당&lt;/th&gt;
&lt;th&gt;월 환산(730h)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;B1s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;$0.0124&lt;/td&gt;
&lt;td&gt;약 $9.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2s&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;$0.0642&lt;/td&gt;
&lt;td&gt;약 $46.87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2ms&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;$0.1140&lt;/td&gt;
&lt;td&gt;약 $83.22&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;B2s(2 vCPU, 4GB)에 관리 디스크 E10(128GB SSD) 약 $9.60/월을 더하면 월 약 &lt;strong&gt;$56(약 76,000원)&lt;/strong&gt; 입니다. 같은 스펙 대비 AWS t3.medium보다 조금 비쌉니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무료 티어:&lt;/strong&gt; 신규 가입 시 $200 크레딧(30일 내 사용), 이후 12개월간 B1s(1 vCPU, 1GB) 월 750시간 무료.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 엔터프라이즈 연계(Active Directory, Office 365)가 강점, GitHub Actions와의 통합이 자연스러움, Visual Studio 구독자 월 크레딧 제공, 한국어 문서가 잘 되어 있음.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt; 같은 스펙 기준 빅3 중 가장 비싼 편, 콘솔(Azure Portal) UI가 직관적이지 않다는 평이 많음, 1인 개발자용 소규모 VM보다는 엔터프라이즈 워크로드에 최적화된 서비스.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-3. GCP Compute Engine (Google Cloud Platform)&lt;/h3&gt;
&lt;p&gt;GCP는 서울 리전(asia-northeast3)을 운영합니다. E2 시리즈가 가장 경제적인 범용 인스턴스입니다. E2는 공유 vCPU 방식으로 AWS T3와 유사한 컨셉입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;서울 리전 온디맨드 가격 (Linux)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;인스턴스&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;시간당&lt;/th&gt;
&lt;th&gt;월 환산(730h)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;e2-micro&lt;/td&gt;
&lt;td&gt;2 (공유)&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;$0.0101&lt;/td&gt;
&lt;td&gt;약 $7.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e2-small&lt;/td&gt;
&lt;td&gt;2 (공유)&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;$0.0215&lt;/td&gt;
&lt;td&gt;약 $15.69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e2-medium&lt;/td&gt;
&lt;td&gt;2 (공유)&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;$0.0430&lt;/td&gt;
&lt;td&gt;약 $31.38&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;디스크는 균형 영구 디스크(pd-balanced) 50GB 기준 약 $5.50/월이므로, e2-medium + 50GB를 쓰면 월 약 &lt;strong&gt;$37(약 50,000원)&lt;/strong&gt; 입니다. e2-small(2GB RAM)이면 월 약 &lt;strong&gt;$21(약 28,000원)&lt;/strong&gt; 으로 AWS t3.small과 비슷한 수준입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무료 티어:&lt;/strong&gt; e2-micro(2 vCPU 공유, 1GB RAM) 1대를 &lt;strong&gt;기간 제한 없이 항상 무료(Always Free)&lt;/strong&gt; 로 제공합니다. 단, 미국 리전(us-east1, us-west1, us-central1)에서만 무료이며 서울 리전은 해당되지 않습니다. 또한 신규 가입 시 $300 크레딧(90일 내 사용)이 제공됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; Always Free 티어가 기간 제한 없이 유지(미국 리전 한정), $300 크레딧으로 서울 리전 테스트 가능, Cloud Run/Cloud Functions 등 서버리스 생태계가 강력, 1년/3년 약정(CUD) 할인이 자동 적용되어 관리가 편리.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt; 한국어 문서가 AWS/Azure 대비 부족한 편, 한국 커뮤니티 규모가 상대적으로 작음, 콘솔은 깔끔하지만 서비스 찾기가 직관적이지 않다는 의견이 있음.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-4. Vultr (서울 리전)&lt;/h3&gt;
&lt;p&gt;Vultr는 전 세계 32개 이상 데이터센터를 운영하는 독립 클라우드 인프라 업체로, &lt;strong&gt;서울 리전&lt;/strong&gt;을 보유하고 있습니다. 요금이 단순하고 저렴해서 1인 개발자에게 인기가 많습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;서울 리전 Cloud Compute 가격 (모든 리전 동일가)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;플랜&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;SSD&lt;/th&gt;
&lt;th&gt;대역폭&lt;/th&gt;
&lt;th&gt;월 가격&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Regular&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;55 GB&lt;/td&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;$10/월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regular&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;80 GB&lt;/td&gt;
&lt;td&gt;3 TB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$20/월&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High Performance (AMD)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;100 GB (NVMe)&lt;/td&gt;
&lt;td&gt;5 TB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$24/월&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High Performance (AMD)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;180 GB (NVMe)&lt;/td&gt;
&lt;td&gt;6 TB&lt;/td&gt;
&lt;td&gt;$48/월&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;가장 추천할 만한 플랜은 &lt;strong&gt;High Performance 2 vCPU / 4GB RAM / 100GB NVMe로 월 $24(약 32,000원)&lt;/strong&gt; 입니다. 디스크와 대역폭이 모두 포함된 가격이라 추가 과금을 걱정할 필요가 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무료 티어:&lt;/strong&gt; 상시 무료 티어는 없지만, 신규 가입 시 &lt;strong&gt;$300 크레딧(30일 사용)&lt;/strong&gt; 프로모션을 수시로 진행합니다(시점에 따라 금액 변동).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 가격이 매우 단순하고 저렴(디스크·대역폭 포함), 서울 리전 레이턴시 우수, 서버 배포가 1~2분 내로 빠름, 콘솔 UI가 직관적, 시간당 과금이라 잠깐 테스트 후 삭제 가능.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt; AWS/Azure/GCP 대비 매니지드 서비스 생태계가 거의 없음(RDS, CDN 등 자체 관리 필요), SLA가 빅3 대비 낮은 편(100% 가동 보장 아님), 한국어 지원 및 한국어 문서 전무.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-5. 네이버 클라우드 플랫폼 (NCP)&lt;/h3&gt;
&lt;p&gt;네이버 클라우드는 국내 기업이 운영하는 클라우드로, 한국 리전만 집중 운영합니다. 콘솔과 문서가 모두 한국어이며 국내 법률(개인정보보호법 등) 준수가 중요한 경우 유리합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;주요 서버 요금 (VPC 환경)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;월 요금&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Micro (프리티어)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;약 10,850원 (프리티어 시 무료)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;약 69,000원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;약 96,000원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;디스크 50GB(SSD)는 별도로 약 5,500원/월이 추가됩니다. 2 vCPU / 4GB 기준 월 약 &lt;strong&gt;74,500원&lt;/strong&gt; 수준입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무료 티어:&lt;/strong&gt; Micro 서버(1 vCPU, 1GB RAM)를 결제 수단 등록 후 &lt;strong&gt;1년간 무료&lt;/strong&gt; 사용 가능. 신규 가입 시 10만 원 크레딧도 제공됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 한국어 콘솔·문서·고객 지원, 국내 데이터 주권 확보, 네이버 서비스(Papago NMT, Clova 등)와 연계 가능, 공공기관·금융권 인증(CSAP 등) 보유.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt; 글로벌 빅3 대비 같은 스펙에서 가격이 2~3배 비쌈, 해외 리전이 제한적(글로벌 서비스 배포 시 불리), 커뮤니티 레퍼런스가 적음, 서비스 종류가 빅3 대비 부족.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-6. 가비아 클라우드 (g클라우드)&lt;/h3&gt;
&lt;p&gt;국내 호스팅 기업 가비아가 운영하는 클라우드 서비스입니다. 심플한 서버 호스팅에 가깝고, 소규모 웹사이트나 개인 프로젝트에 많이 사용됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;주요 서버 요금 (VPC 플랫폼)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;월 요금(VAT 별도)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Micro&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;11,000원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High CPU&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;60,500원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;75,350원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;디스크 50GB(SSD)를 포함하면 2 vCPU / 4GB 기준 월 약 &lt;strong&gt;66,000~70,000원(VAT 포함)&lt;/strong&gt; 정도입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 한국어 지원, 콘솔이 단순해서 초보자가 접근하기 쉬움, 도메인·SSL 등 부가 서비스와 함께 관리 가능.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt; 클라우드 생태계가 매우 제한적(오토스케일링, 로드밸런서 등 부족), 글로벌 확장 불가, 같은 스펙 기준 Vultr 대비 2~3배 비쌈, 레퍼런스가 적음.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-7. 카페24 VPS&lt;/h3&gt;
&lt;p&gt;카페24는 국내에서 가장 오래된 호스팅 업체 중 하나로, 가상서버(VPS) 호스팅을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;주요 VPS 요금&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상품&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;SSD&lt;/th&gt;
&lt;th&gt;트래픽(월)&lt;/th&gt;
&lt;th&gt;월 요금(VAT 포함)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;일반형&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;30 GB&lt;/td&gt;
&lt;td&gt;300 GB&lt;/td&gt;
&lt;td&gt;7,000원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비즈니스&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;60 GB&lt;/td&gt;
&lt;td&gt;500 GB&lt;/td&gt;
&lt;td&gt;14,000원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단독웹 Pro&lt;/td&gt;
&lt;td&gt;2vCPU / 4 GB&lt;/td&gt;
&lt;td&gt;80 GB&lt;/td&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;39,600원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;가장 저렴한 일반형(7,000원)은 RAM 1GB라 Docker 풀 스택에는 부족하고, 단독웹 Pro(2vCPU, 4GB) 기준 월 &lt;strong&gt;39,600원&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 국내 최저가 수준의 VPS 제공, 한국어 지원, 설정이 단순, 웹호스팅에서 마이그레이션 시 편리.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt; 클라우드라기보다 전통적 VPS에 가까움(오토스케일링 없음), root 접근 시 제한이 있는 상품이 있음, Docker 활용 시 호환성 확인 필요, 글로벌 서비스 불가.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 한눈에 비교: 요약표&lt;/h2&gt;
&lt;p&gt;아래 표는 &lt;strong&gt;2 vCPU / 2~4GB RAM&lt;/strong&gt; 온디맨드 기준 월 비용과 핵심 특성을 정리한 것입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;클라우드&lt;/th&gt;
&lt;th&gt;인스턴스&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;디스크 포함 월 비용&lt;/th&gt;
&lt;th&gt;무료 티어&lt;/th&gt;
&lt;th&gt;한국 리전&lt;/th&gt;
&lt;th&gt;한국어 문서&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;t3.small&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;~$23 (≈31,000원)&lt;/td&gt;
&lt;td&gt;12개월 t3.micro&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;△ (일부)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;t3.medium&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;~$42 (≈57,000원)&lt;/td&gt;
&lt;td&gt;12개월 t3.micro&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;△ (일부)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;B2s&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;~$56 (≈76,000원)&lt;/td&gt;
&lt;td&gt;$200 + 12개월 B1s&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;e2-small&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;~$21 (≈28,000원)&lt;/td&gt;
&lt;td&gt;Always Free e2-micro (미국) + $300&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;e2-medium&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;~$37 (≈50,000원)&lt;/td&gt;
&lt;td&gt;Always Free e2-micro (미국) + $300&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vultr&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HP AMD&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$24 (≈32,000원)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;프로모션 크레딧&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;✕&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;~74,500원&lt;/td&gt;
&lt;td&gt;1년 Micro + 10만원&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;가비아&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High CPU&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;~70,000원&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;카페24&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;단독웹 Pro&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;39,600원&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;참고:&lt;/strong&gt; 가격은 2026년 3월 기준 공식 요금표에서 조회한 값이며, 환율 변동·프로모션·약정 할인에 따라 달라질 수 있습니다. &amp;quot;디스크 포함 월 비용&amp;quot;은 SSD 50~80GB를 포함한 대략적 총 비용이며, 데이터 전송(egress) 비용은 제외했습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;4. 비교 항목별 심층 분석&lt;/h2&gt;
&lt;h3&gt;4-1. 가격 대비 성능&lt;/h3&gt;
&lt;p&gt;순수 VM 가격만 놓고 보면 &lt;strong&gt;Vultr High Performance($24/월)&lt;/strong&gt; 가 2 vCPU / 4GB RAM / 100GB NVMe / 5TB 대역폭을 모두 포함해서 가장 가성비가 좋습니다. AWS t3.small(2GB)은 약 $23으로 비슷하지만 RAM이 절반이고 EBS가 별도입니다. GCP e2-small(2GB)이 약 $21로 최저가이지만 역시 RAM이 2GB입니다. 4GB RAM 기준으로는 GCP e2-medium($37)이 빅3 중에서는 가장 저렴하고, AWS t3.medium($42), Azure B2s($56) 순입니다.&lt;/p&gt;
&lt;p&gt;국내 클라우드는 같은 스펙 기준으로 빅3·Vultr 대비 1.5~3배 비쌉니다. 카페24 단독웹 Pro(39,600원)가 국내 클라우드 중에서는 가장 경쟁력 있지만, 클라우드 기능(오토스케일링, API 등)이 제한적입니다.&lt;/p&gt;
&lt;h3&gt;4-2. 무료 티어 &amp;amp; 크레딧&lt;/h3&gt;
&lt;p&gt;처음 시작하는 개발자에게 무료 티어는 중요한 기준입니다. GCP의 Always Free e2-micro는 기간 제한이 없다는 점에서 독보적이지만 미국 리전 전용이라 한국 서비스에는 레이턴시가 있습니다. 학습·테스트 용도로는 최적입니다. AWS 프리 티어(12개월 t3.micro)와 Azure($200 크레딧 + 12개월 B1s)도 초기 탐색에 유용하지만, 1년이 지나면 과금이 시작되므로 주의가 필요합니다. NCP는 Micro 서버 1년 무료 + 10만 원 크레딧으로 국내 클라우드 중에서는 가장 넉넉합니다.&lt;/p&gt;
&lt;h3&gt;4-3. 관리 편의성 (콘솔 UI &amp;amp; 문서)&lt;/h3&gt;
&lt;p&gt;콘솔 UI의 간결함은 Vultr가 가장 뛰어납니다. 서버 생성부터 삭제까지 클릭 몇 번이면 끝나고, 요금 구조도 단순합니다. NCP와 가비아·카페24는 한국어 콘솔이라는 강점이 있지만, 기능 면에서 빅3보다 제한적입니다. AWS 콘솔은 서비스가 워낙 많아 처음에 길을 잃기 쉽지만, 익숙해지면 가장 강력합니다. GCP 콘솔은 디자인이 깔끔하고 Cloud Shell이 내장되어 있어 편리합니다. Azure Portal은 기능이 풍부하지만 탐색이 직관적이지 않다는 평이 많습니다.&lt;/p&gt;
&lt;p&gt;한국어 문서는 NCP &amp;gt; Azure &amp;gt; AWS &amp;gt; GCP 순으로 충실합니다. 영어 레퍼런스(Stack Overflow, 블로그 등)까지 포함하면 AWS가 압도적입니다.&lt;/p&gt;
&lt;h3&gt;4-4. 네트워크 (한국 리전 &amp;amp; 레이턴시)&lt;/h3&gt;
&lt;p&gt;한국 사용자 대상 서비스라면 서울 리전은 필수입니다. AWS, Azure, GCP, Vultr, NCP 모두 서울에 데이터센터를 두고 있어 레이턴시 차이는 크지 않습니다(일반적으로 1~5ms 내외). 가비아와 카페24 역시 국내 IDC를 사용합니다.&lt;/p&gt;
&lt;p&gt;데이터 전송(egress) 비용에서는 차이가 있습니다. AWS는 서울 리전에서 인터넷으로 나가는 트래픽에 약 $0.126/GB를 부과합니다(첫 10TB 기준). GCP는 약 $0.12/GB, Azure는 약 $0.12/GB 수준입니다. 반면 Vultr는 플랜에 대역폭이 포함(5TB)되어 있어 소규모 트래픽에서는 추가 과금이 없습니다. 이 차이는 트래픽이 적을 때는 무시할 수준이지만, CDN 없이 VM에서 직접 대용량 파일을 서빙한다면 고려해야 합니다.&lt;/p&gt;
&lt;h3&gt;4-5. 배포 난이도&lt;/h3&gt;
&lt;p&gt;VM 인스턴스 하나를 띄우고 SSH로 접속해서 Docker Compose를 실행하는 것까지의 과정은 어느 클라우드든 비슷합니다. 다만 &lt;strong&gt;초기 설정의 복잡도&lt;/strong&gt;에서 차이가 납니다.&lt;/p&gt;
&lt;p&gt;AWS는 VPC, 서브넷, 보안 그룹, IAM 등 개념을 이해해야 해서 진입 장벽이 가장 높습니다. GCP는 기본 VPC가 자동 생성되어 조금 더 쉽습니다. Azure도 리소스 그룹 개념이 있지만 AWS보다는 단순합니다. Vultr는 서버 생성 페이지에서 리전, OS, 플랜만 고르면 1~2분 안에 SSH 접속 가능한 서버가 준비되어 배포 난이도가 가장 낮습니다. 국내 클라우드(NCP, 가비아, 카페24)도 비교적 단순하지만, IaC(Terraform 등) 지원이 제한적이어서 자동화 측면에서는 불리합니다.&lt;/p&gt;
&lt;h3&gt;4-6. 커뮤니티 &amp;amp; 레퍼런스&lt;/h3&gt;
&lt;p&gt;구글에서 문제를 검색했을 때 답을 찾을 확률은 &lt;strong&gt;AWS &amp;gt;&amp;gt; GCP &amp;gt; Azure &amp;gt;&amp;gt; Vultr &amp;gt;&amp;gt; NCP &amp;gt; 가비아/카페24&lt;/strong&gt; 순입니다. 특히 &amp;quot;EC2에서 Docker Compose로 배포하기&amp;quot; 같은 가이드는 수백 개가 존재하지만, NCP나 가비아에서 동일한 작업을 설명하는 글은 상대적으로 드뭅니다. Vultr는 공식 블로그에 양질의 튜토리얼이 많고, 영어권 커뮤니티에서도 자주 언급됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 이런 경우엔 이걸 써라: 상황별 추천&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;가성비가 최우선, 빠르게 배포하고 싶다&amp;quot;&lt;/strong&gt; → &lt;strong&gt;Vultr High Performance $24/월&lt;/strong&gt;. 서울 리전에 디스크·대역폭 포함 가격이 단순하고 저렴합니다. 개인 프로젝트, 사이드 프로젝트, MVP 검증에 적합합니다. 매니지드 DB 같은 추가 서비스가 필요 없고 Docker Compose로 모든 것을 관리하는 이 시리즈의 구성과 궁합이 좋습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;무료로 시작하고 싶다, 학습 목적이다&amp;quot;&lt;/strong&gt; → &lt;strong&gt;GCP e2-micro (Always Free)&lt;/strong&gt; 또는 &lt;strong&gt;AWS 프리 티어 t3.micro&lt;/strong&gt;. GCP는 미국 리전이지만 기간 제한 없이 무료이고, AWS는 서울 리전이지만 12개월 한정입니다. 두 곳 모두 가입해서 GCP로 학습하고, AWS 서울 리전으로 실서비스 테스트하는 조합을 추천합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;회사 프로젝트, 확장 가능성을 고려해야 한다&amp;quot;&lt;/strong&gt; → &lt;strong&gt;AWS EC2&lt;/strong&gt;. 나중에 RDS, ElastiCache, CloudFront, Route 53 등으로 확장할 때 같은 AWS 생태계 안에서 해결할 수 있습니다. 초기엔 t3.small로 시작하고, 트래픽이 늘면 인스턴스를 키우거나 ECS/EKS로 전환하는 경로가 가장 자연스럽습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;한국어 지원이 필수, 데이터가 반드시 국내에 있어야 한다&amp;quot;&lt;/strong&gt; → &lt;strong&gt;NCP&lt;/strong&gt;. 공공기관이나 금융권 프로젝트라면 CSAP 인증이 있는 NCP가 사실상 유일한 선택지일 수 있습니다. 비용은 높지만 한국어 기술 지원과 국내 법규 준수가 보장됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Azure를 이미 쓰고 있거나, .NET/MS 생태계와 연계해야 한다&amp;quot;&lt;/strong&gt; → &lt;strong&gt;Azure VM&lt;/strong&gt;. GitHub와의 통합, Azure DevOps, Visual Studio 구독 크레딧 등 Microsoft 생태계와의 시너지가 큽니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;가장 저렴한 국내 VPS가 필요하다, 클라우드 기능은 필요 없다&amp;quot;&lt;/strong&gt; → &lt;strong&gt;카페24 단독웹 Pro(39,600원/월)&lt;/strong&gt; 또는 &lt;strong&gt;가비아 High CPU(약 70,000원/월)&lt;/strong&gt;. 클라우드 생태계가 아닌 전통적 VPS로도 충분한 규모라면 국내 호스팅도 선택지입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 이 시리즈에서의 선택&lt;/h2&gt;
&lt;p&gt;이 시리즈에서는 다음 편(14편)에서 통합 Docker Compose를 구성하고 실제 서버에 배포합니다. &lt;strong&gt;1인 개발자가 사이드 프로젝트를 배포한다는 전제&lt;/strong&gt;에서는 Vultr High Performance(2 vCPU, 4GB RAM, $24/월)가 가성비와 배포 편의성 면에서 가장 합리적입니다. 빅3를 사용하고 싶다면 AWS t3.small + EBS(&lt;del&gt;$23/월, 다만 RAM 2GB)이나 GCP e2-medium(&lt;/del&gt;$37/월)을 추천합니다.&lt;/p&gt;
&lt;p&gt;어떤 클라우드를 선택하든 14편의 Docker Compose 설정은 동일하게 적용됩니다. 차이가 나는 부분은 서버 초기 세팅(SSH 접속, Docker 설치, 방화벽 설정) 정도이며, 이는 14편에서 간략히 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 최종 확인 체크리스트&lt;/h2&gt;
&lt;p&gt;본문을 읽고 아래 항목을 점검해 보세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 내 프로젝트에 필요한 최소 스펙(vCPU, RAM, 디스크)을 파악했는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 한국 리전이 필요한지, 해외 리전이어도 괜찮은지 판단했는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 무료 티어/크레딧을 활용할 수 있는 클라우드를 확인했는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 디스크·데이터 전송 등 숨겨진 추가 비용을 고려했는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 장기적으로 확장 가능성(매니지드 서비스, 오토스케일링 등)이 필요한지 검토했는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 한국어 지원/데이터 주권 등 비기술적 요구사항을 확인했는가&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 사용할 클라우드를 결정하고 계정을 생성했는가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;14편: 통합 Docker Compose &amp;amp; 최종 점검&lt;/strong&gt;에서는 PostgreSQL, MinIO, n8n, Caddy를 하나의 &lt;code&gt;docker-compose.yml&lt;/code&gt;로 통합하고, React SPA 빌드 결과물을 Caddy로 서빙하며, Go Gin과 FastAPI를 리버스 프록시로 연결합니다. 클라우드 서버에서 전체 스택을 기동하고 헬스체크까지 마치면 이 시리즈가 완결됩니다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/319</guid>
      <comments>https://polarcompass.tistory.com/319#entry319comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:30:56 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 12편: Claude Code 설치 &amp;amp; 활용</title>
      <link>https://polarcompass.tistory.com/318</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Now I have enough information to write a comprehensive 12th article. Let me compile it.&lt;/p&gt;
&lt;h1&gt;12편: Claude Code 설치 &amp;amp; 활용&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈&lt;/b&gt;: 윈도우 네이티브 개발 환경 구축부터 클라우드 배포까지 (12/14)&lt;br /&gt;&lt;b&gt;환경&lt;/b&gt;: 로컬 &amp;mdash; Windows 10/11 (PowerShell 7) &amp;middot; 원격 &amp;mdash; Ubuntu/Debian (클라우드 리눅스 서버)&lt;br /&gt;&lt;b&gt;이전 편 전제&lt;/b&gt;: 1~11편까지 로컬 개발 환경 + 클라우드 서버 Caddy 구성 완료&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11편까지 로컬 개발 환경과 클라우드 서버를 모두 세팅했습니다. 이제 이 모든 환경 위에서 &lt;b&gt;AI 코딩 어시스턴트&lt;/b&gt;를 얹을 차례입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code는 Anthropic이 만든 CLI 기반 에이전틱 코딩 도구입니다. 터미널에서 자연어로 코드를 생성하고, 파일을 수정하고, 테스트를 실행하고, Git 커밋까지 해주는 &quot;개발자 옆자리의 시니어 동료&quot; 같은 존재입니다. 일반적인 AI 챗봇과 다른 점은 &lt;b&gt;프로젝트 파일을 직접 읽고 쓸 수 있다&lt;/b&gt;는 것입니다. 질문에 답하는 데 그치지 않고, 실제로 코드를 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 두 가지를 다룹니다. 첫째, &lt;b&gt;윈도우 로컬 환경&lt;/b&gt;에서 Claude Code를 설치하고 Antigravity IDE와 함께 사용하는 워크플로우. 둘째, &lt;b&gt;클라우드 리눅스 서버&lt;/b&gt;에 Claude Code를 설치하여 SSH로 접속한 뒤 Caddy 설정, Docker 관리 등 서버 운영 작업을 맡기는 방법입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 사전 준비&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 필수 요건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code를 사용하려면 다음이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;계정&lt;/b&gt;: Claude Pro($20/월), Max($100 또는 $200/월), Team, 또는 Enterprise 구독. 무료 플랜에서는 Claude Code를 사용할 수 없습니다. 또는 Anthropic Console(API 크레딧 선불 충전) 계정으로도 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Git for Windows&lt;/b&gt;: Claude Code는 내부적으로 Git Bash를 사용하여 명령어를 실행합니다. 2편에서 이미 설치했으므로 별도 작업은 필요 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템 요건&lt;/b&gt;: Windows 10 1809+ 또는 Windows 11, 4GB 이상 RAM, 인터넷 연결.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. Git for Windows 확인&lt;/h3&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;git --version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;git version 2.47.1.windows.2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 출력되면 준비 완료입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 윈도우에 Claude Code 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 방법은 세 가지입니다. 상황에 맞는 것을 선택합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 네이티브 인스톨러 (권장)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell 7에서 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;irm https://claude.ai/install.ps1 | iex&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 새 터미널 창을 열고 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;claude --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이티브 인스톨러는 &lt;b&gt;자동 업데이트&lt;/b&gt;를 지원합니다. 백그라운드에서 새 버전을 감지하면 자동으로 다운로드하고, 다음 실행 시 적용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. WinGet으로 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 설치한 WinGet을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;winget install Anthropic.ClaudeCode&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WinGet으로 설치한 경우 자동 업데이트가 되지 않으므로, 수동으로 업데이트해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;winget upgrade Anthropic.ClaudeCode&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 설치 확인&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 버전 확인
claude --version

# 전체 환경 진단
claude doctor&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;claude doctor&lt;/code&gt;는 설치 상태, Git Bash 경로, 네트워크 연결 등을 종합적으로 점검해 줍니다. 문제가 있으면 원인과 해결 방법을 알려 줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. Git Bash 경로 문제 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code가 Git Bash를 찾지 못하는 경우, 설정 파일에 경로를 명시합니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 설정 파일 위치: ~/.claude/settings.json
notepad $env:USERPROFILE\.claude\settings.json&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;env&quot;: {
    &quot;CLAUDE_CODE_GIT_BASH_PATH&quot;: &quot;C:\\Program Files\\Git\\bin\\bash.exe&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 로그인 &amp;amp; 인증&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 첫 로그인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 디렉토리에서 Claude Code를 시작합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;cd C:\Projects\my-react-app
claude&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 실행 시 브라우저가 열리면서 로그인 화면이 나타납니다. Claude Pro/Max 구독 계정 또는 Anthropic Console 계정으로 로그인합니다. 인증이 완료되면 자격 증명이 로컬에 저장되어 이후에는 다시 로그인할 필요가 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Anthropic Console (API 키) 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독 대신 API 크레딧으로 사용하려면 Console 계정으로 로그인합니다. 첫 로그인 시 Claude Code 전용 워크스페이스가 자동 생성되어 비용을 추적할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 계정 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 로그인한 상태에서 다른 계정으로 전환하려면 세션 안에서 &lt;code&gt;/login&lt;/code&gt; 명령을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/login&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 기본 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 인터랙티브 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 사용 방식입니다. 프로젝트 디렉토리에서 &lt;code&gt;claude&lt;/code&gt;를 실행하면 대화형 세션이 시작됩니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;cd C:\Projects\my-react-app
claude&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연어로 요청하면 Claude가 파일을 읽고, 코드를 수정하고, 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;&amp;gt; 이 프로젝트의 구조를 설명해줘

&amp;gt; src/components/Header.tsx에 다크모드 토글 버튼을 추가해줘

&amp;gt; 변경사항을 커밋해줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude가 파일을 수정하거나 명령어를 실행할 때는 사전에 승인을 요청합니다. &lt;code&gt;y&lt;/code&gt;를 눌러 승인하거나 &lt;code&gt;n&lt;/code&gt;으로 거부할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 원샷 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 질문이나 단일 작업은 &lt;code&gt;-p&lt;/code&gt; 플래그로 세션 없이 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 질문하고 바로 종료
claude -p &quot;이 프로젝트에서 사용하는 기술 스택을 알려줘&quot;

# 빌드 에러 해결
claude -p &quot;빌드 에러를 분석하고 수정해줘&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 원샷 명령 (세션 없이)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 시작하지 않고 단일 작업을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 인용 부호 안에 작업을 지정
claude &quot;lint 에러를 모두 수정해줘&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 이전 세션 이어서 하기&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 가장 최근 대화 이어서 하기
claude --continue
# 또는 짧게
claude -c

# 이전 대화 목록에서 선택
claude --resume
# 또는 짧게
claude -r&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5. 핵심 명령어 정리&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인터랙티브 세션 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude &quot;작업&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원샷 작업 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude -p &quot;질문&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;질문 후 즉시 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude -c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최근 대화 이어서 하기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude -r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이전 대화 선택해서 이어서 하기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude commit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Git 커밋 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;대화 컨텍스트 초기화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;컨텍스트 압축&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용 가능한 명령어 보기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모델 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 작업 중단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt; + &lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;되감기 메뉴 열기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;권한 모드 전환 (Normal &amp;rarr; Auto &amp;rarr; Plan)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exit&lt;/code&gt; 또는 &lt;code&gt;Ctrl+C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;세션 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 코드 생성 &amp;middot; 리팩토링 &amp;middot; 디버깅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. 코드 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에서 구축한 기술 스택 기반으로 실제 예시를 살펴봅니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; React에서 사용자 프로필 페이지 컴포넌트를 만들어줘.
  TypeScript + Tailwind CSS로 작성하고,
  /api/v1/users/:id에서 데이터를 fetch해서 보여줘.
  로딩 상태와 에러 상태도 처리해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;gt; Go Gin에서 /api/v1/users/:id GET 엔드포인트를 만들어줘.
  PostgreSQL에서 사용자 정보를 조회하고,
  JSON으로 응답해줘. 에러 핸들링도 포함해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; FastAPI에서 이미지 업로드 엔드포인트를 만들어줘.
  MinIO에 파일을 저장하고, 업로드된 파일의 URL을 반환해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 프로젝트의 기존 파일을 읽고 패턴을 파악한 뒤, 일관된 스타일로 코드를 생성합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 리팩토링&lt;/h3&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; src/api/handlers.go 파일이 500줄이 넘어.
  관련 기능별로 파일을 분리하고,
  공통 에러 핸들링을 미들웨어로 추출해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;&amp;gt; 콜백 패턴으로 작성된 src/services/auth.ts를
  async/await 패턴으로 리팩토링해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 디버깅&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;&amp;gt; npm run build 하면 이런 에러가 나와:
  [에러 메시지 붙여넣기]
  원인을 분석하고 수정해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 파이프로 에러 로그를 직접 전달할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;npm run build 2&amp;gt;&amp;amp;1 | claude -p &quot;이 빌드 에러의 원인을 분석하고 수정해줘&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-4. 테스트 작성&lt;/h3&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; src/utils/validation.ts에 대한 단위 테스트를 작성해줘.
  정상 케이스뿐 아니라 엣지 케이스도 포함해줘.
  기존 테스트 파일의 패턴을 따라줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 기존 테스트 파일의 프레임워크와 스타일을 분석하여 일관된 테스트를 생성합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-5. Plan Mode 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 작업은 먼저 계획을 세운 뒤 실행하는 것이 효과적입니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# Plan Mode로 시작
claude --permission-mode plan&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; OAuth2 인증 시스템을 구현하려고 해.
  현재 코드베이스를 분석하고 구현 계획을 세워줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plan Mode에서 Claude는 파일을 읽기만 하고 수정하지 않습니다. 계획이 만족스러우면 승인하고, Normal Mode로 전환하여 실행합니다. 세션 중에 &lt;code&gt;Shift+Tab&lt;/code&gt;을 두 번 누르면 Plan Mode로 전환할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. CLAUDE.md로 프로젝트 컨텍스트 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;는 Claude Code가 세션을 시작할 때 자동으로 읽는 특수 파일입니다. 프로젝트의 규칙과 관례를 여기에 적어 두면, 매번 설명할 필요 없이 Claude가 이를 따릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 프로젝트 루트에 CLAUDE.md 생성&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;# 프로젝트: MyApp

## 기술 스택
- 프론트엔드: React 19 + TypeScript + Tailwind CSS + Vite
- 백엔드 API (Go): Go 1.23 + Gin
- 백엔드 API (Python): Python 3.12 + FastAPI
- DB: PostgreSQL 16 (Docker)
- 오브젝트 스토리지: MinIO (Docker)
- 웹 서버: Caddy (프로덕션 리눅스 서버)

## 코드 스타일
- TypeScript: ES modules (import/export), 절대 경로 import 사용 (@/ 접두사)
- Go: 표준 프로젝트 레이아웃, 에러는 반드시 처리 (_ 사용 금지)
- Python: Black 포맷터, type hints 필수, docstring은 Google 스타일

## 명령어
- 프론트엔드 개발 서버: `cd frontend &amp;amp;&amp;amp; npm run dev`
- 프론트엔드 빌드: `cd frontend &amp;amp;&amp;amp; npm run build`
- Go 서버 실행: `cd backend-go &amp;amp;&amp;amp; go run cmd/server/main.go`
- Python 서버 실행: `cd backend-py &amp;amp;&amp;amp; uvicorn app.main:app --reload`
- 전체 Docker 서비스: `docker compose up -d`
- Go 테스트: `cd backend-go &amp;amp;&amp;amp; go test ./...`
- Python 테스트: `cd backend-py &amp;amp;&amp;amp; pytest`

## 규칙
- 커밋 메시지는 Conventional Commits 형식 (feat:, fix:, docs: 등)
- 새 API 엔드포인트를 만들면 반드시 테스트도 작성
- IMPORTANT: 프론트엔드에서 pnpm 사용하지 않음. 반드시 npm 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. CLAUDE.md 작성 팁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포함해야 할 것&lt;/b&gt;: Claude가 코드만 봐서는 알 수 없는 프로젝트 고유의 규칙과 명령어를 적습니다. 빌드 명령어, 테스트 실행 방법, 코드 스타일 규칙, 아키텍처 결정 사항이 대표적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제외해야 할 것&lt;/b&gt;: Claude가 코드를 읽으면 알 수 있는 것은 적지 않습니다. 파일별 설명이나 표준적인 언어 관례 같은 것은 불필요합니다. CLAUDE.md가 너무 길어지면 Claude가 중요한 지시를 놓칠 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;강조&lt;/b&gt;: 특히 중요한 규칙은 &lt;code&gt;IMPORTANT&lt;/code&gt;나 &lt;code&gt;YOU MUST&lt;/code&gt; 같은 강조 표현을 사용하면 Claude의 준수율이 높아집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. /init으로 자동 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 아직 CLAUDE.md가 없다면, Claude Code가 자동으로 분석하여 초안을 만들어 줍니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 빌드 시스템, 테스트 프레임워크, 코드 패턴을 감지하여 CLAUDE.md 초안을 생성합니다. 이를 기반으로 수정하면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Antigravity IDE와 함께 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Antigravity는 VS Code 포크이므로, Claude Code의 VS Code 확장을 그대로 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. 확장 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Antigravity에서 &lt;code&gt;Ctrl+Shift+X&lt;/code&gt;를 눌러 Extensions 뷰를 열고, &lt;b&gt;Claude Code&lt;/b&gt;를 검색하여 설치합니다. Anthropic 공식 확장(&lt;code&gt;anthropic.claude-code&lt;/code&gt;)을 선택합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. GUI 패널에서 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장을 설치하면 에디터 오른쪽에 Claude Code 패널이 나타납니다. 이 패널에서 대화형으로 Claude를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일 참조&lt;/b&gt;: 프롬프트에 &lt;code&gt;@파일명&lt;/code&gt;을 입력하면 해당 파일의 내용을 Claude에게 전달합니다. 퍼지 매칭을 지원하므로 &lt;code&gt;@auth&lt;/code&gt;라고 입력하면 &lt;code&gt;auth.js&lt;/code&gt;, &lt;code&gt;AuthService.ts&lt;/code&gt; 등을 찾아줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택 영역 참조&lt;/b&gt;: 에디터에서 코드를 선택한 상태로 Claude에게 질문하면, 선택한 코드가 자동으로 컨텍스트에 포함됩니다. &lt;code&gt;Alt+K&lt;/code&gt;를 누르면 현재 파일 경로와 줄 번호가 프롬프트에 삽입됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;권한 모드 전환&lt;/b&gt;: 프롬프트 입력창 하단의 모드 표시를 클릭하여 Normal, Plan, Auto-accept 모드를 전환할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-3. 통합 터미널에서 CLI 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GUI 패널 대신 터미널에서 CLI를 사용하고 싶다면, Antigravity의 통합 터미널(&lt;code&gt;Ctrl+``)을 열고&lt;/code&gt;claude`를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;claude&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI는 자동으로 Antigravity와 연결되어 diff 뷰어, 진단 정보 공유 등 IDE 통합 기능을 활용합니다. 외부 터미널에서 Claude Code를 실행한 경우에는 세션 안에서 &lt;code&gt;/ide&lt;/code&gt;를 입력하면 Antigravity와 연결됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-4. 추천 워크플로우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일상적인 개발에서 Antigravity + Claude Code를 함께 사용하는 패턴입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 1 &amp;mdash; GUI 패널로 대화하며 코딩&lt;/b&gt;: Antigravity 오른쪽 패널에서 Claude와 대화하며 코드를 작성합니다. 에디터에서 코드를 선택하고 &quot;이 부분을 리팩토링해줘&quot;라고 요청하면, Claude가 제안한 변경을 diff로 보여주고 승인하면 적용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 2 &amp;mdash; 터미널에서 복잡한 작업 위임&lt;/b&gt;: 통합 터미널에서 &lt;code&gt;claude&lt;/code&gt;를 실행하고, &quot;전체 인증 시스템을 구현해줘&quot;처럼 큰 작업을 위임합니다. Claude가 여러 파일을 읽고, 수정하고, 테스트까지 실행하는 동안 다른 작업을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 3 &amp;mdash; Git 워크플로우 통합&lt;/b&gt;: 작업이 끝나면 터미널에서 &lt;code&gt;claude commit&lt;/code&gt;을 실행합니다. Claude가 변경사항을 분석하여 Conventional Commits 형식의 커밋 메시지를 생성하고, PR 생성까지 도와줍니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 클라우드 리눅스 서버에서 Claude Code 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터가 이번 편의 핵심입니다. 로컬 개발뿐 아니라, &lt;b&gt;원격 서버에 SSH로 접속하여 Caddy 설정, Docker 관리, 배포 작업 등을 Claude Code에 맡길 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-1. 접근 방식: 서버에 직접 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 직관적인 방법은 클라우드 서버에 Claude Code를 직접 설치하고, SSH 세션 안에서 Claude를 실행하는 것입니다. Claude가 서버의 파일 시스템에 직접 접근하므로 Caddyfile 수정, Docker Compose 관리, 로그 확인 등을 자연어로 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-2. 서버에 Claude Code 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH로 서버에 접속합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 윈도우 PowerShell에서
ssh user@your-server-ip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 Claude Code를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Git 설치 확인 (없으면 설치)
sudo apt update &amp;amp;&amp;amp; sudo apt install -y git curl

# Claude Code 설치
curl -fsSL https://claude.ai/install.sh | bash

# 설치 확인
claude --version&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-3. tmux와 함께 사용하기 (필수)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 연결이 끊어지면 Claude Code 세션도 종료됩니다. &lt;b&gt;tmux&lt;/b&gt;를 사용하면 세션을 유지할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# tmux 설치
sudo apt install -y tmux

# 새 세션 생성
tmux new -s claude-server

# Claude Code 실행
cd /etc/caddy
claude&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 연결이 끊어져도 tmux 세션은 유지됩니다. 재접속 후 세션에 다시 붙습니다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;# SSH 재접속 후
tmux attach -t claude-server&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-4. Caddy 설정 맡기기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 Claude Code를 실행한 뒤, Caddy 관련 작업을 자연어로 요청합니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; /etc/caddy/Caddyfile을 확인하고 현재 설정을 설명해줘

&amp;gt; myapp.example.com에 대한 리버스 프록시를 추가해줘.
  Go 백엔드가 localhost:8080에서 돌아가고 있어.
  보안 헤더도 추가하고, 로그도 설정해줘.

&amp;gt; Caddyfile 문법이 맞는지 검증하고,
  문제없으면 caddy reload를 실행해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 &lt;code&gt;caddy validate&lt;/code&gt;로 문법을 검증한 뒤, &lt;code&gt;sudo systemctl reload caddy&lt;/code&gt;로 적용합니다. 변경 전에 항상 승인을 요청하므로 안전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-5. Docker 컨테이너 관리 맡기기&lt;/h3&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; docker compose ps로 현재 실행 중인 컨테이너 상태를 확인해줘

&amp;gt; PostgreSQL 컨테이너의 최근 로그를 확인해줘.
  에러가 있으면 원인을 분석해줘.

&amp;gt; docker-compose.yml에 MinIO 서비스를 추가해줘.
  9편에서 만든 설정을 기반으로 하되,
  프로덕션 환경에 맞게 리소스 제한도 설정해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-6. 서버 모니터링 &amp;amp; 트러블슈팅&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;gt; 디스크 사용량을 확인해줘. 80% 넘는 파티션이 있으면 알려줘.

&amp;gt; 최근 1시간 동안 Caddy 액세스 로그에서
  4xx, 5xx 에러를 분석해줘.

&amp;gt; systemctl status로 caddy, docker 서비스 상태를 확인하고,
  비정상인 게 있으면 알려줘.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-7. 서버용 CLAUDE.md 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 프로젝트 디렉토리(예: &lt;code&gt;/opt/myapp&lt;/code&gt;)에도 CLAUDE.md를 만들어 두면 서버 환경에 특화된 지시를 줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;sudo mkdir -p /opt/myapp
sudo tee /opt/myapp/CLAUDE.md &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'
# 서버: MyApp Production

## 환경 정보
- OS: Ubuntu 22.04 LTS
- IP: 203.0.113.10
- 도메인: myapp.example.com, go-api.example.com, py-api.example.com

## 서비스 구성
- Caddy: systemd 서비스 (/etc/caddy/Caddyfile)
- Docker Compose: /opt/myapp/docker-compose.yml
  - PostgreSQL (포트 5432)
  - MinIO (포트 9000, 9001)
  - n8n (포트 5678)
  - Go API (포트 8080)
  - Python API (포트 8000)

## 주요 명령어
- Caddy 설정 검증: `caddy validate --config /etc/caddy/Caddyfile`
- Caddy 리로드: `sudo systemctl reload caddy`
- Docker 서비스 상태: `docker compose -f /opt/myapp/docker-compose.yml ps`
- Docker 로그: `docker compose -f /opt/myapp/docker-compose.yml logs -f [서비스명]`

## 규칙
- IMPORTANT: Caddyfile 수정 전 반드시 caddy validate 실행
- IMPORTANT: docker-compose.yml 수정 시 기존 볼륨 데이터 보존 확인
- 방화벽 규칙 변경 시 반드시 사용자에게 확인 요청
- 인증서 관련 작업 시 Let's Encrypt 레이트 리밋 주의
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 이 디렉토리에서 Claude Code를 실행하면 서버 컨텍스트가 자동으로 로드됩니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd /opt/myapp
claude&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 로컬에서 SSH 명령을 Claude Code에 맡기기 (대안)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 Claude Code를 설치하지 않고, &lt;b&gt;로컬 Claude Code 세션에서 SSH 명령을 실행&lt;/b&gt;하는 방식도 있습니다. Claude Code는 Bash 도구를 통해 &lt;code&gt;ssh&lt;/code&gt; 명령을 실행할 수 있으므로, 로컬에서 원격 서버를 제어할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-1. SSH 키 기반 인증 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 비밀번호 없이 SSH에 접속할 수 있도록 키를 설정합니다. (2편에서 SSH 키를 이미 생성했다면 해당 키를 사용합니다.)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 공개 키를 서버에 복사
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh user@your-server-ip &quot;mkdir -p ~/.ssh &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 프롬프트 없이 접속되는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ssh user@your-server-ip &quot;hostname&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-2. SSH 설정 간소화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/.ssh/config&lt;/code&gt;에 서버 별칭을 등록합니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;notepad $env:USERPROFILE\.ssh\config&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Host myserver
    HostName 203.0.113.10
    User deploy
    IdentityFile ~/.ssh/id_ed25519&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;ssh myserver&lt;/code&gt;로 간단히 접속할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-3. 로컬 Claude Code에서 원격 명령 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 프로젝트에서 Claude Code를 실행한 뒤, SSH 명령을 요청합니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;&amp;gt; ssh myserver로 접속해서 Caddy 서비스 상태를 확인해줘

&amp;gt; ssh myserver에서 /etc/caddy/Caddyfile을 읽어서 보여줘.
  go-api.example.com에 CORS 헤더를 추가하고 적용해줘.

&amp;gt; ssh myserver에서 docker compose ps를 실행해서
  모든 컨테이너가 정상인지 확인해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 내부적으로 &lt;code&gt;ssh myserver &quot;caddy validate --config /etc/caddy/Caddyfile&quot;&lt;/code&gt; 같은 명령을 생성하여 실행합니다. 각 명령 실행 전에 승인을 요청하므로 안전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-4. 두 방식 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버에 직접 설치&lt;/b&gt; 방식은 Claude가 서버 파일 시스템에 직접 접근하므로 파일 읽기/쓰기가 자연스럽고, 복잡한 작업(여러 파일을 동시에 수정, 로그 분석 등)에 적합합니다. 장시간 작업 시 tmux로 세션을 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로컬에서 SSH로 원격 실행&lt;/b&gt; 방식은 서버에 별도 설치가 필요 없고, 로컬 프로젝트 작업과 서버 관리를 하나의 세션에서 전환하며 할 수 있습니다. 다만 모든 명령이 SSH를 경유하므로 파일을 한 번에 여러 개 수정하는 작업은 다소 번거롭습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 확인이나 설정 변경은 로컬 SSH 방식으로, Caddy 설정을 대폭 변경하거나 Docker Compose를 재구성하는 등의 복잡한 서버 작업은 서버에 직접 접속하여 Claude Code를 실행하는 것을 권장합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 실전 시나리오: 배포 워크플로우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 배운 내용을 종합하여, 프론트엔드 빌드 &amp;rarr; 서버 배포 &amp;rarr; Caddy 설정까지의 워크플로우를 Claude Code로 처리하는 예시입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10-1. 로컬: 프론트엔드 빌드 &amp;amp; 전송&lt;/h3&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;cd C:\Projects\my-react-app\frontend
claude&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;&amp;gt; npm run build를 실행하고,
  빌드가 성공하면 dist 폴더를 scp로 myserver:/var/www/myapp에 전송해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10-2. 서버: Caddy 설정 &amp;amp; 서비스 확인&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 서버에 SSH 접속 후
tmux attach -t claude-server
cd /opt/myapp
claude&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;&amp;gt; /var/www/myapp에 새 빌드 파일이 잘 올라왔는지 확인해줘.

&amp;gt; Caddyfile에서 myapp.example.com 설정이
  11편에서 만든 프로덕션 설정과 일치하는지 확인해줘.
  SPA 폴백, 캐시 헤더, 보안 헤더가 모두 포함되어 있어야 해.

&amp;gt; 모든 Docker 서비스가 정상 동작하는지 확인하고,
  curl로 각 엔드포인트가 응답하는지 테스트해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10-3. 검증&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;&amp;gt; 다음 항목을 순서대로 확인해줘:
  1. https://myapp.example.com 접속 시 React 앱이 로드되는지
  2. https://myapp.example.com/any/path 접속 시 SPA 폴백이 동작하는지
  3. https://go-api.example.com/health 응답이 200인지
  4. https://py-api.example.com/health 응답이 200인지
  5. SSL 인증서가 유효한지&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 유용한 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11-1. 컨텍스트 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code의 성능은 컨텍스트 윈도우 사용량에 영향을 받습니다. 관련 없는 작업을 전환할 때는 &lt;code&gt;/clear&lt;/code&gt;로 컨텍스트를 초기화하고, 긴 세션에서는 &lt;code&gt;/compact&lt;/code&gt;로 요약하여 정리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11-2. 서브에이전트 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 코드베이스를 탐색할 때는 서브에이전트를 사용하면 메인 컨텍스트를 오염시키지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;&amp;gt; 서브에이전트를 사용해서 인증 시스템의 토큰 갱신 로직을 조사해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11-3. Git Worktree로 병렬 작업&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 기능을 동시에 개발할 때 Worktree를 사용하면 각 세션이 독립된 파일 시스템을 가집니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# 기능 A를 위한 Worktree에서 Claude 시작
claude --worktree feature-auth

# 별도 터미널에서 기능 B 작업
claude --worktree bugfix-login&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11-4. 업데이트 채널 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정적인 버전을 사용하고 싶다면 stable 채널로 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// ~/.claude/settings.json
{
  &quot;autoUpdatesChannel&quot;: &quot;stable&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 확인 체크리스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;윈도우 로컬 환경&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 1. Claude Code 설치 확인
claude --version

# 2. 환경 진단
claude doctor

# 3. 인터랙티브 세션 시작 &amp;amp; 로그인 확인
cd C:\Projects\my-react-app
claude
# 세션 내에서:
#   &amp;gt; 이 프로젝트의 구조를 설명해줘
#   정상 응답 확인 후 exit&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Antigravity 확장&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Claude Code 확장 설치 완료&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 에디터 사이드 패널에서 대화 가능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;@파일명&lt;/code&gt;으로 파일 참조 동작 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 통합 터미널에서 &lt;code&gt;claude&lt;/code&gt; CLI 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라우드 리눅스 서버&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 서버 SSH 접속 후
# 1. Claude Code 설치 확인
claude --version

# 2. tmux 세션에서 실행
tmux new -s claude-server
claude

# 3. 서버 작업 테스트
#   &amp;gt; 현재 서버의 디스크 사용량과 메모리 상태를 확인해줘
#   정상 응답 확인 후 exit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;체크리스트 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 윈도우에 Claude Code 설치 완료 &amp;amp; 버전 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;claude doctor&lt;/code&gt; 진단 통과&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Claude 계정 로그인 완료&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 인터랙티브 세션에서 코드 생성 &amp;middot; 수정 동작 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Antigravity에 Claude Code 확장 설치 &amp;amp; 동작 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 프로젝트 루트에 CLAUDE.md 작성 완료&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 클라우드 서버에 Claude Code 설치 완료 (선택)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; SSH를 통한 원격 서버 작업 가능 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; tmux로 서버 세션 유지 가능 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12편에서 Claude Code를 로컬 개발과 원격 서버 관리 양쪽에 모두 활용하는 방법을 다뤘습니다. 이제 AI 어시스턴트가 코딩과 서버 운영 모두를 도와주는 환경이 갖춰졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;13편: 클라우드 서버 비교 &amp;amp; 선택 가이드&lt;/b&gt;에서는 AWS, GCP, Azure, 그리고 Vultr, Hetzner 같은 대안까지 비교하여, 이 시리즈의 기술 스택을 실제로 올릴 서버를 선택하는 기준을 제시합니다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/318</guid>
      <comments>https://polarcompass.tistory.com/318#entry318comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:30:14 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 11편: Caddy 웹 서버 &amp;amp; 리버스 프록시 (리눅스 서버 기준)</title>
      <link>https://polarcompass.tistory.com/317</link>
      <description>&lt;h1&gt;11편: Caddy 웹 서버 &amp;amp; 리버스 프록시 (리눅스 서버 기준)&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈&lt;/b&gt;: 윈도우 네이티브 개발 환경 구축부터 클라우드 배포까지 (11/14)&lt;br /&gt;&lt;b&gt;환경&lt;/b&gt;: Ubuntu 22.04+ / Debian 12+ (클라우드 리눅스 서버)&lt;br /&gt;&lt;b&gt;이전 편 전제&lt;/b&gt;: 1~10편까지 로컬 개발 환경 구축 완료&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10편까지 윈도우 로컬 환경에서 프론트엔드(React SPA), 백엔드(Go Gin, FastAPI), 데이터베이스(PostgreSQL), 오브젝트 스토리지(MinIO), 워크플로우 자동화(n8n)까지 모두 세팅했습니다. 이제 이 서비스들을 실제 사용자에게 제공할 차례입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 &lt;b&gt;클라우드 리눅스 서버&lt;/b&gt;에 Caddy를 설치하고, React SPA 정적 파일 서빙, Go Gin / FastAPI 리버스 프록시, 자동 HTTPS까지 구성합니다. Caddy는 Nginx나 Apache에 비해 설정이 극도로 간결하고, Let's Encrypt 인증서를 자동으로 발급&amp;middot;갱신해 주기 때문에 소규모~중규모 프로젝트에 특히 적합합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Caddy란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caddy는 Go로 작성된 오픈소스 웹 서버입니다. 핵심 특징은 세 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자동 HTTPS&lt;/b&gt;: 도메인을 지정하기만 하면 Let's Encrypt 인증서를 자동으로 발급하고 갱신합니다. 별도의 certbot 설치나 크론잡이 필요 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;간결한 설정&lt;/b&gt;: Nginx의 &lt;code&gt;nginx.conf&lt;/code&gt;에 비해 Caddyfile 문법이 훨씬 직관적입니다. 몇 줄이면 리버스 프록시와 정적 파일 서빙을 구성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 바이너리&lt;/b&gt;: 의존성 없이 바이너리 하나로 동작합니다. 배포와 관리가 단순합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Caddy 설치 (Ubuntu/Debian)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. apt 패키지 매니저로 설치 (권장)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 리포지토리를 등록한 뒤 apt로 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 의존성 패키지 설치
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl

# Caddy 공식 GPG 키 등록
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

# Caddy 공식 리포지토리 등록
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list

# 설치
sudo apt update
sudo apt install -y caddy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 Caddy가 자동으로 systemd 서비스로 등록됩니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# 버전 확인
caddy version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;v2.9.1 h1:...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 공식 바이너리로 직접 설치 (대안)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 버전이 필요하거나 apt를 사용하기 어려운 환경에서는 바이너리를 직접 다운로드합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 바이너리 다운로드 (amd64 기준)
curl -L &quot;https://caddyserver.com/api/download?os=linux&amp;amp;arch=amd64&quot; -o /usr/local/bin/caddy

# 실행 권한 부여
sudo chmod +x /usr/local/bin/caddy

# 버전 확인
caddy version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리로 직접 설치한 경우 systemd 서비스 파일을 수동으로 등록해야 합니다. 이 내용은 8절에서 다룹니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Caddyfile 기본 문법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caddy의 설정 파일은 &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;에 위치합니다. 문법 구조를 먼저 익혀 두겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 기본 구조&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 사이트 주소 (도메인 또는 :포트)
example.com {
    # 지시어(directive)들
    root * /var/www/html
    file_server
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caddyfile은 &lt;b&gt;사이트 블록&lt;/b&gt; 단위로 구성됩니다. 사이트 블록은 &lt;code&gt;주소 { ... }&lt;/code&gt; 형태이고, 블록 안에 지시어를 나열합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 핵심 지시어 요약&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 정적 파일 서빙
root * /var/www/html       # 루트 디렉토리 지정
file_server                 # 파일 서버 활성화

# 리버스 프록시
reverse_proxy localhost:8080

# SPA 폴백 (존재하지 않는 경로를 index.html로 전달)
try_files {path} /index.html

# 응답 헤더 설정
header {
    X-Content-Type-Options nosniff
    X-Frame-Options DENY
}

# Gzip/Zstd 압축
encode gzip zstd

# 로그
log {
    output file /var/log/caddy/access.log
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 전역 옵션 블록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 맨 위에 중괄호 없이 &lt;code&gt;{ }&lt;/code&gt; 블록을 두면 전역 설정이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;{
    # 이메일 주소 (Let's Encrypt 인증서 발급용)
    email admin@example.com

    # 개발 중 HTTPS 비활성화가 필요할 때
    # auto_https off
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. React SPA 정적 파일 서빙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React SPA를 빌드하면 &lt;code&gt;dist/&lt;/code&gt; 폴더에 정적 파일이 생성됩니다. 이를 Caddy로 서빙하는 설정입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 빌드 파일 배치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 빌드한 파일을 서버로 전송합니다. (13편에서 배포 자동화를 다루지만, 여기서는 수동 전송을 가정합니다.)&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 서버에 디렉토리 생성
sudo mkdir -p /var/www/myapp

# 로컬에서 scp로 전송 (로컬 PowerShell에서 실행)
# scp -r .\dist\* user@your-server:/var/www/myapp/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Caddyfile 설정&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;myapp.example.com {
    root * /var/www/myapp
    encode gzip zstd

    # SPA 폴백: 파일이 존재하지 않으면 index.html로
    try_files {path} /index.html

    file_server

    # 정적 자산 캐시 설정
    @static path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ico
    header @static Cache-Control &quot;public, max-age=31536000, immutable&quot;

    # index.html은 캐시하지 않음
    @html path /index.html
    header @html Cache-Control &quot;no-cache, no-store, must-revalidate&quot;

    log {
        output file /var/log/caddy/myapp-access.log
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;try_files {path} /index.html&lt;/code&gt;입니다. React Router 등 클라이언트 사이드 라우팅을 사용할 때, &lt;code&gt;/about&lt;/code&gt;이나 &lt;code&gt;/users/123&lt;/code&gt; 같은 경로로 직접 접근하면 서버에는 해당 파일이 존재하지 않습니다. 이 지시어가 해당 요청을 &lt;code&gt;index.html&lt;/code&gt;로 넘겨주고, React Router가 클라이언트에서 라우팅을 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite로 빌드하면 JS, CSS 파일명에 해시가 포함되므로(예: &lt;code&gt;index-3a7b2c.js&lt;/code&gt;) &lt;code&gt;immutable&lt;/code&gt; 캐시를 적용해도 안전합니다. 반면 &lt;code&gt;index.html&lt;/code&gt;은 항상 최신 버전을 가져와야 하므로 캐시를 비활성화합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Go Gin / FastAPI 리버스 프록시 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. 단일 백엔드 리버스 프록시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go Gin 서버가 &lt;code&gt;localhost:8080&lt;/code&gt;에서 동작한다면 다음과 같이 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;api.example.com {
    reverse_proxy localhost:8080

    log {
        output file /var/log/caddy/api-access.log
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것만으로 &lt;code&gt;https://api.example.com&lt;/code&gt;에 대한 모든 요청이 Go Gin 서버로 전달됩니다. HTTPS 인증서는 Caddy가 자동으로 처리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 경로 기반 라우팅 (하나의 도메인에서 여러 백엔드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드, Go API, Python API를 하나의 도메인에서 경로로 구분하는 구성입니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;myapp.example.com {
    # /api/v1/* &amp;rarr; Go Gin (포트 8080)
    handle_path /api/v1/* {
        reverse_proxy localhost:8080
    }

    # /api/v2/* &amp;rarr; FastAPI (포트 8000)
    handle_path /api/v2/* {
        reverse_proxy localhost:8000
    }

    # 나머지 &amp;rarr; React SPA
    handle {
        root * /var/www/myapp
        try_files {path} /index.html
        file_server
    }

    encode gzip zstd

    log {
        output file /var/log/caddy/myapp-access.log
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;handle_path&lt;/code&gt;는 매칭된 경로 접두사를 &lt;b&gt;제거&lt;/b&gt;한 뒤 백엔드로 전달합니다. 예를 들어 &lt;code&gt;/api/v1/users&lt;/code&gt; 요청은 Go Gin 서버에 &lt;code&gt;/users&lt;/code&gt;로 전달됩니다. 접두사를 유지하고 싶다면 &lt;code&gt;handle_path&lt;/code&gt; 대신 &lt;code&gt;handle&lt;/code&gt;을 사용하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 서브도메인 기반 라우팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스별로 서브도메인을 분리하는 구성입니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# React SPA
myapp.example.com {
    root * /var/www/myapp
    try_files {path} /index.html
    file_server
    encode gzip zstd
}

# Go Gin API
go-api.example.com {
    reverse_proxy localhost:8080
}

# FastAPI
py-api.example.com {
    reverse_proxy localhost:8000
}

# n8n (워크플로우 자동화)
n8n.example.com {
    reverse_proxy localhost:5678
}

# MinIO Console
minio.example.com {
    reverse_proxy localhost:9001
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서브도메인에 대해 Caddy가 개별적으로 HTTPS 인증서를 발급합니다. DNS 레코드에서 각 서브도메인이 서버 IP를 가리키도록 설정해 두어야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 자동 HTTPS (Let's Encrypt)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caddy의 자동 HTTPS는 별도 설정 없이 기본으로 활성화됩니다. Caddyfile에 도메인을 적기만 하면 다음이 자동으로 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, Let's Encrypt에 ACME 프로토콜로 인증서 발급을 요청합니다. 둘째, HTTP-01 또는 TLS-ALPN-01 챌린지로 도메인 소유권을 검증합니다. 셋째, 인증서를 받아 HTTPS를 활성화합니다. 넷째, 만료 전에 자동으로 갱신합니다. 다섯째, HTTP 요청을 HTTPS로 자동 리다이렉트합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 필수 전제 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 HTTPS가 동작하려면 두 가지 조건이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DNS 설정&lt;/b&gt;: 도메인(또는 서브도메인)의 A 레코드가 서버의 공인 IP를 가리켜야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포트 개방&lt;/b&gt;: 서버의 80(HTTP)번과 443(HTTPS)번 포트가 외부에서 접근 가능해야 합니다. 클라우드 방화벽(보안 그룹)과 OS 방화벽(ufw 등) 모두 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# ufw 방화벽 설정
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. 이메일 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let's Encrypt에서 인증서 만료 알림을 받으려면 이메일을 등록합니다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;{
    email admin@example.com
}

myapp.example.com {
    # ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-4. 스테이징 환경에서 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let's Encrypt에는 요청 횟수 제한(Rate Limit)이 있습니다. 설정을 반복적으로 테스트할 때는 스테이징 CA를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;{
    # 스테이징 CA (브라우저에서 신뢰하지 않는 인증서가 발급됨)
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 끝나면 이 줄을 제거하거나 주석 처리하여 프로덕션 CA로 전환합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 로컬 테스트용 vs 프로덕션 Caddyfile 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영에서는 로컬 테스트와 프로덕션 설정을 분리하는 것이 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. 디렉토리 구조&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;/etc/caddy/
├── Caddyfile              # 메인 설정 (프로덕션)
├── Caddyfile.dev          # 로컬 테스트용
└── conf.d/                # 사이트별 분리 (선택)
    ├── myapp.caddy
    ├── go-api.caddy
    └── py-api.caddy&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. 로컬 테스트용 Caddyfile (Caddyfile.dev)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 도메인 없이 빠르게 테스트하고 싶을 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# /etc/caddy/Caddyfile.dev
{
    # 로컬 테스트: 자동 HTTPS 비활성화
    auto_https off
}

:8080 {
    root * /var/www/myapp
    try_files {path} /index.html
    file_server
    encode gzip zstd
}

:8081 {
    reverse_proxy localhost:3000  # Go Gin
}

:8082 {
    reverse_proxy localhost:8000  # FastAPI
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# 테스트용 설정으로 Caddy 실행
sudo caddy run --config /etc/caddy/Caddyfile.dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트 번호만 사용하면(도메인 없이) Caddy는 자동 HTTPS를 시도하지 않으므로 도메인이 연결되지 않은 상태에서도 테스트할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-3. 프로덕션 Caddyfile (import로 분리)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트가 여러 개일 때는 &lt;code&gt;import&lt;/code&gt; 지시어로 설정을 분리합니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;# /etc/caddy/Caddyfile
{
    email admin@example.com
}

import /etc/caddy/conf.d/*.caddy&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# /etc/caddy/conf.d/myapp.caddy
myapp.example.com {
    root * /var/www/myapp
    try_files {path} /index.html
    file_server
    encode gzip zstd

    @static path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ico
    header @static Cache-Control &quot;public, max-age=31536000, immutable&quot;
    @html path /index.html
    header @html Cache-Control &quot;no-cache, no-store, must-revalidate&quot;

    log {
        output file /var/log/caddy/myapp-access.log
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# /etc/caddy/conf.d/go-api.caddy
go-api.example.com {
    reverse_proxy localhost:8080

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        X-XSS-Protection &quot;1; mode=block&quot;
    }

    log {
        output file /var/log/caddy/go-api-access.log
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# /etc/caddy/conf.d/py-api.caddy
py-api.example.com {
    reverse_proxy localhost:8000

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        X-XSS-Protection &quot;1; mode=block&quot;
    }

    log {
        output file /var/log/caddy/py-api-access.log
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 분리하면 사이트 추가&amp;middot;삭제 시 다른 설정에 영향을 주지 않아 관리가 편합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. systemd 서비스 등록 &amp;amp; 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-1. apt로 설치한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apt로 설치하면 systemd 서비스가 자동으로 등록됩니다. 바로 사용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 서비스 시작
sudo systemctl start caddy

# 서비스 상태 확인
sudo systemctl status caddy

# 부팅 시 자동 시작 설정
sudo systemctl enable caddy&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-2. 바이너리로 직접 설치한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd 서비스 파일을 수동으로 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# caddy 전용 사용자/그룹 생성
sudo groupadd --system caddy
sudo useradd --system \
    --gid caddy \
    --create-home \
    --home-dir /var/lib/caddy \
    --shell /usr/sbin/nologin \
    caddy&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 서비스 파일 생성
sudo tee /etc/systemd/system/caddy.service &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 설정 디렉토리 생성 &amp;amp; 권한 설정
sudo mkdir -p /etc/caddy
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy

# systemd 데몬 리로드 &amp;amp; 서비스 시작
sudo systemctl daemon-reload
sudo systemctl enable --now caddy&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-3. 일상적인 관리 명령어&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# Caddyfile 문법 검증 (서비스 재시작 전에 반드시 실행)
caddy validate --config /etc/caddy/Caddyfile

# 설정 변경 후 무중단 리로드 (서비스 재시작 아님, 다운타임 없음)
sudo systemctl reload caddy

# 서비스 재시작 (설정 리로드로 해결이 안 될 때)
sudo systemctl restart caddy

# 로그 확인 (실시간)
sudo journalctl -u caddy -f

# 로그 확인 (최근 100줄)
sudo journalctl -u caddy -n 100 --no-pager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;reload&lt;/code&gt;와 &lt;code&gt;restart&lt;/code&gt;의 차이가 중요합니다. &lt;code&gt;reload&lt;/code&gt;는 Caddy 프로세스를 유지한 채 설정만 교체하므로 다운타임이 없습니다. 일반적인 Caddyfile 수정 후에는 항상 &lt;code&gt;reload&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Docker 컨테이너로 Caddy 구동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd 서비스 대신 Docker로 Caddy를 운영할 수도 있습니다. 14편에서 다룰 통합 Docker Compose에 Caddy를 포함시키고 싶을 때 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-1. 디렉토리 구조&lt;/h3&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;~/caddy-docker/
├── docker-compose.yml
├── Caddyfile
├── site/               # React SPA 빌드 파일
│   └── index.html
├── caddy_data/         # 인증서 저장 (자동 생성)
└── caddy_config/       # Caddy 내부 설정 (자동 생성)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-2. Caddyfile&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;{
    email admin@example.com
}

myapp.example.com {
    root * /srv/site
    try_files {path} /index.html
    file_server
    encode gzip zstd
}

go-api.example.com {
    # Docker 네트워크에서는 컨테이너 이름으로 접근
    reverse_proxy go-app:8080
}

py-api.example.com {
    reverse_proxy py-app:8000
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 컨테이너 환경에서 리버스 프록시 대상을 지정할 때는 &lt;code&gt;localhost&lt;/code&gt; 대신 &lt;b&gt;컨테이너 이름&lt;/b&gt;을 사용합니다. 같은 Docker 네트워크 안에 있으면 컨테이너 이름이 DNS처럼 동작합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-3. docker-compose.yml&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
      - &quot;443:443/udp&quot;   # HTTP/3 (QUIC) 지원
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./site:/srv/site:ro
      - ./caddy_data:/data
      - ./caddy_config:/config
    networks:
      - app-network

  # 예시: Go Gin 앱 (이미지가 빌드되어 있다고 가정)
  go-app:
    image: my-go-app:latest
    container_name: go-app
    restart: unless-stopped
    expose:
      - &quot;8080&quot;
    networks:
      - app-network

  # 예시: FastAPI 앱
  py-app:
    image: my-py-app:latest
    container_name: py-app
    restart: unless-stopped
    expose:
      - &quot;8000&quot;
    networks:
      - app-network

networks:
  app-network:
    driver: bridge&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ports&lt;/code&gt;와 &lt;code&gt;expose&lt;/code&gt;의 차이에 주의합니다. Caddy만 외부 포트(80, 443)를 열고, 백엔드 앱들은 &lt;code&gt;expose&lt;/code&gt;로 Docker 네트워크 내부에서만 접근 가능하게 합니다. 이렇게 하면 백엔드에 직접 접근하는 것을 방지할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-4. 실행 &amp;amp; 관리&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 실행
docker compose up -d

# Caddyfile 수정 후 리로드 (컨테이너 재시작 없이)
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

# 로그 확인
docker compose logs -f caddy

# 중지
docker compose down&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-5. systemd vs Docker, 어떤 방식을 선택할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd 서비스 방식은 서버에 직접 Caddy를 설치하여 운영하는 전통적인 방식입니다. 서버에 이미 다양한 서비스가 systemd로 관리되고 있거나, 백엔드 앱을 Docker 없이 직접 실행하는 환경에 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 방식은 Caddy와 백엔드 앱을 모두 컨테이너로 통일하여 관리합니다. &lt;code&gt;docker compose up -d&lt;/code&gt; 한 번으로 전체 스택을 띄울 수 있어 재현성이 높고, 서버 이전도 간편합니다. 이 시리즈에서 PostgreSQL, MinIO, n8n을 이미 Docker로 운영하고 있으므로, Caddy도 Docker로 통합하면 14편의 통합 Docker Compose와 자연스럽게 연결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식 모두 프로덕션에서 문제없이 사용할 수 있으니, 프로젝트 상황에 맞게 선택하면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 프로덕션 Caddyfile 전체 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 내용을 종합한 프로덕션 Caddyfile입니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# /etc/caddy/Caddyfile
{
    email admin@example.com
}

# ─── React SPA ───
myapp.example.com {
    root * /var/www/myapp
    encode gzip zstd

    try_files {path} /index.html
    file_server

    # 해시가 포함된 정적 자산: 장기 캐시
    @static path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ico
    header @static Cache-Control &quot;public, max-age=31536000, immutable&quot;

    # HTML: 캐시 없음
    @html path /index.html
    header @html Cache-Control &quot;no-cache, no-store, must-revalidate&quot;

    # 보안 헤더
    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
    }

    log {
        output file /var/log/caddy/myapp-access.log {
            roll_size 100mb
            roll_keep 5
        }
    }
}

# ─── Go Gin API ───
go-api.example.com {
    reverse_proxy localhost:8080

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
    }

    log {
        output file /var/log/caddy/go-api-access.log {
            roll_size 100mb
            roll_keep 5
        }
    }
}

# ─── FastAPI ───
py-api.example.com {
    reverse_proxy localhost:8000

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
    }

    log {
        output file /var/log/caddy/py-api-access.log {
            roll_size 100mb
            roll_keep 5
        }
    }
}

# ─── n8n ───
n8n.example.com {
    reverse_proxy localhost:5678
}

# ─── MinIO Console ───
minio.example.com {
    reverse_proxy localhost:9001
}

# ─── MinIO S3 API ───
s3.example.com {
    reverse_proxy localhost:9000
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 설정에서 &lt;code&gt;roll_size&lt;/code&gt;와 &lt;code&gt;roll_keep&lt;/code&gt;은 로그 파일 로테이션을 의미합니다. 100MB에 도달하면 새 파일을 생성하고, 최대 5개 파일을 유지합니다. 디스크 용량 관리에 유용합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 확인 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caddy 설정이 완료되었으면 아래 항목을 하나씩 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 1. Caddy 버전 확인
caddy version

# 2. Caddyfile 문법 검증
caddy validate --config /etc/caddy/Caddyfile

# 3. Caddy 서비스 상태 확인
sudo systemctl status caddy

# 4. 80, 443 포트 리스닝 확인
sudo ss -tlnp | grep -E ':80|:443'

# 5. 방화벽 규칙 확인
sudo ufw status

# 6. HTTPS 인증서 확인 (도메인이 연결된 경우)
curl -I https://myapp.example.com
# HTTP/2 200 을 확인

# 7. SPA 폴백 테스트 (존재하지 않는 경로)
curl -s -o /dev/null -w &quot;%{http_code}&quot; https://myapp.example.com/any/random/path
# 200 이 출력되면 정상

# 8. 리버스 프록시 테스트
curl -I https://go-api.example.com/health
curl -I https://py-api.example.com/health

# 9. 로그 파일 생성 확인
ls -la /var/log/caddy/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;체크리스트 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Caddy 설치 완료 &amp;amp; 버전 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Caddyfile 문법 검증 통과&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; systemd 서비스 활성화 &amp;amp; 정상 동작&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 방화벽에서 80/443 포트 개방&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; DNS A 레코드가 서버 IP를 가리킴&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; HTTPS 자동 인증서 발급 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; React SPA 정적 파일 서빙 정상 동작&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; SPA 폴백(try_files) 정상 동작&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Go Gin 리버스 프록시 정상 동작&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; FastAPI 리버스 프록시 정상 동작&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 로그 파일 정상 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11편에서 클라우드 리눅스 서버에 Caddy를 구성하여 프론트엔드 서빙과 백엔드 리버스 프록시, 자동 HTTPS까지 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;12편: Claude Code 설치 &amp;amp; 활용&lt;/b&gt;에서는 다시 윈도우 로컬 환경으로 돌아와서, AI 코딩 어시스턴트인 Claude Code를 설치하고 프로젝트에 연동합니다. 터미널에서 자연어로 코드를 생성하고, 리팩토링하고, 디버깅하는 워크플로우를 다룹니다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/317</guid>
      <comments>https://polarcompass.tistory.com/317#entry317comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:29:34 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 10편: n8n 워크플로우 자동화 설정</title>
      <link>https://polarcompass.tistory.com/316</link>
      <description>&lt;h1&gt;10편: n8n 워크플로우 자동화 설정&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈&lt;/b&gt;: 윈도우 네이티브 개발 환경 구축 A to Z &amp;mdash; 새 PC부터 클라우드 배포까지&lt;br /&gt;&lt;b&gt;이전 편&lt;/b&gt;: 9편에서 Docker Compose에 MinIO를 추가하고, 웹 콘솔에서 버킷과 Access Key를 생성한 뒤, Go&amp;middot;Python&amp;middot;JavaScript 세 가지 언어에서 파일 업로드&amp;middot;다운로드를 확인했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 개발하다 보면 &quot;어떤 이벤트가 발생하면 자동으로 무언가를 처리해야 하는&quot; 상황이 반복됩니다. 사용자가 회원가입하면 환영 이메일을 보내고, 파일이 업로드되면 썸네일을 생성하고, 매일 자정에 데이터베이스에서 통계를 뽑아 슬랙으로 전송하는 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 자동화 로직을 백엔드 코드 안에 직접 넣을 수도 있지만, 그러면 자동화 흐름을 바꿀 때마다 코드를 수정하고 배포해야 합니다. n8n은 이런 자동화 워크플로우를 &lt;b&gt;코드 없이 웹 UI에서 노드를 연결하는 방식&lt;/b&gt;으로 만들 수 있게 해 주는 오픈소스 도구입니다. Zapier나 Make(Integromat)와 비슷하지만, 셀프호스팅이 가능해서 데이터가 외부 서비스를 거치지 않고 우리 인프라 안에서 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 Docker Compose에 n8n을 추가하고, 웹 UI에서 기본 워크플로우를 만들어 보고, Webhook 트리거로 외부 요청을 받아 처리하는 흐름을 설정한 뒤, 8편의 PostgreSQL과 9편의 MinIO를 n8n에서 연동하는 것까지 다룹니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Docker Compose에 n8n 추가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. docker-compose.yml 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/dev-infra/docker-compose.yml&lt;/code&gt;을 열고 n8n 서비스를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# ~/dev-infra/docker-compose.yml

services:
  postgres:
    image: postgres:17
    container_name: dev-postgres
    restart: unless-stopped
    ports:
      - &quot;5432:5432&quot;
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpass123
      POSTGRES_DB: devdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./initdb:/docker-entrypoint-initdb.d

  minio:
    image: minio/minio:latest
    container_name: dev-minio
    restart: unless-stopped
    ports:
      - &quot;9000:9000&quot;
      - &quot;9001:9001&quot;
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin123
    volumes:
      - minio_data:/data
    command: server /data --console-address &quot;:9001&quot;

  n8n:
    image: n8nio/n8n:latest
    container_name: dev-n8n
    restart: unless-stopped
    ports:
      - &quot;5678:5678&quot;
    environment:
      - N8N_HOST=localhost
      - N8N_PORT=5678
      - N8N_PROTOCOL=http
      - WEBHOOK_URL=http://localhost:5678/
      - N8N_RUNNERS_ENABLED=true
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8ndb
      - DB_POSTGRESDB_USER=devuser
      - DB_POSTGRESDB_PASSWORD=devpass123
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      - postgres

volumes:
  postgres_data:
  minio_data:
  n8n_data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 서비스의 각 설정을 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ports: &quot;5678:5678&quot;&lt;/code&gt;은 n8n 웹 UI와 Webhook 엔드포인트가 모두 이 포트를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WEBHOOK_URL=http://localhost:5678/&lt;/code&gt;은 n8n이 Webhook URL을 생성할 때 사용하는 기본 주소입니다. 로컬 개발 환경이므로 &lt;code&gt;localhost&lt;/code&gt;로 설정합니다. 프로덕션에서는 실제 도메인으로 바꿔야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;N8N_RUNNERS_ENABLED=true&lt;/code&gt;는 n8n의 Task Runner를 활성화합니다. Code 노드 등에서 JavaScript/Python 코드를 실행할 때 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DB_TYPE=postgresdb&lt;/code&gt;와 그 아래 설정들은 n8n의 내부 데이터(워크플로우 정의, 실행 이력, 자격 증명 등)를 PostgreSQL에 저장하도록 지정합니다. 기본값은 SQLite인데, 데이터 안정성과 백업 편의를 위해 PostgreSQL을 사용하는 것이 낫습니다. &lt;code&gt;DB_POSTGRESDB_HOST&lt;/code&gt;가 &lt;code&gt;postgres&lt;/code&gt;인 것에 주목하세요. 이것은 같은 Compose 네트워크 안에 있는 PostgreSQL 서비스의 이름입니다. Docker Compose는 서비스 이름으로 자동 DNS 해석을 해 주기 때문에, 컨테이너끼리 통신할 때는 &lt;code&gt;localhost&lt;/code&gt; 대신 서비스 이름을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DB_POSTGRESDB_DATABASE=n8ndb&lt;/code&gt;는 n8n 전용 데이터베이스입니다. 8편에서 만든 &lt;code&gt;devdb&lt;/code&gt;와 분리해서 n8n 내부 데이터와 애플리케이션 데이터가 섞이지 않도록 합니다. 이 데이터베이스는 아직 존재하지 않으므로 먼저 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;n8n_data:/home/node/.n8n&lt;/code&gt;은 n8n의 암호화 키, 설정 파일 등을 볼륨에 저장합니다. 이 볼륨을 잃으면 저장된 자격 증명(Credentials)을 복호화할 수 없게 되므로 주의하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;depends_on: postgres&lt;/code&gt;는 PostgreSQL 컨테이너가 먼저 시작된 뒤 n8n이 시작되도록 순서를 지정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. n8n 전용 데이터베이스 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n이 사용할 &lt;code&gt;n8ndb&lt;/code&gt; 데이터베이스를 PostgreSQL에 만들어야 합니다. 초기화 SQL 파일을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/dev-infra/initdb/02_n8n_db.sql&lt;/code&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- ~/dev-infra/initdb/02_n8n_db.sql

CREATE DATABASE n8ndb OWNER devuser;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker-entrypoint-initdb.d&lt;/code&gt;의 스크립트는 파일명 알파벳 순으로 실행되므로, &lt;code&gt;01_init.sql&lt;/code&gt; &amp;rarr; &lt;code&gt;02_n8n_db.sql&lt;/code&gt; 순서로 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 PostgreSQL 볼륨이 존재하는 상태에서는 이 스크립트가 자동 실행되지 않습니다(8편에서 설명한 것처럼 초기화 스크립트는 최초 실행 시에만 동작합니다). 기존 볼륨이 있는 경우에는 수동으로 데이터베이스를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;docker exec -it dev-postgres psql -U devuser -d devdb -c &quot;CREATE DATABASE n8ndb OWNER devuser;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE DATABASE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재한다는 에러가 나오면 무시해도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. 컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd ~/dev-infra
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;[+] Running 4/4
 ✔ Volume &quot;dev-infra_n8n_data&quot;  Created
 ✔ Container dev-postgres       Running
 ✔ Container dev-minio          Running
 ✔ Container dev-n8n            Started&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 로그를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;docker logs dev-n8n&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그에서 아래와 비슷한 메시지가 나타나면 정상입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;n8n ready on 0.0.0.0, port 5678
Editor is now accessible via:
http://localhost:5678&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n은 첫 시작 시 PostgreSQL에 필요한 테이블을 자동으로 생성합니다. 별도의 마이그레이션 작업이 필요 없습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 웹 UI 접속 &amp;amp; 초기 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;code&gt;http://localhost:5678&lt;/code&gt;을 엽니다. 최초 접속 시 관리자 계정 생성 화면이 나타납니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Email:      admin@example.com
First Name: Admin
Last Name:  User
Password:   (원하는 비밀번호 입력)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력을 마치면 n8n 대시보드가 나타납니다. 왼쪽 사이드바에서 워크플로우 목록, 자격 증명, 실행 이력 등을 탐색할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 기본 워크플로우 만들기 &amp;mdash; Manual Trigger&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n의 동작 방식을 이해하기 위해 가장 간단한 워크플로우를 하나 만들어 보겠습니다. 버튼을 누르면 현재 시각을 반환하는 워크플로우입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 워크플로우 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우측 상단의 &lt;b&gt;Add workflow&lt;/b&gt; 버튼을 클릭합니다. 새 캔버스가 열리고 기본 트리거 노드가 배치돼 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Manual Trigger 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스에 이미 있는 트리거 노드를 클릭하면 &lt;b&gt;Manual Trigger&lt;/b&gt;가 설정되어 있습니다. 이 트리거는 사용자가 직접 &quot;Test workflow&quot; 버튼을 누를 때 실행됩니다. 그대로 두겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. Code 노드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스에서 Manual Trigger 노드 오른쪽의 &lt;b&gt;+&lt;/b&gt; 버튼을 클릭합니다. 노드 검색창이 나타나면 &lt;code&gt;Code&lt;/code&gt;를 검색하고 &lt;b&gt;Code&lt;/b&gt; 노드를 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Code 노드의 편집 패널이 열리면 Language가 &lt;b&gt;JavaScript&lt;/b&gt;로 되어 있는지 확인하고, 코드 영역에 아래 내용을 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const now = new Date().toISOString();

return [
  {
    json: {
      message: &quot;Hello from n8n!&quot;,
      timestamp: now,
    },
  },
];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n의 Code 노드에서 반환하는 데이터는 &lt;code&gt;[{ json: { ... } }]&lt;/code&gt; 형태의 배열이어야 합니다. 각 배열 원소가 하나의 아이템(item)이 되어 다음 노드로 전달됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. 테스트 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스 하단의 &lt;b&gt;Test workflow&lt;/b&gt; 버튼을 클릭합니다. Manual Trigger &amp;rarr; Code 노드가 순서대로 실행되고, Code 노드 위에 초록색 체크 표시가 나타나면 성공입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Code 노드를 클릭하면 Output 탭에서 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;message&quot;: &quot;Hello from n8n!&quot;,
    &quot;timestamp&quot;: &quot;2026-03-31T12:00:00.000Z&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5. 워크플로우 저장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우 이름을 &lt;code&gt;Hello World&lt;/code&gt;로 바꾸고 &lt;code&gt;Ctrl+S&lt;/code&gt;로 저장합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Webhook 트리거 &amp;mdash; 외부 요청 받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Manual Trigger는 테스트용으로 편리하지만, 실제 운영에서는 외부 이벤트에 반응하는 트리거가 필요합니다. 가장 범용적인 것이 &lt;b&gt;Webhook&lt;/b&gt; 트리거입니다. 특정 URL로 HTTP 요청이 들어오면 워크플로우가 자동으로 실행됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 새 워크플로우 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드에서 &lt;b&gt;Add workflow&lt;/b&gt;를 클릭합니다. 워크플로우 이름을 &lt;code&gt;Webhook Echo&lt;/code&gt;로 설정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Webhook 트리거 노드 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스의 기본 트리거 노드를 클릭하고, 트리거 유형을 &lt;b&gt;Webhook&lt;/b&gt;으로 변경합니다. (또는 기본 트리거를 삭제하고 새로 Webhook 노드를 추가해도 됩니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook 노드의 설정에서 다음을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;HTTP Method: POST
Path:        echo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Path를 &lt;code&gt;echo&lt;/code&gt;로 입력하면 Webhook URL은 아래와 같이 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;테스트용: http://localhost:5678/webhook-test/echo
프로덕션용: http://localhost:5678/webhook/echo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트용 URL&lt;/b&gt;은 워크플로우가 비활성 상태에서도 &quot;Test workflow&quot;로 테스트할 때 사용합니다. &lt;b&gt;프로덕션용 URL&lt;/b&gt;은 워크플로우를 Active로 전환한 뒤 실제로 동작하는 URL입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. Respond to Webhook 노드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook 노드의 &lt;b&gt;+&lt;/b&gt; 버튼을 누르고 &lt;code&gt;Respond to Webhook&lt;/code&gt;을 검색해서 추가합니다. 이 노드는 Webhook 요청에 대한 HTTP 응답을 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Respond to Webhook 노드 설정에서 &lt;b&gt;Respond With&lt;/b&gt;를 &lt;code&gt;All Incoming Items&lt;/code&gt;로 선택합니다. 이렇게 하면 Webhook으로 들어온 요청 데이터를 그대로 응답으로 돌려보냅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 사이에 Code 노드 삽입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook과 Respond to Webhook 사이에 Code 노드를 추가해서 데이터를 가공해 보겠습니다. Webhook 노드와 Respond to Webhook 노드 사이의 연결선 위에 있는 &lt;b&gt;+&lt;/b&gt; 버튼을 클릭하고 Code 노드를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Code 노드에 아래 내용을 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;const items = $input.all();

return items.map((item) =&amp;gt; {
  return {
    json: {
      received: item.json.body,
      processed_at: new Date().toISOString(),
      message: &quot;Webhook received successfully!&quot;,
    },
  };
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;$input.all()&lt;/code&gt;은 이전 노드에서 넘어온 모든 아이템을 가져옵니다. Webhook 노드의 경우 HTTP 요청의 body, headers, query 등이 담겨 있습니다. 여기서는 body만 꺼내서 처리 시각과 메시지를 추가한 뒤 다음 노드로 넘깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 노드 연결 순서는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Webhook &amp;rarr; Code &amp;rarr; Respond to Webhook&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5. 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스 하단의 &lt;b&gt;Test workflow&lt;/b&gt; 버튼을 클릭합니다. n8n이 테스트용 Webhook URL에서 요청을 기다리는 상태가 됩니다. 화면에 &quot;Waiting for test event&quot; 메시지가 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 PowerShell 탭에서 요청을 보냅니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;curl -X POST http://localhost:5678/webhook-test/echo `
  -H &quot;Content-Type: application/json&quot; `
  -d '{&quot;name&quot;: &quot;Alice&quot;, &quot;action&quot;: &quot;signup&quot;}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답이 돌아옵니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;received&quot;: {
      &quot;name&quot;: &quot;Alice&quot;,
      &quot;action&quot;: &quot;signup&quot;
    },
    &quot;processed_at&quot;: &quot;2026-03-31T12:05:00.000Z&quot;,
    &quot;message&quot;: &quot;Webhook received successfully!&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 캔버스에서도 각 노드에 초록색 체크가 표시되고, 각 노드를 클릭하면 입력&amp;middot;출력 데이터를 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-6. 워크플로우 활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 성공했으면 워크플로우를 저장하고, 우측 상단의 &lt;b&gt;Inactive&lt;/b&gt; 토글을 클릭해서 &lt;b&gt;Active&lt;/b&gt;로 전환합니다. 이제 프로덕션용 URL(&lt;code&gt;http://localhost:5678/webhook/echo&lt;/code&gt;)로 요청을 보내면 워크플로우가 자동 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;curl -X POST http://localhost:5678/webhook/echo `
  -H &quot;Content-Type: application/json&quot; `
  -d '{&quot;name&quot;: &quot;Bob&quot;, &quot;action&quot;: &quot;login&quot;}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성화된 워크플로우는 n8n 컨테이너가 실행 중인 한 계속 동작합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. PostgreSQL 연동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n에서 8편의 PostgreSQL에 직접 연결하면 워크플로우 안에서 데이터를 조회하거나, 새 레코드를 삽입하거나, 특정 조건에 맞는 데이터를 가져와서 다른 노드로 넘기는 것이 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. Credential 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n에서 외부 서비스에 연결할 때는 &lt;b&gt;Credential&lt;/b&gt;(자격 증명)을 먼저 등록합니다. 워크플로우와 분리되어 관리되므로, 여러 워크플로우에서 같은 Credential을 재사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 사이드바에서 &lt;b&gt;Credentials&lt;/b&gt;를 클릭하고 &lt;b&gt;Add Credential&lt;/b&gt;을 클릭합니다. 검색창에서 &lt;b&gt;Postgres&lt;/b&gt;를 찾아 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 정보를 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Host:     postgres
Port:     5432
Database: devdb
User:     devuser
Password: devpass123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Host가 &lt;code&gt;localhost&lt;/code&gt;가 아닌 &lt;code&gt;postgres&lt;/code&gt;인 점에 주목하세요. n8n 컨테이너는 Docker Compose 네트워크 안에서 실행되므로, 같은 네트워크의 PostgreSQL 컨테이너에 접근할 때는 서비스 이름인 &lt;code&gt;postgres&lt;/code&gt;를 사용합니다. &lt;code&gt;localhost&lt;/code&gt;를 입력하면 n8n 컨테이너 자기 자신을 가리키게 되어 연결에 실패합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test Connection&lt;/b&gt;을 클릭합니다. &quot;Connection tested successfully&quot;가 나타나면 &lt;b&gt;Save&lt;/b&gt;를 클릭합니다. Credential 이름은 &lt;code&gt;Dev PostgreSQL&lt;/code&gt; 등 알아보기 쉬운 이름으로 바꿔 두면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. PostgreSQL 조회 워크플로우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 워크플로우를 만들고 이름을 &lt;code&gt;Webhook &amp;rarr; PostgreSQL 조회&lt;/code&gt;로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Webhook 노드&lt;/b&gt;를 추가합니다. HTTP Method는 &lt;code&gt;GET&lt;/code&gt;, Path는 &lt;code&gt;users&lt;/code&gt;로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Postgres 노드&lt;/b&gt;를 추가합니다. Webhook 노드 오른쪽에 연결합니다. 설정은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Credential:  Dev PostgreSQL (방금 만든 것)
Operation:   Execute Query
Query:       SELECT id, username, email, created_at FROM users ORDER BY id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Respond to Webhook 노드&lt;/b&gt;를 추가합니다. Postgres 노드 오른쪽에 연결하고, Respond With를 &lt;code&gt;All Incoming Items&lt;/code&gt;로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 노드 연결 순서는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Webhook (GET /users) &amp;rarr; Postgres &amp;rarr; Respond to Webhook&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test workflow&lt;/b&gt;를 클릭한 뒤, PowerShell에서 요청을 보냅니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl http://localhost:5678/webhook-test/users&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;id&quot;: 1,
    &quot;username&quot;: &quot;alice&quot;,
    &quot;email&quot;: &quot;alice@example.com&quot;,
    &quot;created_at&quot;: &quot;2026-03-31T12:00:00.000+00:00&quot;
  },
  {
    &quot;id&quot;: 2,
    &quot;username&quot;: &quot;bob&quot;,
    &quot;email&quot;: &quot;bob@example.com&quot;,
    &quot;created_at&quot;: &quot;2026-03-31T12:00:00.000+00:00&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n이 PostgreSQL에서 데이터를 조회해서 Webhook 응답으로 반환하는 것을 확인했습니다. 워크플로우를 저장하고 Active로 전환합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-4. 데이터 삽입 워크플로우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회뿐 아니라 삽입도 해 보겠습니다. 새 워크플로우를 만들고 이름을 &lt;code&gt;Webhook &amp;rarr; PostgreSQL 삽입&lt;/code&gt;으로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Webhook 노드&lt;/b&gt;: HTTP Method &lt;code&gt;POST&lt;/code&gt;, Path &lt;code&gt;users/create&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Postgres 노드&lt;/b&gt;: 설정은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Credential:  Dev PostgreSQL
Operation:   Execute Query
Query:       INSERT INTO users (username, email)
             VALUES ($1, $2)
             RETURNING id, username, email, created_at&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query Parameters(Values to Send) 설정에서 &lt;b&gt;Add Value&lt;/b&gt;를 두 번 클릭해서 파라미터를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;1: {{ $json.body.username }}
2: {{ $json.body.email }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n에서 &lt;code&gt;{{ }}&lt;/code&gt; 안에 Expression을 작성하면 이전 노드의 데이터를 동적으로 참조할 수 있습니다. &lt;code&gt;$json.body.username&lt;/code&gt;은 Webhook으로 들어온 JSON body의 &lt;code&gt;username&lt;/code&gt; 필드를 가리킵니다. &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt; 파라미터 바인딩을 사용하면 SQL 인젝션을 방지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Respond to Webhook 노드&lt;/b&gt;: Respond With &lt;code&gt;All Incoming Items&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;Webhook (POST /users/create) &amp;rarr; Postgres &amp;rarr; Respond to Webhook&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트합니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;curl -X POST http://localhost:5678/webhook-test/users/create `
  -H &quot;Content-Type: application/json&quot; `
  -d '{&quot;username&quot;: &quot;charlie&quot;, &quot;email&quot;: &quot;charlie@example.com&quot;}'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;id&quot;: 3,
    &quot;username&quot;: &quot;charlie&quot;,
    &quot;email&quot;: &quot;charlie@example.com&quot;,
    &quot;created_at&quot;: &quot;2026-03-31T12:10:00.000+00:00&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBeaver에서 &lt;code&gt;users&lt;/code&gt; 테이블을 새로고침하면 charlie가 추가된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. MinIO 연동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n에는 S3 호환 노드가 내장되어 있어 MinIO에도 그대로 연결할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. Credential 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 사이드바 &amp;rarr; &lt;b&gt;Credentials&lt;/b&gt; &amp;rarr; &lt;b&gt;Add Credential&lt;/b&gt; &amp;rarr; &lt;b&gt;S3&lt;/b&gt;를 검색합니다. 목록에서 &lt;b&gt;S3&lt;/b&gt; 를 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 정보를 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;S3 Endpoint:       http://minio:9000
Region:            us-east-1
Access Key ID:     dev-access-key
Secret Access Key: dev-secret-key-123
Force Path Style:  ON (활성화)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host가 &lt;code&gt;minio&lt;/code&gt;인 이유는 PostgreSQL과 같습니다. Docker Compose 네트워크 안에서 서비스 이름으로 접근해야 합니다. Region은 MinIO에서 실질적인 의미가 없지만 SDK가 요구하므로 &lt;code&gt;us-east-1&lt;/code&gt;을 입력합니다. &lt;b&gt;Force Path Style&lt;/b&gt;은 반드시 활성화해야 합니다. MinIO는 S3의 Virtual-hosted 스타일 URL을 사용하지 않고 Path 스타일(&lt;code&gt;http://endpoint/bucket/object&lt;/code&gt;)을 사용하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test Connection&lt;/b&gt;으로 확인한 뒤 Credential 이름을 &lt;code&gt;Dev MinIO&lt;/code&gt;로 저장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 파일 목록 조회 워크플로우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 워크플로우를 만들고 이름을 &lt;code&gt;Webhook &amp;rarr; MinIO 파일 목록&lt;/code&gt;으로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Webhook 노드&lt;/b&gt;: HTTP Method &lt;code&gt;GET&lt;/code&gt;, Path &lt;code&gt;files&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;S3 노드&lt;/b&gt;: Webhook 뒤에 연결하고 설정은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Credential: Dev MinIO
Operation:  Get Many (파일 목록 조회)
Bucket:     uploads&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Respond to Webhook 노드&lt;/b&gt;: Respond With &lt;code&gt;All Incoming Items&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Webhook (GET /files) &amp;rarr; S3 &amp;rarr; Respond to Webhook&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl http://localhost:5678/webhook-test/files&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9편에서 업로드한 파일들의 목록이 반환됩니다. 각 파일의 Key(파일명), Size, LastModified 등의 정보를 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. 파일 업로드 워크플로우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n에서 MinIO에 파일을 직접 업로드하는 워크플로우도 만들 수 있습니다. 예를 들어 PostgreSQL에서 데이터를 조회한 뒤 JSON 파일로 만들어서 MinIO에 저장하는 자동화를 구성해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 워크플로우를 만들고 이름을 &lt;code&gt;DB Export &amp;rarr; MinIO 저장&lt;/code&gt;으로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Manual Trigger&lt;/b&gt;: 테스트용으로 수동 트리거를 사용합니다. 나중에 Schedule Trigger로 바꾸면 정기 실행도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Postgres 노드&lt;/b&gt;: 전체 사용자 데이터를 조회합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Credential: Dev PostgreSQL
Operation:  Execute Query
Query:      SELECT id, username, email, created_at FROM users ORDER BY id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Code 노드&lt;/b&gt;: 조회 결과를 JSON 문자열로 변환합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const items = $input.all();
const data = items.map((item) =&amp;gt; item.json);
const jsonString = JSON.stringify(data, null, 2);
const timestamp = new Date().toISOString().replace(/[:.]/g, &quot;-&quot;);

return [
  {
    json: {
      fileName: `users_export_${timestamp}.json`,
    },
    binary: {
      data: await this.helpers.prepareBinaryData(
        Buffer.from(jsonString, &quot;utf-8&quot;),
        `users_export_${timestamp}.json`,
        &quot;application/json&quot;
      ),
    },
  },
];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n에서 파일을 다루려면 &lt;code&gt;binary&lt;/code&gt; 속성에 바이너리 데이터를 담아야 합니다. &lt;code&gt;prepareBinaryData&lt;/code&gt; 헬퍼 함수가 문자열을 n8n의 바이너리 형식으로 변환해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;S3 노드&lt;/b&gt;: MinIO에 업로드합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Credential:   Dev MinIO
Operation:    Upload
Bucket:       reports
File Name:    {{ $json.fileName }}
Binary Data:  ON (활성화)
Input Binary Field: data&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Manual Trigger &amp;rarr; Postgres &amp;rarr; Code &amp;rarr; S3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test workflow&lt;/b&gt;를 클릭하면 PostgreSQL에서 사용자 데이터를 조회하고, JSON으로 변환한 뒤, MinIO의 &lt;code&gt;reports&lt;/code&gt; 버킷에 업로드합니다. MinIO 웹 콘솔(&lt;code&gt;http://localhost:9001&lt;/code&gt;) &amp;rarr; Object Browser &amp;rarr; &lt;code&gt;reports&lt;/code&gt;에서 &lt;code&gt;users_export_2026-03-31T12-10-00-000Z.json&lt;/code&gt; 같은 파일이 생성된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 실전 활용 시나리오&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 만든 워크플로우는 기본 예제이지만, 이 패턴을 조합하면 다양한 자동화를 구현할 수 있습니다. 몇 가지 시나리오를 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정기 데이터 백업&lt;/b&gt;: Schedule Trigger를 사용해서 매일 자정에 PostgreSQL 데이터를 MinIO에 JSON으로 내보냅니다. 방금 만든 &quot;DB Export &amp;rarr; MinIO 저장&quot; 워크플로우의 트리거만 Schedule로 바꾸면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;회원가입 후처리&lt;/b&gt;: Go 또는 FastAPI 서버에서 회원가입 완료 후 n8n Webhook을 호출합니다. n8n에서 환영 이메일 발송, 슬랙 알림, 관리자 대시보드 갱신 등을 처리합니다. 백엔드 코드는 Webhook 하나만 호출하면 되고, 이후 처리 흐름은 n8n에서 자유롭게 수정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일 업로드 모니터링&lt;/b&gt;: MinIO에 파일이 업로드되면 Webhook으로 n8n에 알리고, n8n에서 파일 메타데이터를 PostgreSQL에 기록하거나 슬랙으로 알림을 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 시나리오들은 n8n의 400개 이상 내장 노드(Slack, Gmail, Google Sheets, HTTP Request 등)를 조합해서 구현합니다. 코드를 작성하지 않아도 노드를 드래그 앤 드롭으로 연결하는 것만으로 복잡한 자동화를 만들 수 있다는 것이 n8n의 가장 큰 장점입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 자주 겪는 문제와 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;n8n 시작 시 &quot;FATAL: database &quot;n8ndb&quot; does not exist&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 전용 데이터베이스가 없는 상태입니다. 1-2절에서 안내한 대로 수동으로 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;docker exec -it dev-postgres psql -U devuser -d devdb -c &quot;CREATE DATABASE n8ndb OWNER devuser;&quot;
docker restart dev-n8n&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Credential 테스트 시 &quot;connect ECONNREFUSED 127.0.0.1:5432&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host에 &lt;code&gt;localhost&lt;/code&gt;를 입력한 경우 발생합니다. Docker Compose 네트워크 안에서는 서비스 이름(&lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;minio&lt;/code&gt;)을 사용해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Webhook URL로 요청을 보내도 404가 반환되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우가 &lt;b&gt;Active&lt;/b&gt; 상태인지 확인하세요. 비활성 상태에서는 프로덕션 URL(&lt;code&gt;/webhook/...&lt;/code&gt;)이 동작하지 않습니다. 테스트 중이라면 테스트 URL(&lt;code&gt;/webhook-test/...&lt;/code&gt;)을 사용하고, &quot;Test workflow&quot; 버튼을 먼저 클릭해서 대기 상태로 만들어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;S3 노드에서 &quot;Access Denied&quot; 또는 &quot;Bucket not found&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Credential의 &lt;b&gt;Force Path Style&lt;/b&gt;이 활성화되어 있는지 확인하세요. 또한 S3 Endpoint에 프로토콜(&lt;code&gt;http://&lt;/code&gt;)을 포함했는지 확인합니다. &lt;code&gt;minio:9000&lt;/code&gt;이 아니라 &lt;code&gt;http://minio:9000&lt;/code&gt;이어야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 확인 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목을 모두 확인했다면 10편은 완료입니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;✅ docker-compose.yml에 n8n 서비스가 추가되어 있다
✅ docker compose up -d 로 PostgreSQL, MinIO, n8n이 모두 정상 기동된다
✅ 브라우저에서 http://localhost:5678 로 n8n 웹 UI에 접속할 수 있다
✅ Manual Trigger + Code 노드로 기본 워크플로우를 만들고 실행할 수 있다
✅ Webhook 트리거로 외부 HTTP 요청을 받아 처리할 수 있다
✅ PostgreSQL Credential을 등록하고 데이터 조회&amp;middot;삽입이 동작한다
✅ MinIO(S3) Credential을 등록하고 파일 목록 조회&amp;middot;업로드가 동작한다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 인프라 구성 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10편까지 완료한 시점에서 &lt;code&gt;~/dev-infra/docker-compose.yml&lt;/code&gt; 하나로 아래 세 가지 인프라 컴포넌트가 모두 관리되고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;PostgreSQL (5432)  &amp;mdash; 관계형 데이터베이스
MinIO (9000/9001)  &amp;mdash; S3 호환 오브젝트 스토리지
n8n (5678)         &amp;mdash; 워크플로우 자동화 엔진&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell에서 &lt;code&gt;docker compose up -d&lt;/code&gt; 한 줄이면 전체 인프라가 올라오고, &lt;code&gt;docker compose down&lt;/code&gt;이면 깔끔하게 내려갑니다. 이 구성이 14편에서 프로덕션용 통합 Docker Compose로 발전하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 개발 인프라가 모두 갖춰졌습니다. &lt;b&gt;11편: Caddy 웹 서버 &amp;amp; 리버스 프록시 (리눅스 서버 기준)&lt;/b&gt;에서는 무대를 클라우드 리눅스 서버로 옮깁니다. Caddy를 설치하고, 자동 HTTPS 인증서 발급을 설정하고, 리버스 프록시로 Go API, FastAPI, n8n, MinIO를 하나의 도메인 아래에서 서빙하는 구성을 다루겠습니다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/316</guid>
      <comments>https://polarcompass.tistory.com/316#entry316comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:28:38 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 9편: MinIO 컨테이너 설정</title>
      <link>https://polarcompass.tistory.com/315</link>
      <description>&lt;h1&gt;9편: MinIO 컨테이너 설정&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈&lt;/b&gt;: 윈도우 네이티브 개발 환경 구축 A to Z &amp;mdash; 새 PC부터 클라우드 배포까지&lt;br /&gt;&lt;b&gt;이전 편&lt;/b&gt;: 8편에서 Docker Compose로 PostgreSQL을 구동하고, DBeaver로 접속을 확인한 뒤, Go(Gin)와 Python(FastAPI)에서 연결 테스트까지 마쳤습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서비스를 운영하다 보면 사용자가 업로드한 이미지, 문서, 동영상 같은 비정형 파일을 저장할 곳이 필요합니다. 이런 파일을 데이터베이스에 넣는 것은 비효율적입니다. 관계형 데이터베이스는 구조화된 데이터를 다루는 데 최적화되어 있지, 수십 MB짜리 이미지 파일을 저장하고 꺼내는 데는 적합하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 사용하는 것이 &lt;b&gt;오브젝트 스토리지&lt;/b&gt;입니다. AWS S3가 대표적인데, 로컬 개발 환경에서 AWS에 연결하면 비용도 발생하고 네트워크 지연도 있습니다. MinIO는 S3와 동일한 API를 제공하는 오픈소스 오브젝트 스토리지로, Docker 컨테이너 하나로 로컬에 S3 호환 저장소를 띄울 수 있습니다. 개발 중에는 MinIO를 쓰고, 프로덕션에서는 같은 코드로 AWS S3나 Cloudflare R2 같은 서비스에 연결하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 8편에서 만든 &lt;code&gt;docker-compose.yml&lt;/code&gt;에 MinIO 서비스를 추가하고, 웹 콘솔에서 버킷과 Access Key를 생성한 뒤, Go&amp;middot;Python&amp;middot;JavaScript 세 가지 언어에서 파일을 업로드하고 다운로드하는 것까지 다룹니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Docker Compose에 MinIO 추가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. docker-compose.yml 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8편에서 만든 &lt;code&gt;~/dev-infra/docker-compose.yml&lt;/code&gt;을 열고 MinIO 서비스를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# ~/dev-infra/docker-compose.yml

services:
  postgres:
    image: postgres:17
    container_name: dev-postgres
    restart: unless-stopped
    ports:
      - &quot;5432:5432&quot;
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpass123
      POSTGRES_DB: devdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./initdb:/docker-entrypoint-initdb.d

  minio:
    image: minio/minio:latest
    container_name: dev-minio
    restart: unless-stopped
    ports:
      - &quot;9000:9000&quot;
      - &quot;9001:9001&quot;
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin123
    volumes:
      - minio_data:/data
    command: server /data --console-address &quot;:9001&quot;

volumes:
  postgres_data:
  minio_data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MinIO 서비스의 각 설정을 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;image: minio/minio:latest&lt;/code&gt;는 MinIO 공식 이미지를 사용합니다. MinIO는 릴리스 주기가 빠르고 하위 호환을 잘 유지하므로 &lt;code&gt;latest&lt;/code&gt;를 사용해도 무방합니다. 안정성이 중요하면 특정 날짜 태그(예: &lt;code&gt;RELEASE.2025-01-20T14-49-07Z&lt;/code&gt;)를 지정할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ports&lt;/code&gt;에서 9000번은 S3 호환 API 포트입니다. 애플리케이션 코드에서 파일을 업로드하거나 다운로드할 때 이 포트를 사용합니다. 9001번은 웹 관리 콘솔 포트입니다. 브라우저에서 버킷을 만들고, 파일을 확인하고, Access Key를 관리할 때 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MINIO_ROOT_USER&lt;/code&gt;와 &lt;code&gt;MINIO_ROOT_PASSWORD&lt;/code&gt;는 루트 관리자 계정입니다. 이 계정으로 콘솔에 로그인하고, 초기 설정을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;minio_data:/data&lt;/code&gt;는 업로드된 파일을 Docker Named Volume에 저장합니다. 컨테이너를 삭제해도 파일이 유지됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;command: server /data --console-address &quot;:9001&quot;&lt;/code&gt;은 MinIO 서버를 시작하면서 콘솔 포트를 9001로 고정합니다. 이 옵션을 지정하지 않으면 콘솔 포트가 랜덤으로 배정되어 매번 달라질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd ~/dev-infra
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 이미 실행 중이므로 변경 사항이 없다면 그대로 유지되고, MinIO 컨테이너만 새로 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;[+] Running 3/3
 ✔ Volume &quot;dev-infra_minio_data&quot;  Created
 ✔ Container dev-postgres         Running
 ✔ Container dev-minio            Started&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker logs dev-minio&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 비슷한 메시지가 나타나면 정상입니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;MinIO Object Storage Server
...
API: http://0.0.0.0:9000
WebUI: http://0.0.0.0:9001&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 웹 콘솔 접속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;code&gt;http://localhost:9001&lt;/code&gt;을 엽니다. 로그인 화면이 나타나면 아래 계정으로 접속합니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Username: minioadmin
Password: minioadmin123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 MinIO 관리 대시보드가 표시됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 버킷 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오브젝트 스토리지에서 &lt;b&gt;버킷(Bucket)&lt;/b&gt;은 파일을 담는 최상위 폴더입니다. S3에서도 같은 개념을 사용합니다. 용도별로 버킷을 분리하는 것이 일반적입니다. 예를 들어 사용자 업로드 이미지는 &lt;code&gt;uploads&lt;/code&gt; 버킷에, 앱에서 생성하는 리포트 파일은 &lt;code&gt;reports&lt;/code&gt; 버킷에 저장하는 식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 콘솔에서 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 사이드바에서 &lt;b&gt;Buckets&lt;/b&gt;를 클릭한 뒤 우측 상단의 &lt;b&gt;Create Bucket&lt;/b&gt; 버튼을 누릅니다. Bucket Name에 &lt;code&gt;uploads&lt;/code&gt;를 입력하고 &lt;b&gt;Create Bucket&lt;/b&gt;을 클릭합니다. 같은 방법으로 &lt;code&gt;reports&lt;/code&gt; 버킷도 하나 더 만들어 두겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. CLI로 생성하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔 대신 커맨드라인으로도 버킷을 만들 수 있습니다. MinIO 컨테이너 안에 &lt;code&gt;mc&lt;/code&gt;(MinIO Client)가 포함되어 있으므로 &lt;code&gt;docker exec&lt;/code&gt;로 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# MinIO 서버를 mc에 등록
docker exec dev-minio mc alias set local http://localhost:9000 minioadmin minioadmin123

# 버킷 생성
docker exec dev-minio mc mb local/uploads
docker exec dev-minio mc mb local/reports&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Bucket created successfully `local/uploads`.
Bucket created successfully `local/reports`.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 콘솔에서 만들었다면 &quot;Bucket already exists&quot; 메시지가 나옵니다. 무시해도 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Access Key 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 계정(&lt;code&gt;minioadmin&lt;/code&gt;)을 애플리케이션 코드에서 직접 사용하는 것은 보안상 좋지 않습니다. 권한을 제한한 별도의 Access Key를 만들어서 사용하는 것이 권장됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 콘솔에서 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 사이드바에서 &lt;b&gt;Access Keys&lt;/b&gt;를 클릭하고 &lt;b&gt;Create access key&lt;/b&gt; 버튼을 누릅니다. Access Key와 Secret Key가 자동으로 생성됩니다. 기본값을 그대로 사용해도 되고, 원하는 값을 직접 입력해도 됩니다. 이 글에서는 아래 값을 사용하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Access Key: dev-access-key
Secret Key: dev-secret-key-123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Create&lt;/b&gt;를 클릭합니다. Secret Key는 이 화면을 벗어나면 다시 확인할 수 없으므로 반드시 기록해 두세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Go(Gin)에서 MinIO 연결 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go에서 MinIO에 접속할 때는 MinIO 공식 Go SDK를 사용합니다. 이 SDK는 AWS S3 API와 호환되므로, 나중에 S3로 전환할 때도 엔드포인트만 바꾸면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. 의존성 추가&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;cd ~/go-api
go get github.com/minio/minio-go/v7&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 연결 테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main.go&lt;/code&gt;에 MinIO 관련 코드를 추가합니다. 8편에서 작성한 PostgreSQL 코드는 그대로 유지하고, MinIO 업로드&amp;middot;다운로드 엔드포인트를 추가하는 형태입니다. 여기서는 MinIO 관련 부분만 보여 드리겠습니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// main.go (MinIO 관련 부분만 발췌 &amp;mdash; 기존 코드에 추가)
package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;io&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;os&quot;
    &quot;time&quot;

    &quot;github.com/gin-gonic/gin&quot;
    &quot;github.com/jackc/pgx/v5/pgxpool&quot;
    &quot;github.com/minio/minio-go/v7&quot;
    &quot;github.com/minio/minio-go/v7/pkg/credentials&quot;
)

// ... (기존 User 구조체, db 변수, getUsers 함수는 그대로 유지)

var minioClient *minio.Client

func main() {
    // --- PostgreSQL 연결 (기존 코드 유지) ---
    dsn := os.Getenv(&quot;DATABASE_URL&quot;)
    if dsn == &quot;&quot; {
        dsn = &quot;postgres://devuser:devpass123@localhost:5432/devdb?sslmode=disable&quot;
    }
    var err error
    db, err = pgxpool.New(context.Background(), dsn)
    if err != nil {
        log.Fatalf(&quot;Unable to connect to database: %v\n&quot;, err)
    }
    defer db.Close()
    if err := db.Ping(context.Background()); err != nil {
        log.Fatalf(&quot;Unable to ping database: %v\n&quot;, err)
    }
    log.Println(&quot;Connected to PostgreSQL!&quot;)

    // --- MinIO 연결 ---
    minioClient, err = minio.New(&quot;localhost:9000&quot;, &amp;amp;minio.Options{
        Creds:  credentials.NewStaticV4(&quot;dev-access-key&quot;, &quot;dev-secret-key-123&quot;, &quot;&quot;),
        Secure: false,
    })
    if err != nil {
        log.Fatalf(&quot;Unable to connect to MinIO: %v\n&quot;, err)
    }
    log.Println(&quot;Connected to MinIO!&quot;)

    // --- Gin 라우터 ---
    r := gin.Default()

    r.GET(&quot;/&quot;, func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{&quot;message&quot;: &quot;Hello, World!&quot;})
    })
    r.GET(&quot;/users&quot;, getUsers)
    r.POST(&quot;/upload&quot;, uploadFile)
    r.GET(&quot;/files/:filename&quot;, downloadFile)

    r.Run(&quot;:8080&quot;)
}

func uploadFile(c *gin.Context) {
    file, header, err := c.Request.FormFile(&quot;file&quot;)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: &quot;file is required&quot;})
        return
    }
    defer file.Close()

    bucketName := &quot;uploads&quot;
    objectName := fmt.Sprintf(&quot;%d_%s&quot;, time.Now().UnixMilli(), header.Filename)

    info, err := minioClient.PutObject(
        context.Background(),
        bucketName,
        objectName,
        file,
        header.Size,
        minio.PutObjectOptions{ContentType: header.Header.Get(&quot;Content-Type&quot;)},
    )
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        &quot;message&quot;:  &quot;uploaded successfully&quot;,
        &quot;bucket&quot;:   info.Bucket,
        &quot;object&quot;:   objectName,
        &quot;size&quot;:     info.Size,
    })
}

func downloadFile(c *gin.Context) {
    filename := c.Param(&quot;filename&quot;)

    object, err := minioClient.GetObject(
        context.Background(),
        &quot;uploads&quot;,
        filename,
        minio.GetObjectOptions{},
    )
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: err.Error()})
        return
    }
    defer object.Close()

    stat, err := object.Stat()
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{&quot;error&quot;: &quot;file not found&quot;})
        return
    }

    c.Header(&quot;Content-Disposition&quot;, fmt.Sprintf(&quot;attachment; filename=%s&quot;, filename))
    c.Header(&quot;Content-Type&quot;, stat.ContentType)
    c.Header(&quot;Content-Length&quot;, fmt.Sprintf(&quot;%d&quot;, stat.Size))
    io.Copy(c.Writer, object)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 설명하겠습니다. &lt;code&gt;minio.New&lt;/code&gt;로 MinIO 클라이언트를 생성합니다. &lt;code&gt;Secure: false&lt;/code&gt;는 로컬 환경에서 HTTP(비암호화)를 사용한다는 뜻입니다. 프로덕션에서는 HTTPS를 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;POST /upload&lt;/code&gt; 엔드포인트는 &lt;code&gt;multipart/form-data&lt;/code&gt;로 전달된 파일을 받아서 &lt;code&gt;uploads&lt;/code&gt; 버킷에 저장합니다. 파일명 앞에 타임스탬프를 붙여서 이름 충돌을 방지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GET /files/:filename&lt;/code&gt; 엔드포인트는 &lt;code&gt;uploads&lt;/code&gt; 버킷에서 파일을 가져와서 다운로드 응답으로 보냅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 실행 &amp;amp; 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;cd ~/go-api
go run .&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;... Connected to PostgreSQL!
... Connected to MinIO!
... Listening and serving HTTP on :8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 PowerShell 탭에서 파일을 업로드해 봅니다. 테스트용 텍스트 파일을 하나 만들겠습니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;&quot;Hello MinIO from Go!&quot; | Out-File -Encoding utf8 test.txt
curl -X POST -F &quot;file=@test.txt&quot; http://localhost:8080/upload&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;message&quot;: &quot;uploaded successfully&quot;,
  &quot;bucket&quot;: &quot;uploads&quot;,
  &quot;object&quot;: &quot;1743400800000_test.txt&quot;,
  &quot;size&quot;: 23
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답에서 &lt;code&gt;object&lt;/code&gt; 값(타임스탬프가 붙은 파일명)을 복사해서 다운로드를 테스트합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -O http://localhost:8080/files/1743400800000_test.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드된 파일의 내용을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cat 1743400800000_test.txt&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Hello MinIO from Go!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MinIO 웹 콘솔(&lt;code&gt;http://localhost:9001&lt;/code&gt;)에서도 Object Browser &amp;rarr; &lt;code&gt;uploads&lt;/code&gt; 버킷으로 들어가면 업로드된 파일이 보입니다. 확인이 끝났으면 &lt;code&gt;Ctrl+C&lt;/code&gt;로 Go 서버를 종료합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Python(FastAPI)에서 MinIO 연결 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에서도 MinIO 공식 SDK인 &lt;code&gt;minio&lt;/code&gt; 패키지를 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 의존성 추가&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd ~/fastapi-app
.\.venv\Scripts\Activate.ps1
pip install minio&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 연결 테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main.py&lt;/code&gt;에 MinIO 관련 코드를 추가합니다. 8편에서 작성한 PostgreSQL 코드는 그대로 유지합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# main.py (전체)
import os
import time
from contextlib import asynccontextmanager
from datetime import datetime

import psycopg
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.responses import StreamingResponse
from minio import Minio
from pydantic import BaseModel


DATABASE_URL = os.getenv(
    &quot;DATABASE_URL&quot;,
    &quot;postgresql://devuser:devpass123@localhost:5432/devdb&quot;,
)

MINIO_ENDPOINT = os.getenv(&quot;MINIO_ENDPOINT&quot;, &quot;localhost:9000&quot;)
MINIO_ACCESS_KEY = os.getenv(&quot;MINIO_ACCESS_KEY&quot;, &quot;dev-access-key&quot;)
MINIO_SECRET_KEY = os.getenv(&quot;MINIO_SECRET_KEY&quot;, &quot;dev-secret-key-123&quot;)
MINIO_BUCKET = &quot;uploads&quot;


class User(BaseModel):
    id: int
    username: str
    email: str
    created_at: datetime


def get_connection():
    return psycopg.connect(DATABASE_URL)


minio_client = Minio(
    MINIO_ENDPOINT,
    access_key=MINIO_ACCESS_KEY,
    secret_key=MINIO_SECRET_KEY,
    secure=False,
)


@asynccontextmanager
async def lifespan(app: FastAPI):
    # PostgreSQL 연결 확인
    with get_connection() as conn:
        print(&quot;Connected to PostgreSQL!&quot;)

    # MinIO 연결 확인
    if minio_client.bucket_exists(MINIO_BUCKET):
        print(f&quot;Connected to MinIO! Bucket '{MINIO_BUCKET}' exists.&quot;)
    else:
        print(f&quot;Connected to MinIO! Bucket '{MINIO_BUCKET}' not found.&quot;)

    yield


app = FastAPI(lifespan=lifespan)


@app.get(&quot;/&quot;)
def read_root():
    return {&quot;message&quot;: &quot;Hello, World!&quot;}


@app.get(&quot;/users&quot;, response_model=list[User])
def read_users():
    try:
        with get_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(&quot;SELECT id, username, email, created_at FROM users&quot;)
                rows = cur.fetchall()
                return [
                    User(id=row[0], username=row[1], email=row[2], created_at=row[3])
                    for row in rows
                ]
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post(&quot;/upload&quot;)
async def upload_file(file: UploadFile = File(...)):
    try:
        timestamp = int(time.time() * 1000)
        object_name = f&quot;{timestamp}_{file.filename}&quot;

        content = await file.read()
        from io import BytesIO
        data = BytesIO(content)

        minio_client.put_object(
            MINIO_BUCKET,
            object_name,
            data,
            length=len(content),
            content_type=file.content_type or &quot;application/octet-stream&quot;,
        )

        return {
            &quot;message&quot;: &quot;uploaded successfully&quot;,
            &quot;bucket&quot;: MINIO_BUCKET,
            &quot;object&quot;: object_name,
            &quot;size&quot;: len(content),
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get(&quot;/files/{filename}&quot;)
def download_file(filename: str):
    try:
        response = minio_client.get_object(MINIO_BUCKET, filename)
        return StreamingResponse(
            response,
            media_type=&quot;application/octet-stream&quot;,
            headers={&quot;Content-Disposition&quot;: f&quot;attachment; filename={filename}&quot;},
        )
    except Exception as e:
        raise HTTPException(status_code=404, detail=&quot;file not found&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 코드와 동일한 패턴입니다. &lt;code&gt;POST /upload&lt;/code&gt;는 파일을 받아서 &lt;code&gt;uploads&lt;/code&gt; 버킷에 저장하고, &lt;code&gt;GET /files/{filename}&lt;/code&gt;은 버킷에서 파일을 가져와서 스트리밍 응답으로 반환합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. 실행 &amp;amp; 확인&lt;/h3&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;uvicorn main:app --reload --port 8000&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Connected to PostgreSQL!
Connected to MinIO! Bucket 'uploads' exists.
INFO:     Uvicorn running on http://127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 PowerShell 탭에서 파일을 업로드합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;&quot;Hello MinIO from Python!&quot; | Out-File -Encoding utf8 test_py.txt
curl -X POST -F &quot;file=@test_py.txt&quot; http://localhost:8000/upload&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;message&quot;: &quot;uploaded successfully&quot;,
  &quot;bucket&quot;: &quot;uploads&quot;,
  &quot;object&quot;: &quot;1743400860000_test_py.txt&quot;,
  &quot;size&quot;: 27
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드도 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;curl -O http://localhost:8000/files/1743400860000_test_py.txt
cat 1743400860000_test_py.txt&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Hello MinIO from Python!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인이 끝났으면 &lt;code&gt;Ctrl+C&lt;/code&gt;로 서버를 종료하고 &lt;code&gt;deactivate&lt;/code&gt;로 가상 환경을 빠져나옵니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. JavaScript(Node.js)에서 MinIO 연결 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드는 React SPA이므로 브라우저에서 직접 MinIO에 접속하지는 않습니다. 파일 업로드는 보통 백엔드 API를 경유합니다. 하지만 Node.js 스크립트로 MinIO를 다루어야 할 경우가 있을 수 있으므로(빌드 스크립트, 마이그레이션 도구 등) 간단히 확인해 두겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. 테스트 프로젝트 생성&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;mkdir ~/minio-js-test
cd ~/minio-js-test
npm init -y
npm install minio&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. 연결 테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;test.mjs&lt;/code&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// test.mjs
import { Client } from &quot;minio&quot;;
import { readFileSync } from &quot;fs&quot;;
import { Readable } from &quot;stream&quot;;

const minioClient = new Client({
  endPoint: &quot;localhost&quot;,
  port: 9000,
  useSSL: false,
  accessKey: &quot;dev-access-key&quot;,
  secretKey: &quot;dev-secret-key-123&quot;,
});

const BUCKET = &quot;uploads&quot;;

async function main() {
  // 버킷 존재 확인
  const exists = await minioClient.bucketExists(BUCKET);
  console.log(`Bucket '${BUCKET}' exists: ${exists}`);

  // 파일 업로드
  const content = Buffer.from(&quot;Hello MinIO from JavaScript!&quot;);
  const objectName = `${Date.now()}_test_js.txt`;

  await minioClient.putObject(BUCKET, objectName, content, content.length, {
    &quot;Content-Type&quot;: &quot;text/plain&quot;,
  });
  console.log(`Uploaded: ${objectName}`);

  // 파일 다운로드
  const stream = await minioClient.getObject(BUCKET, objectName);
  const chunks = [];
  for await (const chunk of stream) {
    chunks.push(chunk);
  }
  const downloaded = Buffer.concat(chunks).toString();
  console.log(`Downloaded content: ${downloaded}`);

  // 버킷 내 파일 목록 조회
  console.log(`\nFiles in '${BUCKET}':`);
  const objectsStream = minioClient.listObjects(BUCKET, &quot;&quot;, true);
  for await (const obj of objectsStream) {
    console.log(`  - ${obj.name} (${obj.size} bytes)`);
  }
}

main().catch(console.error);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-3. 실행 &amp;amp; 확인&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node test.mjs&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Bucket 'uploads' exists: true
Uploaded: 1743400920000_test_js.txt
Downloaded content: Hello MinIO from JavaScript!

Files in 'uploads':
  - 1743400800000_test.txt (23 bytes)
  - 1743400860000_test_py.txt (27 bytes)
  - 1743400920000_test_js.txt (28 bytes)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go, Python, JavaScript 세 곳에서 업로드한 파일이 모두 같은 &lt;code&gt;uploads&lt;/code&gt; 버킷에 들어 있는 것을 확인할 수 있습니다. MinIO 웹 콘솔에서도 Object Browser &amp;rarr; &lt;code&gt;uploads&lt;/code&gt;를 열면 세 파일이 모두 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 끝났으면 이 폴더는 삭제해도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;cd ~
Remove-Item -Recurse -Force ~/minio-js-test&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. MinIO와 S3의 관계 &amp;mdash; 프로덕션 전환 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MinIO SDK의 클라이언트 설정에서 엔드포인트와 인증 정보만 바꾸면 AWS S3나 Cloudflare R2 같은 S3 호환 서비스에 그대로 연결할 수 있습니다. 코드를 변경할 필요가 없습니다. 예를 들어 Go에서는 이렇게 바뀝니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// 로컬 MinIO
minioClient, _ = minio.New(&quot;localhost:9000&quot;, &amp;amp;minio.Options{
    Creds:  credentials.NewStaticV4(&quot;dev-access-key&quot;, &quot;dev-secret-key-123&quot;, &quot;&quot;),
    Secure: false,
})

// AWS S3
minioClient, _ = minio.New(&quot;s3.amazonaws.com&quot;, &amp;amp;minio.Options{
    Creds:  credentials.NewStaticV4(&quot;AWS_ACCESS_KEY&quot;, &quot;AWS_SECRET_KEY&quot;, &quot;&quot;),
    Secure: true,
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 환경 변수에서 엔드포인트와 키를 읽도록 코드를 작성해 두면, 로컬에서는 MinIO, 프로덕션에서는 S3를 사용하는 전환이 설정 파일 수정만으로 가능합니다. 14편에서 통합 Docker Compose를 구성할 때 이 패턴을 다시 다룰 예정입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 자주 겪는 문제와 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘솔 접속 시 &quot;Access Denied&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MINIO_ROOT_USER&lt;/code&gt;와 &lt;code&gt;MINIO_ROOT_PASSWORD&lt;/code&gt;를 정확히 입력했는지 확인하세요. 이 값을 변경하려면 &lt;code&gt;docker-compose.yml&lt;/code&gt;을 수정한 뒤 볼륨을 삭제하고 다시 시작해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;cd ~/dev-infra
docker compose down -v
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 볼륨을 삭제하면 기존에 업로드한 파일도 전부 사라지므로 주의하세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Access Key로 접속할 때 &quot;Invalid Access Key&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔에서 Access Key를 생성한 뒤 값을 정확히 복사했는지 확인하세요. 특히 앞뒤 공백이 포함되지 않았는지 주의합니다. 어떤 Access Key가 있는지 기억나지 않으면 루트 계정으로 콘솔에 로그인해서 Access Keys 메뉴에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트 충돌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9000 포트가 다른 프로그램에서 사용 중이라면 &lt;code&gt;docker-compose.yml&lt;/code&gt;에서 &lt;code&gt;&quot;9002:9000&quot;&lt;/code&gt; 같이 호스트 포트를 변경하고, 애플리케이션 코드의 엔드포인트도 &lt;code&gt;localhost:9002&lt;/code&gt;로 맞춰 주면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 확인 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목을 모두 확인했다면 9편은 완료입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;✅ docker-compose.yml에 MinIO 서비스가 추가되어 있다
✅ docker compose up -d 로 PostgreSQL과 MinIO가 함께 정상 기동된다
✅ 브라우저에서 http://localhost:9001 로 MinIO 웹 콘솔에 접속할 수 있다
✅ uploads 버킷이 생성되어 있다
✅ Access Key가 생성되어 있다
✅ Go(Gin) 서버에서 파일 업로드&amp;middot;다운로드가 동작한다
✅ Python(FastAPI) 서버에서 파일 업로드&amp;middot;다운로드가 동작한다
✅ JavaScript(Node.js)에서 파일 업로드&amp;middot;다운로드가 동작한다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스(PostgreSQL)와 파일 저장소(MinIO)가 준비되었습니다. &lt;b&gt;10편: n8n 워크플로우 자동화 설정&lt;/b&gt;에서는 Docker Compose에 n8n을 추가하고, 웹 UI에서 워크플로우를 만들어 봅니다. Webhook으로 외부 요청을 받아 처리하는 트리거를 설정하고, 8편의 PostgreSQL과 9편의 MinIO를 n8n에서 연동하는 것까지 다루겠습니다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/315</guid>
      <comments>https://polarcompass.tistory.com/315#entry315comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:28:11 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 8편: PostgreSQL 컨테이너 설정</title>
      <link>https://polarcompass.tistory.com/314</link>
      <description>&lt;h1&gt;8편: PostgreSQL 컨테이너 설정&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈&lt;/b&gt;: 윈도우 네이티브 개발 환경 구축 A to Z &amp;mdash; 새 PC부터 클라우드 배포까지&lt;br /&gt;&lt;b&gt;이전 편&lt;/b&gt;: 7편에서 Docker Desktop을 설치하고, 이미지&amp;middot;컨테이너&amp;middot;볼륨 기본 명령어와 Docker Compose 사용법을 익혔습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 서버가 아무리 잘 돌아가도 데이터를 저장할 곳이 없으면 의미가 없습니다. 사용자 정보, 게시글, 설정값 같은 구조화된 데이터를 안정적으로 저장하고 조회하려면 관계형 데이터베이스가 필요합니다. 이 시리즈에서는 PostgreSQL을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL을 윈도우에 직접 설치할 수도 있지만, 7편에서 익힌 Docker Compose를 활용하면 훨씬 깔끔합니다. &lt;code&gt;docker-compose.yml&lt;/code&gt; 한 줄 수정으로 버전을 바꿀 수 있고, 프로젝트마다 독립된 DB 인스턴스를 띄울 수 있으며, 안 쓰면 컨테이너를 내려서 시스템 리소스를 아낄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 Docker Compose로 PostgreSQL을 구동하고, 초기 데이터베이스와 사용자를 생성하고, GUI 도구로 접속을 확인한 뒤, 마지막으로 5편의 Go(Gin) 서버와 6편의 Python(FastAPI) 서버에서 실제로 연결하는 것까지 다룹니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 폴더 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 PostgreSQL, MinIO, n8n 등 여러 인프라 컨테이너를 함께 관리할 것이므로, 인프라 전용 폴더를 하나 만들어 두겠습니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;mkdir ~/dev-infra
cd ~/dev-infra&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 폴더 안에 &lt;code&gt;docker-compose.yml&lt;/code&gt;을 만들고, 편이 진행될수록 서비스를 하나씩 추가하는 방식으로 진행합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker Compose로 PostgreSQL 구동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. docker-compose.yml 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/dev-infra/docker-compose.yml&lt;/code&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# ~/dev-infra/docker-compose.yml

services:
  postgres:
    image: postgres:17
    container_name: dev-postgres
    restart: unless-stopped
    ports:
      - &quot;5432:5432&quot;
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpass123
      POSTGRES_DB: devdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./initdb:/docker-entrypoint-initdb.d

volumes:
  postgres_data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정을 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;image: postgres:17&lt;/code&gt;은 PostgreSQL 17 공식 이미지를 사용합니다. &lt;code&gt;latest&lt;/code&gt; 대신 메이저 버전을 명시해서 예기치 않은 업그레이드를 방지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;container_name: dev-postgres&lt;/code&gt;는 컨테이너에 고정된 이름을 부여합니다. &lt;code&gt;docker logs dev-postgres&lt;/code&gt;처럼 이름으로 바로 접근할 수 있어서 편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt;는 Docker Desktop이 시작되면 컨테이너도 자동으로 함께 올라오도록 합니다. 수동으로 &lt;code&gt;docker stop&lt;/code&gt;을 실행한 경우에만 꺼진 상태를 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ports: &quot;5432:5432&quot;&lt;/code&gt;는 호스트(윈도우)의 5432 포트를 컨테이너의 5432 포트에 연결합니다. DBeaver 같은 GUI 도구나 애플리케이션 코드에서 &lt;code&gt;localhost:5432&lt;/code&gt;로 접속할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;environment&lt;/code&gt; 블록의 세 변수는 PostgreSQL 공식 이미지가 제공하는 초기화 옵션입니다. &lt;code&gt;POSTGRES_USER&lt;/code&gt;는 슈퍼유저 이름, &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;는 비밀번호, &lt;code&gt;POSTGRES_DB&lt;/code&gt;는 컨테이너가 처음 시작될 때 자동으로 생성할 데이터베이스 이름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;volumes&lt;/code&gt;에서 &lt;code&gt;postgres_data:/var/lib/postgresql/data&lt;/code&gt;는 데이터베이스 파일을 Docker Named Volume에 저장합니다. 컨테이너를 삭제하고 다시 만들어도 데이터가 유지됩니다. &lt;code&gt;./initdb:/docker-entrypoint-initdb.d&lt;/code&gt;는 컨테이너가 &lt;b&gt;최초로&lt;/b&gt; 시작될 때 이 폴더 안의 &lt;code&gt;.sql&lt;/code&gt; 또는 &lt;code&gt;.sh&lt;/code&gt; 파일을 자동 실행합니다. 초기 테이블이나 추가 유저를 생성할 때 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 초기화 SQL 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;initdb&lt;/code&gt; 폴더를 만들고 초기화 스크립트를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;mkdir ~/dev-infra/initdb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/dev-infra/initdb/01_init.sql&lt;/code&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- ~/dev-infra/initdb/01_init.sql

-- 예시 테이블: users
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 테스트 데이터 삽입
INSERT INTO users (username, email) VALUES
    ('alice', 'alice@example.com'),
    ('bob', 'bob@example.com')
ON CONFLICT DO NOTHING;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스크립트는 &lt;code&gt;devdb&lt;/code&gt; 데이터베이스 안에 &lt;code&gt;users&lt;/code&gt; 테이블을 만들고 테스트 데이터 두 건을 넣습니다. &lt;code&gt;docker-entrypoint-initdb.d&lt;/code&gt;에 있는 파일은 볼륨이 비어 있는 최초 실행 시에만 동작합니다. 이미 데이터가 있는 상태에서 컨테이너를 재시작하면 이 스크립트는 다시 실행되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd ~/dev-infra
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;[+] Running 2/2
 ✔ Volume &quot;dev-infra_postgres_data&quot;  Created
 ✔ Container dev-postgres            Started&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 확인해서 정상 기동 여부를 봅니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker logs dev-postgres&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 마지막 부분에 아래와 비슷한 메시지가 나타나면 성공입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;... database system is ready to accept connections&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. CLI로 빠르게 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell에서 컨테이너 안의 &lt;code&gt;psql&lt;/code&gt;을 실행해서 데이터를 조회해 봅니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker exec -it dev-postgres psql -U devuser -d devdb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;psql&lt;/code&gt; 프롬프트가 나타나면 아래 쿼리를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM users;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; id | username |       email        |          created_at
----+----------+--------------------+-------------------------------
  1 | alice    | alice@example.com  | 2026-03-31 12:00:00.000000+00
  2 | bob      | bob@example.com    | 2026-03-31 12:00:00.000000+00
(2 rows)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화 SQL이 제대로 실행된 것을 확인할 수 있습니다. &lt;code&gt;\q&lt;/code&gt;를 입력하면 psql을 빠져나옵니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;\q&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. DBeaver로 GUI 접속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커맨드라인에서 쿼리를 날릴 수 있지만, 테이블 구조를 시각적으로 탐색하거나 데이터를 편집할 때는 GUI 도구가 편합니다. 무료 데이터베이스 도구인 DBeaver Community를 설치합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. DBeaver 설치&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;winget install -e --id dbeaver.dbeaver&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 끝나면 DBeaver를 실행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 연결 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBeaver 좌측 상단의 &lt;b&gt;새 데이터베이스 연결&lt;/b&gt; 버튼(플러그 아이콘)을 클릭하거나 메뉴에서 Database &amp;rarr; New Database Connection을 선택합니다. 데이터베이스 유형 선택 화면에서 &lt;b&gt;PostgreSQL&lt;/b&gt;을 선택하고 Next를 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 정보를 아래와 같이 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Host:     localhost
Port:     5432
Database: devdb
Username: devuser
Password: devpass123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측 하단의 &lt;b&gt;Test Connection&lt;/b&gt; 버튼을 클릭합니다. 처음 연결할 때 PostgreSQL JDBC 드라이버 다운로드를 묻는 팝업이 나타날 수 있습니다. &lt;b&gt;Download&lt;/b&gt;를 눌러 드라이버를 내려받으세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Connected&quot; 메시지가 나타나면 &lt;b&gt;Finish&lt;/b&gt;를 눌러 연결을 저장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 데이터 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 Database Navigator에서 &lt;code&gt;devdb &amp;rarr; Schemas &amp;rarr; public &amp;rarr; Tables &amp;rarr; users&lt;/code&gt;를 펼치면 컬럼 구조를 볼 수 있습니다. &lt;code&gt;users&lt;/code&gt; 테이블을 더블 클릭하면 Data 탭에서 alice와 bob 데이터가 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 GUI에서 편하게 쿼리를 작성하고, 테이블을 탐색하고, 데이터를 수정할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Go(Gin)에서 PostgreSQL 연결 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5편에서 만든 Go + Gin 프로젝트에서 PostgreSQL에 연결해 보겠습니다. Go에서 PostgreSQL에 접속할 때는 &lt;code&gt;pgx&lt;/code&gt; 드라이버를 사용합니다. 현재 Go 생태계에서 가장 널리 쓰이는 PostgreSQL 드라이버입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5편에서 만든 Go 프로젝트 폴더로 이동합니다. (예시 경로는 &lt;code&gt;~/go-api&lt;/code&gt;로 가정합니다. 실제 경로는 본인의 프로젝트에 맞게 바꿔 주세요.)&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;cd ~/go-api
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 연결 테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main.go&lt;/code&gt;를 아래와 같이 수정합니다. 5편의 Hello World 라우트는 유지하고 DB 관련 코드를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// main.go
package main

import (
    &quot;context&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;os&quot;
    &quot;time&quot;

    &quot;github.com/gin-gonic/gin&quot;
    &quot;github.com/jackc/pgx/v5/pgxpool&quot;
)

type User struct {
    ID        int       `json:&quot;id&quot;`
    Username  string    `json:&quot;username&quot;`
    Email     string    `json:&quot;email&quot;`
    CreatedAt time.Time `json:&quot;created_at&quot;`
}

var db *pgxpool.Pool

func main() {
    // DB 연결
    dsn := os.Getenv(&quot;DATABASE_URL&quot;)
    if dsn == &quot;&quot; {
        dsn = &quot;postgres://devuser:devpass123@localhost:5432/devdb?sslmode=disable&quot;
    }

    var err error
    db, err = pgxpool.New(context.Background(), dsn)
    if err != nil {
        log.Fatalf(&quot;Unable to connect to database: %v\n&quot;, err)
    }
    defer db.Close()

    // 연결 확인
    if err := db.Ping(context.Background()); err != nil {
        log.Fatalf(&quot;Unable to ping database: %v\n&quot;, err)
    }
    log.Println(&quot;Connected to PostgreSQL!&quot;)

    // Gin 라우터
    r := gin.Default()

    r.GET(&quot;/&quot;, func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{&quot;message&quot;: &quot;Hello, World!&quot;})
    })

    r.GET(&quot;/users&quot;, getUsers)

    r.Run(&quot;:8080&quot;)
}

func getUsers(c *gin.Context) {
    rows, err := db.Query(context.Background(), &quot;SELECT id, username, email, created_at FROM users&quot;)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: err.Error()})
        return
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&amp;amp;u.ID, &amp;amp;u.Username, &amp;amp;u.Email, &amp;amp;u.CreatedAt); err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: err.Error()})
            return
        }
        users = append(users, u)
    }

    c.JSON(http.StatusOK, users)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 간단히 설명하겠습니다. &lt;code&gt;pgxpool.New&lt;/code&gt;로 커넥션 풀을 생성합니다. 커넥션 풀은 여러 요청이 동시에 들어올 때 DB 연결을 효율적으로 재사용하기 위한 것입니다. &lt;code&gt;DATABASE_URL&lt;/code&gt; 환경 변수가 있으면 그 값을 쓰고, 없으면 로컬 Docker PostgreSQL에 연결하는 기본값을 사용합니다. &lt;code&gt;/users&lt;/code&gt; 엔드포인트에서 &lt;code&gt;users&lt;/code&gt; 테이블의 전체 데이터를 JSON으로 반환합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 실행 &amp;amp; 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 컨테이너가 실행 중인 상태에서 Go 서버를 시작합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;cd ~/go-api
go run .&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;... Connected to PostgreSQL!
... Listening and serving HTTP on :8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 또는 새 PowerShell 탭에서 API를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl http://localhost:8080/users&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;id&quot;: 1,
    &quot;username&quot;: &quot;alice&quot;,
    &quot;email&quot;: &quot;alice@example.com&quot;,
    &quot;created_at&quot;: &quot;2026-03-31T12:00:00Z&quot;
  },
  {
    &quot;id&quot;: 2,
    &quot;username&quot;: &quot;bob&quot;,
    &quot;email&quot;: &quot;bob@example.com&quot;,
    &quot;created_at&quot;: &quot;2026-03-31T12:00:00Z&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 서버가 PostgreSQL 컨테이너에 정상적으로 연결되어 데이터를 가져오는 것을 확인했습니다. 확인이 끝났으면 &lt;code&gt;Ctrl+C&lt;/code&gt;로 Go 서버를 종료합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Python(FastAPI)에서 PostgreSQL 연결 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6편에서 만든 FastAPI 프로젝트에서도 PostgreSQL에 연결해 보겠습니다. Python에서 PostgreSQL에 접속할 때는 비동기를 지원하는 &lt;code&gt;asyncpg&lt;/code&gt;와 &lt;code&gt;databases&lt;/code&gt; 라이브러리를 사용할 수도 있지만, 여기서는 가장 보편적인 동기 드라이버인 &lt;code&gt;psycopg&lt;/code&gt;를 사용하겠습니다. FastAPI는 비동기 프레임워크이지만, 동기 DB 드라이버도 함께 쓸 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. 의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6편에서 만든 FastAPI 프로젝트 폴더로 이동합니다. (예시 경로는 &lt;code&gt;~/fastapi-app&lt;/code&gt;으로 가정합니다.)&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd ~/fastapi-app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 환경을 활성화합니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;.\.venv\Scripts\Activate.ps1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;psycopg&lt;/code&gt; 바이너리 패키지를 설치합니다. &lt;code&gt;psycopg[binary]&lt;/code&gt;는 별도의 C 컴파일러 없이 바로 사용할 수 있는 버전입니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install &quot;psycopg[binary]&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 연결 테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main.py&lt;/code&gt;를 아래와 같이 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# main.py
import os
from contextlib import asynccontextmanager
from datetime import datetime

import psycopg
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel


DATABASE_URL = os.getenv(
    &quot;DATABASE_URL&quot;,
    &quot;postgresql://devuser:devpass123@localhost:5432/devdb&quot;,
)


class User(BaseModel):
    id: int
    username: str
    email: str
    created_at: datetime


def get_connection():
    return psycopg.connect(DATABASE_URL)


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 시작 시 연결 확인
    with get_connection() as conn:
        print(&quot;Connected to PostgreSQL!&quot;)
    yield


app = FastAPI(lifespan=lifespan)


@app.get(&quot;/&quot;)
def read_root():
    return {&quot;message&quot;: &quot;Hello, World!&quot;}


@app.get(&quot;/users&quot;, response_model=list[User])
def read_users():
    try:
        with get_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(&quot;SELECT id, username, email, created_at FROM users&quot;)
                rows = cur.fetchall()
                return [
                    User(id=row[0], username=row[1], email=row[2], created_at=row[3])
                    for row in rows
                ]
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 설명하겠습니다. &lt;code&gt;lifespan&lt;/code&gt; 함수에서 서버가 시작될 때 DB 연결을 한 번 확인합니다. 연결에 실패하면 서버가 바로 에러를 내며 이를 통해 DB가 정상인지 빠르게 알 수 있습니다. &lt;code&gt;/users&lt;/code&gt; 엔드포인트에서 &lt;code&gt;psycopg.connect&lt;/code&gt;로 연결하고, &lt;code&gt;users&lt;/code&gt; 테이블을 조회한 뒤 Pydantic 모델로 변환해서 반환합니다. Go 코드와 마찬가지로 &lt;code&gt;DATABASE_URL&lt;/code&gt; 환경 변수가 없으면 로컬 Docker PostgreSQL에 연결하는 기본값을 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 실행 &amp;amp; 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 컨테이너가 실행 중인 상태에서 FastAPI 서버를 시작합니다. Go 서버가 8080 포트를 사용했으므로, FastAPI는 8000 포트로 띄웁니다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;uvicorn main:app --reload --port 8000&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;Connected to PostgreSQL!
INFO:     Uvicorn running on http://127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 PowerShell 탭에서 API를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl http://localhost:8000/users&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;id&quot;: 1,
    &quot;username&quot;: &quot;alice&quot;,
    &quot;email&quot;: &quot;alice@example.com&quot;,
    &quot;created_at&quot;: &quot;2026-03-31T12:00:00Z&quot;
  },
  {
    &quot;id&quot;: 2,
    &quot;username&quot;: &quot;bob&quot;,
    &quot;email&quot;: &quot;bob@example.com&quot;,
    &quot;created_at&quot;: &quot;2026-03-31T12:00:00Z&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI에서도 PostgreSQL 연결이 정상적으로 동작합니다. FastAPI가 자동 생성하는 API 문서도 확인해 보세요. 브라우저에서 &lt;code&gt;http://localhost:8000/docs&lt;/code&gt;를 열면 Swagger UI에서 &lt;code&gt;/users&lt;/code&gt; 엔드포인트를 대화형으로 테스트할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인이 끝났으면 &lt;code&gt;Ctrl+C&lt;/code&gt;로 FastAPI 서버를 종료하고, &lt;code&gt;deactivate&lt;/code&gt;로 가상 환경을 빠져나옵니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 환경 변수 관리 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;code&gt;DATABASE_URL&lt;/code&gt;의 기본값에 비밀번호가 하드코딩되어 있는 것을 눈치채셨을 것입니다. 로컬 개발 환경에서는 괜찮지만, 프로덕션에서는 절대 이렇게 하면 안 됩니다. 지금 단계에서는 두 가지만 기억해 두세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;code&gt;.env&lt;/code&gt; 파일을 사용하세요. 프로젝트 루트에 &lt;code&gt;.env&lt;/code&gt; 파일을 만들고 환경 변수를 넣어 둡니다. Docker Compose에서는 &lt;code&gt;env_file&lt;/code&gt; 옵션으로, 애플리케이션에서는 &lt;code&gt;dotenv&lt;/code&gt; 라이브러리로 읽을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;code&gt;.env&lt;/code&gt; 파일은 반드시 &lt;code&gt;.gitignore&lt;/code&gt;에 추가하세요. 비밀번호가 담긴 파일이 GitHub에 올라가면 큰 보안 사고로 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션 환경의 시크릿 관리는 13편(클라우드 서버)과 14편(통합 Docker Compose)에서 더 자세히 다룹니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 겪는 문제와 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트 충돌: &quot;port is already allocated&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도우에 PostgreSQL이 이미 설치돼 있으면 5432 포트가 겹칠 수 있습니다. 이 경우 &lt;code&gt;docker-compose.yml&lt;/code&gt;의 포트 매핑을 &lt;code&gt;&quot;5433:5432&quot;&lt;/code&gt;로 바꾸고, 접속 시에도 포트를 5433으로 지정하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초기화 SQL이 실행되지 않는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker-entrypoint-initdb.d&lt;/code&gt;의 스크립트는 볼륨이 비어 있는 &lt;b&gt;최초 실행 시&lt;/b&gt;에만 동작합니다. 이미 한번 컨테이너를 띄운 적이 있다면 볼륨에 데이터가 남아 있어 초기화 스크립트가 무시됩니다. 초기화를 다시 하고 싶으면 볼륨을 삭제하고 처음부터 시작해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;cd ~/dev-infra
docker compose down -v
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-v&lt;/code&gt; 플래그가 볼륨까지 삭제합니다. 당연히 기존 데이터도 전부 날아가므로 주의하세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 시 &quot;password authentication failed&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;POSTGRES_USER&lt;/code&gt;와 &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;는 볼륨이 비어 있는 최초 시작 시에만 적용됩니다. 기존 볼륨이 남아 있는 상태에서 비밀번호만 바꾸면 적용되지 않습니다. 마찬가지로 &lt;code&gt;docker compose down -v&lt;/code&gt; 후 다시 시작해야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 확인 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목을 모두 확인했다면 8편은 완료입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;✅ docker-compose.yml에 PostgreSQL 서비스가 정의되어 있다
✅ docker compose up -d 로 PostgreSQL 컨테이너가 정상 기동된다
✅ initdb/01_init.sql이 최초 실행 시 자동 적용되어 users 테이블과 테스트 데이터가 생성된다
✅ DBeaver에서 localhost:5432 / devuser / devpass123 으로 접속하여 데이터를 확인할 수 있다
✅ Go(Gin) 서버의 /users 엔드포인트가 PostgreSQL 데이터를 JSON으로 반환한다
✅ Python(FastAPI) 서버의 /users 엔드포인트가 PostgreSQL 데이터를 JSON으로 반환한다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스가 준비되었으니, 다음은 파일 저장소입니다. &lt;b&gt;9편: MinIO 컨테이너 설정&lt;/b&gt;에서는 S3 호환 오브젝트 스토리지인 MinIO를 Docker Compose에 추가하고, 웹 콘솔에서 버킷을 만들고, Go&amp;middot;Python&amp;middot;JavaScript 세 가지 언어에서 파일을 업로드하고 다운로드하는 것까지 확인해 보겠습니다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/314</guid>
      <comments>https://polarcompass.tistory.com/314#entry314comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:27:28 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 7편: Docker Desktop 설치 &amp;amp; 기본 사용법</title>
      <link>https://polarcompass.tistory.com/313</link>
      <description>&lt;h1&gt;7편: Docker Desktop 설치 &amp;amp; 기본 사용법&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈&lt;/b&gt;: 윈도우 네이티브 개발 환경 구축 A to Z &amp;mdash; 새 PC부터 클라우드 배포까지&lt;br /&gt;&lt;b&gt;이전 편&lt;/b&gt;: 6편에서 pyenv-win으로 Python을 설치하고, FastAPI + Uvicorn Hello World API를 띄워 봤습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6편까지 프론트엔드(React SPA)와 백엔드 두 개(Go + Gin, Python + FastAPI)의 런타임을 모두 갖췄습니다. 이제 이 서비스들이 실제로 대화할 &lt;b&gt;데이터베이스, 오브젝트 스토리지, 자동화 엔진&lt;/b&gt; 같은 인프라 컴포넌트를 올려야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 인프라를 윈도우에 하나하나 네이티브로 설치할 수도 있지만, 컨테이너로 올리면 세 가지가 편해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 설치와 삭제가 깔끔합니다. 컨테이너를 지우면 흔적이 남지 않습니다. 둘째, 팀원이 같은 &lt;code&gt;docker-compose.yml&lt;/code&gt; 하나로 동일한 환경을 재현할 수 있습니다. 셋째, 나중에 리눅스 서버에 배포할 때도 거의 같은 Compose 파일을 그대로 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 Docker Desktop을 설치하고, 컨테이너&amp;middot;이미지&amp;middot;볼륨을 다루는 기본 명령어를 익힌 뒤, Docker Compose로 여러 컨테이너를 한 번에 띄우는 방법까지 다룹니다. 8편부터는 이 위에 PostgreSQL, MinIO, n8n을 차례로 올릴 예정이니, 이번 편에서 기초를 확실히 잡아 두겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Docker Desktop 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 사전 요구 사항 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop은 내부적으로 WSL 2 백엔드 또는 Hyper-V 백엔드를 사용합니다. 최신 버전은 설치 시 WSL 2 커널을 자동으로 구성해 주므로 별도로 WSL 배포판을 설치하거나 WSL 터미널에 들어갈 필요가 없습니다. 우리가 지키는 원칙&amp;mdash;&quot;사용자가 WSL 터미널에 직접 들어가지 않는다&quot;&amp;mdash;과 충돌하지 않습니다. Docker 명령은 전부 PowerShell에서 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어 가상화(VT-x / AMD-V)가 BIOS에서 켜져 있어야 합니다. 대부분의 최신 PC는 기본으로 활성화돼 있지만, 설치 후 Docker가 시작되지 않으면 BIOS에서 이 항목을 확인하세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. winget으로 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell 7을 &lt;b&gt;관리자 권한&lt;/b&gt;으로 열고 아래 명령을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;winget install -e --id Docker.DockerDesktop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 끝나면 &lt;b&gt;재부팅&lt;/b&gt;을 요구할 수 있습니다. 재부팅 안내가 나오면 반드시 재부팅하세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. 첫 실행 &amp;amp; 초기 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재부팅 후 Docker Desktop이 자동 시작됩니다. 시스템 트레이에 고래 아이콘이 나타나고, 상태가 &lt;b&gt;&quot;Docker Desktop is running&quot;&lt;/b&gt;으로 바뀌면 준비 완료입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 실행 시 라이선스 동의 화면이 나옵니다. 개인 사용자 또는 소규모 팀(직원 250명 미만, 연매출 1,000만 달러 미만)은 무료 Personal 플랜으로 사용할 수 있습니다. 회사 규모가 이를 초과한다면 유료 구독이 필요하니 확인하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Settings &amp;rarr; General에서 다음 두 가지를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Use the WSL 2 based engine&lt;/b&gt; 항목이 체크돼 있는지 확인합니다. 이것은 Docker 엔진의 백엔드를 WSL 2 위에서 돌린다는 뜻이며, 사용자가 WSL 터미널에 진입하는 것과는 관계없습니다. 성능이 좋으므로 켜 두세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Start Docker Desktop when you sign in to Windows&lt;/b&gt; 항목은 개발 중이라면 켜 두는 편이 편합니다. 리소스가 아까우면 꺼도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-4. 설치 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell에서 버전을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;docker --version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Docker version 27.x.x, build xxxxxxx&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker compose version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Docker Compose version v2.x.x&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 명령 모두 버전이 출력되면 정상입니다. &lt;code&gt;docker compose&lt;/code&gt;는 Docker Desktop에 포함된 Compose V2 플러그인이므로 별도 설치가 필요 없습니다. 과거의 &lt;code&gt;docker-compose&lt;/code&gt;(하이픈) 명령 대신 &lt;code&gt;docker compose&lt;/code&gt;(스페이스)를 사용합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Hello World로 동작 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker가 실제로 컨테이너를 만들고 실행할 수 있는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker run hello-world&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 비슷한 메시지가 출력되면 성공입니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Hello from Docker!
This message shows that your installation appears to be working correctly.
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령이 실행되면서 내부적으로 일어나는 일을 순서대로 정리하면 다음과 같습니다. 먼저 로컬에 &lt;code&gt;hello-world&lt;/code&gt; 이미지가 없으므로 Docker Hub에서 이미지를 다운로드(pull)합니다. 그 이미지로 컨테이너를 생성하고 실행합니다. 컨테이너 안의 프로그램이 메시지를 출력한 뒤 종료됩니다. 이 흐름&amp;mdash;&quot;이미지 &amp;rarr; 컨테이너 &amp;rarr; 실행 &amp;rarr; 종료&quot;&amp;mdash;이 Docker의 기본 라이프사이클입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념 세 가지: 이미지, 컨테이너, 볼륨&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 명령어를 다루기 전에, 앞으로 계속 만날 세 가지 개념을 짚고 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이미지(Image)&lt;/b&gt;는 실행 환경을 담은 읽기 전용 템플릿입니다. &quot;PostgreSQL 16이 설치된 리눅스 환경&quot;처럼 특정 소프트웨어와 설정이 스냅샷으로 굳어진 상태라고 보면 됩니다. Docker Hub 같은 레지스트리에서 내려받거나, &lt;code&gt;Dockerfile&lt;/code&gt;을 작성해서 직접 빌드할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨테이너(Container)&lt;/b&gt;는 이미지를 바탕으로 실제 실행되는 인스턴스입니다. 하나의 이미지에서 여러 컨테이너를 만들 수 있고, 각 컨테이너는 서로 격리된 파일 시스템과 네트워크를 가집니다. 컨테이너를 삭제하면 그 안에서 변경된 데이터도 함께 사라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;볼륨(Volume)&lt;/b&gt;은 컨테이너가 사라져도 데이터를 유지할 수 있게 해 주는 저장 공간입니다. 데이터베이스 파일, 업로드된 파일 등 영속적으로 보관해야 하는 데이터는 반드시 볼륨에 저장해야 합니다. 볼륨을 연결하지 않으면 컨테이너를 지울 때 데이터도 전부 날아갑니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 이미지 관련 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 검색&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker search nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Hub에서 &lt;code&gt;nginx&lt;/code&gt; 관련 이미지를 검색합니다. STARS 수와 OFFICIAL 여부를 참고해서 신뢰할 수 있는 이미지를 고릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 다운로드&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;docker pull nginx:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그(&lt;code&gt;:latest&lt;/code&gt;)를 생략하면 자동으로 &lt;code&gt;latest&lt;/code&gt;가 적용됩니다. 프로덕션에서는 &lt;code&gt;nginx:1.27&lt;/code&gt; 같이 구체적인 버전 태그를 쓰는 것이 좋습니다. &lt;code&gt;latest&lt;/code&gt;는 언제 어떤 버전으로 바뀔지 모르기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 이미지 목록 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
nginx         latest    a8758716bb6a   2 weeks ago    187MB
hello-world   latest    d2c94e258dcb   12 months ago  13.3kB&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 삭제&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;docker rmi nginx:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 이미지로 만든 컨테이너가 아직 남아 있으면 삭제가 거부됩니다. 컨테이너를 먼저 지우거나 &lt;code&gt;-f&lt;/code&gt; 플래그를 붙여야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용하지 않는 이미지 일괄 정리&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;docker image prune -a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 어떤 컨테이너에서도 사용하지 않는 이미지를 전부 삭제합니다. 디스크 공간을 확보할 때 유용합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 컨테이너 관련 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker run -d --name my-nginx -p 8080:80 nginx:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 옵션의 의미는 다음과 같습니다. &lt;code&gt;-d&lt;/code&gt;는 백그라운드(detached) 실행입니다. &lt;code&gt;--name my-nginx&lt;/code&gt;는 컨테이너에 이름을 붙여서 나중에 다루기 쉽게 합니다. &lt;code&gt;-p 8080:80&lt;/code&gt;은 호스트(윈도우)의 8080 포트를 컨테이너 내부의 80 포트에 연결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 브라우저에서 &lt;code&gt;http://localhost:8080&lt;/code&gt;을 열면 Nginx 환영 페이지가 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 중인 컨테이너 목록&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모든 컨테이너 목록 (중지된 것 포함)&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 로그 확인&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker logs my-nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 로그를 따라가려면 &lt;code&gt;-f&lt;/code&gt; 플래그를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker logs -f my-nginx&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 중지 &amp;amp; 시작&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker stop my-nginx
docker start my-nginx&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 삭제&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker stop my-nginx
docker rm my-nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 중인 컨테이너를 바로 지우고 싶으면 &lt;code&gt;docker rm -f my-nginx&lt;/code&gt;를 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중지된 컨테이너 일괄 정리&lt;/h3&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;docker container prune&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 볼륨 관련 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;볼륨 생성&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker volume create my-data&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;볼륨 목록 확인&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker volume ls&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;볼륨을 연결해서 컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;docker run -d --name my-nginx -p 8080:80 -v my-data:/usr/share/nginx/html nginx:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-v my-data:/usr/share/nginx/html&lt;/code&gt;은 &lt;code&gt;my-data&lt;/code&gt; 볼륨을 컨테이너 내부의 &lt;code&gt;/usr/share/nginx/html&lt;/code&gt; 경로에 마운트합니다. 컨테이너를 삭제하고 다시 만들어도 볼륨에 저장된 파일은 그대로 남습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;볼륨 삭제&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker volume rm my-data&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용하지 않는 볼륨 일괄 정리&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker volume prune&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;볼륨을 지우면 안에 있던 데이터도 영구적으로 삭제되므로 주의하세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Docker Compose &amp;mdash; 여러 컨테이너를 한 번에 관리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 &lt;code&gt;docker run&lt;/code&gt; 명령으로 하나씩 컨테이너를 띄웠습니다. 하지만 실제 프로젝트에서는 웹 서버, 데이터베이스, 캐시 등 여러 컨테이너가 함께 돌아갑니다. 이걸 매번 긴 &lt;code&gt;docker run&lt;/code&gt; 명령을 하나하나 입력해서 관리하는 것은 비현실적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 &lt;code&gt;docker-compose.yml&lt;/code&gt;(또는 &lt;code&gt;compose.yml&lt;/code&gt;) 파일에 필요한 컨테이너들의 설정을 선언하고, 명령어 하나로 전부 띄우거나 내릴 수 있게 해 줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. 실습: Nginx + Redis 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제로 Compose의 흐름을 익혀 보겠습니다. 적당한 위치에 실습 폴더를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;mkdir ~/docker-practice
cd ~/docker-practice&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.yml

services:
  web:
    image: nginx:latest
    ports:
      - &quot;8080:80&quot;
    depends_on:
      - cache

  cache:
    image: redis:alpine
    ports:
      - &quot;6379:6379&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일이 담고 있는 내용을 설명하겠습니다. &lt;code&gt;services&lt;/code&gt; 아래에 &lt;code&gt;web&lt;/code&gt;과 &lt;code&gt;cache&lt;/code&gt; 두 개의 서비스를 정의했습니다. &lt;code&gt;web&lt;/code&gt;은 Nginx를 8080 포트로 노출하고, &lt;code&gt;cache&lt;/code&gt;는 Redis를 6379 포트로 노출합니다. &lt;code&gt;depends_on&lt;/code&gt;은 &lt;code&gt;web&lt;/code&gt;이 &lt;code&gt;cache&lt;/code&gt;보다 나중에 시작되도록 순서를 지정합니다. Compose가 자동으로 같은 네트워크를 만들어 주기 때문에 컨테이너끼리는 서비스 이름(&lt;code&gt;web&lt;/code&gt;, &lt;code&gt;cache&lt;/code&gt;)으로 서로를 찾을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. Compose 기본 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 폴더에서 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 서비스 시작 (백그라운드)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행하면 이미지를 다운로드한 뒤 컨테이너를 생성&amp;middot;시작합니다. 아래와 비슷한 출력을 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[+] Running 3/3
 ✔ Network docker-practice_default  Created
 ✔ Container docker-practice-cache-1  Started
 ✔ Container docker-practice-web-1    Started&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 상태 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NAME                          IMAGE          ...   STATUS         PORTS
docker-practice-cache-1       redis:alpine   ...   Up 30 seconds  0.0.0.0:6379-&amp;gt;6379/tcp
docker-practice-web-1         nginx:latest   ...   Up 30 seconds  0.0.0.0:8080-&amp;gt;80/tcp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 서비스의 로그만 보려면 서비스 이름을 뒤에 붙입니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose logs cache&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 서비스 중지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;docker compose stop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 중지되지만 삭제되지는 않습니다. &lt;code&gt;docker compose start&lt;/code&gt;로 다시 시작할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 서비스 중지 + 컨테이너 삭제 + 네트워크 삭제&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;볼륨까지 함께 삭제하고 싶으면 &lt;code&gt;-v&lt;/code&gt; 플래그를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose down -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;yml 수정 후 반영&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;을 수정한 뒤 다시 &lt;code&gt;docker compose up -d&lt;/code&gt;를 실행하면 변경된 서비스만 재생성됩니다. 전체를 처음부터 다시 빌드하고 싶으면 &lt;code&gt;--force-recreate&lt;/code&gt; 플래그를 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-3. 동작 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;code&gt;http://localhost:8080&lt;/code&gt;을 열어 Nginx 환영 페이지가 나타나는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis도 확인해 봅니다. PowerShell에서 아래 명령으로 컨테이너 안의 &lt;code&gt;redis-cli&lt;/code&gt;를 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;docker exec -it docker-practice-cache-1 redis-cli ping&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;PONG&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PONG&lt;/code&gt;이 돌아오면 Redis도 정상입니다. 여기서 &lt;code&gt;docker exec&lt;/code&gt;는 실행 중인 컨테이너 안에서 명령어를 실행하는 명령입니다. WSL에 들어가는 것이 아니라 PowerShell에서 컨테이너에게 명령을 보내는 것이므로 우리의 원칙에 위배되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-4. 실습 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인이 끝났으면 깔끔하게 정리합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 자주 쓰는 정리 명령어 모음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발하다 보면 이미지, 중지된 컨테이너, 사용하지 않는 볼륨이 쌓여서 디스크를 차지합니다. 다음 명령어들을 알아 두면 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 한 방 정리 (주의: 사용하지 않는 것을 전부 삭제)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker system prune -a --volumes&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 중지된 컨테이너, 사용하지 않는 네트워크, 태그 없는 이미지, 연결되지 않은 볼륨을 한꺼번에 삭제합니다. 필요한 데이터가 날아갈 수 있으니 확인 후 실행하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디스크 사용량 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker system df&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 항목이 얼마나 공간을 차지하는지 한눈에 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Docker Desktop GUI 간단 안내&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop은 GUI 대시보드도 제공합니다. 시스템 트레이의 고래 아이콘을 더블 클릭하면 열립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Containers&lt;/b&gt; 탭에서 실행 중인 컨테이너를 확인하고, 로그를 보거나 중지&amp;middot;삭제할 수 있습니다. &lt;b&gt;Images&lt;/b&gt; 탭에서 로컬에 저장된 이미지를 관리할 수 있습니다. &lt;b&gt;Volumes&lt;/b&gt; 탭에서 볼륨 목록을 확인하고 삭제할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커맨드라인이 익숙하지 않은 분은 GUI로 상태를 확인하면서 작업하면 편합니다. 이 시리즈에서는 재현 가능성과 정확성을 위해 모든 조작을 CLI 명령어로 안내하지만, GUI에서 같은 작업을 할 수 있다는 것도 알아 두세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 확인 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목을 모두 확인했다면 7편은 완료입니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;✅ Docker Desktop이 정상 실행된다 (시스템 트레이 고래 아이콘 확인)
✅ docker --version 이 버전 정보를 출력한다
✅ docker compose version 이 버전 정보를 출력한다
✅ docker run hello-world 가 성공 메시지를 출력한다
✅ docker-compose.yml로 Nginx + Redis를 띄우고 접속할 수 있다
✅ docker compose down 으로 깔끔하게 정리할 수 있다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker의 기본 사용법을 익혔으니, 이제 실제 프로젝트에 필요한 인프라를 올릴 차례입니다. &lt;b&gt;8편: PostgreSQL 컨테이너 설정&lt;/b&gt;에서는 Docker Compose로 PostgreSQL을 구동하고, 초기 데이터베이스와 사용자를 생성한 뒤, DBeaver에서 접속을 확인합니다. 그리고 5편에서 만든 Go(Gin) 서버와 6편에서 만든 Python(FastAPI) 서버가 PostgreSQL에 연결되는 것까지 테스트해 보겠습니다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/313</guid>
      <comments>https://polarcompass.tistory.com/313#entry313comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:26:24 +0900</pubDate>
    </item>
    <item>
      <title>[윈도우 개발 환경 설정] 6편: Python &amp;amp; FastAPI 환경 구축</title>
      <link>https://polarcompass.tistory.com/312</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 최신 정보를 확인했습니다. 6편을 작성합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6편: Python &amp;amp; FastAPI 환경 구축&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈&lt;/b&gt;: 윈도우 네이티브 개발 환경 세팅부터 클라우드 배포까지 (6/14)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이전 편 전제&lt;/b&gt;: Go, Gin 프레임워크, Hello World API 서버 세팅 완료&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5편에서 Go + Gin으로 첫 번째 백엔드를 만들었다. 이번 편에서는 두 번째 백엔드를 Python + FastAPI로 구축한다. FastAPI는 타입 힌트 기반의 자동 문서화, 비동기(async) 지원, Pydantic 검증 등 현대적인 Python 웹 프레임워크의 장점을 모두 갖추고 있어 API 서버로 널리 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pyenv-win&lt;/b&gt;으로 Python 버전을 관리하고, &lt;b&gt;venv&lt;/b&gt;로 프로젝트별 가상환경을 격리한 뒤, FastAPI + Uvicorn으로 Hello World API 서버를 띄우는 것까지가 이번 편의 목표다. 모든 작업은 &lt;b&gt;PowerShell 7&lt;/b&gt;에서 진행하며, WSL은 사용하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 사전 준비: Windows App Execution Aliases 비활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows 10/11에는 &lt;code&gt;python&lt;/code&gt;이나 &lt;code&gt;python3&lt;/code&gt; 명령을 입력하면 Microsoft Store로 리다이렉트하는 내장 앱 별칭이 있다. 이것이 pyenv-win보다 우선순위가 높으면 pyenv로 설치한 Python이 인식되지 않는다. &lt;b&gt;반드시 먼저 비활성화해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 메뉴에서 &lt;b&gt;&quot;앱 실행 별칭 관리&quot;&lt;/b&gt;(Manage App Execution Aliases)를 검색해서 연다. 목록에서 &lt;b&gt;&quot;앱 설치 관리자 - python.exe&quot;&lt;/b&gt;와 &lt;b&gt;&quot;앱 설치 관리자 - python3.exe&quot;&lt;/b&gt; 두 항목을 모두 &lt;b&gt;끔(Off)&lt;/b&gt;으로 전환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 한 번만 하면 되며, 이후 pyenv-win이 정상적으로 Python 경로를 관리할 수 있게 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. pyenv-win으로 Python 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. pyenv-win 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyenv-win 공식 리포지토리에서 제공하는 PowerShell 설치 스크립트를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Invoke-WebRequest -UseBasicParsing `
  -Uri &quot;https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1&quot; `
  -OutFile &quot;./install-pyenv-win.ps1&quot;; &amp;amp;&quot;./install-pyenv-win.ps1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스크립트는 &lt;code&gt;%USERPROFILE%\.pyenv&lt;/code&gt; 경로에 pyenv-win을 설치하고, 환경변수(&lt;code&gt;PYENV&lt;/code&gt;, &lt;code&gt;PYENV_HOME&lt;/code&gt;, &lt;code&gt;PYENV_ROOT&lt;/code&gt;)와 PATH(&lt;code&gt;bin&lt;/code&gt;, &lt;code&gt;shims&lt;/code&gt;)를 자동으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 후 &lt;b&gt;터미널을 완전히 종료했다가 다시 연다.&lt;/b&gt; 새 터미널에서 확인한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;pyenv --version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;pyenv 3.1.1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 번호가 출력되면 성공이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Execution Policy 에러가 발생하면&lt;/b&gt;: &lt;code&gt;Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned&lt;/code&gt;를 먼저 실행한다. (1편에서 이미 설정했다면 생략)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 설치 가능한 Python 버전 목록 확인&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pyenv install -l&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수백 개의 버전이 출력된다. 특정 버전만 필터링하려면:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;pyenv install -l | findstr 3.13&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;3.13.0-win32
3.13.0
3.13.1-win32
3.13.1
...
3.13.12-win32
3.13.12&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목록이 최신 버전을 포함하지 않는다면&lt;/b&gt;: &lt;code&gt;pyenv update&lt;/code&gt; 명령으로 버전 데이터베이스를 갱신한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. Python 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Python 안정 버전은 &lt;b&gt;3.13&lt;/b&gt; 계열이다. FastAPI 최신 버전(0.135.x)이 Python 3.10 이상을 요구하므로 3.13을 설치한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;pyenv install 3.13.12&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;: 설치 중 Windows 인스톨러 위저드가 나타날 수 있다. 기본 옵션 그대로 진행하면 된다. 조용한 설치를 원하면 &lt;code&gt;pyenv install 3.13.12 -q&lt;/code&gt; 플래그를 추가한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. 글로벌 버전 설정&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;pyenv global 3.13.12&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 시스템 전체에서 기본으로 사용할 Python 버전을 지정한다. 확인한다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;pyenv version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;3.13.12 (set by C:\Users\사용자이름\.pyenv\pyenv-win\.python-version)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python과 pip도 확인한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python --version
pip --version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;Python 3.13.12
pip 24.x.x from C:\Users\사용자이름\.pyenv\pyenv-win\versions\3.13.12\Lib\site-packages\pip (python 3.13)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;python&lt;/code&gt; 명령이 여전히 Microsoft Store로 연결된다면&lt;/b&gt;: 1장의 App Execution Aliases 비활성화를 다시 확인한다. 또한 환경변수에서 pyenv의 &lt;code&gt;shims&lt;/code&gt; 경로가 다른 Python 경로보다 &lt;b&gt;위에(높은 우선순위)&lt;/b&gt; 있는지 확인한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-5. pyenv 핵심 명령어 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyenv-win에서 자주 쓰는 명령어를 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pyenv install -l&lt;/code&gt;은 설치 가능한 모든 버전을 나열한다. &lt;code&gt;pyenv install &amp;lt;version&amp;gt;&lt;/code&gt;은 지정 버전을 설치하고, &lt;code&gt;pyenv uninstall &amp;lt;version&amp;gt;&lt;/code&gt;은 제거한다. &lt;code&gt;pyenv global &amp;lt;version&amp;gt;&lt;/code&gt;은 시스템 전체 기본 버전을 설정하고, &lt;code&gt;pyenv local &amp;lt;version&amp;gt;&lt;/code&gt;은 현재 디렉토리에 &lt;code&gt;.python-version&lt;/code&gt; 파일을 생성해서 해당 폴더에서만 특정 버전을 사용하게 한다. &lt;code&gt;pyenv versions&lt;/code&gt;는 설치된 모든 버전을 보여주고, &lt;code&gt;pyenv version&lt;/code&gt;은 현재 활성 버전을 보여준다. &lt;code&gt;pyenv rehash&lt;/code&gt;는 shim을 재생성하며, pip로 패키지를 설치한 뒤 CLI 도구가 인식되지 않을 때 실행한다. &lt;code&gt;pyenv update&lt;/code&gt;는 설치 가능한 버전 목록 데이터베이스를 갱신한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 가상환경(venv) 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 프로젝트마다 의존성 버전이 다르기 때문에 &lt;b&gt;가상환경으로 격리하는 것이 필수&lt;/b&gt;다. Python 3.3부터 표준 라이브러리에 포함된 &lt;code&gt;venv&lt;/code&gt; 모듈을 사용한다. 별도 설치가 필요 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 프로젝트 디렉토리 생성&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;mkdir my-fastapi-app
cd my-fastapi-app&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 가상환경 생성&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;python -m venv .venv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.venv&lt;/code&gt;는 가상환경 디렉토리 이름이다. 관례적으로 &lt;code&gt;.venv&lt;/code&gt; 또는 &lt;code&gt;venv&lt;/code&gt;를 사용한다. 점(&lt;code&gt;.&lt;/code&gt;)으로 시작하면 숨김 디렉토리가 되어 프로젝트 파일 목록이 깔끔해진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 가상환경 활성화&lt;/h3&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;.\.venv\Scripts\Activate.ps1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성화되면 프롬프트 앞에 &lt;code&gt;(.venv)&lt;/code&gt;가 붙는다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;(.venv) PS C:\Users\사용자이름\my-fastapi-app&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Activate.ps1 실행이 차단된다면&lt;/b&gt;: 이것도 Execution Policy 문제다. &lt;code&gt;Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned&lt;/code&gt;를 실행한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성화 상태에서 &lt;code&gt;python&lt;/code&gt;과 &lt;code&gt;pip&lt;/code&gt;가 가상환경 내부를 가리키는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;python -c &quot;import sys; print(sys.executable)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\사용자이름\my-fastapi-app\.venv\Scripts\python.exe&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로에 &lt;code&gt;.venv&lt;/code&gt;가 포함되어 있으면 정상이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. 가상환경 비활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 끝나면 &lt;code&gt;deactivate&lt;/code&gt; 명령으로 빠져나올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;deactivate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트에서 &lt;code&gt;(.venv)&lt;/code&gt;가 사라진다. 다시 활성화하려면 &lt;code&gt;.\.venv\Scripts\Activate.ps1&lt;/code&gt;을 실행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요&lt;/b&gt;: 이후 모든 pip install과 서버 실행은 &lt;b&gt;가상환경이 활성화된 상태&lt;/b&gt;에서 수행한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. FastAPI + Uvicorn 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 패키지 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상환경이 활성화된 상태에서 설치한다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install fastapi uvicorn[standard]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;fastapi&lt;/code&gt;는 웹 프레임워크 본체이고, &lt;code&gt;uvicorn[standard]&lt;/code&gt;는 ASGI 서버에 추가 의존성(uvloop, httptools 등)을 포함한 버전이다. 현재 FastAPI 최신 버전은 &lt;b&gt;0.135.2&lt;/b&gt;, Uvicorn은 &lt;b&gt;0.42.0&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 확인:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;pip show fastapi
pip show uvicorn&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Version&lt;/code&gt;, &lt;code&gt;Location&lt;/code&gt; 정보가 출력되면 정상이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. requirements.txt 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 가상환경에 설치된 모든 패키지를 파일로 기록한다. 다른 환경에서 동일한 의존성을 재현할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;cat requirements.txt&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;annotated-types==0.7.0
anyio==4.8.0
click==8.1.8
colorama==0.4.6
fastapi==0.135.2
h11==0.14.0
httptools==0.6.4
idna==3.10
pydantic==2.10.6
pydantic_core==2.27.2
sniffio==1.3.1
starlette==0.45.3
typing_extensions==4.12.2
uvicorn==0.42.0
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 이 파일로 의존성을 설치하려면 &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;를 실행하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Hello World API 서버 작성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. main.py 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 &lt;code&gt;main.py&lt;/code&gt; 파일을 만든다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from fastapi import FastAPI

app = FastAPI()


@app.get(&quot;/&quot;)
async def root():
    return {&quot;message&quot;: &quot;Hello, FastAPI!&quot;}


@app.get(&quot;/ping&quot;)
async def ping():
    return {&quot;message&quot;: &quot;pong&quot;}


@app.get(&quot;/hello/{name}&quot;)
async def hello(name: str):
    return {&quot;hello&quot;: name}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 하나씩 짚어 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FastAPI()&lt;/code&gt;로 애플리케이션 인스턴스를 생성한다. 이 객체가 라우트, 미들웨어, 이벤트 핸들러 등 모든 설정의 진입점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@app.get(&quot;/&quot;)&lt;/code&gt;은 데코레이터로 GET 요청에 대한 경로를 등록한다. 함수 이름은 자유롭게 지정할 수 있고, 반환값은 자동으로 JSON 직렬화된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@app.get(&quot;/hello/{name}&quot;)&lt;/code&gt;에서 &lt;code&gt;{name}&lt;/code&gt;은 경로 파라미터다. 함수 인자 &lt;code&gt;name: str&lt;/code&gt;의 타입 힌트를 FastAPI가 읽어서 자동 검증과 문서화에 사용한다. 5편의 Gin 예제에서 &lt;code&gt;:name&lt;/code&gt;으로 작성했던 것과 같은 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;async def&lt;/code&gt;를 사용한 이유는 FastAPI가 비동기 함수를 네이티브로 지원하기 때문이다. 동기 함수(&lt;code&gt;def&lt;/code&gt;)로 작성해도 동작하지만, I/O 바운드 작업에서는 &lt;code&gt;async&lt;/code&gt;가 성능상 유리하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 현재 프로젝트 구조&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;my-fastapi-app/
├── .venv/                &amp;larr; 가상환경 (git에 포함하지 않음)
├── main.py
└── requirements.txt&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 서버 실행 &amp;amp; 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 서버 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상환경이 활성화된 상태에서 Uvicorn으로 서버를 시작한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;uvicorn main:app --reload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main:app&lt;/code&gt;에서 &lt;code&gt;main&lt;/code&gt;은 파일명(&lt;code&gt;main.py&lt;/code&gt;), &lt;code&gt;app&lt;/code&gt;은 FastAPI 인스턴스 변수명이다. &lt;code&gt;--reload&lt;/code&gt;는 코드 변경 시 서버를 자동 재시작하는 개발 모드 옵션이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [xxxxx] using StatReload
INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     Application startup complete.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. API 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 PowerShell 탭을 열어서 요청을 보낸다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl http://localhost:8000/&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;message&quot;:&quot;Hello, FastAPI!&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl http://localhost:8000/ping&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;message&quot;:&quot;pong&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl http://localhost:8000/hello/pythonista&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;hello&quot;:&quot;pythonista&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. 자동 생성 API 문서 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI의 킬러 기능 중 하나는 &lt;b&gt;자동 API 문서 생성&lt;/b&gt;이다. 코드에 작성한 라우트와 타입 힌트를 기반으로 인터랙티브 문서가 자동으로 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;code&gt;http://localhost:8000/docs&lt;/code&gt;를 열면 &lt;b&gt;Swagger UI&lt;/b&gt;가 표시된다. 각 엔드포인트를 펼쳐서 &quot;Try it out&quot; 버튼으로 바로 요청을 보내볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;http://localhost:8000/redoc&lt;/code&gt;을 열면 &lt;b&gt;ReDoc&lt;/b&gt; 스타일의 문서가 표시된다. 읽기 전용이지만 레이아웃이 더 깔끔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 설정이나 코드를 작성하지 않아도 이 문서가 자동 생성된다는 점이 FastAPI의 가장 큰 장점이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-4. 서버 종료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 실행 중인 터미널에서 &lt;code&gt;Ctrl+C&lt;/code&gt;를 누르면 종료된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 포트 정리: Go 서버와 Python 서버&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재까지 두 개의 백엔드 서버를 세팅했다. 각각 다른 포트에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go + Gin 서버는 &lt;code&gt;:8080&lt;/code&gt;, Python + FastAPI 서버는 &lt;code&gt;:8000&lt;/code&gt;이다. 프론트엔드 Vite 개발 서버는 &lt;code&gt;:5173&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 클라우드 서버에 배포할 때 Caddy(11편)가 리버스 프록시로 이 포트들을 하나의 도메인 아래 묶어주게 된다. 예를 들어 &lt;code&gt;/api/go/*&lt;/code&gt;는 &lt;code&gt;:8080&lt;/code&gt;으로, &lt;code&gt;/api/py/*&lt;/code&gt;는 &lt;code&gt;:8000&lt;/code&gt;으로 라우팅하는 식이다. 지금은 로컬에서 각 서버가 서로 다른 포트에서 충돌 없이 동작하는 것만 확인하면 충분하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. .gitignore 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 프로젝트에서 Git에 포함하면 안 되는 파일과 디렉토리를 &lt;code&gt;.gitignore&lt;/code&gt;에 등록한다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;New-Item -Path &quot;.gitignore&quot; -ItemType File&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Python
__pycache__/
*.py[cod]
*.pyo
*.egg-info/
dist/
build/

# 가상환경
.venv/

# IDE
.vscode/
.idea/

# 환경변수
.env&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 최종 확인 체크리스트&lt;/h2&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 1. pyenv &amp;amp; Python 확인
pyenv --version
python --version
pip --version

# 2. 프로젝트 이동 &amp;amp; 가상환경 활성화
cd my-fastapi-app
.\.venv\Scripts\Activate.ps1

# 3. FastAPI &amp;amp; Uvicorn 설치 확인
pip show fastapi
pip show uvicorn

# 4. 서버 실행
uvicorn main:app --reload
# &amp;rarr; http://127.0.0.1:8000 에서 서버 시작

# 5. 새 터미널에서 API 테스트
curl http://localhost:8000/
# &amp;rarr; {&quot;message&quot;:&quot;Hello, FastAPI!&quot;}

curl http://localhost:8000/ping
# &amp;rarr; {&quot;message&quot;:&quot;pong&quot;}

curl http://localhost:8000/hello/pythonista
# &amp;rarr; {&quot;hello&quot;:&quot;pythonista&quot;}

# 6. 자동 API 문서 확인
# &amp;rarr; 브라우저에서 http://localhost:8000/docs 열기 (Swagger UI)
# &amp;rarr; 브라우저에서 http://localhost:8000/redoc 열기 (ReDoc)&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;확인 항목&lt;/th&gt;
&lt;th&gt;기대 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pyenv --version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyenv 3.1.1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python --version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Python 3.13.12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip --version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip 24.x.x&lt;/code&gt; (가상환경 경로 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip show fastapi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Version: 0.135.x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip show uvicorn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Version: 0.42.x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uvicorn main:app --reload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;에서 서버 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;message&quot;:&quot;Hello, FastAPI!&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /ping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;message&quot;:&quot;pong&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /hello/pythonista&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;hello&quot;:&quot;pythonista&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http://localhost:8000/docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Swagger UI 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 원클릭 자동화 스크립트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 PowerShell 스크립트로 pyenv-win 설치 확인부터 Python 설치, 가상환경 생성, FastAPI 프로젝트 세팅까지 자동화할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10-1. 스크립트 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 내용을 &lt;code&gt;setup-fastapi.ps1&lt;/code&gt;로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;powershell&quot;&gt;&lt;code&gt;&amp;lt;#
  setup-fastapi.ps1
  6편 자동화 스크립트: Python + FastAPI + Uvicorn 환경 구축
  실행: PowerShell 7에서 .\setup-fastapi.ps1
  인자: -ProjectName &amp;lt;이름&amp;gt;      (기본값: my-fastapi-app)
        -PythonVersion &amp;lt;버전&amp;gt;    (기본값: 3.13.12)
#&amp;gt;

param(
    [string]$ProjectName = &quot;my-fastapi-app&quot;,
    [string]$PythonVersion = &quot;3.13.12&quot;
)

Set-StrictMode -Version Latest
$ErrorActionPreference = &quot;Stop&quot;

function Write-Step {
    param([string]$Message)
    Write-Host &quot;`n========================================&quot; -ForegroundColor Cyan
    Write-Host &quot; $Message&quot; -ForegroundColor Cyan
    Write-Host &quot;========================================&quot; -ForegroundColor Cyan
}

# ─────────────────────────────────────────────
# 1. pyenv-win 설치 확인
# ─────────────────────────────────────────────
Write-Step &quot;1/8 pyenv-win 확인&quot;

if (-not (Get-Command pyenv -ErrorAction SilentlyContinue)) {
    Write-Host &quot;pyenv-win이 설치되어 있지 않습니다. 설치합니다...&quot; -ForegroundColor Yellow
    Invoke-WebRequest -UseBasicParsing `
        -Uri &quot;https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1&quot; `
        -OutFile &quot;./install-pyenv-win.ps1&quot;
    &amp;amp; &quot;./install-pyenv-win.ps1&quot;
    Remove-Item &quot;./install-pyenv-win.ps1&quot; -ErrorAction SilentlyContinue

    # PATH 새로고침
    $env:Path = [System.Environment]::GetEnvironmentVariable(&quot;Path&quot;, &quot;Machine&quot;) + &quot;;&quot; + [System.Environment]::GetEnvironmentVariable(&quot;Path&quot;, &quot;User&quot;)

    if (-not (Get-Command pyenv -ErrorAction SilentlyContinue)) {
        Write-Host &quot;pyenv-win 설치 후 터미널을 재시작해야 합니다. 스크립트를 종료합니다.&quot; -ForegroundColor Red
        exit 1
    }
}
Write-Host &quot;pyenv version: $(pyenv --version)&quot; -ForegroundColor Green

# ─────────────────────────────────────────────
# 2. 버전 목록 갱신
# ─────────────────────────────────────────────
Write-Step &quot;2/8 pyenv 버전 목록 갱신&quot;

pyenv update 2&amp;gt;&amp;amp;1 | Out-Null
Write-Host &quot;버전 목록 갱신 완료&quot; -ForegroundColor Green

# ─────────────────────────────────────────────
# 3. Python 설치
# ─────────────────────────────────────────────
Write-Step &quot;3/8 Python $PythonVersion 설치&quot;

$installedVersions = pyenv versions 2&amp;gt;&amp;amp;1 | Out-String
if ($installedVersions -match [regex]::Escape($PythonVersion)) {
    Write-Host &quot;Python $PythonVersion 이미 설치되어 있습니다.&quot; -ForegroundColor Yellow
} else {
    Write-Host &quot;Python $PythonVersion 설치 중... (시간이 걸릴 수 있습니다)&quot; -ForegroundColor Yellow
    pyenv install $PythonVersion -q
}

pyenv global $PythonVersion
pyenv rehash

# PATH 새로고침 (shims 반영)
$env:Path = [System.Environment]::GetEnvironmentVariable(&quot;Path&quot;, &quot;Machine&quot;) + &quot;;&quot; + [System.Environment]::GetEnvironmentVariable(&quot;Path&quot;, &quot;User&quot;)

Write-Host &quot;python: $(python --version)&quot; -ForegroundColor Green
Write-Host &quot;pip:    $(pip --version)&quot; -ForegroundColor Green

# ─────────────────────────────────────────────
# 4. 프로젝트 디렉토리 생성
# ─────────────────────────────────────────────
Write-Step &quot;4/8 프로젝트 생성: $ProjectName&quot;

if (Test-Path $ProjectName) {
    Write-Host &quot;'$ProjectName' 폴더가 이미 존재합니다. 건너뜁니다.&quot; -ForegroundColor Yellow
} else {
    New-Item -ItemType Directory -Path $ProjectName | Out-Null
}

Set-Location $ProjectName

# ─────────────────────────────────────────────
# 5. 가상환경 생성 &amp;amp; 활성화
# ─────────────────────────────────────────────
Write-Step &quot;5/8 가상환경 생성&quot;

if (-not (Test-Path &quot;.venv&quot;)) {
    python -m venv .venv
    Write-Host &quot;.venv 생성 완료&quot; -ForegroundColor Green
} else {
    Write-Host &quot;.venv가 이미 존재합니다.&quot; -ForegroundColor Yellow
}

# 활성화
&amp;amp; &quot;.\.venv\Scripts\Activate.ps1&quot;
Write-Host &quot;가상환경 활성화됨: $(python -c 'import sys; print(sys.executable)')&quot; -ForegroundColor Green

# ─────────────────────────────────────────────
# 6. FastAPI + Uvicorn 설치
# ─────────────────────────────────────────────
Write-Step &quot;6/8 FastAPI + Uvicorn 설치&quot;

pip install --upgrade pip 2&amp;gt;&amp;amp;1 | Out-Null
pip install fastapi &quot;uvicorn[standard]&quot;

Write-Host &quot;fastapi:  $(pip show fastapi | Select-String 'Version')&quot; -ForegroundColor Green
Write-Host &quot;uvicorn:  $(pip show uvicorn | Select-String 'Version')&quot; -ForegroundColor Green

# requirements.txt 생성
pip freeze &amp;gt; requirements.txt
Write-Host &quot;requirements.txt 생성 완료&quot; -ForegroundColor Green

# ─────────────────────────────────────────────
# 7. 소스 파일 생성
# ─────────────────────────────────────────────
Write-Step &quot;7/8 소스 파일 생성&quot;

# main.py
$mainPy = @'
from fastapi import FastAPI

app = FastAPI()


@app.get(&quot;/&quot;)
async def root():
    return {&quot;message&quot;: &quot;Hello, FastAPI!&quot;}


@app.get(&quot;/ping&quot;)
async def ping():
    return {&quot;message&quot;: &quot;pong&quot;}


@app.get(&quot;/hello/{name}&quot;)
async def hello(name: str):
    return {&quot;hello&quot;: name}
'@

Set-Content -Path &quot;main.py&quot; -Value $mainPy -Encoding UTF8
Write-Host &quot;  main.py&quot; -ForegroundColor Gray

# .gitignore
$gitignore = @'
# Python
__pycache__/
*.py[cod]
*.pyo
*.egg-info/
dist/
build/

# 가상환경
.venv/

# IDE
.vscode/
.idea/

# 환경변수
.env
'@

Set-Content -Path &quot;.gitignore&quot; -Value $gitignore -Encoding UTF8
Write-Host &quot;  .gitignore&quot; -ForegroundColor Gray

Write-Host &quot;소스 파일 생성 완료&quot; -ForegroundColor Green

# ─────────────────────────────────────────────
# 8. 빌드(import) 테스트
# ─────────────────────────────────────────────
Write-Step &quot;8/8 FastAPI import 테스트&quot;

$testResult = python -c &quot;from fastapi import FastAPI; app = FastAPI(); print('FastAPI import OK')&quot; 2&amp;gt;&amp;amp;1
if ($testResult -match &quot;OK&quot;) {
    Write-Host $testResult -ForegroundColor Green
} else {
    Write-Host &quot;FastAPI import 실패. 에러 로그를 확인하세요.&quot; -ForegroundColor Red
    Write-Host $testResult -ForegroundColor Red
    exit 1
}

# ─────────────────────────────────────────────
# 완료 메시지
# ─────────────────────────────────────────────
Write-Host &quot;`n&quot; -NoNewline
Write-Host &quot;============================================&quot; -ForegroundColor Green
Write-Host &quot; Python + FastAPI 프로젝트 세팅 완료!&quot; -ForegroundColor Green
Write-Host &quot;============================================&quot; -ForegroundColor Green
Write-Host &quot;&quot;
Write-Host &quot;  프로젝트 경로:     $(Get-Location)&quot; -ForegroundColor White
Write-Host &quot;  Python 버전:       $(python --version)&quot; -ForegroundColor White
Write-Host &quot;  가상환경 활성화:   .\.venv\Scripts\Activate.ps1&quot; -ForegroundColor White
Write-Host &quot;  개발 서버 실행:    uvicorn main:app --reload&quot; -ForegroundColor White
Write-Host &quot;  API 문서:          http://localhost:8000/docs&quot; -ForegroundColor White
Write-Host &quot;  테스트 요청:       curl http://localhost:8000/ping&quot; -ForegroundColor White
Write-Host &quot;&quot;
Write-Host &quot;  현재 프로젝트 구조:&quot; -ForegroundColor White
Write-Host &quot;  $ProjectName/&quot; -ForegroundColor Gray
Write-Host &quot;  ├── .venv/&quot; -ForegroundColor Gray
Write-Host &quot;  ├── .gitignore&quot; -ForegroundColor Gray
Write-Host &quot;  ├── main.py&quot; -ForegroundColor Gray
Write-Host &quot;  └── requirements.txt&quot; -ForegroundColor Gray
Write-Host &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10-2. 스크립트 실행&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;# 기본값으로 생성
.\setup-fastapi.ps1

# 커스텀 프로젝트 이름과 Python 버전 지정
.\setup-fastapi.ps1 -ProjectName &quot;my-api-server&quot; -PythonVersion &quot;3.13.12&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트가 완료되면 8단계 모두 녹색 메시지로 결과가 출력된다. 가상환경이 활성화된 상태로 마무리되므로, 바로 &lt;code&gt;uvicorn main:app --reload&lt;/code&gt;로 개발 서버를 시작할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7편: Docker Desktop 설치 &amp;amp; 기본 사용법&lt;/b&gt;에서는 Docker Desktop을 윈도우에 설치하고, 이미지&amp;middot;컨테이너&amp;middot;볼륨 등 Docker의 핵심 개념을 실습한다. 8편부터는 PostgreSQL, MinIO, n8n을 각각 Docker 컨테이너로 띄우게 되므로, 7편에서 Docker의 기본기를 확실히 다져 두는 것이 중요하다.&lt;/p&gt;</description>
      <category>Windows 개발환경 세팅</category>
      <author>polarcompass</author>
      <guid isPermaLink="true">https://polarcompass.tistory.com/312</guid>
      <comments>https://polarcompass.tistory.com/312#entry312comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:25:22 +0900</pubDate>
    </item>
  </channel>
</rss>