Skip to content

Claude Code 실전 스킬 예제 — PR 리뷰, 테스트 생성, 문서 생성

Published: at 05:12 PM

들어가며

24차시에서는 Skills의 개념과 호출, 25차시에서는 처음부터 SKILL.md를 쓰는 법을 다뤘다. 이번 차시는 그 연습이다. 빈 디렉토리에서 시작해 실제로 매일 쓰는 세 가지 스킬을 만든다.

각 스킬에 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: forkPR 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차시 패턴 적용 정리

스킬 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 안의 모든 $ARGUMENTSsrc/utils/parse.ts로 치환된 채 Claude에게 전달.

allowed-toolsBash(npm test*)가 있어 테스트 실행이 매번 묻지 않고 진행된다. Validation loop가 자기 작업을 검증한다 — 작성하고, 돌리고, 실패면 고치고, 통과까지.

25차시 패턴 적용 정리

스킬 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
  1. Plan — Glob으로 18개 파일 발견, Grep으로 exported 있는 파일 필터, 사용자에게 plan 제시
  2. Validate — 사용자가 “yes” → 진행
  3. Execute — 각 파일에 대해 템플릿대로 작성

3단계의 검증이 핵심이다 — 18개 파일을 한 번에 덮어쓰기 전에 plan을 보여 사용자가 잘못된 입력을 잡을 기회를 준다.

25차시 패턴 적용 정리

세 스킬을 함께 사용하기

위 셋은 자연스럽게 한 워크플로로 엮인다.

# 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이 모호해지고 에이전트가 어떤 부분을 활성화할지 헷갈린다.

디버깅 팁

스킬을 만들고 동작이 어색할 때 자주 보는 항목들이다.

자동 호출이 안 됨

너무 자주 호출됨

스킬이 동작 도중 효과가 사라진 것 같음

공식 문서에 명시된 동작이다 — Auto-compaction이 일어나면 스킬은 첫 5,000 토큰만 다시 붙는다. 큰 스킬을 여러 개 호출하면 오래된 것은 잘려 나간다. 다시 효과를 받으려면 그 스킬을 한 번 더 호출하면 풀 콘텐츠가 다시 attach된다.

! 명령이 실행 안 됨

더 보기

/init이 인터랙티브 모드일 때 (환경변수 CLAUDE_CODE_NEW_INIT=1) 스킬 작성을 같이 walk-through해준다 — 빈 프로젝트에 처음 도구를 깔 때 보조 도우미로 유용하다.

anthropics/skills 레포에는 더 큰 스킬들이 공개되어 있다. 위에서 본 패턴이 실제 production 스킬에서 어떻게 쓰이는지 보고 싶으면:

정리

핵심 요점

  1. 빌트인을 먼저 보고 자기 스킬 시작/review, /security-review, /simplify, /init, /batch, /debug, /loop, /claude-api. 같은 작업이면 빌트인을 쓰고, 자기 프로젝트 컨벤션이 들어가야 할 때만 커스텀.
  2. PR 리뷰 — context: fork + agent: Explore + 동적 컨텍스트 — 큰 PR diff를 격리에서 처리, 결과만 main에 옴
  3. 테스트 생성 — $ARGUMENTS + Validation loop — 작성/실행/수정 자동, 통과까지
  4. 문서 생성 — Plan-validate-execute + assets/ 템플릿 — 일괄 destructive 작업 전 사용자 확인, 템플릿은 분리해 토큰 절약
  5. 세 스킬을 한 워크플로로 — 한 coherent unit 원칙. 작업 → 테스트 → 문서 → PR → 리뷰
  6. 공통 패턴 — description에 트리거 phrase, Gotchas 섹션은 이 프로젝트만의 비자명한 사실, 출력 형식 템플릿, Procedures-not-declarations, Defaults-not-menus
  7. 디버깅 — 자동 호출 안 되면 description 점검, compaction 후 효과 사라지면 재호출

다음 단계

이번 차시는 “스킬을 실제로 어떻게 쓰는가”였다. 27차시는 Subagent 스킬을 본격적으로 다룬다 — context: fork로 격리 실행하는 스킬, Explore/Plan/general-purpose 같은 빌트인 서브에이전트 타입, 그리고 .claude/agents/에 직접 만드는 커스텀 서브에이전트를 스킬과 어떻게 엮는지.

참고 자료


Next Post
Claude Code 커스텀 스킬 작성