들어가며
24차시에서는 Skills의 개념과 호출, 25차시에서는 처음부터 SKILL.md를 쓰는 법을 다뤘다. 이번 차시는 그 연습이다. 빈 디렉토리에서 시작해 실제로 매일 쓰는 세 가지 스킬을 만든다.
- PR 리뷰 스킬 —
context: fork+agent: Explore+ 동적 컨텍스트로 현재 브랜치 PR을 격리된 서브에이전트에서 검토 - 테스트 생성 스킬 —
$ARGUMENTS로 파일을 받아 Validation loop로 통과까지 반복 - 문서 생성 스킬 — Plan-validate-execute 패턴과
assets/템플릿으로 API 문서 일괄 생성
각 스킬에 25차시에서 본 패턴이 어떻게 들어가는지를 같이 본다 — description 최적화, Gotchas, 출력 형식 템플릿, Validation loop, Procedures-not-declarations, Defaults-not-menus.
시작 전: 빌트인은 이미 있다
스킬을 짜기 전에 짚어야 할 것이 있다. 이미 비슷한 게 있는가? Claude Code는 빌트인 명령과 번들 스킬을 적지 않게 깔고 들어온다.
| 명령 | 종류 | 하는 일 |
|---|---|---|
/review [PR] | 빌트인 명령 | 현재 세션에서 PR을 로컬로 리뷰 |
/security-review | 빌트인 명령 | 현재 브랜치 변경의 보안 취약점 분석 |
/simplify [focus] | 번들 스킬 | 변경 파일을 코드 재사용/품질/효율 관점으로 검토 후 fix 적용 |
/init | 빌트인 명령 | CLAUDE.md 생성 |
/batch <instruction> | 번들 스킬 | 5–30개 unit으로 분해해 병렬 background agent로 |
(전체 목록은 Commands reference.)
그러면 왜 커스텀을 만드나. 빌트인은 generic이다. 우리 프로젝트의 컨벤션, 도메인 지식, 출력 형식이 들어가지 않는다. 같은 “리뷰”라도 우리 팀은 soft delete 빠뜨림을 항상 잡고 싶고, n+1 쿼리에 더 민감하고, 결과를 표 형식으로 받고 싶다 — 그런 게 들어가야 진짜 자기 스킬이다.
25차시 표현으로: 빌트인
/review는 “코드 리뷰의 일반적 절차”고, 우리 커스텀은 “이 프로젝트의 리뷰 절차”다. 에이전트가 모를 것(우리 팀의 gotchas, 출력 형식, 검토 우선순위)만 SKILL.md에 적는다.
세 스킬 모두 프로젝트 스코프(/.claude/skills/)에 둔다. 팀 전체가 git으로 공유받는 형태다.
스킬 1: PR 리뷰 (review-pr)
디자인 결정
| 결정 | 선택 | 이유 |
|---|---|---|
| 콘텐츠 종류 | Task content | 명시적 액션. 자동 호출도 막지 않음 (사용자가 “리뷰해줘”라고 자연스럽게 말함) |
| 격리 실행 | context: fork | PR diff가 길다. 격리 컨텍스트에서 처리하고 결과만 main에 |
| 서브에이전트 | agent: Explore | 코드 탐색에 최적화된 read-only 도구셋 |
| 도구 사전 승인 | allowed-tools: Bash(gh *) Bash(git *) | gh/git 명령을 매번 묻지 않음 |
| 동적 컨텍스트 | !`gh pr diff` 등 | 스킬 본문이 Claude에 도달하기 전 PR 데이터를 인라인 |
| description | ”review the current branch’s PR” + 트리거 phrase | 자동 호출도 자연스럽게 |
디렉토리
mkdir -p .claude/skills/review-pr
SKILL.md
.claude/skills/review-pr/SKILL.md에 저장.
---
description: >
Review the current branch's pull request for code quality, bugs,
performance, and project conventions. Use when the user asks to
review a PR, audit pending changes before merge, or check what's
about to ship — even if they don't say the word "PR."
context: fork
agent: Explore
allowed-tools: Bash(gh *) Bash(git diff *) Bash(git log *) Bash(git status *)
---
## PR 컨텍스트
- 변경: !`gh pr diff`
- 코멘트: !`gh pr view --comments`
- 변경 파일: !`gh pr diff --name-only`
- 최근 커밋: !`git log -10 --oneline`
## 검토 항목
1. **코드 품질** — 가독성, 중복, 명명, 함수 분리
2. **버그** — null 처리, off-by-one, 동시성, 예외 흐름
3. **성능** — N+1 쿼리, 불필요한 렌더, O(n²) 루프, 큰 응답
4. **보안** — SQL injection, 인증 누락, 비밀값 노출
5. **프로젝트 규칙** — `CLAUDE.md`, `.claude/rules/`와의 일치
## Gotchas (이 프로젝트 한정)
- `users` 테이블은 soft delete. 쿼리에 `WHERE deleted_at IS NULL`이 빠지면 비활성 계정이 결과에 섞여 들어간다. PR에 새 쿼리가 있으면 반드시 확인.
- 같은 사용자 ID가 곳곳에서 다른 이름으로 불린다 — DB는 `user_id`, auth 서비스는 `uid`, billing API는 `accountId`. PR에서 한 곳만 바꾸면 다른 곳이 깨진다.
- `/health`는 web server만 살아 있어도 200을 반환한다. DB까지 보려면 `/ready`. health check 코드 변경은 둘을 헷갈리지 않는지 확인.
- React 컴포넌트는 `function` 선언만 사용. `const X = () =>` 패턴은 PR에서 reject.
- 새 환경변수 추가 시 `.env.example`도 같이 업데이트했는지 확인.
## 출력 형식
먼저 한 줄 총평 (e.g., "전반적으로 양호. 한 곳 보안 이슈와 두 곳 컨벤션 위반").
그다음 표:
| 심각도 | 파일:라인 | 이슈 | 제안 |
|--------|-----------|------|------|
| 높음 | src/api/users.ts:42 | SQL string concat — injection 가능 | parameterized query |
| 중간 | src/components/Card.tsx:15 | const 화살표 컴포넌트 | function 선언으로 변경 |
| 낮음 | docs/api.md:80 | 오래된 endpoint 경로 | `/v2/users` 로 갱신 |
심각도는 **높음**(머지 차단), **중간**(머지 전 수정), **낮음**(추후 정리) 셋 중 하나.
이슈가 없으면 "이슈 없음"이라고만 적고 표는 생략.
동작 방식
!gh pr diff“ 같은 줄은 SKILL.md가 Claude의 컨텍스트에 들어가기 전에 shell이 실행되어 출력이 그 자리를 채운다. 즉 Claude는 “PR diff를 가져와라”가 아니라 실제 diff 텍스트를 받는다. 이게 동적 컨텍스트 주입이다.
context: fork는 이 모든 처리를 별도 서브에이전트에서 돌린다. 메인 대화 컨텍스트는 깨끗하게 유지되고, 결과 표만 다시 흘러 들어온다. 큰 PR에서 효과가 크다.
정책으로 막고 싶다면 settings에
"disableSkillShellExecution": true. 이 경우!줄이[shell command execution disabled by policy]로 치환된다.
실행
> /review-pr
또는 자동 호출 — 트리거 phrase가 description에 있다.
> 이 브랜치 머지해도 될까?
> 변경한 거 검토해줘
gh 인증이 안 되어 있거나 PR이 없으면 빈 diff가 들어가고, 출력은 자연스럽게 “현재 브랜치에 PR이 없다”가 나온다 — 본문에 그런 fallback을 적어두면 더 명시적이다.
25차시 패턴 적용 정리
- description 최적화 — “review”, “PR”, “audit”, “before merge”, “check what’s about to ship” 같은 phrase가 한 줄에. “even if they don’t say the word PR”은 사용자 발화 변형까지 흡수.
- Gotchas 섹션 — 일반 조언이 아니라 이 프로젝트가 자주 빠뜨리는 구체 항목들. 에이전트가 모르면 반드시 틀릴 것만.
- 출력 형식 템플릿 — 표 + 심각도 enum. 자유 서술이 아니라 구조 고정.
- 자유 vs 규정 보정 — 검토 항목은 5가지 카테고리만 (자유), Gotchas는 명시 항목 (규정), 출력 형식은 표 고정 (규정).
스킬 2: 테스트 생성 (generate-tests)
디자인 결정
| 결정 | 선택 | 이유 |
|---|---|---|
| 콘텐츠 종류 | Task content | 명시 액션. 사용자가 “이 파일 테스트 짜줘”로 호출 |
| 인수 | $ARGUMENTS (파일 경로) | 한 가지면 됨 |
| 자동완성 힌트 | argument-hint: [file-path] | /generate-tests 입력 시 힌트 |
| 격리 실행 | 인라인 | 결과를 main 컨텍스트에서 바로 검증 |
| 도구 사전 승인 | Read Write Bash(npm test*) Bash(npx vitest*) | 테스트 실행도 자동 |
| 패턴 | Validation loop | 테스트가 통과할 때까지 반복 |
| Procedures, not declarations | ”method” 작성 | 어떤 파일이든 동작하게 |
디렉토리
mkdir -p .claude/skills/generate-tests
SKILL.md
.claude/skills/generate-tests/SKILL.md.
---
description: >
Generate Vitest unit tests for a TypeScript or JavaScript file.
Covers all exported functions with happy-path, edge-case, and error
cases following our Given-When-Then convention. Runs the tests and
iterates until they pass.
argument-hint: [file-path]
allowed-tools: Read Write Bash(npm test*) Bash(npx vitest*) Glob
---
## 입력
대상 파일: `$ARGUMENTS`
`$ARGUMENTS`가 비었으면 사용자에게 파일 경로를 묻고 멈춘다.
## 작업 흐름
### 1. 분석
1. `$ARGUMENTS`를 Read
2. exported 심볼 추출 — `export function`, `export class`, `export const ... =`
3. 각 심볼의 시그니처와 동작 파악
4. 외부 의존성 확인 — fetch, DB, 다른 모듈. mock 대상 결정.
### 2. 케이스 설계
각 exported 함수/메서드에 대해 최소 다음 셋:
- **Happy path** — 의도된 정상 입력
- **Edge case** — 빈 입력, 경계값, 큰 입력
- **Error case** — invalid input, 외부 호출 실패
비동기 함수는 resolve와 reject 둘 다.
### 3. 작성
테스트 파일 위치: `<원본파일경로>/__tests__/<원본파일명>.test.ts`
(원본이 `src/utils/parse.ts`면 → `src/utils/__tests__/parse.test.ts`)
구조 (Given-When-Then):
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { parseInput } from '../parse';
describe('parseInput', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('valid input', () => {
it('should parse a well-formed string', () => {
// Given
const input = '...';
// When
const result = parseInput(input);
// Then
expect(result).toEqual(...);
});
});
describe('invalid input', () => {
it('should throw on empty string', () => {
// Given / When / Then
expect(() => parseInput('')).toThrow();
});
});
});
```
### 4. Validation loop
1. `npm test -- <테스트 파일 경로>` 실행
2. 통과 → 종료
3. 실패하면:
- 에러 메시지를 정확히 읽는다
- **테스트의 기대값**이 틀렸으면 — 테스트 수정
- **원본 코드**의 버그가 드러났으면 — 사용자에게 알리고 멈춤 (수정은 별도 작업)
- mock setup이 잘못됐으면 — `beforeEach`/`vi.fn()` 점검
4. 다시 실행, 모두 통과까지 반복
5회 반복해도 통과 못 하면 멈추고 결과를 사용자에게 보고.
## 컨벤션
- **이름**: `describe`는 모듈/클래스/함수명, `it`은 "should ..."로 시작
- **독립성**: 각 테스트는 다른 테스트에 의존하지 않는다 (`beforeEach`로 setup)
- **mock**: `vi.fn()`, `vi.spyOn()`. global mock은 피한다
- **비동기**: `await expect(...).resolves.toBe(...)` 또는 `.rejects.toThrow()`
- **시간**: `Date.now`나 `setTimeout`은 `vi.useFakeTimers()` 먼저
## Gotchas
- `__tests__/`는 src 밖이 아니라 **원본 파일 옆 폴더**. 잘못된 위치에 만들면 vitest config가 못 찾는다.
- React 컴포넌트면 `@testing-library/react`로 `render`, `screen`. enzyme은 사용 안 함.
- 외부 fetch는 `vi.stubGlobal('fetch', vi.fn())`로 mock. MSW는 통합 테스트용으로만 사용.
- 절대 import 경로는 `~/...` (vitest config의 alias). 상대 import도 가능하지만 alias 우선.
## 출력
마지막에 요약을 한 단락:
- 추가한 케이스 수
- 통과 / 실패 수
- 원본 코드에서 발견된 의심 지점 (있으면)
동작 방식
$ARGUMENTS는 호출 시 입력으로 치환된다.
> /generate-tests src/utils/parse.ts
→ SKILL.md 안의 모든 $ARGUMENTS가 src/utils/parse.ts로 치환된 채 Claude에게 전달.
allowed-tools에 Bash(npm test*)가 있어 테스트 실행이 매번 묻지 않고 진행된다. Validation loop가 자기 작업을 검증한다 — 작성하고, 돌리고, 실패면 고치고, 통과까지.
25차시 패턴 적용 정리
- Procedures, not declarations — “이 정확한 테스트를 작성하라”가 아니라 “1) Read → 2) 추출 → 3) 케이스 설계 → 4) 작성 → 5) 실행/수정”. 어느 파일에든 적용 가능한 method.
- Validation loop — 통과까지 반복, 5회 한도. 자기 검증.
- Defaults, not menus — 테스트 파일 위치를 정확히 한 패턴 (
__tests__/<원본명>.test.ts)으로 고정. enzyme이나 jest를 옵션으로 늘어놓지 않음. - Gotchas 섹션 — 위치 컨벤션, alias, React 컴포넌트 테스트 라이브러리 — 일반 LLM 지식으로는 못 잡는 것들만.
스킬 3: 문서 생성 (generate-docs)
디자인 결정
| 결정 | 선택 | 이유 |
|---|---|---|
| 콘텐츠 종류 | Task content | 명시 액션 |
| 인수 | $ARGUMENTS (디렉토리) | src/api/ 같은 경로 |
| 자동완성 힌트 | argument-hint: [source-dir] | |
assets/ 사용 | 출력 템플릿 분리 | SKILL.md를 짧게 유지, 템플릿이 길어져도 토큰 압박 안 받음 |
| 패턴 | Plan-validate-execute | 일괄 생성 전 사용자 확인 |
| 도구 사전 승인 | Read Write Glob Grep | 파일 탐색·작성만 |
디렉토리
mkdir -p .claude/skills/generate-docs/assets
SKILL.md
.claude/skills/generate-docs/SKILL.md.
---
description: >
Generate API reference documentation in markdown for a TypeScript
source directory. Extracts exported functions, classes, and types
with signatures, parameter descriptions, return values, and usage
examples from JSDoc. Use when the user wants API docs, reference
docs, or documentation for a module or directory.
argument-hint: [source-dir]
allowed-tools: Read Write Glob Grep
---
## 입력
대상 디렉토리: `$ARGUMENTS` (없으면 `src/`)
## 출력
`docs/api/` 아래에 모듈별 마크다운. 디렉토리 구조는 입력의 구조를 그대로 미러.
(예: `src/api/users.ts` → `docs/api/users.md`, `src/api/auth/login.ts` → `docs/api/auth/login.md`)
## 작업 흐름
### 1. Plan
1. `Glob $ARGUMENTS/**/*.ts`로 대상 파일 목록
2. 각 파일에서 `Grep "^export "`로 exported 심볼이 있는지 확인 (없는 파일은 제외)
3. 사용자에게 plan을 보여준다:
```
다음 18개 파일을 문서화할 예정.
- src/api/users.ts → docs/api/users.md (4개 함수)
- src/api/auth/login.ts → docs/api/auth/login.md (2개 함수, 1개 타입)
- ...
기존 docs/api/ 파일 중 3개가 덮어쓰기 됨:
- docs/api/users.md
- docs/api/auth/login.md
- docs/api/sessions.md
진행할까?
```
### 2. Validate
사용자 응답을 기다린다.
- "yes"/"go" → 3단계 진행
- "no"/"수정" → 어디를 바꿀지 물어보고 plan 갱신, 다시 Validate
- 그 외 → 명확화 요청
### 3. Execute
각 파일에 대해:
1. Read 원본
2. exported 심볼 추출 (함수, 클래스, type, interface, const)
3. 각 심볼의 JSDoc 코멘트 파싱
4. [`assets/api-template.md`](assets/api-template.md) 형식으로 마크다운 작성
5. 출력 경로에 Write
진행 상황을 한 줄씩 로그:
```
[3/18] docs/api/auth/login.md 작성 완료
```
마지막에 요약:
- 작성한 파일 수
- skip한 파일 수 (이유 — JSDoc 없음, exported 없음 등)
## 컨벤션
- 함수 시그니처는 코드 블록 안에 TypeScript 그대로
- 파라미터 설명은 JSDoc `@param`에서, 없으면 "(설명 없음)"
- 반환값은 `@returns`, 없으면 타입만
- `@example` 블록이 있으면 "Example" 섹션에 포함, 없으면 생략
## Gotchas
- `export default`도 잡는다. 이 경우 심볼 이름은 파일명 (PascalCase 변환).
- `export type` / `export interface`는 별도 "Types" 섹션.
- `export const X = () => ...` 화살표 함수도 함수로 취급.
- 내부용 `_` prefix 함수는 export여도 문서에서 제외.
- 빈 파일 (exported 없음)은 `docs/api/`에 만들지 않는다.
## 출력 형식
[`assets/api-template.md`](assets/api-template.md) 참조. 그 형식을 정확히 따른다.
assets/api-template.md
.claude/skills/generate-docs/assets/api-template.md.
# {모듈명}
{파일 상단 JSDoc 또는 한 줄 설명. 없으면 생략.}
## Functions
### `functionName(param1: T1, param2: T2): ReturnType`
{JSDoc summary}
**Parameters**
- `param1` (`T1`) — {설명}
- `param2` (`T2`) — {설명}
**Returns** `ReturnType` — {설명}
**Example**
```ts
{@example 블록 내용}
```
---
## Types
### `TypeName`
{JSDoc summary}
```ts
type TypeName = {
field1: string;
field2: number;
};
```
동작 방식
> /generate-docs src/api
- Plan — Glob으로 18개 파일 발견, Grep으로 exported 있는 파일 필터, 사용자에게 plan 제시
- Validate — 사용자가 “yes” → 진행
- Execute — 각 파일에 대해 템플릿대로 작성
3단계의 검증이 핵심이다 — 18개 파일을 한 번에 덮어쓰기 전에 plan을 보여 사용자가 잘못된 입력을 잡을 기회를 준다.
25차시 패턴 적용 정리
- Plan-validate-execute — 일괄 작업 + destructive (덮어쓰기)이라 plan 단계가 가치를 만든다.
- assets/ 분리 — 템플릿이 SKILL.md에 인라인되면 호출할 때마다 토큰을 쓴다.
assets/로 빼고 SKILL.md에서 명시적으로 가리키면, Claude는 필요한 순간에만 읽는다. - 출력 형식 템플릿 — prose 묘사가 아니라 실제 마크다운 구조를 템플릿으로. 에이전트가 패턴 매칭으로 따라 쓰기 쉽다.
세 스킬을 함께 사용하기
위 셋은 자연스럽게 한 워크플로로 엮인다.
# 1. 기능 작업 → 테스트 생성
> /generate-tests src/api/users.ts
# 2. 문서 갱신
> /generate-docs src/api
# 3. 커밋, push, PR 생성 (빌트인 /commit, /pr)
> /commit
> /pr
# 4. PR 리뷰
> /review-pr
각 스킬이 자기 영역만 다루고 깔끔하게 조합된다 — 25차시의 “한 coherent unit” 원칙이다. 한 스킬에 다 욱여넣으면 description이 모호해지고 에이전트가 어떤 부분을 활성화할지 헷갈린다.
디버깅 팁
스킬을 만들고 동작이 어색할 때 자주 보는 항목들이다.
자동 호출이 안 됨
- description에 사용자가 자연스럽게 쓸 phrase가 들어 있나 (
/skills에 노출되는 첫 1,536자에 있는지 확인) - description+when_to_use 합산 1,536자를 넘어 잘려나갔나
disable-model-invocation: true가 잘못 켜져 있지는 않나- 비슷한 빌트인이 먼저 트리거되지 않나 (이 경우 description을 더 specific하게)
너무 자주 호출됨
- description을 좁힌다 — 어느 상황에서만 쓰는지 명시
- 의도가 수동만이라면
disable-model-invocation: true
스킬이 동작 도중 효과가 사라진 것 같음
공식 문서에 명시된 동작이다 — Auto-compaction이 일어나면 스킬은 첫 5,000 토큰만 다시 붙는다. 큰 스킬을 여러 개 호출하면 오래된 것은 잘려 나간다. 다시 효과를 받으려면 그 스킬을 한 번 더 호출하면 풀 콘텐츠가 다시 attach된다.
! 명령이 실행 안 됨
- settings에
"disableSkillShellExecution": true가 켜져 있는지 확인 (보통 managed settings) - 명령 자체의 syntax 오류 —
!다음 백틱 안에 인용 부호가 깨져 있지 않은지
더 보기
/init이 인터랙티브 모드일 때 (환경변수 CLAUDE_CODE_NEW_INIT=1) 스킬 작성을 같이 walk-through해준다 — 빈 프로젝트에 처음 도구를 깔 때 보조 도우미로 유용하다.
anthropics/skills 레포에는 더 큰 스킬들이 공개되어 있다. 위에서 본 패턴이 실제 production 스킬에서 어떻게 쓰이는지 보고 싶으면:
webapp-testing— Playwright 헬퍼 스크립트와 decision tree, “Common Pitfall” 섹션이 잘 정리됨doc-coauthoring— 3단계 워크플로(Context Gathering → Refinement → Reader Testing) + 단계별 exit conditionskill-creator— 메타 스킬. 평가 루프와 description 자동 최적화
정리
핵심 요점
- 빌트인을 먼저 보고 자기 스킬 시작 —
/review,/security-review,/simplify,/init,/batch,/debug,/loop,/claude-api. 같은 작업이면 빌트인을 쓰고, 자기 프로젝트 컨벤션이 들어가야 할 때만 커스텀. - PR 리뷰 —
context: fork+agent: Explore+ 동적 컨텍스트 — 큰 PR diff를 격리에서 처리, 결과만 main에 옴 - 테스트 생성 —
$ARGUMENTS+ Validation loop — 작성/실행/수정 자동, 통과까지 - 문서 생성 — Plan-validate-execute +
assets/템플릿 — 일괄 destructive 작업 전 사용자 확인, 템플릿은 분리해 토큰 절약 - 세 스킬을 한 워크플로로 — 한 coherent unit 원칙. 작업 → 테스트 → 문서 → PR → 리뷰
- 공통 패턴 — description에 트리거 phrase, Gotchas 섹션은 이 프로젝트만의 비자명한 사실, 출력 형식 템플릿, Procedures-not-declarations, Defaults-not-menus
- 디버깅 — 자동 호출 안 되면 description 점검, compaction 후 효과 사라지면 재호출
다음 단계
이번 차시는 “스킬을 실제로 어떻게 쓰는가”였다. 27차시는 Subagent 스킬을 본격적으로 다룬다 — context: fork로 격리 실행하는 스킬, Explore/Plan/general-purpose 같은 빌트인 서브에이전트 타입, 그리고 .claude/agents/에 직접 만드는 커스텀 서브에이전트를 스킬과 어떻게 엮는지.