Claude Code 자동화 시스템 구축 7단계: 스킬·훅·배치 실전 아키텍처

목차

결론부터 말하면, Claude Code 자동화 시스템 구축은 생각보다 진입장벽이 낮고 실무 효과는 꽤 크다. 커스텀 스킬 몇 개 만들고, 훅으로 파이프라인 엮고, 배치 API로 대량 처리 붙이면 혼자서 반복 업무의 절반 이상을 자동화할 수 있다.

나는 서버를 혼자 관리하는 입장이라 자동화에 민감한 편이다. Claude Code 자동화 시스템 구축을 본격적으로 시작한 건 매일 똑같은 코드 리뷰, 로그 분석, 문서 생성 작업에 시간을 너무 많이 쓰고 있다는 걸 깨달았을 때다. 지금은 .claude/ 디렉토리 하나에 스킬, 에이전트, 훅 설정을 모아두고 프로젝트마다 재사용하고 있다. 이 글에서 그 구조를 처음부터 풀어본다.

Claude Code 자동화의 전체 아키텍처

자동화 시스템을 만들기 전에 큰 그림부터 잡아야 한다. Claude Code는 단순한 코드 어시스턴트가 아니라 하나의 에이전트 런타임이다. 스킬, 훅, 에이전트, MCP 서버라는 네 가지 축이 있고, 이걸 조합하면 꽤 다양한 파이프라인이 나온다.

graph TB
    A[사용자 입력 / 스케줄 트리거] --> B[Claude Code CLI]
    B --> C{라우팅}
    C --> D[커스텀 스킬]
    C --> E[에이전트 서브프로세스]
    C --> F[MCP 서버]
    D --> G[훅 pre/post]
    E --> G
    G --> H[배치 API]
    H --> I[결과 출력 / 파일 저장]

핵심은 진입점을 하나로 통일하는 거다. CLI에서 슬래시 커맨드 하나 치면 스킬이 실행되고, 훅이 전후처리를 하고, 필요하면 에이전트가 서브태스크를 처리한다. 이 흐름을 이해하면 나머지는 설정 파일 작성이다.

Claude Code 프로젝트 구조
`.claude/` 디렉토리 아래에 `commands/`, `agents/`, `skills/` 폴더를 만들어 관리한다. 프로젝트 루트의 `CLAUDE.md`가 전체 컨텍스트를 제공하고, `settings.json`이 훅과 권한을 제어한다.
각 구성 요소를 하나씩 비교해보자.
구성 요소 역할 트리거 방식 적합한 작업
커스텀 스킬 슬래시 커맨드로 실행하는 프롬프트 묶음 /skill-name 입력 코드 리뷰, 문서 생성, 설정 변경
에이전트 독립 서브프로세스로 복잡한 태스크 처리 Agent 도구 호출 멀티스텝 분석, 병렬 탐색
훅 특정 이벤트 전후에 자동 실행 settings.json 설정 린트, 포맷팅, 검증
MCP 서버 외부 API/DB를 도구로 노출 도구 호출 시 자동 DB 조회, API 연동, 파일 시스템

사실 처음에는 이 네 가지가 어떻게 다른 건지 좀 헷갈렸다. 스킬이랑 에이전트가 비슷해 보이는데, 차이는 실행 컨텍스트다. 스킬은 현재 대화 안에서 실행되고, 에이전트는 별도 프로세스로 떠서 독립적으로 돌아간다.

커스텀 스킬 설계: 슬래시 커맨드 만들기

Claude Code 자동화 시스템 구축의 첫 단계는 커스텀 스킬이다. .claude/commands/ 디렉토리에 마크다운 파일을 만들면 그게 바로 슬래시 커맨드가 된다.

기본 스킬 파일 구조

# .claude/commands/review.md

코드 리뷰를 수행한다. 변경된 파일을 분석하고 아래 기준으로 피드백한다:

1. 보안 취약점 (SQL Injection, XSS, 인증 우회)
2. 성능 이슈 (N+1 쿼리, 불필요한 루프)
3. 에러 핸들링 누락
4. 네이밍 컨벤션 위반

출력 형식:
- 파일명: 이슈 설명 (심각도: high/medium/low)
- 수정 제안 코드 포함

이렇게 만들면 Claude Code에서 /review라고 입력하면 해당 프롬프트가 실행된다. 단순해 보이지만, 이게 쌓이면 팀 전체의 워크플로우가 된다.

인자를 받는 스킬

스킬에 $ARGUMENTS 플레이스홀더를 넣으면 동적으로 입력을 받을 수 있다.

# .claude/commands/deploy-check.md

$ARGUMENTS 환경에 배포하기 전 체크리스트를 실행한다.

1. 해당 환경의 설정 파일 확인 (config/{environment}.yaml)
2. 마이그레이션 스크립트 누락 여부
3. 환경 변수 diff 확인
4. 의존성 버전 충돌 체크

문제 발견 시 배포 차단 사유를 명확히 출력한다.

/deploy-check staging이라고 치면 $ARGUMENTS에 "staging"이 들어간다. 환경별 배포 전 검증을 자동화할 때 쓰기 좋다.

스킬 파일 네이밍 팁
파일명이 곧 커맨드명이다. `review.md`는 `/review`, `deploy-check.md`는 `/deploy-check`가 된다. 팀 프로젝트면 접두사를 붙여서 `team-review.md`, `team-deploy.md`처럼 네임스페이스를 나누는 게 좋다.
### 프로젝트 스킬 vs 개인 스킬

스킬은 두 군데에 둘 수 있다:

  • 프로젝트 스킬: .claude/commands/ — Git에 커밋해서 팀 공유
  • 개인 스킬: ~/.claude/commands/ — 내 머신에서만 동작

나는 코드 리뷰, 배포 체크 같은 건 프로젝트 스킬로, 개인적인 메모 생성이나 로그 분석은 개인 스킬로 분리해 두고 있다. 근데 솔직히 개인 스킬은 자주 안 쓰게 되더라. 프로젝트마다 맥락이 달라서 결국 프로젝트 스킬로 옮기는 경우가 많다.

훅 시스템으로 자동 전후처리 붙이기

스킬이 "뭘 할지"를 정의한다면, 훅은 "언제 자동으로 뭘 할지"를 정의한다. settings.json에서 설정하며, 특정 이벤트 발생 시 셸 명령을 자동 실행한다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "command": "echo 'Checking lint before edit...'"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx eslint --fix $CLAUDE_FILE_PATH"
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "command": "terminal-notifier -message '$CLAUDE_NOTIFICATION'"
      }
    ]
  }
}

PreToolUse는 도구 실행 전, PostToolUse는 실행 후에 트리거된다. matcher로 어떤 도구에 반응할지 필터링한다.

개인적으로 이 훅 시스템이 Claude Code 자동화에서 제일 과소평가된 기능이다. 파일을 수정할 때마다 자동으로 린트를 돌리고, 테스트를 실행하고, 포맷팅을 맞춘다. 사람이 까먹을 수 있는 걸 시스템이 잡아주니까.

실전에서 쓰는 훅 조합

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "sh -c 'if echo \"$CLAUDE_FILE_PATH\" | grep -q \"\\.py$\"; then ruff check --fix \"$CLAUDE_FILE_PATH\" && ruff format \"$CLAUDE_FILE_PATH\"; fi'"
      },
      {
        "matcher": "Bash",
        "command": "sh -c 'if echo \"$CLAUDE_BASH_COMMAND\" | grep -q \"git commit\"; then echo \"[Hook] Commit detected. Running post-commit checks...\"; fi'"
      }
    ]
  }
}

Python 파일을 수정하면 자동으로 Ruff가 돌아간다. ruff를 안 쓰고 있다면 한번 써보길 권한다. 기존 flake8 + black 조합보다 체감상 훨씬 빠르다.

훅 실행 시 주의점
훅 커맨드가 0이 아닌 exit code를 반환하면 해당 도구 실행이 차단된다. 린트 에러가 있을 때 의도적으로 차단하려면 이걸 활용하면 되지만, 실수로 차단되면 당황스럽다. 처음에 이거 몰라서 한참 헤맸다.
## 에이전트 서브프로세스 활용법

단일 스킬로 처리하기 어려운 복잡한 태스크는 에이전트를 쓴다. Claude Code의 Agent 도구는 독립 서브프로세스를 띄워서 병렬로 작업을 처리한다.

에이전트 프로필은 .claude/agents/ 디렉토리에 마크다운으로 정의한다.

# .claude/agents/log-analyzer.md

서버 로그를 분석하는 에이전트다.

## 역할
- 에러 로그 패턴 분류
- 빈도 상위 10개 에러 추출
- 시간대별 에러 분포 분석

## 사용 도구
- Bash: 로그 파일 읽기, grep/awk 처리
- Grep: 패턴 검색
- Read: 설정 파일 확인

## 출력 형식
에러 유형 | 발생 횟수 | 최근 발생 시각 | 심각도
형식의 마크다운 테이블로 출력한다.

에이전트가 스킬과 다른 점은 독립 컨텍스트에서 실행된다는 거다. 메인 대화의 컨텍스트를 오염시키지 않으면서 무거운 작업을 돌릴 수 있다. 여러 에이전트를 동시에 띄워서 병렬 처리도 가능하다.

# 메인 대화에서 에이전트를 병렬로 호출하는 예시

1. log-analyzer 에이전트: 최근 24시간 에러 로그 분석
2. perf-checker 에이전트: API 응답시간 추세 분석
3. dep-auditor 에이전트: 의존성 보안 취약점 스캔

세 에이전트가 동시에 돌고, 결과를 모아서 일일 리포트를 생성한다.

다만 에이전트를 너무 잘게 쪼개면 오히려 오버헤드가 생긴다. 토큰도 각각 쓰니까. 나는 최소 5분 이상 걸리는 작업만 에이전트로 분리하고, 간단한 건 스킬로 처리한다.

CLAUDE.md로 프로젝트 컨텍스트 주입하기

Claude Code 자동화 시스템 구축에서 놓치기 쉬운 게 CLAUDE.md다. 프로젝트 루트에 이 파일을 두면 Claude Code가 매 세션 시작 시 자동으로 읽는다. 프로젝트의 컨벤션, 아키텍처, 금지 사항 같은 걸 여기에 적어둔다.

# CLAUDE.md

## 프로젝트 개요
Python 3.12 + FastAPI 기반 REST API 서버.
PostgreSQL 16, Redis 7.2 사용.

## 코드 컨벤션
- 타입 힌트 필수. Any 사용 금지.
- 모든 엔드포인트에 Pydantic v2 모델 사용.
- 에러 응답은 RFC 7807 (Problem Details) 형식.
- 테스트는 pytest + httpx.AsyncClient.

## 디렉토리 구조
- src/api/ : 라우터
- src/core/ : 비즈니스 로직
- src/models/ : SQLAlchemy 모델
- src/schemas/ : Pydantic 스키마

## 금지 사항
- print() 디버깅 금지. logging 모듈 사용.
- SELECT * 금지. 컬럼 명시.
- .env 파일 커밋 금지.

이게 있고 없고의 차이가 크다. 없으면 Claude Code가 매번 프로젝트 구조를 파악하느라 시간을 쓰고, 컨벤션도 들쭉날쭉해진다. 솔직히 이거 하나만 잘 써놔도 코드 품질이 확 올라간다.

CLAUDE.md 계층 구조

CLAUDE.md는 여러 위치에 둘 수 있다:

  • ~/CLAUDE.md — 전역 설정. 모든 프로젝트에 적용
  • ./CLAUDE.md — 프로젝트 루트. 해당 프로젝트에만 적용
  • ./src/CLAUDE.md — 하위 디렉토리. 해당 디렉토리 작업 시 추가 로드

계층이 깊어질수록 구체적인 규칙을 넣는다. 전역에는 "한국어로 커밋 메시지 작성" 같은 공통 규칙, 프로젝트에는 기술 스택과 컨벤션, 하위 디렉토리에는 해당 모듈 특화 규칙을 넣는 식이다.

![Claude Code 프로젝트 설정 구조]({{image:claude-code-config|Isometric 3D diagram showing layered configuration files in a project directory tree with dotted lines connecting parent and child config files, modern flat design, blue and teal gradient, dark background}})

배치 처리: 대량 작업 자동화

파일 100개를 일괄 리팩토링하거나, PR 50개의 코드 리뷰를 돌려야 할 때. 이때 Claude의 Batch API가 필요하다.

Batch API 기본 구조

import anthropic
import json

client = anthropic.Anthropic()

# 배치 요청 생성
requests = []
for i, file_path in enumerate(target_files):
    with open(file_path, "r") as f:
        code = f.read()
    
    requests.append({
        "custom_id": f"review-{i}",
        "params": {
            "model": "claude-sonnet-4-20250514",
            "max_tokens": 4096,
            "messages": [
                {
                    "role": "user",
                    "content": f"다음 코드를 리뷰해라. 보안, 성능, 에러 핸들링 관점에서.\n\n```python\n{code}\n```"
                }
            ]
        }
    })

# JSONL 파일로 저장
with open("batch_input.jsonl", "w") as f:
    for req in requests:
        f.write(json.dumps(req) + "\n")

배치 요청을 JSONL 파일로 만들고, API에 제출한다:

# 배치 제출
batch = client.messages.batches.create(
    requests=requests
)

print(f"Batch ID: {batch.id}")
print(f"Status: {batch.processing_status}")

결과는 비동기로 돌아온다. 완료까지 최대 24시간인데 보통 수십 분이면 끝난다. 비용도 일반 API 호출 대비 50% 할인이 적용된다. 바로 결과가 필요 없는 대량 작업이면 무조건 배치가 낫다.

배치 결과 수집 및 처리

import time

# 폴링으로 완료 대기
while True:
    status = client.messages.batches.retrieve(batch.id)
    if status.processing_status == "ended":
        break
    print(f"Processing... {status.request_counts.succeeded}/{status.request_counts.processing}")
    time.sleep(30)

# 결과 수집
results = []
for result in client.messages.batches.results(batch.id):
    if result.result.type == "succeeded":
        review = result.result.message.content[0].text
        results.append({
            "id": result.custom_id,
            "review": review
        })

# 결과를 파일로 저장
with open("review_results.json", "w") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)
배치 처리 실전 팁
`custom_id`를 의미 있는 값으로 설정해라. 나중에 결과를 매칭할 때 `review-0`, `review-1`보다 `review-src/api/users.py` 식으로 파일 경로를 넣는 게 훨씬 편하다.
## MCP 서버 연동으로 Claude Code 확장하기

MCP(Model Context Protocol)는 Claude Code에 외부 도구를 붙이는 표준 프로토콜이다. DB 조회, 외부 API 호출, 파일 시스템 접근 같은 걸 Claude Code가 직접 할 수 있게 해준다.

MCP 서버 기본 구현

# mcp_server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import asyncio
import asyncpg

app = Server("db-query-server")

# DB 커넥션 풀
pool = None

@app.list_tools()
async def list_tools():
    return [
        Tool(
            name="query_db",
            description="PostgreSQL 데이터베이스에 읽기 전용 쿼리를 실행한다",
            inputSchema={
                "type": "object",
                "properties": {
                    "sql": {
                        "type": "string",
                        "description": "실행할 SELECT 쿼리"
                    }
                },
                "required": ["sql"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "query_db":
        sql = arguments["sql"]
        
        # SELECT만 허용
        if not sql.strip().upper().startswith("SELECT"):
            return [TextContent(
                type="text",
                text="Error: SELECT 쿼리만 허용됩니다."
            )]
        
        global pool
        if pool is None:
            pool = await asyncpg.create_pool(
                "postgresql://localhost:5432/mydb"
            )
        
        rows = await pool.fetch(sql)
        result = [dict(row) for row in rows]
        
        return [TextContent(
            type="text",
            text=str(result)
        )]

if __name__ == "__main__":
    asyncio.run(app.run())

이 서버를 Claude Code에 연결하려면 settings.json에 추가한다:

{
  "mcpServers": {
    "db-query": {
      "command": "python",
      "args": ["mcp_server.py"],
      "cwd": "/path/to/project"
    }
  }
}

이러면 Claude Code에서 자연어로 "최근 가입한 유저 10명 보여줘"라고 하면 알아서 query_db 도구를 호출해서 SQL을 생성하고 결과를 보여준다.

![MCP 서버 아키텍처]({{image:mcp-architecture|Technical diagram showing MCP server connecting Claude Code to external databases and APIs with bidirectional arrows, minimalist wireframe style, white and blue color scheme, light background}})

보안 주의사항

MCP 서버에서 DB에 직접 연결할 때 절대로 DELETE, UPDATE, DROP을 허용하면 안 된다. 위 코드처럼 SELECT만 화이트리스트로 열어두는 게 기본이다. 아니면 읽기 전용 DB 유저를 따로 만들어서 연결하는 게 더 안전하다.

Claude Code 자동화 시스템 구축 시 비교: 접근 방식별 트레이드오프

자동화를 만드는 방법이 여러 가지다. 어떤 걸 써야 할지 판단 기준을 정리했다.

단일 작업 vs 파이프라인 vs 풀 자동화

기준 커스텀 스킬 단독 스킬 + 훅 파이프라인 에이전트 + MCP + 배치
구축 난이도 낮음 (마크다운 1개) 중간 (settings.json 편집) 높음 (코드 작성 필요)
유지보수 쉬움 보통 복잡
처리 가능 범위 단일 대화 내 작업 파일 수정 전후 자동화 외부 시스템 연동, 대량 처리
적합한 상황 코드 리뷰, 문서 생성 CI 전 검증, 포맷팅 로그 분석, 배치 리팩토링, DB 조회
토큰 비용 낮음 낮음 높음 (에이전트별 독립 세션)

내 경험상, 대부분의 팀은 스킬 + 훅 파이프라인이면 충분하다. 에이전트와 MCP까지 동원해야 하는 상황은 생각보다 많지 않다. 근데 한번 맛보면 돌아가기 어렵긴 하다.

비용 최적화 포인트

배치 API는 일반 호출 대비 50% 저렴하다. 그런데 모든 걸 배치로 돌리면 결과를 기다리는 시간이 문제다. 나는 이렇게 나눈다:

  • 즉시 응답 필요: 일반 API (스킬, 에이전트)
  • 1시간 이내 결과 OK: 배치 API
  • 정기 실행: 배치 + cron

코드 리뷰처럼 PR 올릴 때 바로 결과가 필요한 건 일반 API, 주간 코드 품질 리포트 같은 건 배치로 돌리는 식이다.

실전 파이프라인 예시: PR 자동 리뷰 시스템

지금까지 다룬 요소들을 조합해서 실전 파이프라인 하나를 만들어보자. PR이 올라오면 자동으로 코드 리뷰를 돌리는 시스템이다.

스킬 정의

# .claude/commands/pr-review.md

$ARGUMENTS PR을 리뷰한다.

1. 변경된 파일 목록을 가져온다 (gh pr diff)
2. 파일별로 아래 기준을 점검한다:
   - 보안: 하드코딩된 시크릿, SQL 인젝션, XSS
   - 성능: N+1 쿼리, 불필요한 동기 호출
   - 테스트: 새 기능에 테스트가 있는지
   - 타입: 타입 힌트 누락
3. 결과를 심각도별로 정리한다
4. gh pr comment로 리뷰 코멘트를 남긴다

훅으로 포맷팅 강제

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "sh -c 'ext=\"${CLAUDE_FILE_PATH##*.}\"; case $ext in py) ruff format \"$CLAUDE_FILE_PATH\";; ts|tsx) npx prettier --write \"$CLAUDE_FILE_PATH\";; esac'"
      }
    ]
  }
}

GitHub Actions와 연동

# .github/workflows/claude-review.yml
name: Claude PR Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Claude Code Review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          npm install -g @anthropic-ai/claude-code
          claude -p "/pr-review ${{ github.event.pull_request.number }}" --output-format json > review.json

claude -p 플래그로 비대화형 모드에서 실행한다. CI/CD에서 돌리려면 이 플래그가 필수다. --output-format json을 붙이면 파싱하기 좋은 JSON으로 결과가 나온다.

GitHub Actions 비용 참고
Claude API 호출 비용 + GitHub Actions 러너 비용이 동시에 발생한다. PR이 많은 프로젝트에서는 draft PR에서는 건너뛰는 조건을 넣는 게 좋다. `if: github.event.pull_request.draft == false` 한 줄이면 된다.
![자동 PR 리뷰 파이프라인]({{image:pr-review-pipeline|Flowchart showing automated PR review pipeline from GitHub webhook to Claude Code analysis to comment posting, clean modern design with rounded boxes, green and blue accents, white background}})

트러블슈팅과 운영 팁

Claude Code 자동화 시스템 구축을 하다 보면 자잘한 문제들이 생긴다. 내가 겪은 것들을 정리해둔다.

흔한 에러와 해결

훅이 실행 안 될 때: settings.json 경로를 확인해라. 프로젝트 루트의 .claude/settings.json이 아니라 ~/.claude/settings.json에 넣어야 하는 경우가 있다. 스코프가 다르다.

MCP 서버 연결 실패: command에 절대 경로를 쓰는 게 안전하다. "command": "python"이 아니라 "command": "/usr/bin/python3"처럼. Claude Code가 실행되는 환경의 PATH와 내 터미널 PATH가 다를 수 있다.

에이전트가 컨텍스트를 잃을 때: 에이전트에 넘기는 프롬프트에 충분한 맥락을 포함해야 한다. "이전 대화를 참고해서"는 안 통한다. 에이전트는 독립 프로세스라 메인 대화를 모른다. 처음에 이것 때문에 시간 좀 날렸다.

API 키 관리
`settings.json`이나 `CLAUDE.md`에 API 키를 절대 넣지 마라. 환경 변수로 관리하고, `.gitignore`에 `.env`를 반드시 추가해야 한다. GitHub에 키가 올라가면 몇 분 안에 악용된다.
### 디버깅 방법

Claude Code는 --verbose 플래그로 내부 동작을 볼 수 있다. 훅이 어떤 순서로 실행되는지, MCP 서버와 통신이 되는지 확인할 때 쓴다.

claude --verbose -p "/review"

로그에서 [Hook], [MCP], [Agent] 태그를 보면 각 컴포넌트의 동작을 추적할 수 있다.

이 자동화 구조가 실무에 남긴 것

Claude Code 자동화 시스템 구축을 하고 나서 바뀐 건 단순히 시간 절약만이 아니다. 반복 작업을 시스템에 맡기니까 정작 중요한 설계와 아키텍처 결정에 집중할 여유가 생겼다. 개인적으로 가장 체감이 큰 건 코드 리뷰 자동화다. 기계적인 체크(린트, 타입, 보안 패턴)는 Claude가 하고, 나는 비즈니스 로직과 설계 리뷰에만 집중한다.

핵심만 정리하면 이렇다. 스킬과 훅만으로도 일상 업무의 상당 부분을 자동화할 수 있고, MCP와 배치 API는 외부 연동과 대량 처리가 필요할 때 붙이면 된다. 처음부터 풀스택 자동화를 목표로 잡지 말고, 가장 반복적인 작업 하나를 스킬로 만드는 것부터 시작하는 게 맞다.

다음 단계로 갈 거라면 Anthropic의 Agent SDK를 한번 살펴볼 만하다. 에이전트 간 통신이나 상태 관리를 좀 더 체계적으로 할 수 있다. 그리고 MCP 서버를 직접 만들기 귀찮으면 MCP 공식 서버 목록에서 이미 만들어진 걸 가져다 쓰는 것도 방법이다. Postgres, GitHub, Slack 등 주요 서비스용 서버가 이미 있다.

관련 글

이 글 공유하기