<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>CHHB stroy</title>
    <link>https://chhb-miscellaneous.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 10 Jun 2026 06:08:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>CHHB</managingEditor>
    <item>
      <title>Claude Code로 레거시 PHP 리팩토링하기 &amp;mdash; 실제로 해보고 정리한 워크플로우</title>
      <link>https://chhb-miscellaneous.tistory.com/47</link>
      <description>&lt;p&gt;회사에 10년 묵은 PHP 프로젝트가 하나 있다. PHP 5 시절에 짠 코드가 그대로 살아있고, 함수 하나가 500줄씩 되고, 전역 변수가 사방에 깔려있고, SQL이 코드에 그냥 박혀있는 그런 코드. 손대기 무서워서 다들 피하던 녀석인데, Claude Code로 조금씩 리팩토링해봤더니 생각보다 할 만했다.&lt;/p&gt;
&lt;p&gt;물론 &amp;quot;Claude한테 던지면 알아서 다 해주겠지&amp;quot; 하면 큰코다친다. 레거시 리팩토링은 AI한테도 어렵다. 근데 제대로 된 워크플로우로 접근하면 혼자 할 때보다 훨씬 빠르고 안전하게 할 수 있다. 오늘은 내가 시행착오 끝에 정리한 PHP 리팩토링 방법을 공유한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시작 전에: 리팩토링의 대전제&lt;/h2&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;1. Git 커밋 — 깨끗한 상태에서 시작. 언제든 되돌릴 수 있게.
2. 테스트 — 리팩토링 전후 동작이 같은지 검증할 수단.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;테스트가 없는 레거시 코드라면? 그게 대부분의 현실이긴 하다. 이 경우엔 리팩토링 전에 &lt;strong&gt;먼저 테스트부터 만드는 것&lt;/strong&gt;이 순서다. 이것도 Claude Code가 도와줄 수 있다. 뒤에서 다룬다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 0: CLAUDE.md부터 세팅&lt;/h2&gt;
&lt;p&gt;본격적으로 시작하기 전에, 프로젝트 루트에 &lt;code&gt;CLAUDE.md&lt;/code&gt;를 만들어두자. 리팩토링은 일관성이 생명인데, Claude가 매번 다른 스타일로 고치면 오히려 코드가 더 엉망이 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# CLAUDE.md

## 프로젝트
레거시 PHP 프로젝트를 점진적으로 현대화하는 중입니다.
현재 PHP 8.1에서 동작하지만 코드는 PHP 5 스타일이 많습니다.

## 리팩토링 원칙
- 동작을 절대 바꾸지 말 것 (기능 변경 금지, 구조만 개선)
- 한 번에 하나씩만 변경 (큰 변경을 여러 작은 변경으로 쪼갤 것)
- 변경 전 반드시 동작을 설명하고, 변경 후 무엇이 달라졌는지 명시

## 코딩 컨벤션
- PSR-12 코딩 스타일 준수
- 타입 힌트 적극 사용 (파라미터, 반환 타입)
- 함수명은 camelCase, 클래스명은 PascalCase
- mysqli 직접 호출 대신 준비된 구문(prepared statement) 사용
- 새 코드에는 PHPDoc 주석 추가

## 하지 말 것
- 검증 안 된 라이브러리 임의 추가 금지 (먼저 물어볼 것)
- 한 번에 여러 파일을 대대적으로 수정하지 말 것
- SQL 인젝션 위험이 있는 코드는 발견 즉시 알릴 것&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 해두면 매번 &amp;quot;타입 힌트 넣어줘&amp;quot;, &amp;quot;PSR-12로 해줘&amp;quot;를 반복하지 않아도 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 1: 코드 파악부터 시키기&lt;/h2&gt;
&lt;p&gt;레거시 코드는 일단 뭐가 뭔지 모르는 게 문제다. 바로 고치라고 하지 말고, 먼저 분석부터 시킨다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@legacy/UserManager.php 이 파일을 분석해줘.
- 이 클래스가 하는 일이 뭔지
- 주요 메서드와 각각의 역할
- 코드에서 보이는 문제점 (긴 함수, 중복, 보안 위험, 안티패턴 등)
- 리팩토링이 시급한 부분 우선순위

아직 코드는 수정하지 말고 분석만 해줘.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;quot;수정하지 말고 분석만&amp;quot;이라고 못 박는 게 중요하다. 안 그러면 분석하다가 바로 코드를 뜯어고치기 시작한다. 우선 전체 그림을 파악하는 게 먼저다.&lt;/p&gt;
&lt;p&gt;Claude가 분석 결과를 주면, 그걸 보고 어디부터 손댈지 내가 판단한다. 보통 이런 것들이 우선순위로 올라온다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL 인젝션 같은 보안 위험 (최우선)&lt;/li&gt;
&lt;li&gt;500줄짜리 거대 함수&lt;/li&gt;
&lt;li&gt;같은 코드가 복붙된 중복&lt;/li&gt;
&lt;li&gt;전역 변수 남발&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Step 2: 테스트부터 만들기 (테스트가 없다면)&lt;/h2&gt;
&lt;p&gt;리팩토링하려는데 테스트가 없으면, 리팩토링이 코드를 망가뜨렸는지 알 방법이 없다. 그래서 먼저 &lt;strong&gt;현재 동작을 고정하는 테스트(characterization test)&lt;/strong&gt;를 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@legacy/PriceCalculator.php 의 calculate() 메서드에 대한 테스트를 작성해줘.

지금 이 메서드가 어떻게 동작하는지를 그대로 검증하는 테스트가 필요해.
(코드가 &amp;quot;올바른지&amp;quot;가 아니라, &amp;quot;현재 어떻게 동작하는지&amp;quot;를 고정하는 목적)

- PHPUnit 사용
- 정상 케이스, 경계값, 0이나 음수 같은 엣지 케이스 포함
- 외부 의존성(DB 등)은 모킹 처리&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 테스트의 목적은 코드가 &amp;quot;맞는지&amp;quot;를 검증하는 게 아니라, &lt;strong&gt;현재 동작을 그대로 박제&lt;/strong&gt;하는 거다. 리팩토링 후에 이 테스트가 깨지면 동작이 바뀐 거니까 뭔가 잘못된 거다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 테스트 실행해서 통과 확인
./vendor/bin/phpunit tests/PriceCalculatorTest.php&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;테스트가 통과하는 걸 확인하고 나서야 리팩토링을 시작한다. 이게 안전망이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 3: 작게, 하나씩 리팩토링&lt;/h2&gt;
&lt;p&gt;여기서 제일 중요한 원칙. &lt;strong&gt;한 번에 하나씩&lt;/strong&gt;. &amp;quot;이 파일 전체를 현대적으로 리팩토링해줘&amp;quot;라고 하면 Claude가 100군데를 한꺼번에 바꿔놓는데, 뭐가 잘못됐는지 추적이 안 된다.&lt;/p&gt;
&lt;h3&gt;예시 1: 거대 함수 쪼개기&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@legacy/OrderProcessor.php 의 processOrder() 메서드가 320줄이야.
이걸 작은 메서드들로 분리해줘.

- 각 메서드는 한 가지 일만 하도록
- 검증 / 재고 확인 / 결제 / 알림 같은 논리적 단위로 나눠줘
- 동작은 절대 바꾸지 말고 구조만 분리
- 분리 후 processOrder()는 각 메서드를 순서대로 호출하는 형태로&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;변경이 끝나면 diff를 확인하고, 테스트를 돌려본다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./vendor/bin/phpunit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;테스트 통과하면 커밋. 안 되면 되돌리고 다시.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git add .
git commit -m &amp;quot;refactor: processOrder 메서드를 논리 단위로 분리&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;커밋을 자주 하는 게 핵심이다.&lt;/strong&gt; 변경 하나 = 커밋 하나. 그래야 문제 생겼을 때 정확히 어디서 깨졌는지 알 수 있다.&lt;/p&gt;
&lt;h3&gt;예시 2: SQL 인젝션 제거&lt;/h3&gt;
&lt;p&gt;레거시 PHP에서 제일 흔하고 위험한 패턴이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@legacy/SearchController.php 에서 SQL 인젝션 위험이 있는 쿼리를 찾아서
준비된 구문(prepared statement)으로 바꿔줘.

- mysqli 또는 PDO의 prepared statement 사용
- 사용자 입력이 직접 쿼리에 들어가는 부분을 전부 파라미터 바인딩으로
- 동작은 동일하게 유지&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이런 건 Claude가 정말 잘 잡아준다. 문자열 연결로 만든 쿼리를 파라미터 바인딩 방식으로 깔끔하게 바꿔준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Before (위험)
$query = &amp;quot;SELECT * FROM users WHERE name = &amp;#39;&amp;quot; . $_GET[&amp;#39;name&amp;#39;] . &amp;quot;&amp;#39;&amp;quot;;
$result = mysqli_query($conn, $query);

// After (안전)
$stmt = $conn-&amp;gt;prepare(&amp;quot;SELECT * FROM users WHERE name = ?&amp;quot;);
$stmt-&amp;gt;bind_param(&amp;quot;s&amp;quot;, $_GET[&amp;#39;name&amp;#39;]);
$stmt-&amp;gt;execute();
$result = $stmt-&amp;gt;get_result();&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;예시 3: 타입 힌트 추가&lt;/h3&gt;
&lt;p&gt;PHP 5 코드에는 타입 선언이 없다. PHP 8 시대에는 타입을 명시하는 게 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@src/Service/ 폴더의 모든 메서드에 타입 힌트를 추가해줘.
- 파라미터 타입과 반환 타입 모두
- nullable이 필요한 경우 ?type 사용
- 기존 동작을 깨지 않는 선에서만 (확신 안 서면 표시해줘)
- strict_types declare 추가&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;quot;확신 안 서면 표시해줘&amp;quot;가 포인트다. 레거시 코드는 같은 변수에 여러 타입이 들어가는 경우가 있어서, 무작정 타입을 박으면 오히려 깨진다. Claude가 애매한 부분을 표시해주면 내가 직접 판단한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 4: 중복 코드 제거&lt;/h2&gt;
&lt;p&gt;레거시 PHP의 단골 문제. 복붙으로 떡칠된 코드.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;프로젝트 전체에서 중복되는 코드 패턴을 찾아줘.
특히 DB 연결, 입력 검증, 응답 포맷팅 같은 게 여러 곳에 반복되는지.

찾으면 공통 함수나 클래스로 추출할 수 있는지 제안해줘.
(바로 수정하지 말고 먼저 제안만)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;제안을 보고 괜찮으면 하나씩 추출한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;방금 제안한 것 중에서 DB 연결 코드를 Database 클래스로 추출해줘.
싱글톤 패턴으로, 한 곳에서 연결을 관리하도록.
기존에 직접 연결하던 코드들은 이 클래스를 쓰도록 바꿔줘.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이런 작업은 여러 파일에 걸치니까, 변경 후 반드시 전체 테스트를 돌려야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 5: 점진적 현대화&lt;/h2&gt;
&lt;p&gt;한 방에 다 바꾸려 하지 말고, PHP 버전 기능을 활용해서 점진적으로 개선한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@src/Model/User.php 를 현대적인 PHP 8 스타일로 개선해줘.

- 생성자 프로퍼티 승격(constructor property promotion) 활용
- 적절한 곳에 readonly 프로퍼티
- match 표현식으로 바꿀 수 있는 switch가 있으면 변경
- enum으로 바꿀 수 있는 상수 그룹이 있으면 제안
- 단, 동작은 동일하게&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;PHP 8의 기능들(생성자 프로퍼티 승격, match, enum, named arguments 등)을 활용하면 코드가 훨씬 간결해진다. Claude가 이런 변환을 잘한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Before (PHP 5 스타일)
class User {
    private $name;
    private $email;
    public function __construct($name, $email) {
        $this-&amp;gt;name = $name;
        $this-&amp;gt;email = $email;
    }
}

// After (PHP 8 스타일)
class User {
    public function __construct(
        private readonly string $name,
        private readonly string $email,
    ) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;실전 워크플로우 요약&lt;/h2&gt;
&lt;p&gt;내가 실제로 도는 사이클은 이렇다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 분석 시키기 (수정 금지, 분석만)
2. 테스트 없으면 → 현재 동작 고정 테스트 작성
3. 작은 단위 하나 리팩토링 요청
4. diff 확인
5. 테스트 실행
6. 통과하면 커밋 / 실패하면 되돌리고 재시도
7. 다음 단위로 (3번부터 반복)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 사이클을 짧게 반복하는 게 핵심이다. 큰 변경 하나보다, 작은 변경 열 개가 훨씬 안전하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Plan Mode를 적극 활용하자&lt;/h2&gt;
&lt;p&gt;큰 리팩토링은 Claude Code의 &lt;strong&gt;Plan Mode&lt;/strong&gt;로 계획을 먼저 받는 게 좋다. 바로 코드를 고치는 게 아니라 &amp;quot;이렇게 할 거다&amp;quot;라는 계획을 보여주니까, 방향이 틀어지기 전에 잡을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@legacy/ReportGenerator.php 전체를 리팩토링하고 싶어. (Plan Mode)

먼저 어떤 순서로 어떻게 리팩토링할지 계획을 세워줘.
실행은 내가 승인한 다음에.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;계획을 보고 &amp;quot;3번은 빼고&amp;quot;, &amp;quot;이건 순서 바꿔서&amp;quot; 같은 조정을 한 뒤에 실행시키면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;주의할 점들&lt;/h2&gt;
&lt;h3&gt;1. 무조건 diff를 확인하자&lt;/h3&gt;
&lt;p&gt;AI가 아무리 잘해도 무조건 수락은 금물이다. 특히 레거시 코드는 겉보기엔 이상한데 실제론 의도된 동작인 경우가 있다(이상한 if문이 사실은 특정 버그 회피용이라든지). diff를 보면서 &amp;quot;이거 왜 이렇게 바꿨지?&amp;quot; 싶으면 물어보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;방금 이 조건문을 제거했는데, 원래 코드에서 이게 있던 이유가 뭐였을 것 같아?
혹시 특정 엣지 케이스를 처리하던 건 아닐까?&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;2. 테스트 없이 리팩토링하지 말자&lt;/h3&gt;
&lt;p&gt;다시 강조한다. 테스트 없는 리팩토링은 눈 감고 운전하는 거다. 최소한 핵심 로직만이라도 테스트를 만들고 시작하자.&lt;/p&gt;
&lt;h3&gt;3. 한 번에 너무 많이 시키지 말자&lt;/h3&gt;
&lt;p&gt;&amp;quot;전체 프로젝트 리팩토링해줘&amp;quot;는 재앙의 지름길이다. 파일 하나, 메서드 하나 단위로 끊어서 진행하자. 컨텍스트도 관리되고, 문제 추적도 쉽다.&lt;/p&gt;
&lt;h3&gt;4. 동작 변경과 구조 변경을 섞지 말자&lt;/h3&gt;
&lt;p&gt;리팩토링(구조 변경)과 기능 추가(동작 변경)를 같은 커밋에서 하면 안 된다. 리팩토링 먼저 끝내고 테스트로 검증한 다음, 별도로 기능을 추가하자. 섞으면 문제 생겼을 때 원인 파악이 지옥이 된다.&lt;/p&gt;
&lt;h3&gt;5. 커밋 메시지를 Claude한테 맡겨도 된다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;지금까지 변경한 내용으로 커밋해줘. 
conventional commit 형식으로, refactor: 접두사 사용해서.&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;레거시 PHP 리팩토링은 여전히 신경 쓸 게 많은 작업이지만, Claude Code와 함께라면 훨씬 수월해진다. 핵심은 &amp;quot;AI한테 다 맡기기&amp;quot;가 아니라 &amp;quot;AI를 잘 부리기&amp;quot;다.&lt;/p&gt;
&lt;p&gt;정리하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;로 리팩토링 원칙과 컨벤션을 미리 박아두자&lt;/li&gt;
&lt;li&gt;바로 고치지 말고 분석부터 시키자&lt;/li&gt;
&lt;li&gt;테스트 없으면 현재 동작 고정 테스트부터 만들자&lt;/li&gt;
&lt;li&gt;작게, 하나씩, 자주 커밋&lt;/li&gt;
&lt;li&gt;큰 작업은 Plan Mode로 계획 먼저&lt;/li&gt;
&lt;li&gt;diff는 항상 확인. 무조건 수락 금지&lt;/li&gt;
&lt;li&gt;구조 변경과 기능 변경을 섞지 말자&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI코딩</category>
      <category>Anthropic</category>
      <category>Claude</category>
      <category>claudecode</category>
      <category>php</category>
      <category>PHP리팩토링</category>
      <category>리팩토링</category>
      <category>코드리팩토링</category>
      <category>코딩에이전트</category>
      <category>클로드코드</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/47</guid>
      <comments>https://chhb-miscellaneous.tistory.com/47#entry47comment</comments>
      <pubDate>Fri, 5 Jun 2026 09:30:01 +0900</pubDate>
    </item>
    <item>
      <title>IntelliJ에서 Claude Code 쓰는 법 &amp;mdash; JetBrains 유저를 위한 완전 가이드</title>
      <link>https://chhb-miscellaneous.tistory.com/46</link>
      <description>&lt;p&gt;Claude Code 관련 글이 대부분 VSCode 기준이라, IntelliJ 쓰는 사람 입장에서는 좀 답답했다. 나처럼 백엔드 Java/Kotlin 개발하면서 IntelliJ를 메인으로 쓰는 사람들 많을 텐데. 그래서 JetBrains IDE에서 Claude Code를 어떻게 세팅하고 쓰는지 정리해봤다.&lt;/p&gt;
&lt;p&gt;결론부터 말하면, IntelliJ에서도 Claude Code 잘 된다. VSCode랑 거의 똑같은 경험을 준다. diff를 IDE 안에서 보고, 선택한 코드를 자동으로 공유하고, 에러까지 실시간으로 Claude한테 넘겨준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시작하기 전에 알아둘 것&lt;/h2&gt;
&lt;p&gt;Claude Code의 JetBrains 플러그인은 &lt;strong&gt;단독으로 동작하는 게 아니다&lt;/strong&gt;. Claude Code CLI가 먼저 설치돼 있어야 하고, 플러그인은 그 위에서 IDE 통합 기능을 추가해주는 역할이다.&lt;/p&gt;
&lt;p&gt;그러니까 순서가 이렇다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Claude Code CLI 설치&lt;/li&gt;
&lt;li&gt;JetBrains 플러그인 설치&lt;/li&gt;
&lt;li&gt;둘을 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 구조를 모르면 &amp;quot;플러그인 깔았는데 왜 안 되지?&amp;quot; 하면서 헤맨다. 나도 처음에 플러그인만 깔고 한참 삽질했다.&lt;/p&gt;
&lt;h3&gt;지원하는 IDE&lt;/h3&gt;
&lt;p&gt;JetBrains 계열은 대부분 지원한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- IntelliJ IDEA
- PyCharm
- WebStorm
- PhpStorm
- GoLand
- Android Studio
- RubyMine, CLion 등 기타 JetBrains IDE&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;필요한 것&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code CLI&lt;/li&gt;
&lt;li&gt;Anthropic 유료 계정 (Pro $20/월 이상)&lt;/li&gt;
&lt;li&gt;JetBrains IDE (가급적 최신 버전)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Step 1: Claude Code CLI 설치&lt;/h2&gt;
&lt;p&gt;플러그인보다 CLI가 먼저다.&lt;/p&gt;
&lt;h3&gt;macOS / Linux&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://cli.claude.ai/install.sh | sh

# 설치 확인
claude --version&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;npm으로 설치&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @anthropic-ai/claude-code
claude --version&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows (WSL)&lt;/h3&gt;
&lt;p&gt;Claude Code는 Windows 네이티브를 지원하지 않아서 WSL에서 돌려야 한다. WSL 안에서 위의 설치 과정을 그대로 진행하면 된다.&lt;/p&gt;
&lt;h3&gt;첫 로그인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 프로젝트 폴더에서
cd ~/my-project
claude

# 브라우저가 열리면서 Anthropic 로그인 → 인증 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기까지 하면 터미널에서 Claude Code를 쓸 수 있다. 이제 IntelliJ와 연결할 차례.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 2: JetBrains 플러그인 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;1. IntelliJ 실행
2. Settings (Cmd+, / Ctrl+Alt+S) 열기
3. Plugins 메뉴
4. Marketplace 탭에서 &amp;quot;Claude Code&amp;quot; 검색
5. Anthropic 공식 플러그인 [Install]
6. IDE 완전히 재시작&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;중요&lt;/strong&gt;: 설치 후에는 IDE를 &lt;strong&gt;완전히 재시작&lt;/strong&gt;해야 한다. 그냥 창만 새로고침하는 게 아니라 IntelliJ를 완전히 종료했다가 다시 켜자. 이거 안 하면 기능이 안 나타나는 경우가 많다. 안 되면 두 번 재시작해보라는 게 공식 문서 권고일 정도다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&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;  마켓플레이스에 &amp;quot;Claude Code&amp;quot; 검색하면 비슷한 이름의 서드파티 플러그인이 여러 개 나온다(Claude Code Plus, Claude Code with GUI 등). 공식 플러그인은 Anthropic이 배포한 것이니 vendor를 확인하고 설치하자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;Step 3: CLI와 IDE 연결&lt;/h2&gt;
&lt;p&gt;플러그인을 설치했으면 이제 연결해야 한다. 방법이 두 가지다.&lt;/p&gt;
&lt;h3&gt;방법 A: IDE 내장 터미널에서 실행 (가장 간단)&lt;/h3&gt;
&lt;p&gt;IntelliJ의 내장 터미널(Alt+F12 / View → Tool Windows → Terminal)을 열고 &lt;code&gt;claude&lt;/code&gt;를 실행하면, 통합 기능이 자동으로 활성화된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# IntelliJ 내장 터미널에서
claude&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이게 제일 간단하다. 내장 터미널에서 실행했으니 IDE랑 자동으로 연결된다.&lt;/p&gt;
&lt;h3&gt;방법 B: 외부 터미널에서 /ide 명령어&lt;/h3&gt;
&lt;p&gt;별도 터미널에서 Claude Code를 돌리고 있다면, &lt;code&gt;/ide&lt;/code&gt; 명령어로 JetBrains IDE에 연결할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/ide&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;팁&lt;/strong&gt;: Claude가 IDE와 같은 파일에 접근하게 하려면, &lt;strong&gt;IDE 프로젝트 루트와 같은 디렉토리에서&lt;/strong&gt; Claude Code를 실행해야 한다. 엉뚱한 디렉토리에서 실행하면 다른 파일을 보게 된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;핵심 기능들&lt;/h2&gt;
&lt;p&gt;연결이 되면 VSCode 못지않은 통합 기능을 쓸 수 있다.&lt;/p&gt;
&lt;h3&gt;1. 빠른 실행 단축키&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Cmd+Esc (Mac)
Ctrl+Esc (Windows/Linux)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;에디터에서 바로 Claude Code를 열 수 있다. 또는 UI의 Claude Code 버튼을 클릭해도 된다.&lt;/p&gt;
&lt;h3&gt;2. IDE 안에서 diff 보기&lt;/h3&gt;
&lt;p&gt;Claude가 코드를 수정하면, 그 변경사항을 &lt;strong&gt;IntelliJ의 diff 뷰어&lt;/strong&gt;에서 볼 수 있다. 터미널에서 텍스트로 보는 것보다 훨씬 편하다. IntelliJ의 diff 뷰어는 워낙 잘 만들어져 있어서, 변경 전후를 나란히 비교하면서 검토할 수 있다.&lt;/p&gt;
&lt;p&gt;이 기능을 켜려면 설정에서:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. claude 실행
2. /config 명령어 입력
3. diff tool을 &amp;quot;auto&amp;quot;로 설정 (IDE에서 diff 표시)
   - &amp;quot;terminal&amp;quot;로 하면 터미널에서 diff 표시&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;3. 선택 영역 컨텍스트 자동 공유&lt;/h3&gt;
&lt;p&gt;에디터에서 코드를 선택하거나 특정 탭을 열어두면, 그 내용이 &lt;strong&gt;자동으로 Claude한테 공유&lt;/strong&gt;된다. &amp;quot;지금 보고 있는 이 코드 리팩토링해줘&amp;quot;라고 하면, Claude가 내가 선택한 코드를 알고 있다. 매번 코드를 복붙할 필요가 없다.&lt;/p&gt;
&lt;h3&gt;4. 파일 참조 단축키&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Cmd+Option+K (Mac)
Alt+Ctrl+K (Windows/Linux)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이걸 누르면 파일 참조를 프롬프트에 삽입할 수 있다. 예를 들어 &lt;code&gt;@src/auth.ts#L1-99&lt;/code&gt; 같은 형식으로 특정 파일의 특정 줄 범위를 정확히 가리킬 수 있다. 큰 파일에서 &amp;quot;이 부분만&amp;quot; 작업시킬 때 유용하다.&lt;/p&gt;
&lt;h3&gt;5. 진단 정보(에러) 자동 공유&lt;/h3&gt;
&lt;p&gt;IntelliJ가 잡아내는 lint 에러, 문법 에러 같은 진단 정보가 &lt;strong&gt;실시간으로 Claude한테 공유&lt;/strong&gt;된다. 빨간 줄 쳐진 에러를 Claude가 알고 있으니까, &amp;quot;이 에러 고쳐줘&amp;quot;라고만 해도 어떤 에러인지 안다. 이게 은근 편하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;플러그인 설정&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Settings → Tools → Claude Code&lt;/code&gt; 에서 세부 설정을 만질 수 있다.&lt;/p&gt;
&lt;h3&gt;Claude 명령어 경로 지정&lt;/h3&gt;
&lt;p&gt;Claude를 실행하는 커스텀 명령어를 지정할 수 있다. CLI가 PATH에 안 잡히는 경우 직접 경로를 넣어주면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude
/usr/local/bin/claude
npx @anthropic-ai/claude-code&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;WSL 사용자 설정&lt;/h3&gt;
&lt;p&gt;WSL에서 Claude Code를 쓴다면, 명령어를 이렇게 지정해야 한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl -d Ubuntu -- bash -lic &amp;quot;claude&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(Ubuntu는 본인의 WSL 배포판 이름으로 바꾸면 된다)&lt;/p&gt;
&lt;h3&gt;macOS에서 여러 줄 입력&lt;/h3&gt;
&lt;p&gt;기본적으로 Enter를 누르면 프롬프트가 전송된다. 여러 줄을 입력하고 싶으면 설정에서 &lt;code&gt;Option+Enter&lt;/code&gt;로 줄바꿈하는 옵션을 켤 수 있다. (변경 후 터미널 재시작 필요)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ESC 키가 안 먹힐 때&lt;/h2&gt;
&lt;p&gt;JetBrains 터미널에서 ESC 키로 Claude Code 작업을 중단하려는데 안 되는 경우가 있다. 이건 IntelliJ의 기본 단축키 설정 때문이다.&lt;/p&gt;
&lt;p&gt;해결 방법:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Settings → Tools → Terminal
2. 다음 중 하나:
   - &amp;quot;Move focus to the editor with Escape&amp;quot; 체크 해제
   - 또는 &amp;quot;Configure terminal keybindings&amp;quot;에서
     &amp;quot;Switch focus to Editor&amp;quot; 단축키 삭제
3. 변경사항 적용&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이렇게 하면 ESC가 IntelliJ의 포커스 이동이 아니라 Claude Code 중단으로 제대로 동작한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;원격 개발(Remote Development) 사용 시&lt;/h2&gt;
&lt;p&gt;JetBrains의 Remote Development 기능을 쓴다면 주의할 점이 있다. 플러그인을 &lt;strong&gt;로컬 클라이언트가 아니라 원격 호스트에 설치&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Settings → Plugin (Host) 에서 플러그인 설치&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로컬에 설치하면 안 된다. 원격 호스트에서 코드가 돌아가니까 플러그인도 거기 있어야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;WSL에서 &amp;quot;No available IDEs detected&amp;quot; 에러&lt;/h2&gt;
&lt;p&gt;WSL2 + JetBrains 조합에서 흔히 겪는 문제다. &lt;code&gt;claude&lt;/code&gt; 실행했더니 &amp;quot;사용 가능한 IDE를 찾을 수 없다&amp;quot;고 나오는 경우. 원인은 보통 WSL2의 NAT 네트워킹이나 Windows 방화벽이 WSL2와 IDE 간 연결을 막아서다.&lt;/p&gt;
&lt;h3&gt;해결법 1: 방화벽 규칙 추가 (권장)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# WSL 셸에서 IP 확인
hostname -I
# 예: 172.21.123.45 → 172.21.0.0/16 서브넷&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# PowerShell 관리자 모드에서 (서브넷은 본인 것에 맞게)
New-NetFirewallRule -DisplayName &amp;quot;Allow WSL2 Internal Traffic&amp;quot; -Direction Inbound -Protocol TCP -Action Allow -RemoteAddress 172.21.0.0/16 -LocalAddress 172.21.0.0/16&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 IDE와 Claude Code를 재시작.&lt;/p&gt;
&lt;h3&gt;해결법 2: 미러 네트워킹으로 전환&lt;/h3&gt;
&lt;p&gt;Windows 11 22H2 이상이면 미러 네트워킹을 쓸 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# Windows 사용자 디렉토리의 .wslconfig 파일에 추가
[wsl2]
networkingMode=mirrored&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# PowerShell에서 WSL 재시작
wsl --shutdown&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 10이면 미러 네트워킹이 안 되니까 해결법 1(방화벽)을 쓰자.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 문제 정리&lt;/h2&gt;
&lt;h3&gt;플러그인은 깔았는데 기능이 안 보임&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 프로젝트 루트 디렉토리에서 Claude Code를 실행했는지 확인
2. IDE 설정에서 플러그인이 활성화돼 있는지 확인
3. IDE 완전히 재시작 (여러 번 해야 할 수도 있음)
4. Remote Development면 원격 호스트에 플러그인 설치했는지 확인&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;&amp;quot;command not found&amp;quot;&lt;/h3&gt;
&lt;p&gt;Claude 아이콘 눌렀는데 command not found가 뜨면:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# CLI 설치 확인
claude --version

# 안 나오면 PATH 문제
# 플러그인 설정에서 Claude 명령어 경로 직접 지정&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;IDE가 감지 안 됨&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 플러그인 설치/활성화 확인
2. IDE 완전 재시작
3. 내장 터미널에서 Claude Code 실행했는지 확인
4. WSL이면 위의 WSL 설정 참고&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;보안 관련 주의사항&lt;/h2&gt;
&lt;p&gt;이건 한번 짚고 넘어가야 한다. Claude Code가 JetBrains IDE에서 &lt;strong&gt;자동 편집(auto-edit) 권한&lt;/strong&gt;으로 돌아갈 때, IDE 설정 파일을 수정할 수 있다. 그리고 IDE 설정 파일 중에는 자동으로 실행되는 것들이 있어서, 이게 보안 위험이 될 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 JetBrains에서 Claude Code를 쓸 때는:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;수동 승인(manual approval) 모드&lt;/strong&gt;를 쓰는 걸 고려하자&lt;/li&gt;
&lt;li&gt;Claude를 신뢰할 수 있는 프롬프트로만 사용하자&lt;/li&gt;
&lt;li&gt;Claude가 어떤 파일을 수정할 수 있는지 항상 인지하자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;편하다고 무작정 auto-edit + 전체 권한으로 놓고 쓰면, 의도치 않은 설정 변경이 일어날 수 있다. 특히 잘 모르는 코드베이스에서 작업할 때는 조심하자.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;VSCode vs IntelliJ, 뭐가 더 나은가&lt;/h2&gt;
&lt;p&gt;둘 다 써본 입장에서:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;기능적으로는 거의 동일
- diff 뷰어, 선택 컨텍스트, 진단 공유, 파일 참조 다 됨
- 같은 CLI를 쓰니까 코드 작업 품질은 똑같다

차이점
- IntelliJ: diff 뷰어가 더 강력하고 정교함. 
  리팩토링 검토할 때 IntelliJ 쪽이 보기 편함.
- VSCode: 확장 프로그램 생태계가 더 풍부. 가벼움.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;결국 평소 쓰던 IDE를 그대로 쓰면 된다. Java/Kotlin 백엔드면 IntelliJ, 프론트엔드면 VSCode를 쓰는 게 자연스러운데, 어느 쪽이든 Claude Code 경험은 비슷하다. 굳이 Claude Code 때문에 IDE를 바꿀 필요는 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;IntelliJ에서 Claude Code 쓰는 과정을 정리하면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Claude Code CLI 설치 (&lt;code&gt;curl&lt;/code&gt; 또는 npm)&lt;/li&gt;
&lt;li&gt;JetBrains 마켓플레이스에서 플러그인 설치 → IDE 완전 재시작&lt;/li&gt;
&lt;li&gt;내장 터미널에서 &lt;code&gt;claude&lt;/code&gt; 실행 (또는 외부 터미널에서 &lt;code&gt;/ide&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;프로젝트 루트에서 실행하는 거 잊지 말기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;핵심 팁:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;플러그인만으로는 안 된다. CLI가 먼저 깔려 있어야 한다&lt;/li&gt;
&lt;li&gt;설치 후 IDE 완전 재시작 (안 되면 두 번)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/config&lt;/code&gt;에서 diff tool을 &lt;code&gt;auto&lt;/code&gt;로 하면 IDE diff 뷰어를 쓸 수 있다&lt;/li&gt;
&lt;li&gt;코드 선택하면 자동 공유되고, 에러도 실시간 공유된다&lt;/li&gt;
&lt;li&gt;ESC 안 먹히면 터미널 키바인딩 설정을 확인하자&lt;/li&gt;
&lt;li&gt;WSL이면 방화벽/네트워킹 설정이 필요할 수 있다&lt;/li&gt;
&lt;li&gt;보안상 JetBrains에서는 수동 승인 모드를 권장한다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI</category>
      <category>AI코딩</category>
      <category>Antropic</category>
      <category>Claude</category>
      <category>claude code</category>
      <category>IntelliJ</category>
      <category>Jetbrains</category>
      <category>바이브코딩</category>
      <category>인텔리제이</category>
      <category>젯브레인즈</category>
      <category>클로드코드</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/46</guid>
      <comments>https://chhb-miscellaneous.tistory.com/46#entry46comment</comments>
      <pubDate>Wed, 3 Jun 2026 18:00:24 +0900</pubDate>
    </item>
    <item>
      <title>AGENTS.md 제대로 쓰는 법 &amp;mdash; Codex를 팀원처럼 만드는 파일</title>
      <link>https://chhb-miscellaneous.tistory.com/45</link>
      <description>&lt;p&gt;지난번에 Claude Code의 &lt;code&gt;CLAUDE.md&lt;/code&gt;에 대해 글을 썼는데, Codex를 쓰는 사람들한테서 &amp;quot;Codex는 그런 거 없냐&amp;quot;는 질문을 받았다. 있다. &lt;code&gt;AGENTS.md&lt;/code&gt;다. 개념은 비슷한데, 재밌는 건 이게 Codex 전용이 아니라 거의 표준처럼 굳어지고 있다는 점이다.&lt;/p&gt;
&lt;p&gt;Codex 처음 쓸 때 매번 &amp;quot;pnpm 써&amp;quot;, &amp;quot;테스트는 vitest로 돌려&amp;quot;, &amp;quot;이 폴더는 건드리지 마&amp;quot; 같은 걸 반복하다가, &lt;code&gt;AGENTS.md&lt;/code&gt; 하나 써두니까 그 뒤로 Codex가 알아서 프로젝트 규칙을 지키더라. 한 번 세팅에 한 시간 정도 투자했는데, 그 효과는 계속 누적된다.&lt;/p&gt;
&lt;p&gt;오늘은 이 &lt;code&gt;AGENTS.md&lt;/code&gt;를 어떻게 쓰는지, 어떻게 써야 Codex가 잘 따르는지 정리해본다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AGENTS.md가 뭔데&lt;/h2&gt;
&lt;p&gt;한 줄로 말하면, &lt;strong&gt;AI 코딩 에이전트에게 주는 프로젝트 설명서&lt;/strong&gt;다. README가 사람을 위한 문서라면, &lt;code&gt;AGENTS.md&lt;/code&gt;는 AI 에이전트를 위한 문서다.&lt;/p&gt;
&lt;p&gt;OpenAI 공식 설명을 빌리면, &lt;code&gt;AGENTS.md&lt;/code&gt;는 README.md와 비슷한 텍스트 파일인데, Codex에게 코드베이스를 어떻게 탐색하고, 어떤 명령어로 테스트하고, 프로젝트의 표준 관행을 어떻게 따르는지 알려주는 파일이다.&lt;/p&gt;
&lt;p&gt;Codex는 사람 개발자와 마찬가지로 잘 구성된 개발 환경, 신뢰할 수 있는 테스트 설정, 명확한 문서가 있을 때 가장 잘 동작한다. &lt;code&gt;AGENTS.md&lt;/code&gt;가 바로 그 &amp;quot;명확한 문서&amp;quot; 역할을 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AGENTS.md는 사실 Codex만의 것이 아니다&lt;/h2&gt;
&lt;p&gt;이게 &lt;code&gt;CLAUDE.md&lt;/code&gt;와의 가장 큰 차이점이자 &lt;code&gt;AGENTS.md&lt;/code&gt;의 강점이다. &lt;code&gt;AGENTS.md&lt;/code&gt;는 &lt;strong&gt;여러 AI 도구가 공통으로 지원하는 사실상의 표준&lt;/strong&gt;이 됐다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AGENTS.md를 지원하는 도구들:
- OpenAI Codex
- GitHub Copilot (2025년 8월부터 네이티브 지원)
- Cursor
- Google Jules / Gemini
- Windsurf
- Zed
- Aider
- Factory, Amp, RooCode 등&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;무슨 뜻이냐면, &lt;code&gt;AGENTS.md&lt;/code&gt; 하나만 잘 써두면 Codex를 쓰든 Copilot을 쓰든 Cursor를 쓰든 같은 설정이 적용된다는 거다. 도구마다 별도 설정 파일(&lt;code&gt;.cursorrules&lt;/code&gt;, &lt;code&gt;CLAUDE.md&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;  팀에서 여러 AI 도구를 섞어 쓰는 경우라면 &lt;code&gt;AGENTS.md&lt;/code&gt;가 특히 유용하다. 한 곳에서 관리하면 모두가 같은 규칙을 공유한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이 표준은 일부러 &lt;strong&gt;스키마 없이 단순하게&lt;/strong&gt; 유지된다. 그냥 평범한 마크다운이고, 정해진 구조가 없다. 이 유연함 덕분에 빠르게 퍼졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AGENTS.md 만들기&lt;/h2&gt;
&lt;p&gt;프로젝트 루트에 &lt;code&gt;AGENTS.md&lt;/code&gt; 파일을 만들면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;touch AGENTS.md&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Codex는 저장소에서 &lt;code&gt;AGENTS.md&lt;/code&gt; 파일을 자동으로 찾아서 읽는다. 별도 설정이 필요 없다. 파일을 만들고 내용을 채우기만 하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 원칙: 설명보다 명령어를 먼저&lt;/h2&gt;
&lt;p&gt;가장 효과적인 &lt;code&gt;AGENTS.md&lt;/code&gt;의 공통점이 하나 있다. &lt;strong&gt;설명(explanation)보다 명령어(command)를 앞세운다&lt;/strong&gt;는 거다.&lt;/p&gt;
&lt;p&gt;OpenAI 샘플과 여러 베스트 프랙티스에서 권장하는 순서:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 셋업(setup) 명령어 — 가장 먼저
2. 테스트(testing) 명령어 — 두 번째
3. 배포(deployment) 명령어 — 세 번째
4. 디버깅(debugging) — 마지막&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;왜냐하면 Codex는 에이전트라서, &amp;quot;이 코드는 클린 아키텍처를 따르고 도메인 주도 설계를...&amp;quot; 같은 추상적인 설명보다 &amp;quot;테스트는 &lt;code&gt;pnpm test&lt;/code&gt;로 돌려라&amp;quot; 같은 구체적인 명령어에 훨씬 잘 반응한다. 에이전트한테 필요한 건 철학 강의가 아니라 실행 가능한 지시다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;잘 쓴 AGENTS.md 예시&lt;/h2&gt;
&lt;p&gt;내가 실제로 쓰는 구조다. 그대로 가져다 수정해서 써도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# AGENTS.md

## 프로젝트 개요
Next.js 14 기반 SaaS 대시보드. 사용자 구독 관리 및 사용량 추적 서비스.

## 개발 환경 셋업
- 의존성 설치: `pnpm install`
- 환경변수: `.env.example`을 복사해서 `.env.local` 생성
- DB 준비: `pnpm prisma migrate dev`
- 개발 서버: `pnpm dev`

## 테스트
- 전체 테스트: `pnpm test`
- 특정 파일: `pnpm test &amp;lt;파일명&amp;gt;`
- 커버리지: `pnpm test:coverage`
- 작업 완료 전에 반드시 `pnpm test`와 `pnpm typecheck`를 통과시킬 것

## 빌드 &amp;amp; 배포
- 빌드: `pnpm build`
- 린트: `pnpm lint`
- 배포: main 브랜치 푸시 시 Vercel 자동 배포

## 아키텍처 (코드만 봐서는 모를 수 있는 것들)
- 모든 DB 접근은 `src/lib/repository.ts`를 통해서만 (raw SQL 금지)
- `src/payments/` 모듈은 별도의 인증 흐름을 가짐 (`src/auth/`와 분리)
- API 응답은 `src/core/errors.ts`의 ErrorResponse 클래스 사용
- 클라이언트 컴포넌트는 최상단에 &amp;#39;use client&amp;#39; 명시

## 코딩 컨벤션
- 모든 새 코드는 TypeScript로 작성
- 각 파일의 기존 코드 스타일을 따를 것
- 컴포넌트: PascalCase, 유틸 함수: camelCase
- 들여쓰기 2칸, 세미콜론 사용

## 하지 말아야 할 것
- 새 프로덕션 의존성 추가 시 PR에서 먼저 논의 ([ASSUMPTION]으로 후보 명시)
- console.log를 프로덕션 코드에 남기지 말 것
- any 타입 사용 금지
- .env 파일 커밋 금지

## 작업 완료 전 체크리스트
- [ ] 테스트 통과 (`pnpm test`)
- [ ] 타입 체크 통과 (`pnpm typecheck`)
- [ ] 린트 통과 (`pnpm lint`)

## PR 메시지 규칙
- conventional commit 형식 사용 (feat:, fix:, refactor: 등)
- 변경 이유와 영향 범위를 본문에 명시&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;특히 중요한 섹션들&lt;/h2&gt;
&lt;h3&gt;1. &amp;quot;코드만 봐서는 모르는 것&amp;quot;을 적어라&lt;/h3&gt;
&lt;p&gt;Codex는 코드를 읽고 많은 걸 추론한다. 그러니까 &lt;code&gt;AGENTS.md&lt;/code&gt;에는 &lt;strong&gt;코드만 봐서는 추론할 수 없는 구조적 규칙&lt;/strong&gt;을 적는 게 효과적이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;✅ 좋은 예 (코드만으로는 모를 정보)
- 모든 DB 접근은 src/db/repository.py를 통해서만, 다른 곳에서 raw SQL 금지
- src/payments/ 모듈은 src/auth/와 분리된 별도 인증 흐름을 가짐
- 에러 응답은 src/core/errors.py의 ErrorResponse 클래스 사용

❌ 굳이 안 적어도 되는 것 (코드 보면 아는 것)
- 이 프로젝트는 React를 사용합니다 (package.json 보면 앎)
- 함수는 function 키워드로 정의합니다 (코드 보면 앎)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드를 봐서 알 수 있는 건 Codex가 알아서 파악한다. 코드 어디에도 안 적혀있는 암묵적 규칙, 팀의 약속, 아키텍처 의도 — 이런 걸 적어야 가치가 있다.&lt;/p&gt;
&lt;h3&gt;2. &amp;quot;작업 완료 전 검증&amp;quot; 규칙&lt;/h3&gt;
&lt;p&gt;이게 진짜 중요하다. Codex는 가끔 자신감 넘치게 틀린 코드를 만든다. 특히 학습 데이터가 적은 프레임워크나 코드베이스에서. 그래서 &lt;code&gt;AGENTS.md&lt;/code&gt;에 &amp;quot;작업을 완료했다고 선언하기 전에 무엇을 해야 하는지&amp;quot;를 명시해두는 게 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;## 작업 완료 전 반드시
1. `pnpm test` 실행해서 전부 통과 확인
2. `pnpm typecheck`로 타입 에러 없는지 확인
3. `pnpm lint` 통과 확인
4. 위 셋 중 하나라도 실패하면 수정 후 재실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 해두면 Codex가 코드를 던지고 끝내는 게 아니라, 스스로 검증하고 마무리한다. 테스트, 린터, 타입 체커는 선택이 아니다. Codex 출력을 무조건 신뢰하지 말고, 검증을 거치게 만들어야 한다.&lt;/p&gt;
&lt;h3&gt;3. PR 메시지 규칙&lt;/h3&gt;
&lt;p&gt;Codex는 GitHub Pull Request를 만들 수 있는데, &lt;code&gt;AGENTS.md&lt;/code&gt;에 PR 메시지 형식을 지정해두면 일관된 PR을 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;## PR 규칙
- 제목은 conventional commit 형식
- 본문에 &amp;quot;무엇을, 왜&amp;quot; 변경했는지 명시
- Breaking change가 있으면 명확히 표시
- 관련 이슈 번호 링크&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;계층 구조 — AGENTS.md도 여러 위치에 둘 수 있다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;도 디렉토리 계층에 따라 여러 개를 둘 수 있다. Codex는 가까운 파일, 중첩된(nested) 파일을 우선시한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-monorepo/
├── AGENTS.md              ← 전체 공통 규칙
├── packages/
│   ├── frontend/
│   │   └── AGENTS.md      ← 프론트엔드 전용
│   └── backend/
│       └── AGENTS.md      ← 백엔드 전용&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;packages/frontend/&lt;/code&gt;에서 작업하면 루트 + 프론트엔드 &lt;code&gt;AGENTS.md&lt;/code&gt;가 같이 적용된다. 모노레포에서 패키지마다 다른 규칙을 줄 때 유용하다. 가까운 파일일수록 더 큰 영향을 준다는 점만 기억하자.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;효과 없는 AGENTS.md 피하기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; 글에서도 다뤘지만, AI 설정 파일의 함정은 비슷하다.&lt;/p&gt;
&lt;h3&gt;1. 추상적인 성격 지시는 효과 없다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;❌ 효과 없음
- 시니어 개발자처럼 신중하게 작업해줘
- 최선을 다해서 깔끔한 코드를 작성해줘
- 항상 모범 사례를 따라줘&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 건 그럴듯해 보이지만 Codex의 행동을 측정 가능하게 바꾸지 못한다. 구체적인 규칙으로 바꿔야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;✅ 효과 있음
- 함수는 한 가지 일만 하도록, 20줄 넘으면 분리
- 모든 public 함수에 JSDoc 주석
- 에러는 throw하지 말고 Result 타입으로 반환&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 너무 길면 핵심이 묻힌다&lt;/h3&gt;
&lt;p&gt;토큰 한도가 있다(Codex는 보통 128k~192k 토큰). &lt;code&gt;AGENTS.md&lt;/code&gt;가 길수록 실제 작업을 위한 컨텍스트가 줄어든다. 중요한 규칙만 간결하게.&lt;/p&gt;
&lt;h3&gt;3. AGENTS.md만으로 모든 걸 막을 순 없다&lt;/h3&gt;
&lt;p&gt;이건 솔직하게 알아둬야 한다. OpenAI 문서에서도 명시하는데, &lt;code&gt;AGENTS.md&lt;/code&gt;는 규칙을 항상 보이게(always-on) 만들어서 위반을 &lt;strong&gt;줄이는&lt;/strong&gt; 거지, &lt;strong&gt;완전히 없애는&lt;/strong&gt; 게 아니다.&lt;/p&gt;
&lt;p&gt;그러니까 되돌릴 수 없거나 비용이 큰 작업(프로덕션 배포, DB 작업, 인증 정보 변경 같은)은 &lt;code&gt;AGENTS.md&lt;/code&gt; 지시만 믿지 말고, &lt;strong&gt;승인 게이트(approval gate)나 샌드박스 설정&lt;/strong&gt;을 같이 써야 한다. 규칙을 적어두는 것과 강제하는 것은 다른 문제다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# config.toml에서 승인 정책 설정
approval_policy = &amp;quot;on-request&amp;quot;
# 위험한 작업은 무조건 사람 승인을 거치게&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;AGENTS.md + config.toml 조합&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;가 &amp;quot;무엇을, 어떻게&amp;quot; 작업할지 알려주는 거라면, &lt;code&gt;config.toml&lt;/code&gt;은 Codex의 권한과 동작을 제어한다. 둘을 같이 쓰면 마찰이 크게 줄어든다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AGENTS.md      → 프로젝트 규칙, 명령어, 컨벤션 (무엇을 할지)
config.toml    → 승인 정책, 샌드박스, 모델 설정 (어떻게 동작할지)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;실제로 대부분의 지속적인 마찰은 이 둘이 프로젝트의 실제 동작 방식을 제대로 반영하면 사라진다. Codex가 일상적인 작업마다 자꾸 승인을 요청하면 신뢰할 수 있는 저장소에 한해 승인 정책을 완화하고, 파일을 못 쓰면 샌드박스 모드를 확인하는 식으로.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 팁&lt;/h2&gt;
&lt;h3&gt;1. 작게 시작해서 키워가자&lt;/h3&gt;
&lt;p&gt;처음부터 완벽한 &lt;code&gt;AGENTS.md&lt;/code&gt;를 쓰려고 하지 마라. 셋업 명령어, 테스트 명령어, 핵심 규칙 몇 개로 시작하고, 작업하면서 &amp;quot;이거 매번 설명하네&amp;quot; 싶은 게 나올 때마다 추가하자.&lt;/p&gt;
&lt;h3&gt;2. Git에 커밋하자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;를 저장소에 커밋하면 버전 관리되고, 리뷰 가능하고, 팀 전체가 일관되게 쓴다. 신입이 들어와도 Codex가 프로젝트 규칙을 이미 알고 있는 셈이다.&lt;/p&gt;
&lt;h3&gt;3. 명령어가 진짜 동작하는지 확인하자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;에 적은 명령어가 실제로 동작하는지 확인하자. &lt;code&gt;pnpm test&lt;/code&gt;라고 적었는데 실제로는 &lt;code&gt;npm run test&lt;/code&gt;면 Codex가 헷갈린다. 명령어는 정확하게.&lt;/p&gt;
&lt;h3&gt;4. &amp;quot;왜&amp;quot;를 적으면 더 잘 따른다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;❌ &amp;quot;raw SQL 쓰지 마&amp;quot;
✅ &amp;quot;raw SQL 쓰지 마 (마이그레이션 추적이 안 되고 환경 간 스키마 불일치가 생김)&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이유를 적으면 Codex가 규칙의 의도를 이해하고 비슷한 상황에서도 일관되게 판단한다.&lt;/p&gt;
&lt;h3&gt;5. 검증을 항상 강제하자&lt;/h3&gt;
&lt;p&gt;다시 강조하지만, &amp;quot;작업 완료 전 테스트/타입체크/린트 통과&amp;quot; 규칙은 꼭 넣자. Codex 출력을 그냥 믿지 말고 검증을 거치게 만드는 것. 이게 안정적인 워크플로우의 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;CLAUDE.md vs AGENTS.md&lt;/h2&gt;
&lt;p&gt;둘 다 써본 입장에서 비교하면:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;공통점
- 둘 다 AI 에이전트에게 주는 프로젝트 설명서
- 세션마다 자동 로드
- 계층 구조 지원 (글로벌/프로젝트/하위)
- 마크다운 형식

차이점
- AGENTS.md: 범용 표준. Codex, Copilot, Cursor 등 여러 도구가 공통 지원
- CLAUDE.md: Claude Code 전용. Auto Memory 같은 Claude 고유 기능과 연동&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여러 도구를 섞어 쓴다면 &lt;code&gt;AGENTS.md&lt;/code&gt;가 관리 부담이 적다. Claude Code만 쓴다면 &lt;code&gt;CLAUDE.md&lt;/code&gt;의 고유 기능(Auto Memory, @import 등)을 활용하는 게 낫고. 실제로 두 파일을 다 두고, 공통 내용은 한쪽에 쓰고 다른 쪽에서 참조하는 사람들도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;는 Codex를 &amp;quot;일회성 도구&amp;quot;가 아니라 &amp;quot;설정하고 개선해나가는 팀원&amp;quot;으로 만드는 핵심이다. OpenAI 표현을 빌리면 &amp;quot;지속적인 가이드(durable guidance)&amp;quot;를 두는 곳이다.&lt;/p&gt;
&lt;p&gt;핵심 정리:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;는 AI 에이전트에게 주는 프로젝트 설명서, 저장소 루트에 두면 자동 로드&lt;/li&gt;
&lt;li&gt;Codex 전용이 아니라 Copilot, Cursor 등 여러 도구가 지원하는 사실상 표준&lt;/li&gt;
&lt;li&gt;설명보다 명령어를 먼저 (셋업 → 테스트 → 배포 → 디버깅 순)&lt;/li&gt;
&lt;li&gt;&amp;quot;코드만 봐서는 모르는&amp;quot; 구조적 규칙을 적어야 가치 있다&lt;/li&gt;
&lt;li&gt;&amp;quot;작업 완료 전 검증&amp;quot; 규칙을 꼭 넣자. Codex 출력을 무조건 믿지 말 것&lt;/li&gt;
&lt;li&gt;추상적 성격 지시는 효과 없다. 구체적이고 측정 가능하게&lt;/li&gt;
&lt;li&gt;되돌릴 수 없는 작업은 AGENTS.md만 믿지 말고 승인 게이트/샌드박스를 같이 쓰자&lt;/li&gt;
&lt;li&gt;Git에 커밋해서 팀과 공유하자&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/45</guid>
      <comments>https://chhb-miscellaneous.tistory.com/45#entry45comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:06:11 +0900</pubDate>
    </item>
    <item>
      <title>CLAUDE.md 제대로 쓰는 법 &amp;mdash; Claude Code 생산성을 두 배로 올리는 파일</title>
      <link>https://chhb-miscellaneous.tistory.com/44</link>
      <description>&lt;p&gt;Claude Code 쓰면서 처음에 제일 답답했던 게 뭐냐면, 매번 같은 말을 반복해야 한다는 거였다. &amp;quot;들여쓰기는 2칸으로 해줘&amp;quot;, &amp;quot;패키지 매니저는 pnpm 쓰고 있어&amp;quot;, &amp;quot;CSS는 Tailwind만 써&amp;quot;, &amp;quot;console.log 남기지 마&amp;quot;... 세션 새로 열 때마다 이걸 다시 설명하고 있는 나를 발견했다.&lt;/p&gt;
&lt;p&gt;그러다 &lt;code&gt;CLAUDE.md&lt;/code&gt;를 알게 됐고, 이게 게임 체인저였다. 한 시간 투자해서 제대로 써두니까 그 뒤로 같은 설명을 반복할 일이 없어졌다. 어떤 글에서 &amp;quot;CLAUDE.md는 투자 대비 효과(ROI)가 가장 높은 작업&amp;quot;이라고 했는데, 정말 공감한다.&lt;/p&gt;
&lt;p&gt;오늘은 이 &lt;code&gt;CLAUDE.md&lt;/code&gt;를 어떻게 쓰는지, 어떻게 써야 효과가 있는지 정리해보려고 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;CLAUDE.md가 뭔데&lt;/h2&gt;
&lt;p&gt;한 줄로 말하면, &lt;strong&gt;Claude Code가 세션 시작할 때마다 자동으로 읽는 프로젝트 설명서&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;Claude Code는 매번 새로운 세션으로 시작한다. 사람처럼 &amp;quot;어제 우리가 뭘 했더라&amp;quot; 하고 기억하지 못한다. 그래서 매번 프로젝트의 맥락을 알려줘야 하는데, &lt;code&gt;CLAUDE.md&lt;/code&gt;에 한 번 써두면 세션이 시작될 때 Claude가 자동으로 이 파일을 읽고 작업을 시작한다.&lt;/p&gt;
&lt;p&gt;쉽게 비유하면, 신입 개발자가 입사할 때 받는 온보딩 문서 같은 거다. &amp;quot;우리 팀은 이런 컨벤션을 쓰고, 이런 도구를 쓰고, 이런 건 하면 안 된다&amp;quot;를 정리해둔 문서. Claude한테 매 세션마다 이 문서를 쥐어주는 셈이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;메모리 계층 구조 — CLAUDE.md는 한 곳에만 있는 게 아니다&lt;/h2&gt;
&lt;p&gt;이게 처음에 잘 모르고 넘어가는 부분인데, &lt;code&gt;CLAUDE.md&lt;/code&gt;는 여러 위치에 둘 수 있고 각각 역할이 다르다. Claude Code가 세션을 시작하면 여러 위치의 파일을 모아서 합쳐 읽는다.&lt;/p&gt;
&lt;h3&gt;3가지 주요 위치&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 글로벌 (~/.claude/CLAUDE.md)
   → 내 모든 프로젝트에 적용
   → 개인 코딩 스타일, 자주 쓰는 단축어 등

2. 프로젝트 (./CLAUDE.md)
   → 해당 프로젝트에만 적용
   → 가장 많이 쓰는 위치. Git에 커밋해서 팀과 공유.

3. 로컬/개인 (.claude/CLAUDE.md 또는 하위 디렉토리)
   → 프로젝트 안에서 개인적인 설정
   → Git에 커밋 안 함 (.gitignore에 추가)&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;우선순위&lt;/h3&gt;
&lt;p&gt;같은 지시사항이 여러 위치에 있으면 &lt;strong&gt;더 구체적인(specific) 것이 우선&lt;/strong&gt;한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;구체적 ← → 일반적
개인 &amp;gt; 프로젝트 &amp;gt; 글로벌&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;예를 들어:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;글로벌 파일: &amp;quot;들여쓰기는 2칸&amp;quot;&lt;/li&gt;
&lt;li&gt;프로젝트 파일: &amp;quot;들여쓰기는 4칸&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ 이 프로젝트에서는 &lt;strong&gt;4칸&lt;/strong&gt;이 적용된다. 프로젝트가 글로벌보다 구체적이니까.&lt;/p&gt;
&lt;h3&gt;디렉토리 트리 탐색&lt;/h3&gt;
&lt;p&gt;Claude Code는 현재 작업 디렉토리에서 시작해서 상위로 올라가며 &lt;code&gt;CLAUDE.md&lt;/code&gt;를 찾는다. 모노레포 같은 구조에서 유용하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-monorepo/
├── CLAUDE.md              ← 전체 공통 규칙
├── packages/
│   ├── frontend/
│   │   └── CLAUDE.md      ← 프론트엔드 전용 규칙
│   └── backend/
│       └── CLAUDE.md      ← 백엔드 전용 규칙&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;packages/frontend/&lt;/code&gt;에서 작업하면 루트의 &lt;code&gt;CLAUDE.md&lt;/code&gt; + 프론트엔드 &lt;code&gt;CLAUDE.md&lt;/code&gt;가 같이 적용된다. 현재 위치에 가까운 파일일수록 더 큰 영향을 준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;CLAUDE.md 만들기&lt;/h2&gt;
&lt;h3&gt;방법 1: 직접 만들기&lt;/h3&gt;
&lt;p&gt;프로젝트 루트에 &lt;code&gt;CLAUDE.md&lt;/code&gt; 파일을 만들고 내용을 쓰면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;touch CLAUDE.md&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;방법 2: /init 명령어&lt;/h3&gt;
&lt;p&gt;Claude Code 세션에서 &lt;code&gt;/init&lt;/code&gt;을 실행하면 Claude가 프로젝트를 분석해서 &lt;code&gt;CLAUDE.md&lt;/code&gt; 초안을 만들어준다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/init&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이러면 프로젝트의 기술 스택, 디렉토리 구조, 빌드 명령어 같은 걸 자동으로 파악해서 기본 틀을 만들어준다. 여기서 시작해서 다듬는 게 편하다.&lt;/p&gt;
&lt;h3&gt;방법 3: # 키로 즉석 추가&lt;/h3&gt;
&lt;p&gt;세션 중에 &lt;code&gt;#&lt;/code&gt; 키를 누르고 지시사항을 입력하면 자동으로 &lt;code&gt;CLAUDE.md&lt;/code&gt;에 저장된다. 작업하다가 &amp;quot;아, 이건 기억해뒀으면 좋겠다&amp;quot; 싶을 때 흐름 안 끊고 바로 추가할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 항상 함수에 JSDoc 주석을 달아줘&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;잘 쓴 CLAUDE.md 예시&lt;/h2&gt;
&lt;p&gt;내가 실제로 쓰는 구조다. 그대로 가져다 수정해서 써도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 프로젝트 개요

이 프로젝트는 Next.js 14 기반 SaaS 대시보드입니다.
사용자가 구독을 관리하고 사용량을 추적하는 서비스입니다.

## 기술 스택

- 프레임워크: Next.js 14 (App Router)
- 언어: TypeScript (strict mode)
- 스타일링: Tailwind CSS
- DB: PostgreSQL + Prisma
- 인증: NextAuth.js
- 패키지 매니저: pnpm
- 테스트: Vitest + Testing Library
- 배포: Vercel

## 명령어

- 개발 서버: `pnpm dev`
- 빌드: `pnpm build`
- 테스트: `pnpm test`
- 테스트(watch): `pnpm test:watch`
- 린트: `pnpm lint`
- 타입 체크: `pnpm typecheck`
- DB 마이그레이션: `pnpm prisma migrate dev`

## 코딩 컨벤션

- 함수형 컴포넌트만 사용 (클래스 컴포넌트 금지)
- 컴포넌트 파일명: PascalCase (예: UserCard.tsx)
- 유틸 함수 파일명: camelCase (예: formatDate.ts)
- 들여쓰기: 2칸
- 세미콜론 사용
- import는 절대 경로 사용 (@/components/...)

## 아키텍처 규칙

- API 라우트는 src/app/api/ 아래에 위치
- 비즈니스 로직은 src/services/에 분리
- DB 접근은 반드시 src/lib/db.ts의 Prisma 클라이언트 통해서만
- API 응답 형식: { success: boolean, data?: T, error?: string }
- 클라이언트 컴포넌트는 파일 최상단에 &amp;#39;use client&amp;#39; 명시

## 하지 말아야 할 것

- console.log를 프로덕션 코드에 남기지 말 것 (logger 사용)
- any 타입 사용 금지 (unknown 쓰고 타입 가드)
- .env 파일 절대 커밋하지 말 것
- DB 스키마 변경은 반드시 마이그레이션으로 (직접 SQL 금지)
- 새 라이브러리 추가 전에 먼저 물어볼 것

## 테스트 규칙

- 새 기능에는 반드시 테스트 작성
- 테스트 파일은 대상 파일과 같은 위치에 *.test.ts
- 외부 API는 모킹 처리&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;효과 없는 CLAUDE.md의 특징 (이러지 마라)&lt;/h2&gt;
&lt;p&gt;여러 글에서 공통적으로 지적하는 실패 패턴이 있다. 나도 처음에 이 함정에 빠졌었다.&lt;/p&gt;
&lt;h3&gt;1. 성격(personality) 지시만 잔뜩&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;❌ 이런 거 효과 없다

- 시니어 엔지니어처럼 행동해줘
- 답변하기 전에 깊이 생각해줘
- 철저하고 신중하게 작업해줘
- 최고의 코드를 작성해줘&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 추상적인 지시는 Claude의 행동을 측정 가능한 수준으로 바꾸지 못한다. 그럴듯해 보이지만 실제 효과는 거의 제로다. 신호 대 잡음 비율(signal-to-noise)이 바닥이라, 정작 중요한 규칙이 이런 노이즈에 묻혀버린다.&lt;/p&gt;
&lt;h3&gt;2. 너무 긴 파일&lt;/h3&gt;
&lt;p&gt;&amp;quot;관련된 거 다 넣자&amp;quot;는 마음으로 시작하면 어느새 수백 줄짜리 괴물 파일이 된다. 스캔이 불가능하고, 중복이 생기고, 진짜 중요한 규칙이 묻힌다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;권장: 200줄 이내로 유지하자.&lt;/strong&gt; 컨텍스트 윈도우는 유한한 자원이고, &lt;code&gt;CLAUDE.md&lt;/code&gt;가 길수록 실제 코드를 위한 공간이 줄어든다.&lt;/p&gt;
&lt;h3&gt;3. 구체적이지 않은 규칙&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;❌ &amp;quot;코드를 깔끔하게 작성해줘&amp;quot;
✅ &amp;quot;함수는 한 가지 일만 하도록 작성하고, 20줄을 넘으면 분리해줘&amp;quot;

❌ &amp;quot;테스트를 잘 작성해줘&amp;quot;
✅ &amp;quot;각 함수마다 정상 케이스 1개, 엣지 케이스 최소 2개의 테스트를 작성해줘&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;측정 가능하고 구체적인 규칙을 써야 한다. 추상적인 형용사(&amp;quot;깔끔하게&amp;quot;, &amp;quot;잘&amp;quot;, &amp;quot;효율적으로&amp;quot;)는 Claude가 해석할 여지가 너무 크다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;@ import로 모듈화하기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;가 길어지면 &lt;code&gt;@경로/파일.md&lt;/code&gt; 문법으로 다른 파일을 가져올 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 프로젝트 개요
...

## 상세 규칙
코딩 컨벤션은 @docs/coding-standards.md 참고
API 설계 규칙은 @docs/api-conventions.md 참고&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 메인 &lt;code&gt;CLAUDE.md&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;: import도 결국 컨텍스트를 차지한다. 정말 유용한 것(코딩 표준, API 컨벤션, 아키텍처 결정)만 import하자. 모든 걸 다 넣으면 모듈화의 의미가 없다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;개인 설정을 팀 프로젝트에 섞고 싶다면, 홈 디렉토리 파일을 import하는 방법도 있다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;@~/personal-preferences.md&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이러면 개인 취향은 커밋 안 하면서 적용할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;.claude/rules/ 로 규칙 관리하기&lt;/h2&gt;
&lt;p&gt;최근 버전에서는 &lt;code&gt;.claude/rules/&lt;/code&gt; 디렉토리에 규칙 파일을 두면 자동으로 로드된다. import 안 해도 Claude Code가 알아서 읽는다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.claude/
└── rules/
    ├── typescript.md
    ├── testing.md
    └── git-workflow.md&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 파일들은 메인 &lt;code&gt;CLAUDE.md&lt;/code&gt;와 함께 로드되고, Git에 커밋되니까 팀 전체가 공유한다. 규칙이 많아지면 이렇게 주제별로 나누는 게 메인 파일을 비대하게 만드는 것보다 낫다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Auto Memory — Claude가 스스로 쓰는 메모&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;가 내가 쓰는 파일이라면, Auto Memory는 &lt;strong&gt;Claude가 스스로 쓰고 관리하는 메모&lt;/strong&gt;다. 일종의 AI의 노트북이다.&lt;/p&gt;
&lt;p&gt;작업하면서 Claude가 프로젝트에 대해 학습한 것들을 자동으로 기록해둔다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프로젝트 패턴: 발견한 빌드 명령어, 테스트 컨벤션, 코드 스타일&lt;/li&gt;
&lt;li&gt;디버깅 인사이트: 같이 해결한 까다로운 문제의 해법&lt;/li&gt;
&lt;li&gt;아키텍처 노트: 핵심 파일, 모듈 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이건 &lt;code&gt;~/.claude/projects/&amp;lt;프로젝트&amp;gt;/memory/&lt;/code&gt;에 저장된다. 내가 일일이 안 알려줘도 Claude가 알아서 축적하니까, &lt;code&gt;CLAUDE.md&lt;/code&gt;에는 핵심 규칙만 쓰고 세세한 건 Auto Memory가 학습하게 두면 된다.&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;명시적이고 중요한 규칙 → CLAUDE.md&lt;/strong&gt;, &lt;strong&gt;세부적인 패턴 학습 → Auto Memory&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;세션 중에 CLAUDE.md를 수정했다면&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;는 &lt;strong&gt;세션 시작할 때만 자동으로 로드&lt;/strong&gt;된다. 작업 도중에 파일을 수정했으면 Claude는 그걸 모른다.&lt;/p&gt;
&lt;p&gt;이때는 프롬프트에서 &lt;code&gt;@CLAUDE.md&lt;/code&gt;를 언급하면 Claude가 파일을 다시 읽는다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@CLAUDE.md 방금 업데이트한 규칙 반영해서 작업해줘&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;또는 세션을 새로 시작하면 당연히 최신 버전이 로드된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 팁&lt;/h2&gt;
&lt;h3&gt;1. 처음부터 완벽하게 쓰려고 하지 마라&lt;/h3&gt;
&lt;p&gt;빈 파일 앞에서 막막하면 &lt;code&gt;/init&lt;/code&gt;으로 초안을 만들고 시작하자. 그리고 작업하면서 &amp;quot;이거 반복 설명하네&amp;quot; 싶은 게 나올 때마다 &lt;code&gt;#&lt;/code&gt; 키로 추가하면 된다. 점진적으로 다듬어가는 게 현실적이다.&lt;/p&gt;
&lt;h3&gt;2. 팀 프로젝트면 반드시 Git에 커밋&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;를 커밋하면 팀원 전체가 같은 컨텍스트에서 Claude를 쓸 수 있다. 코드 스타일이 통일되고, 온보딩도 빨라진다. 신입이 들어와도 Claude가 프로젝트 규칙을 이미 알고 있는 셈이다.&lt;/p&gt;
&lt;h3&gt;3. 주기적으로 정리하자&lt;/h3&gt;
&lt;p&gt;프로젝트가 진행되면서 &lt;code&gt;CLAUDE.md&lt;/code&gt;도 낡는다. 기술 스택이 바뀌거나, 규칙이 추가되거나. 한 달에 한 번 정도는 들여다보면서 쓸모없어진 규칙을 지우고, 새 규칙을 추가하자. 200줄 넘어가면 모듈화를 고민하고.&lt;/p&gt;
&lt;h3&gt;4. &amp;quot;왜&amp;quot;를 적으면 더 잘 따른다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;❌ &amp;quot;직접 SQL 쓰지 말 것&amp;quot;
✅ &amp;quot;직접 SQL 쓰지 말 것 (마이그레이션 이력 추적이 안 되고, 
    여러 환경 간 스키마 불일치가 생기기 때문)&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이유를 적어두면 Claude가 규칙의 의도를 이해하고, 비슷한 상황에서도 일관되게 판단한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;는 단순한 설정 파일이 아니라, Claude Code를 제대로 쓰기 위한 핵심이다. 한 시간 투자해서 잘 써두면, 그 뒤로 수십 시간의 반복 설명을 아낄 수 있다.&lt;/p&gt;
&lt;p&gt;핵심 정리:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;는 Claude가 세션마다 자동으로 읽는 프로젝트 설명서다&lt;/li&gt;
&lt;li&gt;글로벌/프로젝트/개인 3계층이 있고, 구체적인 것이 우선한다&lt;/li&gt;
&lt;li&gt;성격 지시(&amp;quot;시니어처럼 행동해줘&amp;quot;)는 효과 없다. 구체적이고 측정 가능한 규칙을 쓰자&lt;/li&gt;
&lt;li&gt;200줄 이내로 유지하고, 길어지면 &lt;code&gt;@import&lt;/code&gt;나 &lt;code&gt;.claude/rules/&lt;/code&gt;로 모듈화하자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/init&lt;/code&gt;으로 시작하고 &lt;code&gt;#&lt;/code&gt; 키로 점진적으로 추가하자&lt;/li&gt;
&lt;li&gt;세부 패턴은 Auto Memory가 학습하니까, CLAUDE.md에는 핵심만 쓰자&lt;/li&gt;
&lt;li&gt;팀 프로젝트면 Git에 커밋해서 공유하자&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/44</guid>
      <comments>https://chhb-miscellaneous.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:02:40 +0900</pubDate>
    </item>
    <item>
      <title>VSCode에서 OpenAI Codex 쓰는 법 &amp;mdash; Copilot 그 다음 단계</title>
      <link>https://chhb-miscellaneous.tistory.com/42</link>
      <description>&lt;p&gt;Copilot이 자동완성의 끝판왕이라고 생각했는데, Codex를 써보니까 레벨이 다르다. Copilot이 &amp;quot;이 줄 다음에 뭐가 올까?&amp;quot; 를 맞추는 거라면, Codex는 &amp;quot;이 프로젝트에서 이 기능을 구현하려면 어떤 파일을 어떻게 고쳐야 하는지&amp;quot; 를 통째로 처리하는 에이전트다. 파일 읽고, 수정하고, 터미널 명령어까지 실행한다.&lt;/p&gt;
&lt;p&gt;처음에 &amp;quot;이게 Copilot이랑 뭐가 다르다는 거지?&amp;quot; 했는데, 한번 써보면 안다. 차원이 다른 물건이다. VSCode에서 Codex 세팅하는 법을 처음부터 끝까지 정리해본다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Codex가 정확히 뭔데&lt;/h2&gt;
&lt;p&gt;OpenAI가 만든 &lt;strong&gt;코딩 에이전트&lt;/strong&gt;다. 이름이 같아서 헷갈릴 수 있는데, 2023년에 없어진 옛날 Codex API와는 완전히 다른 제품이다. 2025년 4월에 CLI로 처음 나왔고, 지금은 VSCode 확장 프로그램, 데스크톱 앱, ChatGPT 웹 버전까지 다양한 형태로 쓸 수 있다.&lt;/p&gt;
&lt;p&gt;핵심 특징:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프로젝트 전체 코드베이스를 읽고 이해한다&lt;/li&gt;
&lt;li&gt;여러 파일을 동시에 수정할 수 있다&lt;/li&gt;
&lt;li&gt;터미널 명령어를 직접 실행한다 (빌드, 테스트, Git 등)&lt;/li&gt;
&lt;li&gt;샌드박스 안에서 안전하게 동작한다&lt;/li&gt;
&lt;li&gt;Codex Cloud로 클라우드에서 비동기 작업도 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2026년 3월 기준으로 주간 활성 사용자가 200만 명을 넘었다고 한다. 확실히 주변에서 쓰는 사람이 부쩍 늘었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설치 전 준비물&lt;/h2&gt;
&lt;h3&gt;1. ChatGPT 유료 구독&lt;/h3&gt;
&lt;p&gt;Codex는 무료 플랜에서 사용할 수 없다. 다음 플랜 중 하나가 필요하다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ChatGPT Plus       — 기본 Codex 사용 가능
ChatGPT Pro        — 더 많은 사용량
Business / Edu     — 팀/교육용
Enterprise         — 기업용&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;주의할 점은 &lt;strong&gt;ChatGPT 구독&lt;/strong&gt;이라는 거다. OpenAI API 크레딧과는 별개다. ChatGPT Plus($20/월)만 있으면 Codex를 쓸 수 있다.&lt;/p&gt;
&lt;h3&gt;2. 운영체제&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;macOS&lt;/li&gt;
&lt;li&gt;Windows (네이티브 + WSL 둘 다 지원)&lt;/li&gt;
&lt;li&gt;Linux&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이전에는 Windows에서 WSL이 필수였는데, 지금은 Windows 네이티브 샌드박스도 지원한다. 다만 Linux 네이티브 환경이 필요한 프로젝트라면 WSL을 쓰는 게 낫다.&lt;/p&gt;
&lt;h3&gt;3. VSCode&lt;/h3&gt;
&lt;p&gt;당연히 필요하다. Cursor, Windsurf 같은 VSCode 포크에서도 동작한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 1: Codex CLI 설치&lt;/h2&gt;
&lt;p&gt;VSCode 확장 프로그램은 CLI 위에서 동작한다. CLI부터 깔아야 한다.&lt;/p&gt;
&lt;h3&gt;macOS / Linux&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://chatgpt.com/codex/install.sh | sh&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;powershell -ExecutionPolicy ByPass -c &amp;quot;irm https://chatgpt.com/codex/install.ps1 | iex&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;패키지 매니저로 설치&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS (Homebrew)
brew install openai-codex

# npm
npm install -g @openai/codex&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codex --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;버전 번호가 나오면 성공이다.&lt;/p&gt;
&lt;h3&gt;첫 로그인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codex&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 실행하면 로그인 프롬프트가 뜬다. ChatGPT 계정으로 인증하면 된다. API 키로도 인증 가능하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Step 2: VSCode 확장 프로그램 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;1. VSCode 열기
2. Cmd+Shift+X (Mac) 또는 Ctrl+Shift+X (Windows/Linux)
3. &amp;quot;Codex&amp;quot; 검색
4. OpenAI 공식 배포본의 [Install] 클릭&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Cursor나 Windsurf를 쓰고 있다면 해당 마켓플레이스에서 검색하면 된다.&lt;/p&gt;
&lt;p&gt;설치 후 VSCode를 재시작하면 사이드바에 Codex 아이콘이 나타난다.&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;: 확장 프로그램이 CLI 설정(&lt;code&gt;~/.codex/config.toml&lt;/code&gt;)을 공유한다. CLI에서 이미 로그인했으면 확장 프로그램에서 따로 인증할 필요 없다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;Step 3: 기본 사용법&lt;/h2&gt;
&lt;h3&gt;Codex 패널 열기&lt;/h3&gt;
&lt;p&gt;VSCode에서 Codex를 여는 방법:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사이드바의 Codex 아이콘 클릭&lt;/li&gt;
&lt;li&gt;명령 팔레트(Cmd+Shift+P) → &amp;quot;Codex&amp;quot; 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;패널이 열리면 채팅 입력창에 자연어로 요청하면 된다.&lt;/p&gt;
&lt;h3&gt;승인 모드(Approval Mode) — 이게 핵심이다&lt;/h3&gt;
&lt;p&gt;Codex는 세 가지 모드로 동작한다. 얼마나 자유를 줄지 내가 정할 수 있다.&lt;/p&gt;
&lt;h4&gt;Chat 모드&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- 파일을 읽지 않는다
- 코드를 수정하지 않는다
- 명령어를 실행하지 않는다
- 그냥 대화만 한다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;설계를 상의하거나 아키텍처를 논의할 때 쓴다. &amp;quot;이 구조에서 인증은 어떻게 하는 게 좋을까?&amp;quot; 같은 질문.&lt;/p&gt;
&lt;h4&gt;Agent 모드 (기본값)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- 파일을 읽을 수 있다
- 코드를 수정할 수 있다
- 작업 디렉토리 안에서 명령어를 실행할 수 있다
- 작업 디렉토리 밖이나 네트워크 접근은 승인 필요&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;대부분의 작업에서 이 모드를 쓴다. Codex가 알아서 파일을 읽고 수정하지만, 위험한 작업은 내 승인을 먼저 받는다. 신입 개발자한테 코드 리뷰 받는 느낌이랄까.&lt;/p&gt;
&lt;h4&gt;Agent (Full Access) 모드&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- 파일 읽기/쓰기 자유
- 명령어 실행 자유
- 네트워크 접근도 자유
- 승인 없이 다 한다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;완전 자율 모드. 뭘 하든 안 물어본다. 편하긴 한데, 신뢰할 수 있는 환경에서만 쓰자. 프로덕션 서버가 연결된 환경에서 Full Access 켜놓으면... 상상에 맡기겠다.&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;⚠️ 나는 기본적으로 Agent 모드를 쓰고, 반복적인 작업(테스트 실행, 빌드 같은)을 많이 시킬 때만 잠깐 Full Access로 바꾼다. 끝나면 바로 돌려놓는다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;실전에서 이렇게 쓴다&lt;/h2&gt;
&lt;h3&gt;코드 작성 요청&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;src/api/users.ts에 비밀번호 변경 API를 추가해줘. 
현재 비밀번호 확인 → 새 비밀번호 유효성 검사 → bcrypt로 해싱 → DB 업데이트 순서로.&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Codex가 파일을 읽어서 기존 코드 스타일에 맞춰 구현해준다. 변경사항이 diff로 표시되고, 수락하면 적용된다.&lt;/p&gt;
&lt;h3&gt;버그 수정&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;npm run test 돌리면 UserService.test.ts에서 3개 실패하는데, 고쳐줘.&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이러면 Codex가 직접 테스트를 실행하고, 에러 메시지를 읽고, 원인을 분석해서 수정 코드를 제안한다. 테스트 코드가 잘못된 건지 실제 코드가 잘못된 건지까지 판단해준다.&lt;/p&gt;
&lt;h3&gt;리팩토링&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;src/utils/ 폴더의 모든 함수를 TypeScript strict mode에 맞게 타입을 추가해줘.&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여러 파일을 한 번에 수정해야 하는 작업에서 Codex가 진가를 발휘한다. 파일 하나하나 열어서 고치는 것보다 열 배는 빠르다.&lt;/p&gt;
&lt;h3&gt;코드 이해&lt;/h3&gt;
&lt;p&gt;새 프로젝트에 투입됐을 때 이게 진짜 유용하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;이 프로젝트의 전체 아키텍처를 설명해줘. 어떤 프레임워크를 쓰고, 
주요 모듈은 뭐고, 데이터 흐름이 어떻게 되는지.&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;README를 읽는 것보다 빠르고 정확하다. 코드를 직접 읽고 분석하니까.&lt;/p&gt;
&lt;h3&gt;Git 작업&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;지금까지 변경한 내용으로 커밋해줘. 커밋 메시지는 conventional commit 형식으로.&amp;quot;

&amp;quot;이 브랜치의 변경사항으로 PR 설명을 작성해줘.&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;Codex Cloud — 클라우드에서 비동기 작업&lt;/h2&gt;
&lt;p&gt;이게 Codex의 킬러 피처 중 하나다. 오래 걸리는 작업을 클라우드에서 돌릴 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. VSCode에서 작업 요청
2. &amp;quot;Run in the cloud&amp;quot; 선택
3. 환경 설정 (Node.js, Python 등)
4. Codex Cloud에서 작업 실행
5. 진행 상황을 IDE에서 실시간으로 확인
6. 완료되면 결과 리뷰 + 적용&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;예를 들어 &amp;quot;이 프로젝트의 테스트 커버리지를 80%까지 올려줘&amp;quot; 같은 작업을 던져놓고, 나는 다른 일을 하면 된다. 작업이 끝나면 알려주니까 그때 리뷰하면 된다.&lt;/p&gt;
&lt;p&gt;로컬에서 돌리면 내 맥북이 몇 시간 동안 끙끙대야 할 작업을 클라우드에서 알아서 처리해준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AGENTS.md — 프로젝트별 지시사항&lt;/h2&gt;
&lt;p&gt;프로젝트 루트에 &lt;code&gt;AGENTS.md&lt;/code&gt; 파일을 만들면 Codex가 해당 프로젝트에서 항상 참고하는 커스텀 지시사항이 된다. Claude Code의 &lt;code&gt;CLAUDE.md&lt;/code&gt;와 같은 개념이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# AGENTS.md

## 프로젝트 정보
- Next.js 14 + TypeScript + Prisma + PostgreSQL
- 패키지 매니저: pnpm
- 테스트: Vitest + Testing Library

## 코딩 규칙
- 함수형 컴포넌트만 사용 (클래스 컴포넌트 금지)
- CSS는 Tailwind CSS만 사용
- API 응답 형식: { success: boolean, data?: T, error?: string }
- 에러 핸들링은 try-catch로 통일

## 명령어
- 개발 서버: pnpm dev
- 테스트: pnpm test
- 빌드: pnpm build
- 린트: pnpm lint

## 주의사항
- .env 파일은 절대 커밋하지 말 것
- DB 마이그레이션은 반드시 별도 PR로 올릴 것
- console.log는 프로덕션 코드에 남기지 말 것&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이걸 써두면 매번 &amp;quot;pnpm 쓰고 있어&amp;quot;, &amp;quot;Tailwind로 해줘&amp;quot; 같은 말을 반복하지 않아도 된다. 팀원 전체가 같은 &lt;code&gt;AGENTS.md&lt;/code&gt;를 공유하면 Codex가 일관된 코드를 만들어준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;샌드박스와 보안&lt;/h2&gt;
&lt;p&gt;Codex가 내 컴퓨터에서 명령어를 실행한다는 게 좀 무섭게 느껴질 수 있다. 나도 처음에 그랬다. 근데 보안 구조를 알고 나면 괜찮다.&lt;/p&gt;
&lt;h3&gt;샌드박스 모드&lt;/h3&gt;
&lt;p&gt;Codex는 OS 수준의 샌드박스 안에서 동작한다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;네트워크 접근&lt;/strong&gt;: 기본적으로 차단. 필요하면 승인 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파일 쓰기&lt;/strong&gt;: 작업 디렉토리 안에서만 가능. 밖은 승인 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;민감한 경로&lt;/strong&gt;: &lt;code&gt;.git/&lt;/code&gt;이나 &lt;code&gt;.codex/&lt;/code&gt; 같은 경로는 쓰기 잠금.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그러니까 Codex가 갑자기 시스템 파일을 건드리거나, 몰래 네트워크 요청을 보내거나 하는 일은 없다. Agent 모드에서는 위험한 작업마다 &amp;quot;이거 해도 돼?&amp;quot; 하고 물어본다.&lt;/p&gt;
&lt;h3&gt;config.toml로 세부 설정&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# ~/.codex/config.toml

# 기본 승인 정책
approval_policy = &amp;quot;on-request&amp;quot;

# 샌드박스 모드
sandbox_mode = &amp;quot;workspace-write&amp;quot;

# 파일 열기에 사용할 에디터
file_opener = &amp;quot;vscode&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보안 수준을 프로젝트별로 다르게 가져갈 수도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Copilot이랑 같이 쓸 수 있나?&lt;/h2&gt;
&lt;p&gt;쓸 수 있다. 역할이 다르니까 오히려 같이 쓰면 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copilot  → 타이핑 중 실시간 자동완성. 한 줄, 두 줄 빠르게 채우기.
Codex    → &amp;quot;이 모듈 전체를 리팩토링해줘&amp;quot; 같은 대규모 에이전트 작업.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;둘 다 켜놓고 쓰면 자동완성은 Copilot이 처리하고, 구조적인 작업은 Codex에게 맡기는 식으로 자연스럽게 분담된다. 나도 이 조합으로 쓰고 있다.&lt;/p&gt;
&lt;p&gt;Claude Code와 비교하자면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Codex&lt;/strong&gt;: ChatGPT 계정으로 쓸 수 있고, Codex Cloud(비동기 클라우드 작업)가 강점. Windows 네이티브 지원.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: Anthropic 계정 필요. 터미널 중심. 코드 품질은 체감상 비슷한데, 각자 잘하는 영역이 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;둘 다 써봤는데, 결국 어느 쪽 계정/생태계에 더 익숙한지에 따라 선택하게 되는 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 쓰는 슬래시 명령어&lt;/h2&gt;
&lt;p&gt;Codex 채팅창에서 &lt;code&gt;/&lt;/code&gt;를 입력하면 슬래시 명령어를 쓸 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/permissions     — 현재 승인 모드 확인/변경
/model           — 사용할 모델 변경
/goal            — 장기 목표 설정 (지속적으로 추적)
/compact         — 대화 기록 압축 (컨텍스트 절약)
/clear           — 대화 초기화&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;/goal&lt;/code&gt;은 꽤 유용한 기능인데, &amp;quot;이 프로젝트의 테스트 커버리지를 80%까지 올리기&amp;quot; 같은 장기 목표를 설정하면 Codex가 진행 상황을 추적해준다. 일시중지했다가 나중에 이어서 할 수도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 문제와 해결법&lt;/h2&gt;
&lt;h3&gt;&amp;quot;codex: command not found&amp;quot;&lt;/h3&gt;
&lt;p&gt;CLI가 PATH에 안 잡힌 거다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 설치 경로 확인
which codex

# 없으면 재설치
curl -fsSL https://chatgpt.com/codex/install.sh | sh

# shell 설정 새로고침
source ~/.zshrc  # 또는 source ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;확장 프로그램에서 로그인이 안 됨&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# CLI에서 인증 상태 확인
codex auth

# 재인증
codex auth login

# 그래도 안 되면 설정 디렉토리 삭제 후 재시도
rm -rf ~/.codex/credentials*
codex auth login&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;환경 진단&lt;/h3&gt;
&lt;p&gt;뭔가 안 되는데 원인을 모르겠으면:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codex doctor&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;환경, Git, 터미널, 인증 상태를 전부 체크해서 알려준다. 이게 은근 유용하다.&lt;/p&gt;
&lt;h3&gt;Windows에서 환경변수 문제&lt;/h3&gt;
&lt;p&gt;Windows에서 VSCode를 앱 런처(시작 메뉴)로 열면 환경변수가 안 잡히는 경우가 있다. 터미널에서 &lt;code&gt;code .&lt;/code&gt;로 VSCode를 열면 해결된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# 터미널에서 VSCode 실행
cd C:\Users\myuser\projects\myapp
code .&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;실전 팁&lt;/h2&gt;
&lt;h3&gt;1. 작은 작업부터 시작하자&lt;/h3&gt;
&lt;p&gt;처음부터 &amp;quot;전체 프로젝트 리팩토링해줘&amp;quot; 이러면 결과가 정신없다. 파일 하나, 함수 하나부터 시작해서 Codex가 어떻게 동작하는지 감을 잡자.&lt;/p&gt;
&lt;h3&gt;2. Agent 모드로 시작하고, 필요할 때만 Full Access&lt;/h3&gt;
&lt;p&gt;기본 Agent 모드에서 대부분의 작업이 가능하다. Full Access는 반복적으로 승인 누르기 귀찮을 때만 쓰고, 작업 끝나면 바로 돌려놓자.&lt;/p&gt;
&lt;h3&gt;3. AGENTS.md를 잘 써두면 생산성이 배로 올라간다&lt;/h3&gt;
&lt;p&gt;프로젝트 컨벤션, 사용 기술 스택, 빌드 명령어 같은 걸 정리해두면 Codex가 처음부터 맥락을 이해한 상태에서 작업한다. 이 초기 세팅 시간이 나중에 열 배로 돌아온다.&lt;/p&gt;
&lt;h3&gt;4. diff는 꼭 확인하자&lt;/h3&gt;
&lt;p&gt;아무리 AI가 잘 해줘도 무조건 수락은 위험하다. diff를 확인하는 습관을 들이자. 특히 보안 관련 코드, DB 쿼리, 외부 API 호출 부분은 꼼꼼하게.&lt;/p&gt;
&lt;h3&gt;5. Codex Cloud를 적극 활용하자&lt;/h3&gt;
&lt;p&gt;테스트 전체 실행, 대규모 리팩토링, 문서 생성 같은 시간 오래 걸리는 작업은 클라우드에서 돌리자. 로컬 머신 리소스를 아낄 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;VSCode에서 Codex 쓰는 과정을 정리하면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ChatGPT 유료 구독 확인 (Plus 이상)&lt;/li&gt;
&lt;li&gt;Codex CLI 설치 (&lt;code&gt;curl&lt;/code&gt; 한 줄)&lt;/li&gt;
&lt;li&gt;VSCode 확장 프로그램 설치 (마켓플레이스에서 &amp;quot;Codex&amp;quot; 검색)&lt;/li&gt;
&lt;li&gt;로그인 (ChatGPT 계정)&lt;/li&gt;
&lt;li&gt;사이드바에서 Codex 패널 열고 사용 시작&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;핵심 정리:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세 가지 승인 모드(Chat, Agent, Full Access)를 상황에 맞게 쓰자&lt;/li&gt;
&lt;li&gt;기본은 Agent 모드. 위험한 작업은 승인을 거치니까 안전하다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;에 프로젝트 컨벤션을 써두면 반복 설명이 필요 없다&lt;/li&gt;
&lt;li&gt;Codex Cloud로 오래 걸리는 작업은 비동기로 던져놓을 수 있다&lt;/li&gt;
&lt;li&gt;Copilot과 같이 쓸 수 있다. 자동완성은 Copilot, 에이전트 작업은 Codex&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex doctor&lt;/code&gt;로 환경 문제를 빠르게 진단할 수 있다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/42</guid>
      <comments>https://chhb-miscellaneous.tistory.com/42#entry42comment</comments>
      <pubDate>Mon, 1 Jun 2026 14:13:25 +0900</pubDate>
    </item>
    <item>
      <title>VSCode에서 Claude Code 쓰는 법 &amp;mdash; 설치부터 실전 워크플로우까지 한 방에 정리</title>
      <link>https://chhb-miscellaneous.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Copilot 쓰다가 Claude Code로 갈아탄 지 몇 달 됐다. 솔직히 처음에는 &quot;터미널에서 AI를 쓴다고?&quot; 하면서 좀 회의적이었는데, 한번 써보니까 차원이 다르더라. 코드 한 줄 자동완성하는 수준이 아니라, 프로젝트 전체 구조를 이해하고 여러 파일을 동시에 수정해준다. 리팩토링 시켜보면 진짜 소름 돋는다.&lt;br&gt;근데 설치 과정에서 좀 헤매는 사람이 많은 것 같다. 나도 처음에 Node.js 버전 때문에 삽질했었고. 그래서 VSCode에서 Claude Code 쓰는 방법을 처음부터 끝까지 정리해봤다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude Code가 뭔데&lt;/h3&gt;&lt;p&gt;먼저 개념 정리부터. &lt;strong&gt;Claude Code&lt;/strong&gt;는 Anthropic이 만든 터미널 기반 AI 코딩 에이전트다. Copilot이나 Cursor 같은 에디터 내장 자동완성 도구와는 성격이 다르다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;차이를 비교하면:&lt;/p&gt;&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;Copilot/Cursor&lt;/strong&gt;: 에디터 안에서 커서 위치의 코드를 자동완성. 한 파일 내에서 동작.&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: 프로젝트 전체를 읽고, 여러 파일을 동시에 수정하고, 쉘 명령어도 실행하고, Git 커밋까지 한다. &lt;strong&gt;에이전트&lt;/strong&gt;라는 표현이 맞다.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode 확장 프로그램을 설치하면 이 터미널 에이전트를 GUI로 편하게 쓸 수 있다. 인라인 diff로 변경사항을 확인하고, 수락/거부를 클릭 한 번으로 할 수 있고, 체크포인트로 되감기도 가능하다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 전 준비물&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code를 쓰려면 몇 가지가 필요하다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Node.js (v18 이상)&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code는 npm으로 설치하기 때문에 Node.js가 필요하다. LTS 버전(현재 v22)을 권장한다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 버전 확인
node -v
# v22.x.x 이상이면 OK

npm -v
# 10.x.x 이상이면 OK&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js가 없거나 버전이 낮으면 설치/업데이트하자.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS (Homebrew)
brew install node

# 또는 nvm으로 버전 관리 (추천)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
nvm install 22
nvm use 22&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Windows는 &lt;a href=&quot;https://nodejs.org&quot; target=&quot;_self&quot;&gt;&lt;span&gt;Node.js 공식 사이트&lt;/span&gt;&lt;/a&gt;에서 설치 파일을 다운로드하면 된다. WSL을 쓰고 있다면 WSL 안에서 설치하는 게 좋다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Anthropic 유료 계정&lt;/h3&gt;&lt;p&gt;이게 중요한데, &lt;strong&gt;Claude Code는 무료 플랜에서 사용할 수 없다&lt;/strong&gt;. 최소 Pro 구독($20/월) 이상이 필요하다.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Pro ($20/월)       — 일일 사용량 제한 있지만 일반적인 코딩 세션은 충분
Max ($100/월)      — 전문 개발자 일일 작업량 커버
Max ($200/월)      — 대규모 자율 작업, 팀 공유 워크플로우
Team ($30/유저/월)  — 팀 단위 사용
Enterprise         — 기업용
API 크레딧          — 종량제도 가능&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;처음이면 Pro로 시작해서, 사용량 제한에 자주 걸리면 Max로 올리는 게 합리적이다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 운영체제&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;macOS 13.0 (Ventura) 이상&lt;/li&gt;&lt;li&gt;Ubuntu 20.04+ / Debian 10+&lt;/li&gt;&lt;li&gt;Windows 10 (1809+) + WSL&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Windows에서는 WSL(Windows Subsystem for Linux) 환경에서 돌려야 한다. 네이티브 Windows 터미널에서는 안 된다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 1: Claude Code CLI 설치&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode 확장 프로그램은 CLI 위에서 돌아간다. CLI가 먼저 설치돼 있어야 한다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 A: Native 설치 (권장)&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 기준 Anthropic이 공식 권장하는 방법이다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS / Linux
curl -fsSL https://cli.claude.ai/install.sh | sh

# 설치 확인
claude --version&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 B: npm으로 설치&lt;/h3&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @anthropic-ai/claude-code

# 설치 확인
claude --version&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;npm 설치 시 macOS/Linux에서 권한 에러가 나면, &lt;code&gt;sudo&lt;/code&gt;를 쓰지 말고 npm 경로를 사용자 디렉토리로 바꾸자:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
export PATH=~/.npm-global/bin:$PATH

# 위 export를 ~/.zshrc 또는 ~/.bashrc에 추가해두자
echo 'export PATH=~/.npm-global/bin:$PATH' &amp;gt;&amp;gt; ~/.zshrc&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 로그인&lt;/h3&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 프로젝트 폴더로 이동
cd ~/my-project

# Claude Code 실행
claude

# 처음 실행하면 브라우저가 열리면서 Anthropic 로그인 화면이 뜬다
# 로그인하면 인증 완료&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하면 터미널에서 Claude Code를 쓸 수 있다. 근데 우리의 목표는 VSCode에서 쓰는 거니까 계속 가보자.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 2: VSCode 확장 프로그램 설치&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;1. VSCode 열기
2. Cmd+Shift+X (Mac) 또는 Ctrl+Shift+X (Windows/Linux)로 확장 프로그램 탭 열기
3. &quot;Claude Code&quot; 검색
4. Anthropic이 배포한 공식 확장 프로그램의 [Install] 클릭&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;설치 후 확장 프로그램이 안 보이면 VSCode를 재시작하거나, 명령 팔레트(Cmd+Shift+P)에서 &lt;code&gt;Developer: Reload Window&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;/span&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;참고&lt;/strong&gt;: VSCode 1.98.0 이상이 필요하다. 오래된 버전이면 업데이트부터 하자.&lt;/p&gt; 
 &lt;p&gt;&lt;/p&gt;
&lt;/blockquote&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장 프로그램 열기&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code를 여는 방법이 여러 가지 있다:&lt;/p&gt;&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;Spark 아이콘&lt;/strong&gt;: 파일을 열어놓은 상태에서 에디터 오른쪽 위 툴바의 불꽃 모양 아이콘 클릭&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;명령 팔레트&lt;/strong&gt;: Cmd+Shift+P → &quot;Claude Code: Open&quot; 입력&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;활동 표시줄&lt;/strong&gt;: 왼쪽 사이드바의 Spark 아이콘 클릭 (세션 목록 열림)&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;상태 바&lt;/strong&gt;: 하단 상태 바의 &quot;✱ Claude Code&quot; 클릭&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;처음 열면 로그인 창이 뜬다. CLI에서 이미 로그인했으면 자동으로 인증이 잡힐 수도 있다.&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;/span&gt;&lt;/p&gt;
 &lt;p&gt;⚠️ Spark 아이콘이 안 보이면? 파일이 하나도 열려있지 않으면 아이콘이 안 나타난다. 파일을 하나 열어보자. 그래도 안 보이면 다른 AI 확장 프로그램(Copilot 등)과 충돌일 수 있으니, 명령 팔레트나 상태 바로 우회하면 된다.&lt;/p&gt; 
 &lt;p&gt;&lt;/p&gt;
&lt;/blockquote&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 3: 기본 사용법&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;자연어로 코드 작성 요청&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 패널이 열리면 프롬프트 입력창에 자연어로 요청하면 된다.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# 이런 식으로 요청
&quot;이 프로젝트에 로그인 API를 추가해줘. JWT 인증 방식으로.&quot;

&quot;src/utils/helpers.ts 파일의 날짜 포맷 함수를 리팩토링해줘. dayjs 쓰지 말고 네이티브 Intl API로.&quot;

&quot;현재 프로젝트의 전체 구조를 분석하고, 개선할 점을 알려줘.&quot;

&quot;이 에러 로그를 분석해줘: [에러 메시지 붙여넣기]&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude가 변경사항을 제안하면 인라인 diff로 보여준다. 하이라이트된 부분을 확인하고 수락(Accept)하면 코드가 실제로 적용된다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;@-멘션으로 파일 참조&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;특정 파일이나 폴더를 직접 지정해서 맥락을 줄 수 있다. 이게 정말 유용하다.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;@src/controllers/auth.ts 이 파일의 에러 핸들링을 개선해줘

@src/models/ 이 폴더의 모든 모델에 타입스크립트 타입을 추가해줘

@package.json 사용하지 않는 의존성을 찾아줘&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;에디터에서 코드 범위를 드래그해서 선택한 뒤 Claude에게 보내는 것도 가능하다:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;1. 에디터에서 코드 블록 선택 (드래그)
2. Claude Code 프롬프트에서 @로 참조
3. &quot;선택한 코드를 async/await 방식으로 바꿔줘&quot; 요청&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 대화 동시에 열기&lt;/h3&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;탭 1: 프론트엔드 컴포넌트 작업&lt;/li&gt;&lt;li&gt;탭 2: 백엔드 API 작업&lt;/li&gt;&lt;li&gt;탭 3: 테스트 코드 작성&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;명령 팔레트에서 &quot;Claude Code: Open in New Tab&quot;을 선택하면 새 탭이 열린다. 맥락이 분리되니까 작업별로 나눠서 쓰는 게 효율적이다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;실전 워크플로우&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;워크플로우 1: 코드 리뷰 + 리팩토링&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이게 내가 제일 많이 쓰는 패턴이다.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;1. 작업할 파일을 에디터에서 연다
2. Claude Code에 요청: &quot;@src/services/payment.ts 이 파일을 리뷰해줘. 보안 이슈, 성능 문제, 코드 스타일 개선점을 찾아줘&quot;
3. Claude가 분석 결과를 보여줌
4. &quot;3번 이슈를 수정해줘&quot; 같은 식으로 후속 요청
5. diff 확인 후 수락&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;워크플로우 2: 테스트 코드 생성&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;&quot;@src/utils/calculator.ts 이 파일에 대한 단위 테스트를 Jest로 작성해줘. 엣지 케이스도 포함해서.&quot;&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;워크플로우 3: 버그 디버깅&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;&quot;앱을 실행하면 TypeError: Cannot read property 'map' of undefined 에러가 나. 
@src/components/UserList.tsx 여기서 발생하는 것 같은데, 원인이랑 수정 방법 알려줘.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude가 코드를 분석해서 원인을 찾아주고, 수정 코드를 제안한다. API 응답이 null일 때의 방어 코드가 없다든지, 비동기 처리에서 초기값을 안 넣었다든지.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;워크플로우 4: Git 작업 자동화&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code는 Git도 다룰 수 있다. 이건 터미널 모드에서 더 강력하다.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&quot;지금까지 변경한 내용으로 커밋해줘. 커밋 메시지는 conventional commit 형식으로.&quot;

&quot;이 브랜치의 변경사항을 요약해서 PR 설명 초안을 만들어줘.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Plan Mode — 실행 전에 계획 먼저 확인&lt;/h3&gt;&lt;p&gt;Claude에게 큰 작업을 시킬 때는 &lt;strong&gt;Plan Mode&lt;/strong&gt;를 쓰는 게 좋다. Claude가 바로 코드를 수정하는 게 아니라, 먼저 &quot;이런 식으로 할 거다&quot;라는 계획을 보여준다. 계획을 검토하고 수정한 뒤에 실행을 승인하는 방식.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&quot;이 프로젝트를 Express에서 Fastify로 마이그레이션해줘&quot; (Plan Mode)

→ Claude의 계획:
1. package.json에서 express 제거, fastify 추가
2. src/app.ts 라우팅 구조 변경
3. 미들웨어를 Fastify 플러그인으로 교체
4. 에러 핸들링 수정
5. 테스트 코드 업데이트

→ &quot;2번에서 라우팅은 기존 구조 유지하면서 해줘&quot;처럼 수정 가능
→ 승인하면 실행&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;큰 변경을 할 때 이 과정 없이 바로 실행하면 의도와 다르게 갈 수 있다. Plan Mode는 습관처럼 쓰자.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Checkpoints — 되감기 기능&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude가 여러 파일을 수정했는데, 결과가 마음에 안 들 때가 있다. 이때 Checkpoint로 되감을 수 있다.&lt;br&gt;Claude가 작업할 때 자동으로 체크포인트를 만들어두니까, 특정 시점으로 돌아가는 게 가능하다. Git stash나 undo보다 편하다. &quot;아까 그 상태로 돌아가줘&quot;가 된다는 거다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;CLAUDE.md — 프로젝트별 지시사항&lt;/h3&gt;&lt;p&gt;프로젝트 루트에 &lt;code&gt;CLAUDE.md&lt;/code&gt; 파일을 만들면 Claude가 해당 프로젝트에서 항상 참고하는 지시사항이 된다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# CLAUDE.md

## 프로젝트 개요
이 프로젝트는 Next.js 14 + TypeScript + Prisma + PostgreSQL 기반 이커머스 플랫폼입니다.

## 코딩 컨벤션
- 함수명은 camelCase
- 컴포넌트명은 PascalCase
- CSS는 Tailwind CSS만 사용 (styled-components 사용 금지)
- API 응답은 항상 { success: boolean, data?: T, error?: string } 형태

## 중요 사항
- DB 마이그레이션은 Prisma Migrate 사용
- 환경변수는 .env.local에서 관리
- 테스트는 Vitest 사용
- 절대 console.log를 프로덕션 코드에 남기지 말 것&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이걸 써두면 매번 &quot;Tailwind CSS로 해줘&quot;, &quot;Prisma 쓰고 있어&quot; 같은 걸 반복하지 않아도 된다. 팀원들이 같은 &lt;code&gt;CLAUDE.md&lt;/code&gt;를 공유하면 모두가 동일한 컨텍스트에서 Claude를 쓸 수 있다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;터미널 모드 vs 확장 프로그램 모드&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode 안에서 Claude Code를 쓰는 방법이 두 가지다:&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장 프로그램 (GUI)&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;Spark 아이콘으로 패널 열기&lt;/li&gt;&lt;li&gt;인라인 diff로 변경사항 확인&lt;/li&gt;&lt;li&gt;마우스 클릭으로 수락/거부&lt;/li&gt;&lt;li&gt;체크포인트 시각적 관리&lt;/li&gt;&lt;li&gt;@-멘션으로 파일 참조&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;터미널에서 직접 실행&lt;/h3&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# VSCode 내장 터미널에서
# Ctrl+` 로 터미널 열고
claude&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;같은 모델, 같은 인증을 쓴다. 기능적으로 동일한데, 터미널이 더 빠르게 느껴지는 경우가 있다. 복잡한 쉘 명령어나 스크립트 실행이 필요한 작업은 터미널이 편하고, 코드 수정이 주인 작업은 확장 프로그램이 편하다.&lt;br&gt;나는 보통 이렇게 쓴다:&lt;/p&gt;&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;확장 프로그램&lt;/strong&gt;: 코드 리뷰, 리팩토링, 컴포넌트 작성 같은 에디터 중심 작업&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;터미널&lt;/strong&gt;: 프로젝트 초기 세팅, 대규모 마이그레이션, Git 작업, 스크립트 실행&lt;/li&gt; 
&lt;/ul&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 단축키&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Cmd+Shift+P → &quot;Claude Code&quot;    — 명령 팔레트에서 Claude 관련 명령 검색
Cmd+Shift+P → &quot;Open in New Tab&quot; — 새 탭에서 Claude 열기
Cmd+N (Claude 패널에서)          — 새 대화 시작
Ctrl+`                          — 터미널 열기 (터미널 모드 사용 시)&lt;/code&gt;&lt;/pre&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 겪는 문제와 해결법&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;claude: command not found&quot;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;CLI가 설치 안 됐거나 PATH에 안 잡힌 거다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# PATH 확인
which claude

# 안 나오면 다시 설치
npm install -g @anthropic-ai/claude-code

# npm 글로벌 경로 확인
npm config get prefix
# 이 경로/bin이 PATH에 있어야 함&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장 프로그램에서 Claude가 안 뜸&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;1. CLI가 설치돼 있는지 확인 (claude --version)
2. VSCode 버전 확인 (1.98.0 이상)
3. 확장 프로그램 재설치
4. Developer: Reload Window 실행
5. Restricted Mode인지 확인 (좌하단에 표시됨)&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증 문제&lt;/h3&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 인증 상태 확인
claude auth status

# 재인증
claude auth login

# 그래도 안 되면 credentials 삭제 후 재인증
rm ~/.claude/credentials.json
claude auth login&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Windows에서 안 됨&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code는 Windows 네이티브를 지원하지 않는다. WSL 환경에서 써야 한다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# WSL 설치 (PowerShell 관리자 모드)
wsl --install

# WSL 안에서 Node.js + Claude Code 설치
# (위의 설치 과정과 동일)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;VSCode의 WSL 확장 프로그램을 설치하면 VSCode에서 WSL 환경을 바로 쓸 수 있다. &lt;code&gt;Ctrl+Shift+P → &quot;WSL: Connect to WSL&quot;&lt;/code&gt;로 연결하면 된다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Copilot이랑 같이 쓸 수 있나?&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;쓸 수 있다. 충돌 안 한다. 나도 둘 다 켜놓고 쓰고 있다.&lt;br&gt;역할 분담이 꽤 자연스럽다:&lt;/p&gt;&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;Copilot&lt;/strong&gt;: 타이핑하는 중에 한 줄, 두 줄 자동완성. 단순 반복 코드 빠르게 채우기.&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: &quot;이 모듈 전체를 리팩토링해줘&quot;, &quot;테스트 파일 만들어줘&quot;, &quot;이 에러 분석해줘&quot; 같은 대규모 작업.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;자동완성은 Copilot이 처리하고, 맥락이 필요한 큰 작업은 Claude Code에게 맡기는 식. 실제로 이 조합이 꽤 좋다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;비용 관련&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 자체는 무료다. 비용은 Anthropic 구독료에 포함돼 있다.&lt;br&gt;Pro($20/월)로 시작하면 하루에 코딩 세션 몇 시간 정도는 커버된다. 나는 처음에 Pro로 쓰다가 &quot;일일 사용량 한도에 도달했습니다&quot; 메시지를 일주일에 두세 번 이상 보게 되면서 Max로 올렸다.&lt;br&gt;API 크레딧 방식으로 종량제로 쓸 수도 있는데, 코딩 에이전트 특성상 컨텍스트 윈도우를 많이 쓰기 때문에 생각보다 비용이 빠르게 늘 수 있다. 월정액이 마음 편하다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode에서 Claude Code를 쓰는 과정을 정리하면:&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;Node.js 설치 (v18 이상, v22 LTS 권장)&lt;/li&gt;&lt;li&gt;Claude Code CLI 설치 (native 또는 npm)&lt;/li&gt;&lt;li&gt;VSCode 확장 프로그램 설치 (마켓플레이스에서 &quot;Claude Code&quot; 검색)&lt;/li&gt;&lt;li&gt;로그인 (Anthropic 유료 계정 필요)&lt;/li&gt;&lt;li&gt;Spark 아이콘 클릭해서 사용 시작&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 팁:&lt;/p&gt;&lt;ul&gt; 
 &lt;li&gt;@-멘션으로 파일을 직접 지정하면 정확도가 올라간다&lt;/li&gt; 
 &lt;li&gt;큰 작업은 Plan Mode로 계획 먼저 확인하자&lt;/li&gt; 
 &lt;li&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;에 프로젝트 컨벤션을 써두면 매번 반복 설명할 필요 없다&lt;/li&gt; 
 &lt;li&gt;Copilot과 같이 쓸 수 있다. 역할이 다르니까 충돌 없다&lt;/li&gt; 
 &lt;li&gt;코드 수정 작업은 확장 프로그램, 쉘 작업은 터미널 모드가 편하다&lt;/li&gt; 
 &lt;li&gt;Pro로 시작해서 한도에 자주 걸리면 Max로 올리자&lt;/li&gt; 
&lt;/ul&gt;</description>
      <category>AI</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/41</guid>
      <comments>https://chhb-miscellaneous.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 1 Jun 2026 13:38:58 +0900</pubDate>
    </item>
    <item>
      <title>MSSQL 인덱스의 INCLUDE, 이거 모르면 인덱스를 반만 쓰는 거다</title>
      <link>https://chhb-miscellaneous.tistory.com/40</link>
      <description>&lt;p&gt;쿼리 튜닝하다 보면 &amp;quot;인덱스 걸었는데 왜 안 빨라지지?&amp;quot; 하는 순간이 온다. 실행 계획 열어보면 인덱스를 타긴 타는데, 그 뒤에 &lt;strong&gt;Key Lookup&lt;/strong&gt;이라는 놈이 붙어 있다. 이게 뭔가 하고 파봤더니, 인덱스에서 원하는 데이터를 다 못 가져와서 원본 테이블을 다시 뒤지고 있는 거였다. 인덱스를 탔는데 결국 테이블도 읽는, 이 모순적인 상황.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;INCLUDE&lt;/code&gt;를 알기 전과 후로 내 인덱스 설계가 완전히 달라졌다. 과장 좀 보태면 MSSQL 인덱스에서 제일 중요한 기능이 &lt;code&gt;INCLUDE&lt;/code&gt;라고 생각한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;먼저 인덱스 구조를 알아야 한다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;INCLUDE&lt;/code&gt;가 왜 필요한지 이해하려면, MSSQL 인덱스가 내부적으로 어떻게 생겼는지를 알아야 한다.&lt;/p&gt;
&lt;p&gt;MSSQL의 Non-Clustered Index는 &lt;strong&gt;B-Tree 구조&lt;/strong&gt;다. 크게 두 부분으로 나뉜다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;리프 레벨이 아닌 노드 (상위 노드)&lt;/strong&gt;: 검색 방향을 결정하는 키 값만 들어있다. &amp;quot;여기로 가면 이 범위의 데이터가 있어&amp;quot; 하고 길을 안내하는 역할.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리프 레벨 (최하위 노드)&lt;/strong&gt;: 실제 키 값 + 클러스터드 인덱스 키(또는 RID)가 들어있다. 여기서 데이터를 못 찾으면 원본 테이블로 가야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문제는 SELECT하는 컬럼이 인덱스 키에 없을 때 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders (CustomerID);

-- 쿼리
SELECT CustomerID, OrderDate, TotalAmount
FROM Orders
WHERE CustomerID = 12345;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 쿼리가 실행되면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;IX_Orders_CustomerID&lt;/code&gt; 인덱스에서 &lt;code&gt;CustomerID = 12345&lt;/code&gt;를 찾는다 (Index Seek, 빠름)&lt;/li&gt;
&lt;li&gt;근데 &lt;code&gt;OrderDate&lt;/code&gt;랑 &lt;code&gt;TotalAmount&lt;/code&gt;는 인덱스에 없다&lt;/li&gt;
&lt;li&gt;원본 테이블(클러스터드 인덱스)로 가서 해당 행을 다시 읽는다 (&lt;strong&gt;Key Lookup&lt;/strong&gt;, 느림)&lt;/li&gt;
&lt;li&gt;결과 합치기 (Nested Loop Join)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Key Lookup이 한두 건이면 괜찮다. 근데 &lt;code&gt;CustomerID = 12345&lt;/code&gt;인 행이 1000개면? Key Lookup을 1000번 한다. 인덱스를 안 탄 것보다 오히려 느려지는 경우도 있다. 옵티마이저가 판단하기에 Key Lookup 비용이 너무 크면 아예 인덱스를 안 타고 Table Scan을 선택하기도 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;INCLUDE가 뭔데&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;INCLUDE&lt;/code&gt;는 &lt;strong&gt;인덱스의 리프 레벨에만 추가 컬럼을 저장&lt;/strong&gt;하는 기능이다. SQL Server 2005에서 도입됐다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders (CustomerID)
INCLUDE (OrderDate, TotalAmount);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CustomerID&lt;/code&gt;는 B-Tree의 모든 레벨에서 검색 키로 사용된다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OrderDate&lt;/code&gt;, &lt;code&gt;TotalAmount&lt;/code&gt;는 리프 레벨에만 같이 저장된다&lt;/li&gt;
&lt;li&gt;쿼리가 인덱스만으로 모든 데이터를 가져올 수 있다 → &lt;strong&gt;Key Lookup이 사라진다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 인덱스를 &lt;strong&gt;커버링 인덱스(Covering Index)&lt;/strong&gt;라고 부른다. 쿼리가 필요한 모든 컬럼을 인덱스가 &amp;quot;커버&amp;quot;하고 있으니까.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;INCLUDE vs 키 컬럼에 추가하기&lt;/h2&gt;
&lt;p&gt;&amp;quot;그러면 그냥 인덱스 키에 컬럼을 더 넣으면 되는 거 아냐?&amp;quot;라는 생각이 들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 방법 1: 키 컬럼에 전부 넣기
CREATE NONCLUSTERED INDEX IX_Orders_V1
ON Orders (CustomerID, OrderDate, TotalAmount);

-- 방법 2: INCLUDE 사용
CREATE NONCLUSTERED INDEX IX_Orders_V2
ON Orders (CustomerID)
INCLUDE (OrderDate, TotalAmount);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;둘 다 커버링 인덱스가 된다. 근데 차이가 크다.&lt;/p&gt;
&lt;h3&gt;키 컬럼에 넣으면&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;B-Tree의 &lt;strong&gt;모든 레벨&lt;/strong&gt;에 해당 컬럼이 들어간다&lt;/li&gt;
&lt;li&gt;상위 노드도 커지니까 &lt;strong&gt;인덱스 전체 크기가 커진다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;키 순서대로 정렬되기 때문에 &lt;strong&gt;정렬/범위 검색에 사용 가능&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;키 컬럼이 많아지면 INSERT/UPDATE 시 &lt;strong&gt;정렬 유지 비용이 증가&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;INCLUDE로 넣으면&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;리프 레벨에만&lt;/strong&gt; 데이터가 들어간다&lt;/li&gt;
&lt;li&gt;상위 노드는 작게 유지되니까 &lt;strong&gt;인덱스 전체 크기가 작다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;검색 키로는 사용 불가 — 정렬이나 WHERE 조건에 쓸 수 없다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INSERT/UPDATE 성능 영향이 적다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정리하면 이렇다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WHERE, ORDER BY, GROUP BY, JOIN 조건에 쓰는 컬럼 → 키 컬럼
SELECT에서 가져오기만 하는 컬럼 → INCLUDE&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 기준만 기억하면 된다. 검색에 쓰이는 건 키로, 결과에만 필요한 건 INCLUDE로.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 예제로 보는 효과&lt;/h2&gt;
&lt;h3&gt;시나리오: 주문 조회&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 테이블 구조
CREATE TABLE Orders (
    OrderID       INT IDENTITY PRIMARY KEY,     -- 클러스터드 인덱스
    CustomerID    INT NOT NULL,
    OrderDate     DATETIME NOT NULL,
    Status        NVARCHAR(20) NOT NULL,
    TotalAmount   DECIMAL(18,2) NOT NULL,
    ShippingAddr  NVARCHAR(200),
    Notes         NVARCHAR(MAX)
);

-- 자주 실행되는 쿼리
SELECT OrderDate, Status, TotalAmount
FROM Orders
WHERE CustomerID = 12345
ORDER BY OrderDate DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Case 1: INCLUDE 없는 인덱스&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders (CustomerID);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행 계획:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Index Seek (IX_Orders_CustomerID)    → Cost: 15%
  ↓
Key Lookup (Clustered Index)         → Cost: 80%  ← 이게 문제
  ↓
Sort (OrderDate DESC)                → Cost: 5%&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Key Lookup이 전체 비용의 80%를 차지하고 있다. 인덱스는 열심히 타는데 정작 느린 부분은 원본 테이블 읽기.&lt;/p&gt;
&lt;h4&gt;Case 2: INCLUDE 추가&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE NONCLUSTERED INDEX IX_Orders_CustomerID_V2
ON Orders (CustomerID, OrderDate DESC)
INCLUDE (Status, TotalAmount);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행 계획:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Index Seek (IX_Orders_CustomerID_V2) → Cost: 100%&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;끝. Key Lookup 없음. Sort도 없음 (&lt;code&gt;OrderDate DESC&lt;/code&gt;가 키에 있으니까 이미 정렬돼 있다). 완벽한 커버링 인덱스.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;OrderDate&lt;/code&gt;를 키 컬럼에 넣은 이유는 &lt;code&gt;ORDER BY&lt;/code&gt;에 쓰이기 때문이다. INCLUDE에 넣으면 정렬은 안 되니까 별도 Sort 연산이 필요해진다.&lt;/p&gt;
&lt;h3&gt;성능 차이 확인하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 실행 전에 통계 초기화
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

-- Case 1 실행
-- 논리적 읽기: 150 (인덱스 3 + 테이블 147)

-- Case 2 실행
-- 논리적 읽기: 3 (인덱스 3)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;논리적 읽기가 150에서 3으로 줄었다. 50배 차이. 이게 &lt;code&gt;INCLUDE&lt;/code&gt;의 위력이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Key Lookup 찾는 방법&lt;/h2&gt;
&lt;p&gt;실행 계획에서 Key Lookup을 찾으려면:&lt;/p&gt;
&lt;h3&gt;SSMS에서&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 쿼리 작성
2. Ctrl + L (예상 실행 계획) 또는 Ctrl + M (실제 실행 계획 포함 실행)
3. 실행 계획에서 &amp;quot;Key Lookup&amp;quot; 노드를 찾는다
4. 해당 노드에 마우스 올리면 &amp;quot;Output List&amp;quot;에 어떤 컬럼을 가져오는지 보인다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Output List에 나오는 컬럼들이 바로 &lt;code&gt;INCLUDE&lt;/code&gt;에 넣어야 할 후보들이다.&lt;/p&gt;
&lt;h3&gt;DMV(동적 관리 뷰)로 찾기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Key Lookup이 많이 발생하는 인덱스 찾기
SELECT
    OBJECT_NAME(s.object_id) AS TableName,
    i.name AS IndexName,
    s.user_lookups AS KeyLookupCount,
    s.user_seeks AS SeekCount,
    s.user_scans AS ScanCount,
    s.last_user_lookup AS LastLookup
FROM sys.dm_db_index_usage_stats s
JOIN sys.indexes i
    ON s.object_id = i.object_id
    AND s.index_id = i.index_id
WHERE s.database_id = DB_ID()
    AND i.type_desc = &amp;#39;CLUSTERED&amp;#39;
    AND s.user_lookups &amp;gt; 0
ORDER BY s.user_lookups DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;user_lookups&lt;/code&gt;가 높은 테이블이 Key Lookup이 많이 발생하는 곳이다. 여기가 &lt;code&gt;INCLUDE&lt;/code&gt;를 추가해야 할 1순위.&lt;/p&gt;
&lt;h3&gt;Missing Index DMV 활용&lt;/h3&gt;
&lt;p&gt;MSSQL은 누락된 인덱스를 알려주는 기능이 있다. 옵티마이저가 &amp;quot;이런 인덱스가 있었으면 좋겠는데...&amp;quot;라고 생각한 것들을 기록해둔다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    OBJECT_NAME(mid.object_id) AS TableName,
    mid.equality_columns AS EqualityColumns,
    mid.inequality_columns AS InequalityColumns,
    mid.included_columns AS IncludedColumns,
    migs.avg_user_impact AS AvgImpactPercent,
    migs.user_seeks AS SeekCount,
    &amp;#39;CREATE NONCLUSTERED INDEX IX_&amp;#39; 
        + OBJECT_NAME(mid.object_id) + &amp;#39;_Missing&amp;#39;
        + &amp;#39; ON &amp;#39; + mid.statement
        + &amp;#39; (&amp;#39; + ISNULL(mid.equality_columns, &amp;#39;&amp;#39;)
        + CASE 
            WHEN mid.inequality_columns IS NOT NULL 
            THEN &amp;#39;, &amp;#39; + mid.inequality_columns 
            ELSE &amp;#39;&amp;#39; 
          END
        + &amp;#39;)&amp;#39;
        + CASE 
            WHEN mid.included_columns IS NOT NULL 
            THEN &amp;#39; INCLUDE (&amp;#39; + mid.included_columns + &amp;#39;)&amp;#39;
            ELSE &amp;#39;&amp;#39; 
          END AS CreateIndexStatement
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups mig
    ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats migs
    ON mig.index_group_handle = migs.group_handle
WHERE mid.database_id = DB_ID()
ORDER BY migs.avg_user_impact * migs.user_seeks DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 쿼리가 &lt;code&gt;INCLUDE&lt;/code&gt;에 넣어야 할 컬럼까지 알려준다. 단, 이건 참고용이지 무작정 다 만들면 안 된다. 인덱스가 너무 많아지면 쓰기 성능이 떨어진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;INCLUDE 사용 시 주의사항&lt;/h2&gt;
&lt;h3&gt;1. INCLUDE에 너무 많은 컬럼을 넣지 마라&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- ❌ 이러면 안 된다
CREATE NONCLUSTERED INDEX IX_Orders_Bad
ON Orders (CustomerID)
INCLUDE (OrderDate, Status, TotalAmount, ShippingAddr, 
         Notes, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;INCLUDE 컬럼이 많으면 리프 레벨이 커지고, 그만큼 디스크 공간을 더 쓰고, INSERT/UPDATE 시 인덱스 유지 비용이 올라간다. &lt;strong&gt;자주 실행되는 쿼리에서 필요한 컬럼만&lt;/strong&gt; 넣어야 한다.&lt;/p&gt;
&lt;h3&gt;2. NVARCHAR(MAX), VARCHAR(MAX)에 주의&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 큰 데이터 타입은 INCLUDE에 넣을 수 있지만 신중하게
CREATE NONCLUSTERED INDEX IX_Orders_WithNotes
ON Orders (CustomerID)
INCLUDE (Notes);  -- NVARCHAR(MAX)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;키 컬럼에는 &lt;code&gt;MAX&lt;/code&gt; 타입을 넣을 수 없지만, &lt;code&gt;INCLUDE&lt;/code&gt;에는 넣을 수 있다. 이게 &lt;code&gt;INCLUDE&lt;/code&gt;의 또 다른 장점이다. 근데 &lt;code&gt;MAX&lt;/code&gt; 타입은 데이터가 클 수 있으니까 인덱스 크기가 급격히 커질 수 있다. 정말 필요한 경우에만 쓰자.&lt;/p&gt;
&lt;h3&gt;3. INCLUDE 컬럼으로 검색할 수 없다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE NONCLUSTERED INDEX IX_Orders_V2
ON Orders (CustomerID)
INCLUDE (OrderDate, TotalAmount);

-- ✅ 이건 인덱스를 탄다
SELECT OrderDate, TotalAmount
FROM Orders
WHERE CustomerID = 12345;

-- ❌ 이건 인덱스의 INCLUDE 컬럼으로 검색 불가
-- OrderDate가 키가 아니라 INCLUDE이니까
SELECT CustomerID, TotalAmount
FROM Orders
WHERE OrderDate = &amp;#39;2024-01-15&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;INCLUDE 컬럼은 검색에 사용되지 않는다. WHERE, JOIN, ORDER BY, GROUP BY 조건에 써야 하는 컬럼이면 키 컬럼에 넣어야 한다.&lt;/p&gt;
&lt;h3&gt;4. 인덱스 크기 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스별 크기 확인
SELECT
    OBJECT_NAME(i.object_id) AS TableName,
    i.name AS IndexName,
    i.type_desc AS IndexType,
    CAST(SUM(ps.used_page_count) * 8.0 / 1024 AS DECIMAL(10,2)) AS SizeMB,
    SUM(ps.row_count) AS RowCount
FROM sys.indexes i
JOIN sys.dm_db_partition_stats ps
    ON i.object_id = ps.object_id
    AND i.index_id = ps.index_id
WHERE OBJECT_NAME(i.object_id) = &amp;#39;Orders&amp;#39;
GROUP BY i.object_id, i.name, i.type_desc
ORDER BY SizeMB DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;INCLUDE 추가 전후로 인덱스 크기가 얼마나 변하는지 확인하는 습관을 들이자.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 만나는 패턴별 인덱스 설계&lt;/h2&gt;
&lt;h3&gt;패턴 1: 목록 조회 + 페이지네이션&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 쿼리
SELECT OrderID, OrderDate, Status, TotalAmount
FROM Orders
WHERE CustomerID = @CustomerID
  AND Status = &amp;#39;Completed&amp;#39;
ORDER BY OrderDate DESC
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY;

-- 인덱스
CREATE NONCLUSTERED INDEX IX_Orders_List
ON Orders (CustomerID, Status, OrderDate DESC)
INCLUDE (TotalAmount);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CustomerID&lt;/code&gt;와 &lt;code&gt;Status&lt;/code&gt;는 WHERE에, &lt;code&gt;OrderDate&lt;/code&gt;는 ORDER BY에 쓰이니까 키 컬럼. &lt;code&gt;TotalAmount&lt;/code&gt;는 SELECT에서만 필요하니까 INCLUDE. &lt;code&gt;OrderID&lt;/code&gt;는 클러스터드 인덱스 키라서 자동으로 포함된다.&lt;/p&gt;
&lt;h3&gt;패턴 2: 집계 쿼리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 쿼리
SELECT
    CustomerID,
    COUNT(*) AS OrderCount,
    SUM(TotalAmount) AS TotalSpent,
    MAX(OrderDate) AS LastOrderDate
FROM Orders
WHERE OrderDate &amp;gt;= &amp;#39;2024-01-01&amp;#39;
GROUP BY CustomerID;

-- 인덱스
CREATE NONCLUSTERED INDEX IX_Orders_Stats
ON Orders (OrderDate, CustomerID)
INCLUDE (TotalAmount);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OrderDate&lt;/code&gt;는 WHERE 조건이니까 첫 번째 키. &lt;code&gt;CustomerID&lt;/code&gt;는 GROUP BY에 쓰이니까 두 번째 키. &lt;code&gt;TotalAmount&lt;/code&gt;는 SUM 집계에만 필요하니까 INCLUDE. &lt;code&gt;COUNT(*)&lt;/code&gt;와 &lt;code&gt;MAX(OrderDate)&lt;/code&gt;는 키 컬럼에서 처리 가능.&lt;/p&gt;
&lt;h3&gt;패턴 3: JOIN 쿼리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 쿼리
SELECT o.OrderID, o.OrderDate, c.CustomerName, c.Email
FROM Orders o
JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE o.Status = &amp;#39;Pending&amp;#39;
ORDER BY o.OrderDate;

-- Orders 테이블 인덱스
CREATE NONCLUSTERED INDEX IX_Orders_Pending
ON Orders (Status, OrderDate)
INCLUDE (CustomerID);

-- Customers 테이블 인덱스 (이미 PK가 CustomerID면 불필요)
-- CREATE NONCLUSTERED INDEX IX_Customers_Lookup
-- ON Customers (CustomerID)
-- INCLUDE (CustomerName, Email);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;패턴 4: EXISTS / IN 서브쿼리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 쿼리
SELECT CustomerID, CustomerName
FROM Customers c
WHERE EXISTS (
    SELECT 1 FROM Orders o
    WHERE o.CustomerID = c.CustomerID
      AND o.TotalAmount &amp;gt; 100000
);

-- 인덱스
CREATE NONCLUSTERED INDEX IX_Orders_BigSpender
ON Orders (CustomerID, TotalAmount);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EXISTS 안에서는 SELECT 1이니까 INCLUDE가 필요 없다. 검색 조건인 &lt;code&gt;CustomerID&lt;/code&gt;와 &lt;code&gt;TotalAmount&lt;/code&gt;만 키 컬럼에 있으면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;필터링된 인덱스 + INCLUDE 조합&lt;/h2&gt;
&lt;p&gt;특정 조건의 데이터만 인덱스에 넣는 필터링된 인덱스와 INCLUDE를 같이 쓰면 아주 효율적인 인덱스를 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 활성 주문만 인덱싱 (전체 주문의 5%라면 인덱스가 아주 작아짐)
CREATE NONCLUSTERED INDEX IX_Orders_Active
ON Orders (CustomerID, OrderDate DESC)
INCLUDE (Status, TotalAmount)
WHERE Status IN (&amp;#39;Pending&amp;#39;, &amp;#39;Processing&amp;#39;, &amp;#39;Shipped&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이러면 완료된 주문은 인덱스에 안 들어가니까 인덱스 크기가 확 줄어든다. 근데 필터링된 인덱스는 쿼리의 WHERE 조건이 인덱스 필터와 맞아야 사용된다는 점을 기억하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- ✅ 이 쿼리는 위의 필터링 인덱스를 탄다
SELECT OrderDate, TotalAmount
FROM Orders
WHERE CustomerID = 12345 AND Status = &amp;#39;Pending&amp;#39;;

-- ❌ 이 쿼리는 안 탄다 (Status 조건이 인덱스 필터에 없음)
SELECT OrderDate, TotalAmount
FROM Orders
WHERE CustomerID = 12345 AND Status = &amp;#39;Cancelled&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스 설계 체크리스트&lt;/h2&gt;
&lt;p&gt;새 인덱스를 만들기 전에 이 순서로 생각하면 실수가 줄어든다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. WHERE 조건에 등호(=)로 비교하는 컬럼 → 키 컬럼 (앞쪽에 배치)
2. WHERE 조건에 범위(&amp;gt;, &amp;lt;, BETWEEN)로 비교하는 컬럼 → 키 컬럼 (등호 뒤에 배치)
3. ORDER BY에 쓰이는 컬럼 → 키 컬럼 (범위 조건 뒤에 배치)
4. GROUP BY에 쓰이는 컬럼 → 키 컬럼 (상황에 따라)
5. SELECT에서 가져오기만 하는 컬럼 → INCLUDE
6. 특정 조건만 자주 검색한다면 → 필터링 인덱스 고려&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;INCLUDE&lt;/code&gt;는 단순해 보이지만 인덱스 성능을 극적으로 바꿀 수 있는 기능이다. Key Lookup 하나 없애는 것만으로 쿼리 속도가 수십 배 빨라지는 경우를 여러 번 봤다.&lt;/p&gt;
&lt;p&gt;핵심 정리:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Key Lookup이 보이면 &lt;code&gt;INCLUDE&lt;/code&gt; 추가를 먼저 고려하자&lt;/li&gt;
&lt;li&gt;WHERE, ORDER BY, GROUP BY에 쓰는 컬럼은 키 컬럼으로, SELECT에서만 쓰는 컬럼은 INCLUDE로&lt;/li&gt;
&lt;li&gt;INCLUDE에 넣으면 리프 레벨에만 저장돼서 인덱스 크기와 유지 비용이 키 컬럼 추가보다 적다&lt;/li&gt;
&lt;li&gt;Missing Index DMV가 INCLUDE 후보까지 알려주니까 참고하자&lt;/li&gt;
&lt;li&gt;단, 무작정 컬럼을 많이 넣으면 인덱스가 비대해진다. 자주 실행되는 쿼리 기준으로 설계하자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;쿼리 하나 튜닝할 때마다 실행 계획을 열어보는 습관이 중요하다. Key Lookup이 보일 때마다 &amp;quot;이거 INCLUDE로 없앨 수 있나?&amp;quot; 한번씩 생각해보면, 어느새 인덱스 설계가 손에 익게 된다.&lt;/p&gt;
&lt;p&gt;궁금한 점이나 다른 튜닝 경험이 있으면 댓글로 공유해주세요!  &lt;/p&gt;</description>
      <category>DB</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/40</guid>
      <comments>https://chhb-miscellaneous.tistory.com/40#entry40comment</comments>
      <pubDate>Tue, 19 May 2026 16:33:02 +0900</pubDate>
    </item>
    <item>
      <title>.htaccess로 특정 URL 차단하는 방법 &amp;mdash; 서버 털리기 전에 읽어야 할 글</title>
      <link>https://chhb-miscellaneous.tistory.com/39</link>
      <description>&lt;p&gt;서버 운영하다 보면 액세스 로그를 한번쯤 열어보게 된다. 그리고 놀란다. 내가 만든 적도 없는 경로로 요청이 미친 듯이 들어오고 있거든. &lt;code&gt;/wp-login.php&lt;/code&gt;, &lt;code&gt;/admin&lt;/code&gt;, &lt;code&gt;/phpmyadmin&lt;/code&gt;, &lt;code&gt;/.env&lt;/code&gt; 같은 것들. WordPress 안 쓰는데 &lt;code&gt;wp-login&lt;/code&gt;으로 로그인 시도를 하고 있고, &lt;code&gt;.env&lt;/code&gt; 파일을 긁어가려는 봇이 하루에도 수백 번씩 찔러본다.&lt;/p&gt;
&lt;p&gt;처음 봤을 때 좀 소름 돋았다. &amp;quot;이게 매일 이러고 있었어?&amp;quot; 싶어서.&lt;/p&gt;
&lt;p&gt;이런 요청들을 그냥 놔두면 서버 리소스도 잡아먹고, 혹시라도 실제로 취약점이 있으면 뚫릴 수도 있다. &lt;code&gt;.htaccess&lt;/code&gt;로 이런 요청들을 차단하는 방법을 정리해봤다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;.htaccess가 뭔데&lt;/h2&gt;
&lt;p&gt;Apache 웹서버에서 디렉토리 단위로 설정을 제어하는 파일이다. 서버 설정 파일(&lt;code&gt;httpd.conf&lt;/code&gt;)을 직접 건드리지 않고도 URL 리다이렉트, 접근 제어, 인증 같은 걸 할 수 있다.&lt;/p&gt;
&lt;p&gt;파일명이 &lt;code&gt;.&lt;/code&gt;으로 시작해서 숨김 파일이다. FTP나 파일 매니저에서 안 보이면 숨김 파일 표시 옵션을 켜야 한다.&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;: Nginx를 쓰고 있다면 &lt;code&gt;.htaccess&lt;/code&gt;는 아무 효과 없다. Nginx는 이 파일을 무시한다. Nginx에서는 서버 설정 파일(&lt;code&gt;nginx.conf&lt;/code&gt; 또는 사이트별 설정)에서 직접 처리해야 한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;기본 전제: mod_rewrite 활성화 확인&lt;/h2&gt;
&lt;p&gt;URL 차단에 &lt;code&gt;RewriteRule&lt;/code&gt;을 쓰려면 Apache의 &lt;code&gt;mod_rewrite&lt;/code&gt; 모듈이 활성화돼 있어야 한다. 대부분의 웹호스팅에서는 기본으로 켜져 있는데, 혹시 안 되면 확인해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 모듈 확인 (서버 접근 가능한 경우)
apache2ctl -M | grep rewrite

# 결과에 rewrite_module이 있으면 OK&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.htaccess&lt;/code&gt; 파일의 시작 부분에 이렇게 써주면 안전하다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# mod_rewrite가 있을 때만 실행
&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On
    # 여기에 규칙 작성
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;특정 URL 경로 차단하기&lt;/h2&gt;
&lt;h3&gt;1. 정확한 경로 차단&lt;/h3&gt;
&lt;p&gt;가장 기본적인 방식. 특정 경로로 들어오는 요청을 403(Forbidden)으로 막는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    # wp-login.php 차단
    RewriteRule ^wp-login\.php$ - [F,L]

    # wp-admin 디렉토리 전체 차단
    RewriteRule ^wp-admin - [F,L]

    # phpMyAdmin 차단
    RewriteRule ^phpmyadmin - [F,L]
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;[F,L]&lt;/code&gt;의 의미:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;F&lt;/code&gt; = Forbidden (403 에러 반환)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;L&lt;/code&gt; = Last (이 규칙에 매치되면 이후 규칙은 검사 안 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 여러 경로를 한 번에 차단&lt;/h3&gt;
&lt;p&gt;하나씩 쓰기 귀찮으면 &lt;code&gt;|&lt;/code&gt;(OR)로 묶을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    # WordPress 관련 경로 일괄 차단
    RewriteRule ^(wp-login|wp-admin|wp-config|wp-includes|xmlrpc) - [F,L]

    # 흔한 공격 경로 일괄 차단
    RewriteRule ^(phpmyadmin|pma|myadmin|mysql|adminer) - [F,L]
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이게 훨씬 깔끔하다. 나는 이 방식을 선호한다.&lt;/p&gt;
&lt;h3&gt;3. 특정 파일 확장자 차단&lt;/h3&gt;
&lt;p&gt;설정 파일이나 백업 파일이 외부에서 접근되면 큰일난다. &lt;code&gt;.env&lt;/code&gt; 파일에 DB 비밀번호가 다 들어있는데, 이게 웹에서 접근 가능하면... 생각만 해도 아찔하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    # 민감한 파일 차단
    RewriteRule \.env$ - [F,L]
    RewriteRule \.git - [F,L]
    RewriteRule \.sql$ - [F,L]
    RewriteRule \.bak$ - [F,L]
    RewriteRule \.log$ - [F,L]
    RewriteRule \.ini$ - [F,L]
    RewriteRule \.yml$ - [F,L]
    RewriteRule \.yaml$ - [F,L]
    RewriteRule \.toml$ - [F,L]
    RewriteRule \.lock$ - [F,L]
    RewriteRule \.conf$ - [F,L]
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 한 줄로:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;RewriteRule \.(env|git|sql|bak|log|ini|yml|yaml|toml|lock|conf)$ - [F,L]&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;팁&lt;/strong&gt;: &lt;code&gt;.git&lt;/code&gt; 디렉토리가 웹에서 접근 가능하면 소스코드 전체가 유출될 수 있다. 실제로 이걸로 뚫리는 사이트가 꽤 있다. &lt;code&gt;/.git/HEAD&lt;/code&gt;로 접속해봐서 뭔가 나오면 바로 차단하자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;4. 쿼리스트링(Query String) 기반 차단&lt;/h3&gt;
&lt;p&gt;URL 뒤에 붙는 파라미터로 공격하는 경우도 많다. SQL 인젝션 시도 같은 거.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    # 쿼리스트링에 의심스러운 패턴 차단
    RewriteCond %{QUERY_STRING} (union|select|insert|drop|delete|update|concat|char\() [NC]
    RewriteRule .* - [F,L]

    # base64 인코딩된 공격 시도 차단
    RewriteCond %{QUERY_STRING} base64_encode [NC]
    RewriteRule .* - [F,L]

    # &amp;lt;script&amp;gt; 태그 삽입 시도 차단
    RewriteCond %{QUERY_STRING} (&amp;lt;script|%3Cscript) [NC]
    RewriteRule .* - [F,L]
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;[NC]&lt;/code&gt;는 대소문자 무시(No Case)라는 뜻이다. 공격자가 &lt;code&gt;UNION&lt;/code&gt;, &lt;code&gt;Union&lt;/code&gt;, &lt;code&gt;union&lt;/code&gt; 이런 식으로 바꿔가면서 시도하니까 꼭 넣어줘야 한다.&lt;/p&gt;
&lt;p&gt;⚠️ &lt;strong&gt;주의&lt;/strong&gt;: 이 규칙이 너무 공격적이면 정상적인 요청도 차단될 수 있다. 예를 들어 게시판에서 SQL 관련 글을 쓸 때 &amp;quot;SELECT&amp;quot; 같은 단어가 쿼리스트링에 포함될 수 있다. 사이트 특성에 맞게 조절해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;특정 IP 차단하기&lt;/h2&gt;
&lt;p&gt;로그를 보면 특정 IP에서 집중적으로 공격하는 경우가 있다. 이런 건 IP 자체를 막아버리는 게 효율적이다.&lt;/p&gt;
&lt;h3&gt;Apache 2.4 이상&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# 특정 IP 차단
&amp;lt;RequireAll&amp;gt;
    Require all granted
    Require not ip 192.168.1.100
    Require not ip 10.0.0.0/8
    Require not ip 203.0.113.50
&amp;lt;/RequireAll&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Apache 2.2 (레거시)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# 옛날 방식 — 아직 이걸 쓰는 서버도 많다
Order Allow,Deny
Allow from all
Deny from 192.168.1.100
Deny from 203.0.113.0/24&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;특정 IP만 허용하고 나머지 차단&lt;/h3&gt;
&lt;p&gt;관리자 페이지 같은 데는 아예 특정 IP만 접근 가능하게 하는 게 안전하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# /admin 경로는 내 IP만 접근 가능
&amp;lt;If &amp;quot;%{REQUEST_URI} =~ m#^/admin#&amp;quot;&amp;gt;
    Require ip 123.456.789.0
&amp;lt;/If&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 &lt;code&gt;Files&lt;/code&gt;/&lt;code&gt;Directory&lt;/code&gt; 디렉티브로:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# 특정 파일 보호
&amp;lt;Files &amp;quot;admin.php&amp;quot;&amp;gt;
    Require ip 123.456.789.0
&amp;lt;/Files&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;User-Agent 기반 차단&lt;/h2&gt;
&lt;p&gt;악성 봇은 User-Agent를 보면 대충 알 수 있다. 물론 위조가 쉬워서 완벽한 방법은 아니지만, 뻔한 봇은 걸러진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    # 악성 봇 / 스크래퍼 차단
    RewriteCond %{HTTP_USER_AGENT} (SemrushBot|AhrefsBot|DotBot|MJ12bot) [NC]
    RewriteRule .* - [F,L]

    # 빈 User-Agent 차단 (정상적인 브라우저는 UA가 있다)
    RewriteCond %{HTTP_USER_AGENT} ^$
    RewriteRule .* - [F,L]
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SEO 도구 봇(Semrush, Ahrefs)을 차단할지는 사이트 성격에 따라 다르다. 경쟁사 분석 도구라서 막고 싶을 수도 있고, 그냥 두고 싶을 수도 있다. 사이트에 부하를 많이 주는 봇이면 차단하는 게 낫다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;특정 HTTP 메서드 차단&lt;/h2&gt;
&lt;p&gt;웹사이트가 GET이랑 POST만 쓰는데 TRACE, DELETE, PUT 같은 요청이 들어오면 공격일 가능성이 높다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    # GET, POST, HEAD만 허용
    RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
    RewriteRule .* - [F,L]
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;REST API를 운영하고 있다면 PUT, PATCH, DELETE도 허용해야 하니까 상황에 맞게.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전: 내가 실제로 쓰는 .htaccess&lt;/h2&gt;
&lt;p&gt;위의 내용을 종합해서 내가 실제 서버에서 쓰고 있는 설정이다. 그대로 복붙해도 대부분의 사이트에서 잘 동작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# ============================================
# 보안 설정
# ============================================

# mod_rewrite 활성화
&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    # ----------------------------------------
    # 1. 민감한 파일 접근 차단
    # ----------------------------------------
    RewriteRule \.(env|git|sql|bak|log|ini|yml|yaml|toml|lock|conf|swp)$ - [F,L]
    RewriteRule (composer\.(json|lock)|package\.json|package-lock\.json)$ - [F,L]
    RewriteRule (Makefile|Dockerfile|docker-compose|Vagrantfile) - [F,L]

    # .git 디렉토리 전체 차단
    RewriteRule ^\.git - [F,L]

    # .well-known은 허용 (SSL 인증서 갱신 등에 필요)
    RewriteRule ^\.(?!well-known) - [F,L]

    # ----------------------------------------
    # 2. WordPress 공격 차단 (WP 안 쓰는 경우)
    # ----------------------------------------
    RewriteRule ^(wp-login|wp-admin|wp-config|wp-content|wp-includes|xmlrpc)\.?.*$ - [F,L]

    # ----------------------------------------
    # 3. 흔한 공격 경로 차단
    # ----------------------------------------
    RewriteRule ^(phpmyadmin|pma|myadmin|adminer|mysql|dbadmin) - [F,L]
    RewriteRule ^(cgi-bin|scripts|shell|cmd) - [F,L]
    RewriteRule ^(eval|base64|exec)\b - [F,L]

    # ----------------------------------------
    # 4. 쿼리스트링 공격 차단
    # ----------------------------------------
    # SQL 인젝션 시도
    RewriteCond %{QUERY_STRING} (union\s+select|concat\s*\(|char\s*\(|0x[0-9a-f]+) [NC]
    RewriteRule .* - [F,L]

    # XSS 시도
    RewriteCond %{QUERY_STRING} (&amp;lt;script|%3Cscript|javascript:|vbscript:) [NC]
    RewriteRule .* - [F,L]

    # 원격 파일 포함 시도
    RewriteCond %{QUERY_STRING} (https?://|ftp://) [NC]
    RewriteRule .* - [F,L]

    # ----------------------------------------
    # 5. 허용하지 않는 HTTP 메서드 차단
    # ----------------------------------------
    RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
    RewriteRule .* - [F,L]

    # ----------------------------------------
    # 6. 빈 User-Agent 차단
    # ----------------------------------------
    RewriteCond %{HTTP_USER_AGENT} ^$
    RewriteRule .* - [F,L]
&amp;lt;/IfModule&amp;gt;

# ============================================
# 디렉토리 리스팅 비활성화
# ============================================
Options -Indexes

# ============================================
# 서버 정보 노출 방지
# ============================================
ServerSignature Off

# ============================================
# 보안 헤더
# ============================================
&amp;lt;IfModule mod_headers.c&amp;gt;
    # 클릭재킹 방지
    Header always set X-Frame-Options &amp;quot;SAMEORIGIN&amp;quot;

    # XSS 필터 활성화
    Header always set X-XSS-Protection &amp;quot;1; mode=block&amp;quot;

    # MIME 타입 스니핑 방지
    Header always set X-Content-Type-Options &amp;quot;nosniff&amp;quot;

    # Referrer 정보 제한
    Header always set Referrer-Policy &amp;quot;strict-origin-when-cross-origin&amp;quot;

    # 서버 헤더에서 PHP 버전 숨기기
    Header unset X-Powered-By
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주요 포인트 설명:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.well-known&lt;/code&gt; 디렉토리는 예외 처리했다. Let&amp;#39;s Encrypt SSL 인증서 갱신할 때 이 경로를 쓰거든. 이걸 막아버리면 인증서 갱신이 안 돼서 HTTPS가 끊긴다. 한 번 겪으면 안 잊는다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Options -Indexes&lt;/code&gt;는 디렉토리 목록 노출 방지. 이거 안 하면 &lt;code&gt;images/&lt;/code&gt; 같은 폴더에 접속했을 때 파일 목록이 쭉 보인다.&lt;/li&gt;
&lt;li&gt;보안 헤더는 &lt;code&gt;.htaccess&lt;/code&gt;에서도 설정 가능하다. 클릭재킹, XSS, MIME 스니핑 같은 기본적인 공격을 막아준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;차단 대신 리다이렉트하기&lt;/h2&gt;
&lt;p&gt;403 에러를 보여주는 대신 홈페이지로 보내버릴 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# 403 대신 홈으로 리다이렉트
RewriteRule ^wp-login\.php$ / [R=301,L]

# 특정 페이지로 리다이렉트
RewriteRule ^wp-admin /not-found [R=404,L]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;근데 나는 그냥 403으로 끊는 걸 선호한다. 리다이렉트하면 봇이 &amp;quot;어, 응답이 왔네&amp;quot; 하고 계속 시도하는 경우가 있거든. 403이면 좀 더 빨리 포기하는 느낌이다 (물론 집요한 봇은 뭘 해도 온다).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;차단이 제대로 되는지 확인하는 방법&lt;/h2&gt;
&lt;p&gt;설정 적용 후에 반드시 테스트하자. 잘못 쓰면 본인 사이트가 안 열린다. 나도 정규식 하나 잘못 써서 사이트 전체가 403 뜬 적 있다. 식은땀 장난 아니었다.&lt;/p&gt;
&lt;h3&gt;curl로 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 차단된 경로 테스트
curl -I https://mysite.com/wp-login.php
# HTTP/1.1 403 Forbidden ← 이렇게 나오면 성공

# 정상 경로 테스트
curl -I https://mysite.com/
# HTTP/1.1 200 OK ← 이건 정상이어야 함

# .env 파일 접근 테스트
curl -I https://mysite.com/.env
# HTTP/1.1 403 Forbidden

# 빈 User-Agent로 테스트
curl -I -A &amp;quot;&amp;quot; https://mysite.com/
# HTTP/1.1 403 Forbidden&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;브라우저에서 확인&lt;/h3&gt;
&lt;p&gt;그냥 브라우저 주소창에 차단한 URL을 직접 쳐보면 된다. 403 에러 페이지가 뜨면 정상.&lt;/p&gt;
&lt;h3&gt;문법 오류 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Apache 설정 문법 체크 (서버 접근 가능한 경우)
apachectl configtest
# Syntax OK ← 이게 나와야 함&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;.htaccess 수정할 때 주의사항&lt;/h2&gt;
&lt;h3&gt;1. 반드시 백업&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 수정 전에 무조건 백업
cp .htaccess .htaccess.backup&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;잘못 수정하면 사이트가 안 열리니까. 백업 있으면 바로 복원할 수 있다.&lt;/p&gt;
&lt;h3&gt;2. 규칙 순서가 중요하다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.htaccess&lt;/code&gt;는 위에서 아래로 실행된다. &lt;code&gt;[L]&lt;/code&gt; 플래그가 붙은 규칙에 매치되면 그 아래 규칙은 검사하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# 이 순서가 맞다
RewriteRule ^api/ - [L]           # API 경로는 통과
RewriteRule ^wp-login - [F,L]     # wp-login은 차단

# 이 순서는 위험하다
RewriteRule .* - [F,L]            # 모든 요청 차단 (아래 규칙 도달 불가)
RewriteRule ^api/ - [L]           # 이 규칙은 영원히 실행 안 됨&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 정규식 실수&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# ❌ 틀린 예 — 마침표를 이스케이프 안 함
RewriteRule ^wp-login.php$ - [F,L]
# &amp;quot;wp-loginXphp&amp;quot; 같은 것도 매치됨 (.은 정규식에서 &amp;#39;아무 문자&amp;#39; 의미)

# ✅ 맞는 예
RewriteRule ^wp-login\.php$ - [F,L]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정규식에서 &lt;code&gt;.&lt;/code&gt;은 아무 문자를 의미한다. 진짜 마침표를 매치하려면 &lt;code&gt;\.&lt;/code&gt;으로 이스케이프해야 한다. 사소한 것 같지만 보안 규칙에서 이런 실수가 있으면 의도와 다르게 동작한다.&lt;/p&gt;
&lt;h3&gt;4. 캐시 주의&lt;/h3&gt;
&lt;p&gt;브라우저가 301 리다이렉트를 캐시하는 경우가 있다. 테스트할 때 시크릿 모드(프라이빗 브라우징)를 쓰거나 &lt;code&gt;curl&lt;/code&gt;로 확인하는 게 정확하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;.htaccess의 성능 이슈&lt;/h2&gt;
&lt;p&gt;하나 알아둘 게 있다. &lt;code&gt;.htaccess&lt;/code&gt;는 &lt;strong&gt;요청이 올 때마다 매번 파일을 읽는다&lt;/strong&gt;. 규칙이 많아지면 그만큼 처리 시간이 늘어난다. Apache 공식 문서에서도 가능하면 &lt;code&gt;.htaccess&lt;/code&gt; 대신 서버 설정 파일에 직접 쓰라고 권장한다.&lt;/p&gt;
&lt;p&gt;근데 현실적으로 공유 호스팅에서는 서버 설정 파일을 못 건드리니까 &lt;code&gt;.htaccess&lt;/code&gt;를 쓸 수밖에 없다. VPS나 전용 서버를 쓰고 있다면 &lt;code&gt;httpd.conf&lt;/code&gt;나 &lt;code&gt;sites-available&lt;/code&gt; 설정 파일에 넣는 게 성능상 낫다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-apache&quot;&gt;# httpd.conf에서 .htaccess 비활성화 (성능 향상)
&amp;lt;Directory /var/www/html&amp;gt;
    AllowOverride None
&amp;lt;/Directory&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AllowOverride None&lt;/code&gt;으로 하면 Apache가 &lt;code&gt;.htaccess&lt;/code&gt; 파일을 아예 안 찾는다. 대신 모든 설정을 서버 설정 파일에서 해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이것만으로는 부족한 것들&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.htaccess&lt;/code&gt;만으로 모든 보안을 해결할 수는 없다. 추가로 고려할 것들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;fail2ban&lt;/strong&gt;: 반복적으로 접근 시도하는 IP를 자동으로 방화벽에서 차단한다. &lt;code&gt;.htaccess&lt;/code&gt;보다 더 근본적인 차단.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WAF(Web Application Firewall)&lt;/strong&gt;: Cloudflare 같은 서비스를 쓰면 서버에 도달하기도 전에 악성 요청을 걸러준다. 무료 플랜으로도 꽤 많이 막아준다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ModSecurity&lt;/strong&gt;: Apache 모듈로 설치하는 WAF. OWASP 룰셋을 적용하면 SQL 인젝션, XSS 같은 공격을 체계적으로 막을 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서버 업데이트&lt;/strong&gt;: PHP, Apache, OS 보안 패치를 꾸준히 하는 게 사실 제일 중요하다. 아무리 &lt;code&gt;.htaccess&lt;/code&gt;를 잘 써도 서버 자체에 취약점이 있으면 의미 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.htaccess&lt;/code&gt; 설정은 한 번 해놓으면 계속 적용되니까 초기에 잘 세팅해두는 게 좋다. 특히 WordPress 안 쓰는데 &lt;code&gt;wp-login&lt;/code&gt; 공격이 들어온다거나, &lt;code&gt;.env&lt;/code&gt; 파일이 노출될 수 있는 상황이면 지금 당장 확인해보길 추천한다.&lt;/p&gt;
&lt;p&gt;핵심 정리:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt; 같은 민감한 파일은 반드시 차단하자&lt;/li&gt;
&lt;li&gt;안 쓰는 CMS(WordPress 등) 관련 경로도 차단하면 불필요한 요청을 줄일 수 있다&lt;/li&gt;
&lt;li&gt;쿼리스트링 기반 SQL 인젝션, XSS 시도도 막을 수 있다&lt;/li&gt;
&lt;li&gt;수정 전에 무조건 백업. 정규식 한 글자 실수로 사이트가 죽을 수 있다&lt;/li&gt;
&lt;li&gt;설정 후 &lt;code&gt;curl&lt;/code&gt;로 반드시 테스트하자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.htaccess&lt;/code&gt;만으로는 한계가 있다. fail2ban, Cloudflare, ModSecurity도 같이 고려하자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;궁금한 점이나 본인이 쓰는 차단 규칙이 있으면 댓글로 공유해주세요!  &lt;/p&gt;</description>
      <category>기타</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/39</guid>
      <comments>https://chhb-miscellaneous.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 18 May 2026 15:56:15 +0900</pubDate>
    </item>
    <item>
      <title>Docker + CI/CD 파이프라인 구성과 프로덕션 배포 전략 &amp;mdash; 삽질 끝에 정리한 실전 가이드</title>
      <link>https://chhb-miscellaneous.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 Docker Desktop 기초를 다뤘는데, 로컬에서 &lt;code&gt;docker compose up&lt;/code&gt; 하는 것과 실제 프로덕션에 배포하는 것 사이에는 꽤 큰 간극이 있다. 나도 처음에 &quot;로컬에서 잘 되니까 서버에 올리면 되겠지&quot; 하고 순진하게 생각했다가 한참 고생했다.&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;CI/CD가 뭔데&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀어서 쓰면 &lt;b&gt;Continuous Integration / Continuous Delivery(또는 Deployment)&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;b&gt;CI (지속적 통합)&lt;/b&gt;: 코드 푸시할 때마다 자동으로 빌드하고 테스트한다. &quot;머지했더니 터졌어요&quot;를 미리 잡는 거다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CD (지속적 배포)&lt;/b&gt;: 테스트 통과하면 자동으로 스테이징이나 프로덕션에 배포한다. 수동으로 SSH 접속해서 &lt;code&gt;git pull&lt;/code&gt; 치는 시대는 끝났다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손으로 하면 이런 과정이다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 코드 수정
2. 로컬에서 테스트
3. git push
4. 서버에 SSH 접속
5. git pull
6. docker build
7. docker compose down
8. docker compose up -d
9. 잘 되나 확인
10. 안 되면 롤백 (또 수동)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 매번 하다 보면 실수가 생기고, 시간도 잡아먹고, 배포가 무서워진다. CI/CD를 세팅하면 &lt;code&gt;git push&lt;/code&gt;만 하면 나머지는 자동이다. 처음 세팅할 때 시간이 좀 들지만, 한번 해놓으면 그 뒤로는 인생이 편해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub Actions로 CI/CD 파이프라인 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD 도구가 여러 개 있는데 &amp;mdash; Jenkins, GitLab CI, CircleCI, GitHub Actions 등 &amp;mdash; 나는 GitHub Actions를 제일 많이 쓴다. GitHub 쓰면 별도 설정 없이 바로 쓸 수 있고, 무료 tier도 넉넉해서.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 구조&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;my-app/
├── .github/
│   └── workflows/
│       ├── ci.yml          # PR 시 테스트
│       └── deploy.yml      # main 브랜치 배포
├── Dockerfile
├── docker-compose.yml
├── docker-compose.prod.yml
├── src/
├── tests/
└── ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 1: CI &amp;mdash; 푸시할 때마다 자동 테스트&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [develop]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_PASSWORD: testpass
        ports:
          - 5432:5432
        options: &amp;gt;-
          --health-cmd &quot;pg_isready -U postgres&quot;
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Run tests
        run: npm test
        env:
          DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379

      - name: Build check
        run: npm run build&lt;/code&gt;&lt;/pre&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;code&gt;services&lt;/code&gt;로 Postgres, Redis를 바로 띄울 수 있다. 테스트용 DB를 따로 관리할 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;actions/setup-node&lt;/code&gt;의 &lt;code&gt;cache: 'npm'&lt;/code&gt;으로 &lt;code&gt;node_modules&lt;/code&gt; 캐시를 활용한다. 이거 안 하면 매번 의존성 설치에 1~2분씩 날린다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;healthcheck&lt;/code&gt;로 Postgres가 준비될 때까지 기다린다. 이거 없으면 DB 접속 실패로 테스트가 뻗는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 2: Docker 이미지 빌드 + 레지스트리 푸시&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    # 위의 CI와 동일한 테스트 job (생략)
    # 테스트 통과해야 다음 단계로 진행

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=raw,value=latest

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/myapp
            docker compose -f docker-compose.prod.yml pull
            docker compose -f docker-compose.prod.yml up -d
            docker image prune -f&lt;/code&gt;&lt;/pre&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;b&gt;GitHub Container Registry(ghcr.io)&lt;/b&gt; 를 쓰면 별도 레지스트리 세팅이 필요 없다. Docker Hub도 되지만, private 레포는 유료라서.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;cache-from: type=gha&lt;/code&gt;&lt;/b&gt;: GitHub Actions 캐시를 빌드 캐시로 쓴다. 레이어 캐시가 먹히면 빌드 시간이 확 줄어든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 태그에 커밋 SHA를 쓴다&lt;/b&gt;: &lt;code&gt;latest&lt;/code&gt;만 쓰면 어떤 버전이 돌아가는지 추적이 안 된다. SHA 태그를 같이 붙여야 &quot;지금 프로덕션에서 어떤 커밋 기반으로 돌아가는지&quot; 바로 알 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 3: 프로덕션용 Docker Compose&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 개발용이랑 프로덕션용은 분리하는 게 맞다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# docker-compose.prod.yml
services:
  app:
    image: ghcr.io/myuser/myapp:latest
    restart: always
    ports:
      - &quot;3000:3000&quot;
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
    logging:
      driver: &quot;json-file&quot;
      options:
        max-size: &quot;10m&quot;
        max-file: &quot;5&quot;
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

  db:
    image: postgres:16-alpine
    restart: always
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U postgres&quot;]
      interval: 10s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    restart: always
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
    depends_on:
      - app

volumes:
  pgdata:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 compose와 다른 점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;build: .&lt;/code&gt; 대신 &lt;code&gt;image:&lt;/code&gt;로 레지스트리에서 이미지를 pull한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restart: always&lt;/code&gt;로 크래시 시 자동 재시작&lt;/li&gt;
&lt;li&gt;로그 크기 제한 설정&lt;/li&gt;
&lt;li&gt;리소스 제한(memory, cpus)&lt;/li&gt;
&lt;li&gt;Redis에 메모리 상한 설정&lt;/li&gt;
&lt;li&gt;Nginx를 앞에 둬서 리버스 프록시 + SSL 처리&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;자, 이제 CI/CD로 이미지를 빌드하고 서버에 올리는 것까지 됐다. 근데 문제가 하나 있다. &lt;b&gt;배포하는 동안 서비스가 끊기면 어떡하지?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새벽 3시에 배포하면 괜찮겠지만, 그건 개발자의 삶의 질 문제가 있고... 사용자가 전 세계에 있으면 새벽이 없다. 서비스 중단 없이 배포하는 전략이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 1: Rolling Update (롤링 업데이트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 무중단 배포 방식. 구버전 인스턴스를 하나씩 신버전으로 교체한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;시작 상태:    [v1] [v1] [v1] [v1]

1단계:       [v2] [v1] [v1] [v1]   &amp;larr; 하나 교체
2단계:       [v2] [v2] [v1] [v1]   &amp;larr; 하나 더
3단계:       [v2] [v2] [v2] [v1]   &amp;larr; 하나 더
완료:        [v2] [v2] [v2] [v2]   &amp;larr; 전부 교체&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 추가 서버가 필요 없다. 리소스 효율적.&lt;br /&gt;&lt;b&gt;단점&lt;/b&gt;: 배포 중에 v1과 v2가 동시에 떠 있다. DB 스키마 변경이 있으면 호환성을 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Swarm으로 구현하면 이렇다:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# docker-compose.prod.yml (Swarm 모드)
services:
  app:
    image: ghcr.io/myuser/myapp:latest
    deploy:
      replicas: 4
      update_config:
        parallelism: 1        # 한 번에 1개씩 교체
        delay: 30s             # 교체 간 30초 대기
        failure_action: rollback
        order: start-first     # 새 컨테이너 먼저 띄우고 옛것 내림
      rollback_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Swarm 초기화 (한 번만)
docker swarm init

# 배포
docker stack deploy -c docker-compose.prod.yml myapp

# 업데이트 (이미지 변경 후)
docker service update --image ghcr.io/myuser/myapp:v2 myapp_app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;order: start-first&lt;/code&gt;가 핵심이다. 새 컨테이너가 먼저 떠서 정상 동작 확인되면 그때 옛 컨테이너를 내린다. 이걸 &lt;code&gt;stop-first&lt;/code&gt;로 하면 내리고 올리는 사이에 서비스가 끊긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 2: Blue-Green 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 동일한 환경(Blue와 Green)을 준비해놓고, 하나에만 트래픽을 보내는 방식.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;현재 상태:
  Blue  (v1) &amp;larr; 트래픽 여기로
  Green (v1)   대기 중

배포:
  Blue  (v1) &amp;larr; 트래픽 아직 여기
  Green (v2)   새 버전 배포 + 테스트

전환:
  Blue  (v1)   이제 대기
  Green (v2) &amp;larr; 트래픽 전환!

문제 발생 시:
  Blue  (v1) &amp;larr; 다시 여기로 (즉시 롤백)
  Green (v2)   문제 있는 버전&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx로 구현하는 게 제일 간단하다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# /etc/nginx/conf.d/default.conf

# 두 개의 upstream 정의
upstream blue {
    server app-blue:3000;
}

upstream green {
    server app-green:3000;
}

# 현재 활성 환경을 변수로 관리
# active_upstream.conf에서 set $active blue; 또는 set $active green;
include /etc/nginx/conf.d/active_upstream.conf;

server {
    listen 80;
    server_name myapp.com;

    location / {
        proxy_pass http://$active;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /health {
        proxy_pass http://$active/health;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# /etc/nginx/conf.d/active_upstream.conf
# 이 파일만 바꾸면 트래픽이 전환된다
set $active blue;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose로 두 환경을 구성:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.blue-green.yml
services:
  app-blue:
    image: ghcr.io/myuser/myapp:${BLUE_TAG:-latest}
    environment:
      - NODE_ENV=production
    networks:
      - appnet

  app-green:
    image: ghcr.io/myuser/myapp:${GREEN_TAG:-latest}
    environment:
      - NODE_ENV=production
    networks:
      - appnet

  nginx:
    image: nginx:alpine
    ports:
      - &quot;80:80&quot;
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./active_upstream.conf:/etc/nginx/conf.d/active_upstream.conf
    depends_on:
      - app-blue
      - app-green
    networks:
      - appnet

networks:
  appnet:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 스크립트:&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
# deploy-blue-green.sh

set -e

CURRENT=$(cat active_upstream.conf | grep -oP '(?&amp;lt;=set \$active )\w+')
NEW_VERSION=$1

if [ &quot;$CURRENT&quot; = &quot;blue&quot; ]; then
    TARGET=&quot;green&quot;
else
    TARGET=&quot;blue&quot;
fi

echo &quot;현재 활성: $CURRENT&quot;
echo &quot;배포 대상: $TARGET (버전: $NEW_VERSION)&quot;

# 1. 비활성 환경에 새 버전 배포
export ${TARGET^^}_TAG=$NEW_VERSION
docker compose -f docker-compose.blue-green.yml pull app-$TARGET
docker compose -f docker-compose.blue-green.yml up -d app-$TARGET

# 2. 헬스체크 대기
echo &quot;헬스체크 대기 중...&quot;
for i in $(seq 1 30); do
    if curl -sf http://app-$TARGET:3000/health &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then
        echo &quot;헬스체크 통과!&quot;
        break
    fi
    if [ $i -eq 30 ]; then
        echo &quot;헬스체크 실패! 배포 중단.&quot;
        exit 1
    fi
    sleep 2
done

# 3. 트래픽 전환
echo &quot;set \$active $TARGET;&quot; &amp;gt; active_upstream.conf
docker compose -f docker-compose.blue-green.yml exec nginx nginx -s reload

echo &quot;배포 완료! 활성 환경: $TARGET&quot;
echo &quot;롤백하려면: ./rollback.sh&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
# rollback.sh &amp;mdash; 문제 생기면 이거 한 방

CURRENT=$(cat active_upstream.conf | grep -oP '(?&amp;lt;=set \$active )\w+')

if [ &quot;$CURRENT&quot; = &quot;blue&quot; ]; then
    ROLLBACK_TO=&quot;green&quot;
else
    ROLLBACK_TO=&quot;blue&quot;
fi

echo &quot;set \$active $ROLLBACK_TO;&quot; &amp;gt; active_upstream.conf
docker compose -f docker-compose.blue-green.yml exec nginx nginx -s reload

echo &quot;롤백 완료! 활성 환경: $ROLLBACK_TO&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 롤백이 초 단위로 된다. Nginx 설정 하나만 바꾸면 되니까. 새 버전을 충분히 테스트한 후에 전환할 수 있다.&lt;br /&gt;&lt;b&gt;단점&lt;/b&gt;: 서버 리소스가 2배 필요하다. 작은 서비스에서는 오버킬일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략 3: Canary 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽의 일부만 먼저 새 버전으로 보내서 문제 없는지 확인하고, 점진적으로 비율을 늘리는 방식.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1단계: v1 (90%) / v2 (10%)   &amp;larr; 소수에게만 새 버전
2단계: v1 (70%) / v2 (30%)   &amp;larr; 문제 없으면 늘림
3단계: v1 (30%) / v2 (70%)   &amp;larr; 점점 더
4단계: v1 (0%)  / v2 (100%)  &amp;larr; 완전 전환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx upstream의 weight로 간단하게 구현할 수 있다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;upstream backend {
    server app-stable:3000 weight=9;   # 90%
    server app-canary:3000 weight=1;   # 10%
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 제대로 하려면 Kubernetes + Istio 같은 서비스 메시를 쓰는 게 맞지만, 소규모라면 Nginx weight로도 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 리스크가 가장 낮다. 전체 사용자에게 영향 가기 전에 문제를 잡을 수 있다.&lt;br /&gt;&lt;b&gt;단점&lt;/b&gt;: 모니터링이 잘 돼 있어야 의미가 있다. 에러율이나 응답 시간을 비교할 수 없으면 canary를 띄워봤자 모른다.&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;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;소규모 프로젝트, 혼자 또는 소수 개발   &amp;rarr; Rolling Update
중간 규모, 빠른 롤백이 중요            &amp;rarr; Blue-Green
대규모, 사용자 많고 리스크 최소화       &amp;rarr; Canary&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음이면 Rolling Update부터 시작하고, 서비스가 커지면 Blue-Green으로 넘어가는 게 자연스럽다. Canary는 모니터링 인프라가 갖춰져야 의미 있으니까 나중에 고려해도 된다.&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 헬스체크 엔드포인트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 자동화하려면 앱에 헬스체크 엔드포인트가 있어야 한다. 컨테이너가 떴다고 앱이 준비된 건 아니니까.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Express.js 예시
app.get('/health', async (req, res) =&amp;gt; {
  try {
    // DB 연결 확인
    await db.query('SELECT 1');
    // Redis 연결 확인
    await redis.ping();

    res.status(200).json({
      status: 'ok',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
    });
  } catch (error) {
    res.status(503).json({
      status: 'error',
      message: error.message,
    });
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 200 OK 반환하는 것보다 DB, Redis 같은 의존성까지 확인하는 게 좋다. 앱은 떠 있는데 DB 연결이 끊겨있으면 의미 없으니까.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Graceful Shutdown&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 종료할 때 처리 중인 요청을 끊지 않고 마무리하는 게 중요하다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// Node.js graceful shutdown
const server = app.listen(3000);

const gracefulShutdown = (signal) =&amp;gt; {
  console.log(`${signal} 수신. 서버 종료 시작...`);

  server.close(() =&amp;gt; {
    console.log('HTTP 서버 종료 완료');

    // DB 연결 정리
    db.end().then(() =&amp;gt; {
      console.log('DB 연결 종료 완료');
      process.exit(0);
    });
  });

  // 10초 안에 종료 안 되면 강제 종료
  setTimeout(() =&amp;gt; {
    console.error('강제 종료');
    process.exit(1);
  }, 10000);
};

process.on('SIGTERM', () =&amp;gt; gracefulShutdown('SIGTERM'));
process.on('SIGINT', () =&amp;gt; gracefulShutdown('SIGINT'));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile에서도 &lt;code&gt;STOPSIGNAL&lt;/code&gt;을 신경 써야 한다:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# SIGTERM을 앱이 받을 수 있도록
# exec form으로 CMD 작성 (중요!)
CMD [&quot;node&quot;, &quot;server.js&quot;]

# 이렇게 하면 shell이 SIGTERM을 가로챔 &amp;mdash; 나쁜 예
# CMD node server.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CMD&lt;/code&gt;를 exec form(&lt;code&gt;[&quot;node&quot;, &quot;server.js&quot;]&lt;/code&gt;)으로 쓰지 않으면 shell이 PID 1이 되어서 SIGTERM을 앱이 못 받는다. 이거 모르면 컨테이너 종료할 때 항상 10초(기본 타임아웃) 기다리게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. DB 마이그레이션 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포할 때 DB 스키마 변경이 있으면 순서가 중요하다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
# deploy.sh

# 1. 마이그레이션 먼저 실행 (별도 컨테이너에서)
docker compose -f docker-compose.prod.yml run --rm app npm run migrate

# 2. 그 다음 앱 배포
docker compose -f docker-compose.prod.yml pull app
docker compose -f docker-compose.prod.yml up -d app&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;그리고 Rolling Update나 Blue-Green에서 v1과 v2가 동시에 뜨는 구간이 있으니까, &lt;b&gt;마이그레이션은 하위 호환이 되게&lt;/b&gt; 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- ❌ 나쁜 예: 컬럼 이름 바꾸기 (v1이 터짐)
ALTER TABLE users RENAME COLUMN name TO full_name;

-- ✅ 좋은 예: 2단계로 나누기
-- 1차 배포: 새 컬럼 추가
ALTER TABLE users ADD COLUMN full_name VARCHAR(100);
UPDATE users SET full_name = name;

-- 2차 배포: 앱 코드가 full_name을 쓰도록 변경 후
-- 3차 배포: 옛 컬럼 삭제
ALTER TABLE users DROP COLUMN name;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 &quot;expand and contract&quot; 패턴이라고 하는데, 무중단 배포에서는 거의 필수다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 시크릿 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions에서 비밀번호, API 키 같은 건 절대 코드에 넣지 말고 Secrets를 쓰자.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# GitHub Actions에서 secrets 사용
- name: Deploy
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    JWT_SECRET: ${{ secrets.JWT_SECRET }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서는 &lt;code&gt;.env&lt;/code&gt; 파일로 관리하되, 이 파일은 배포 스크립트로 전달하지 말고 서버에 직접 만들어두자. CI/CD 파이프라인 로그에 시크릿이 찍히는 사고를 방지하려면.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 제대로 하려면 HashiCorp Vault나 AWS Secrets Manager 같은 걸 쓰는 게 맞지만, 소규모에서는 GitHub Secrets + 서버 &lt;code&gt;.env&lt;/code&gt; 조합이면 충분하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 배포 알림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포됐는데 아무도 모르면 안 된다. 최소한 Slack 알림은 보내자.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# GitHub Actions workflow에 추가
- name: Notify Slack
  if: always()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    text: |
      배포 ${{ job.status == 'success' &amp;amp;&amp;amp; '성공 ✅' || '실패 ❌' }}
      커밋: ${{ github.sha }}
      작성자: ${{ github.actor }}
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;if: always()&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;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;개발자가 코드 푸시
    &amp;darr;
[CI] GitHub Actions 트리거
    &amp;darr;
[CI] 의존성 설치 + 린트 + 테스트
    &amp;darr; (실패 시 여기서 중단 + 슬랙 알림)
[CD] Docker 이미지 빌드
    &amp;darr;
[CD] 이미지를 레지스트리에 푸시 (ghcr.io)
    &amp;darr;
[CD] 서버에 SSH 접속
    &amp;darr;
[CD] DB 마이그레이션 실행
    &amp;darr;
[CD] 새 이미지 pull + 배포 (Rolling / Blue-Green)
    &amp;darr;
[CD] 헬스체크 확인
    &amp;darr; (실패 시 자동 롤백 + 슬랙 알림)
[CD] 배포 완료 슬랙 알림&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 GitHub Actions 완성본&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 모든 것을 합친 실전 워크플로우.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# .github/workflows/deploy.yml
name: Build &amp;amp; Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-production
  cancel-in-progress: false

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_PASSWORD: testpass
        ports: ['5432:5432']
        options: --health-cmd &quot;pg_isready&quot; --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test
        env:
          DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/myapp

            # 마이그레이션
            docker compose -f docker-compose.prod.yml run --rm app npm run migrate

            # 새 이미지 pull &amp;amp; 재시작
            docker compose -f docker-compose.prod.yml pull app
            docker compose -f docker-compose.prod.yml up -d app

            # 헬스체크
            sleep 10
            curl -f http://localhost:3000/health || exit 1

            # 정리
            docker image prune -f

      - name: Notify Slack - Success
        if: success()
        uses: 8398a7/action-slack@v3
        with:
          status: success
          text: &quot;  배포 성공! (${{ github.sha }})&quot;
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

      - name: Notify Slack - Failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: failure
          text: &quot;❌ 배포 실패! (${{ github.sha }}) - 확인 필요&quot;
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;concurrency&lt;/code&gt; 설정이 은근 중요하다. 배포가 동시에 두 번 돌면 꼬이니까, &lt;code&gt;cancel-in-progress: false&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;CI/CD 파이프라인이랑 배포 전략은 처음 세팅할 때 시간이 좀 드는 게 사실이다. 근데 한번 만들어놓으면 그 뒤로는 코드 푸시만 하면 알아서 테스트하고 배포해주니까, 투자 대비 효과가 확실하다. 매번 SSH 접속해서 수동 배포하던 시절을 생각하면 지금은 천국이다.&lt;/p&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;GitHub Actions + Docker로 CI/CD 파이프라인을 구성하자&lt;/li&gt;
&lt;li&gt;이미지 태그에 커밋 SHA를 쓰면 버전 추적이 쉽다&lt;/li&gt;
&lt;li&gt;소규모는 Rolling Update, 빠른 롤백이 필요하면 Blue-Green&lt;/li&gt;
&lt;li&gt;헬스체크 엔드포인트와 Graceful Shutdown은 무중단 배포의 전제 조건이다&lt;/li&gt;
&lt;li&gt;DB 마이그레이션은 하위 호환이 되게 작성하자 (expand and contract)&lt;/li&gt;
&lt;li&gt;시크릿은 코드에 절대 넣지 말자&lt;/li&gt;
&lt;li&gt;배포 알림은 실패할 때도 보내야 의미가 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이나 다른 방식으로 하고 계신 분들은 댓글로 공유해주세요!  &lt;/p&gt;</description>
      <category>기타</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/38</guid>
      <comments>https://chhb-miscellaneous.tistory.com/38#entry38comment</comments>
      <pubDate>Mon, 18 May 2026 14:42:47 +0900</pubDate>
    </item>
    <item>
      <title>Docker Desktop, 설치부터 실무 활용까지 &amp;mdash; 내가 처음에 알았으면 좋았을 것들</title>
      <link>https://chhb-miscellaneous.tistory.com/37</link>
      <description>&lt;p&gt;Docker 처음 접했을 때 솔직히 멘붕이었다. 가상머신이랑 뭐가 다른 건지, 이미지랑 컨테이너가 뭔 차인지, 왜 다들 도커 도커 하는 건지. 근데 한번 제대로 잡고 나니까 이제는 도커 없이 개발하는 게 상상이 안 된다. &amp;quot;내 컴에서는 되는데요?&amp;quot;를 안 듣게 된 것만으로도 충분한 가치가 있다.&lt;/p&gt;
&lt;p&gt;오늘은 Docker Desktop 기준으로, 설치부터 실무에서 자주 쓰는 패턴까지 한번에 정리해보려고 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Docker가 뭔데, 왜 써야 하는 건데&lt;/h2&gt;
&lt;p&gt;한 줄로 말하면, &lt;strong&gt;앱을 실행하는 데 필요한 모든 것을 하나의 패키지로 묶어서 어디서든 똑같이 돌리는 도구&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;예를 들어 PHP + MySQL + Redis 조합의 프로젝트를 한다고 치자. 팀원 A는 Mac이고, B는 Windows고, C는 Ubuntu다. 셋 다 PHP 버전이 다르고, MySQL 설정이 다르고, 뭔가 하나씩 안 된다. 이걸 Docker로 묶어놓으면 세 사람 다 &lt;code&gt;docker compose up&lt;/code&gt; 한 방이면 똑같은 환경에서 개발할 수 있다.&lt;/p&gt;
&lt;p&gt;가상머신(VM)이랑 뭐가 다르냐는 질문을 많이 받는데, VM은 운영체제 전체를 올리는 거고 Docker는 필요한 프로세스만 격리해서 올린다. 그래서 훨씬 가볍고 빠르다. 내 맥북에서 VM으로 Ubuntu 3개 띄우면 팬이 미쳐 돌아가는데, 컨테이너 10개 띄워도 별로 티가 안 난다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Docker Desktop 설치&lt;/h2&gt;
&lt;h3&gt;macOS&lt;/h3&gt;
&lt;p&gt;두 가지 방법이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Homebrew로 설치 (내가 선호하는 방법)
brew install --cask docker

# 또는 공식 사이트에서 .dmg 다운로드
# https://www.docker.com/products/docker-desktop/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치하고 실행하면 상단 메뉴바에 고래 아이콘이 뜬다. 이게 떠 있어야 Docker 명령어가 동작한다. 가끔 터미널에서 &lt;code&gt;docker&lt;/code&gt; 쳤는데 안 되면 십중팔구 Docker Desktop이 안 켜져 있는 거다.&lt;/p&gt;
&lt;p&gt;Apple Silicon(M1/M2/M3) 맥이면 ARM 버전이 자동으로 설치된다. 근데 가끔 x86 전용 이미지를 돌려야 할 때가 있는데, 이때는 Rosetta 에뮬레이션이 알아서 처리해준다. 느리긴 하지만 동작은 한다.&lt;/p&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. Docker Desktop 공식 사이트에서 설치 파일 다운로드
2. 설치 진행 — WSL 2 백엔드 사용 옵션 체크 (기본값)
3. 재부팅
4. WSL 2 커널 업데이트가 필요하다고 뜨면 안내대로 설치&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Windows는 WSL 2(Windows Subsystem for Linux 2)가 핵심이다. 예전에는 Hyper-V를 썼는데 요즘은 WSL 2가 기본이고 성능도 훨씬 좋다. WSL 2가 없으면 Docker Desktop 설치 과정에서 같이 설치해준다.&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;: Windows Home 에디션도 WSL 2 방식이면 Docker Desktop 사용 가능하다. 예전에는 Pro 이상만 됐는데 그건 Hyper-V 시절 얘기다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Linux&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Ubuntu 기준
sudo apt-get update
sudo apt-get install ./docker-desktop-&amp;lt;version&amp;gt;-amd64.deb&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;솔직히 Linux에서는 Docker Desktop 안 쓰고 Docker Engine만 깔아도 된다. Desktop은 GUI가 주된 장점인데, Linux에서 Docker 쓸 정도면 CLI가 더 편한 경우가 많으니까. 근데 요즘 Docker Desktop에 Kubernetes 원클릭 활성화, Extensions 같은 기능이 있어서 쓰는 사람도 꽤 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 개념 정리 — 이것만 알면 된다&lt;/h2&gt;
&lt;h3&gt;이미지 vs 컨테이너&lt;/h3&gt;
&lt;p&gt;이게 처음에 제일 헷갈리는 건데, 비유하면 이렇다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;이미지&lt;/strong&gt; = 붕어빵 틀. 설계도. 실행에 필요한 모든 게 들어있는 스냅샷.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;컨테이너&lt;/strong&gt; = 그 틀로 찍어낸 붕어빵. 실제로 돌아가는 인스턴스.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하나의 이미지로 컨테이너를 여러 개 만들 수 있다. Node.js 이미지 하나로 컨테이너 10개 띄울 수 있는 거다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 이미지 다운로드
docker pull nginx

# 이미지로 컨테이너 생성 + 실행
docker run -d -p 8080:80 nginx

# 같은 이미지로 하나 더
docker run -d -p 8081:80 nginx&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;볼륨 (Volume)&lt;/h3&gt;
&lt;p&gt;컨테이너는 기본적으로 일회용이다. 컨테이너 지우면 안의 데이터도 날아간다. DB 컨테이너 재시작했는데 데이터가 다 사라져서 멘탈 나간 적이 있다면 (나도 그랬다), 볼륨을 안 붙여서 그런 거다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 볼륨 생성
docker volume create mydata

# 볼륨 붙여서 컨테이너 실행
docker run -d \
  -v mydata:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 컨테이너를 지우고 다시 만들어도 데이터가 유지된다.&lt;/p&gt;
&lt;h3&gt;네트워크&lt;/h3&gt;
&lt;p&gt;컨테이너끼리 통신하려면 같은 네트워크에 있어야 한다. docker compose를 쓰면 자동으로 해주지만, 수동으로 할 때는 이렇게 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 네트워크 생성
docker network create mynet

# 같은 네트워크에 컨테이너 두 개 띄우기
docker run -d --name db --network mynet mysql:8
docker run -d --name app --network mynet node:20&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 네트워크 안에서는 컨테이너 이름으로 서로 접근할 수 있다. &lt;code&gt;app&lt;/code&gt; 컨테이너에서 &lt;code&gt;db:3306&lt;/code&gt;으로 접속하면 된다. IP 주소 외울 필요 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Dockerfile — 이미지 만드는 레시피&lt;/h2&gt;
&lt;p&gt;남이 만든 이미지만 쓸 수는 없다. 내 앱을 이미지로 만들려면 Dockerfile을 작성해야 한다.&lt;/p&gt;
&lt;h3&gt;기본 구조&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# 베이스 이미지 선택
FROM node:20-alpine

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 파일 먼저 복사 (캐시 활용)
COPY package*.json ./

# 의존성 설치
RUN npm ci --production

# 소스코드 복사
COPY . .

# 포트 명시 (문서화 목적, 실제 열리지는 않음)
EXPOSE 3000

# 실행 명령
CMD [&amp;quot;node&amp;quot;, &amp;quot;server.js&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;레이어 캐시 — 이거 모르면 빌드가 느리다&lt;/h3&gt;
&lt;p&gt;Dockerfile의 각 명령어는 하나의 레이어가 된다. 변경된 레이어부터 아래는 전부 다시 빌드된다. 그래서 &lt;strong&gt;자주 바뀌는 것은 아래에, 안 바뀌는 것은 위에&lt;/strong&gt; 두는 게 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# ❌ 나쁜 예 — 소스 바꿀 때마다 npm install도 다시 함
COPY . .
RUN npm ci

# ✅ 좋은 예 — package.json 안 바뀌면 npm install 캐시 적중
COPY package*.json ./
RUN npm ci
COPY . .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 차이를 모르면 소스 한 줄 고칠 때마다 &lt;code&gt;npm ci&lt;/code&gt;가 돌아간다. 프로젝트 크면 빌드에 몇 분씩 잡아먹는다.&lt;/p&gt;
&lt;h3&gt;멀티스테이지 빌드 — 이미지 크기 줄이기&lt;/h3&gt;
&lt;p&gt;프론트엔드 프로젝트에서 특히 유용하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# 1단계: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 2단계: 실행 (빌드 결과물만 가져옴)
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD [&amp;quot;nginx&amp;quot;, &amp;quot;-g&amp;quot;, &amp;quot;daemon off;&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;빌드에 필요한 Node.js, node_modules 같은 건 최종 이미지에 안 들어간다. 이미지 크기가 1GB에서 50MB로 줄어드는 경우도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Docker Compose — 여러 컨테이너 한 방에 관리&lt;/h2&gt;
&lt;p&gt;실무에서는 앱 하나에 컨테이너 여러 개를 쓰는 경우가 대부분이다. 웹서버, DB, 캐시, 큐 워커... 이걸 하나씩 &lt;code&gt;docker run&lt;/code&gt;으로 띄우면 미친다. Docker Compose가 이걸 해결해준다.&lt;/p&gt;
&lt;h3&gt;실전 예제 — 웹 앱 + DB + Redis&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# docker-compose.yml (또는 compose.yaml)
services:
  app:
    build: .
    ports:
      - &amp;quot;3000:3000&amp;quot;
    environment:
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - &amp;quot;5432:5432&amp;quot;
    healthcheck:
      test: [&amp;quot;CMD-SHELL&amp;quot;, &amp;quot;pg_isready -U postgres&amp;quot;]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    ports:
      - &amp;quot;6379:6379&amp;quot;

volumes:
  pgdata:&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 전부 띄우기
docker compose up -d

# 로그 보기
docker compose logs -f app

# 전부 내리기
docker compose down

# 볼륨까지 삭제 (DB 데이터도 날아감!)
docker compose down -v&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;depends_on과 healthcheck&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;depends_on&lt;/code&gt;만 쓰면 컨테이너 시작 순서만 보장하지, 실제로 서비스가 준비됐는지는 확인 안 한다. DB 컨테이너가 뜨긴 했는데 아직 커넥션 안 받는 상태에서 앱이 접속 시도하면 에러가 뜬다.&lt;/p&gt;
&lt;p&gt;위 예제처럼 &lt;code&gt;healthcheck&lt;/code&gt; + &lt;code&gt;condition: service_healthy&lt;/code&gt; 조합을 쓰면, DB가 진짜 준비된 다음에 앱이 뜬다. 이거 안 하면 앱 시작할 때 DB 접속 실패로 크래시 나는 일이 생긴다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 쓰는 Docker 명령어&lt;/h2&gt;
&lt;p&gt;매일 치는 것들 위주로 정리.&lt;/p&gt;
&lt;h3&gt;컨테이너 관리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 실행 중인 컨테이너 목록
docker ps

# 전체 (중지된 것 포함)
docker ps -a

# 컨테이너 중지
docker stop &amp;lt;컨테이너ID 또는 이름&amp;gt;

# 컨테이너 삭제
docker rm &amp;lt;컨테이너ID&amp;gt;

# 중지된 컨테이너 일괄 삭제
docker container prune

# 컨테이너 안에 들어가기 (디버깅할 때 필수)
docker exec -it &amp;lt;컨테이너&amp;gt; /bin/sh
# 또는
docker exec -it &amp;lt;컨테이너&amp;gt; bash&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;docker exec -it&lt;/code&gt;은 정말 많이 쓴다. 컨테이너 안에서 뭐가 어떻게 돌아가는지 직접 확인하고 싶을 때, DB에 직접 접속하고 싶을 때, 로그 파일 확인하고 싶을 때.&lt;/p&gt;
&lt;h3&gt;이미지 관리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 로컬 이미지 목록
docker images

# 이미지 삭제
docker rmi &amp;lt;이미지ID&amp;gt;

# 사용 안 하는 이미지 일괄 삭제
docker image prune

# 빌드
docker build -t myapp:latest .

# 태그 붙이기
docker tag myapp:latest myregistry.com/myapp:v1.0&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;디스크 정리 — 이거 안 하면 디스크 꽉 찬다&lt;/h3&gt;
&lt;p&gt;Docker 오래 쓰면 이미지, 컨테이너, 볼륨, 빌드 캐시가 쌓여서 디스크를 엄청 먹는다. 나는 한 번은 맥북 용량 50GB가 Docker한테 먹혀있던 적도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Docker가 먹고 있는 디스크 확인
docker system df

# 안 쓰는 것 전부 정리 (컨테이너, 네트워크, 이미지, 빌드 캐시)
docker system prune

# 볼륨까지 정리 (주의! 데이터 날아감)
docker system prune --volumes

# 진짜 다 날리기 (핵폭탄 옵션)
docker system prune -a --volumes&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;code&gt;prune -a&lt;/code&gt;는 사용 중이 아닌 &lt;strong&gt;모든&lt;/strong&gt; 이미지를 삭제한다. 다시 pull 받아야 하니까 네트워크 느린 환경에서는 신중하게.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;Docker Desktop GUI 활용&lt;/h2&gt;
&lt;p&gt;CLI만 써도 되지만, Docker Desktop GUI가 은근 편한 것들이 있다.&lt;/p&gt;
&lt;h3&gt;Containers 탭&lt;/h3&gt;
&lt;p&gt;실행 중인 컨테이너 목록, 로그, 터미널 접속, 환경 변수 확인을 한 화면에서 할 수 있다. &lt;code&gt;docker logs&lt;/code&gt; + &lt;code&gt;docker exec&lt;/code&gt; + &lt;code&gt;docker inspect&lt;/code&gt;를 GUI로 하는 거라고 보면 된다. 로그를 실시간으로 보면서 필터링하는 건 CLI보다 GUI가 편하다.&lt;/p&gt;
&lt;h3&gt;Images 탭&lt;/h3&gt;
&lt;p&gt;로컬 이미지 관리. 이미지 크기 확인하고, 안 쓰는 이미지 정리할 때 좋다. 각 이미지의 레이어 구조도 볼 수 있는데, Dockerfile 최적화할 때 참고가 된다.&lt;/p&gt;
&lt;h3&gt;Volumes 탭&lt;/h3&gt;
&lt;p&gt;볼륨 목록이랑 사용량 확인. 어떤 볼륨이 어떤 컨테이너에 붙어있는지 한눈에 보인다.&lt;/p&gt;
&lt;h3&gt;Dev Environments (개발 환경)&lt;/h3&gt;
&lt;p&gt;Git 레포 URL 넣으면 자동으로 개발 환경을 만들어주는 기능인데, 솔직히 나는 잘 안 쓴다. docker compose가 더 익숙해서. 근데 팀에 Docker 처음 접하는 사람 있으면 온보딩할 때 괜찮긴 하다.&lt;/p&gt;
&lt;h3&gt;Kubernetes 원클릭&lt;/h3&gt;
&lt;p&gt;Settings &amp;gt; Kubernetes &amp;gt; Enable Kubernetes 체크하면 끝. 로컬에서 K8s 환경을 바로 쓸 수 있다. minikube 같은 거 따로 설치 안 해도 된다. K8s 학습하거나 매니페스트 테스트할 때 편하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실무 팁 모음&lt;/h2&gt;
&lt;h3&gt;.dockerignore 필수&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt;처럼 빌드 컨텍스트에서 제외할 파일을 지정한다. 이거 안 하면 &lt;code&gt;node_modules&lt;/code&gt;나 &lt;code&gt;.git&lt;/code&gt; 폴더가 이미지에 들어가서 이미지가 쓸데없이 커진다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .dockerignore
node_modules
.git
.env
.env.local
*.log
dist
.DS_Store&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;환경변수 관리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# docker-compose.yml에서 .env 파일 사용
services:
  app:
    image: myapp
    env_file:
      - .env&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# .env
DATABASE_URL=postgresql://user:pass@db:5432/myapp
JWT_SECRET=my-super-secret-key
REDIS_URL=redis://cache:6379&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; 파일은 &lt;code&gt;.gitignore&lt;/code&gt;에 넣고, &lt;code&gt;.env.example&lt;/code&gt;만 커밋하자. 비밀번호가 깃 히스토리에 남으면 골치 아프다.&lt;/p&gt;
&lt;h3&gt;로그 관리&lt;/h3&gt;
&lt;p&gt;컨테이너 로그가 무한으로 쌓이면 디스크를 다 먹는다. 운영 환경이면 로그 드라이버를 설정하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  app:
    image: myapp
    logging:
      driver: &amp;quot;json-file&amp;quot;
      options:
        max-size: &amp;quot;10m&amp;quot;
        max-file: &amp;quot;3&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이러면 로그 파일이 10MB씩 최대 3개까지만 유지된다.&lt;/p&gt;
&lt;h3&gt;빌드 속도 올리기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# BuildKit 활성화 (Docker Desktop은 기본 활성화)
# 환경변수로도 강제 가능
# DOCKER_BUILDKIT=1 docker build .

# 캐시 마운트로 패키지 매니저 캐시 재활용
RUN --mount=type=cache,target=/root/.npm \
    npm ci --production&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BuildKit의 캐시 마운트를 쓰면 &lt;code&gt;npm ci&lt;/code&gt;나 &lt;code&gt;pip install&lt;/code&gt; 같은 패키지 설치가 캐시 덕분에 빨라진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;흔한 트러블슈팅&lt;/h2&gt;
&lt;h3&gt;포트 충돌&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Error: Bind for 0.0.0.0:3000 failed: port is already allocated&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로컬에서 이미 3000번 포트를 쓰고 있는 거다. 다른 컨테이너가 쓰고 있거나, 로컬에 직접 띄운 앱이 있거나.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 어떤 프로세스가 포트 쓰는지 확인 (macOS/Linux)
lsof -i :3000

# 다른 포트로 매핑
docker run -p 3001:3000 myapp&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;권한 문제 (Linux)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Permission denied&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Linux에서 Docker 명령어에 &lt;code&gt;sudo&lt;/code&gt;를 붙여야 하는 경우. docker 그룹에 사용자를 추가하면 해결된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo usermod -aG docker $USER
# 로그아웃 후 다시 로그인&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;컨테이너가 바로 종료됨&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 로그 확인
docker logs &amp;lt;컨테이너ID&amp;gt;

# 대화형으로 띄워서 디버깅
docker run -it myapp /bin/sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CMD나 ENTRYPOINT가 잘못됐거나, 앱이 크래시 나는 경우가 대부분이다. 로그를 먼저 보자.&lt;/p&gt;
&lt;h3&gt;Docker Desktop이 느릴 때&lt;/h3&gt;
&lt;p&gt;macOS에서 Docker Desktop이 CPU나 메모리를 너무 잡아먹으면 Settings &amp;gt; Resources에서 조절할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;기본 추천 설정:
- CPUs: 전체 코어의 절반
- Memory: 전체 RAM의 25~50%
- Disk image size: 필요한 만큼 (기본 60GB)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;특히 파일 I/O가 느린 문제가 있다면 (macOS에서 흔함), &lt;code&gt;docker-compose.yml&lt;/code&gt;에서 볼륨 마운트에 캐시 옵션을 추가해볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;volumes:
  - .:/app:cached&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Docker Desktop 라이선스 얘기&lt;/h2&gt;
&lt;p&gt;이건 한번 짚고 넘어가야 한다. 2021년부터 Docker Desktop은 &lt;strong&gt;대기업(직원 250명 이상 또는 연매출 $10M 이상)에서 유료&lt;/strong&gt;다. 개인, 소규모 기업, 교육, 오픈소스 프로젝트는 무료.&lt;/p&gt;
&lt;p&gt;회사에서 쓰는데 라이선스가 걸린다면 대안도 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rancher Desktop&lt;/strong&gt; — 무료, K8s 통합이 강점&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Podman Desktop&lt;/strong&gt; — Red Hat에서 만든 거, Docker CLI 호환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Colima&lt;/strong&gt; — macOS에서 CLI 기반으로 Docker 돌리기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;근데 솔직히 Docker Desktop의 편의성을 따라오는 건 아직 없다고 느낀다. 라이선스 문제가 없다면 Docker Desktop이 제일 낫다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;Docker Desktop은 단순히 Docker를 GUI로 쓰는 것 이상이다. 로컬 개발 환경 통일, Kubernetes 테스트, 빠른 프로토타이핑까지. 한번 익숙해지면 없이 개발하는 게 오히려 불편해진다.&lt;/p&gt;
&lt;p&gt;핵심 정리:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미지는 설계도, 컨테이너는 실행 인스턴스. 이 구분만 확실히 하자&lt;/li&gt;
&lt;li&gt;Dockerfile은 레이어 캐시를 고려해서 작성하자 (자주 바뀌는 건 아래에)&lt;/li&gt;
&lt;li&gt;Docker Compose는 실무에서 거의 필수다. 단일 컨테이너만 쓸 일은 별로 없다&lt;/li&gt;
&lt;li&gt;볼륨 안 붙이면 데이터 날아간다. DB는 반드시 볼륨 설정하자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker system prune&lt;/code&gt;으로 주기적으로 디스크 정리하자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.dockerignore&lt;/code&gt;랑 멀티스테이지 빌드로 이미지 크기를 줄이자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음에는 Docker + CI/CD 파이프라인 구성이나, 프로덕션 배포 전략(Blue-Green, Rolling Update) 같은 주제도 다뤄볼 생각이다.&lt;/p&gt;</description>
      <category>기타</category>
      <author>CHHB</author>
      <guid isPermaLink="true">https://chhb-miscellaneous.tistory.com/37</guid>
      <comments>https://chhb-miscellaneous.tistory.com/37#entry37comment</comments>
      <pubDate>Fri, 15 May 2026 16:54:43 +0900</pubDate>
    </item>
  </channel>
</rss>