Skill·Plugin의 부품을 이해하고, 내가 실제로 만들어 쓰는 fe-plugin(fe-harness)까지.
개발을 하면서 비슷한 작업이 계속 반복돼요. 컴포넌트 만들고, API 붙이고, 폼 짜고, 폴더 구조 잡고… 패턴은 거의 같은데 매번 비슷한 작업을 합니다.
그래서 생각했죠. "이 반복 작업을 AI에게 넘겨서 자동화할 수 없을까?"
오늘은 그 도구를 만드는 부품인 Skill과 Plugin을 설명하고, 마지막에 제가 실제로 만들어 쓰는 fe-plugin(fe-harness)을 보여드립니다.
Claude Code를 쓰다 보면 같은 지시를 매번 반복하게 됩니다. 예를 들어 회의록 정리를 시킬 때마다:
"회의 내용 줄게. 참석자 / 안건 / 결정사항 / 액션아이템(담당자·기한)으로 정리해줘"
문제는 두 가지입니다:
이를 위해 Skill을 사용합니다. 이 반복 지시를 파일에 한 번 적어두고, /이름 한 단어로 다시 꺼내 쓰는 거죠.
자주 하는 요리의 레시피 카드를 만들어두는 것과 같습니다. 매번 재료와 순서를 떠올리는 대신, 카드 한 장 꺼내면 끝.
정리하면 Skill = AI 명령어 하나. /회의록, /커밋처럼 /이름으로 부르는 나만의 명령어입니다. 만드는 법은 딱 2가지:
~/.claude/skills/
└── 인사/ ← ① 폴더 이름 = /명령어 이름
└── SKILL.md ← ② 이 파일에 AI에게 시킬 내용을 적는다
SKILL.md 내용은 그냥 평범한 마크다운입니다:
오늘 날짜를 확인하고 이렇게 인사해줘:
"좋은 아침이에요! 오늘은 {날짜} {요일}입니다."
이게 전부입니다. 폴더 하나 + 파일 하나 = /인사 명령어 완성. 진짜 5분이면 됩니다.
Skill을 부르는 방법은 두 가지입니다. 수동(내가 /이름을 직접 친다) / 자동(Claude가 상황을 보고 알아서 부른다). 자동 호출의 열쇠는 frontmatter의 description 필드 — Claude가 이 설명을 읽고 "지금 이 Skill을 쓸 때인가?"를 판단합니다.
fe-principles (코드 짤 때 알아서 끼어든다)--- name: fe-principles description: > FE 코드 작성 시 rules + patterns를 로드한다. ... "컴포넌트 만들어줘", "API 연동", "코드 작성해줘", "구현해줘" 등. ---
→ "로그인 컴포넌트 만들어줘"라고만 해도, Claude가 description 키워드를 보고 이 Skill을 알아서 로드합니다.
harness (보통 직접 부른다, 인자를 받으니까)--- name: harness description: "FE 하네스 — '하네스 실행', '자동 구현' 등으로 트리거." argument-hint: <요구사항 또는 파일 경로> user-invocable: true allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, ... ---
→ /fe:harness 주문 목록 페이지처럼 인자를 받아 실행합니다. 제어 필드 둘만 알면 됩니다:
| 필드 | 효과 | 비유 |
|---|---|---|
disable-model-invocation: true | 자동 호출 끔 → 사용자만 /로 호출 | "묻기 전엔 나서지 마" |
user-invocable: false | 수동 호출 끔 → Claude만 자동 호출 | "무대 뒤에서만 일해" |
(아무것도 안 적으면 둘 다 됩니다.)
예전 자료엔 .claude/commands/에 파일을 넣는 Custom Command가 따로 있었습니다. "Skill이랑 뭐가 다르지?" 헷갈리셨을 텐데, 공식 문서가 정리해줍니다:
Custom commands have been merged into skills..claude/commands/deploy.md와.claude/skills/deploy/SKILL.md는 둘 다 똑같은/deploy를 만들고 동일하게 작동한다.
즉 Command는 죽은 게 아니라, Skill이 Command를 흡수한 상위호환입니다.
Command = 슬래시로 부르는 기능 (옛 이름) Skill = Command + 자동 호출 + 디렉토리 + 제어 필드 (지금)
그래서 제 fe-plugin도 commands/ 폴더 없이 전부 skills/로 만들었습니다.
Skill은 강력하지만 한 가지 약점이 있습니다. "무엇을 보라"는 기준은 주지만, "어떤 순서로, 누가" 할지는 강제하지 못합니다. 예를 들어 "코드 리뷰해줘"를 Skill로만 시키면:
"코드 리뷰해줘" → Skill이 기준은 로드함 (보안, 복잡도, 네이밍…) → 근데 어떤 순서로 볼지는 Claude 마음 → 대화가 길어지면 기준을 잊기도 함 → 결과 품질이 들쑥날쑥 ⚠️
혼자 다 하는 만능 비서에게 "알아서 잘해줘"라고 맡긴 셈이라, 매번 결과가 다릅니다.
Skill 혼자서는 "기준"은 줘도 "절차와 역할"을 강제하지 못합니다. 그래서 부품을 더합니다. 플러그인은 네 가지 부품으로 구성됩니다:
이 부품들을 하나로 묶어 배포하는 게 Plugin이에요.
Agent 도구(예전엔 Task라고 불렀습니다)는 새 Claude를 하나 더 띄우는 것입니다. 별도의 대화창이 열린다고 보면 돼요.
메인 Claude ──Agent()──→ [새 Claude 인스턴스] ──→ 결과만 반환
독립된 기억 공간(컨텍스트)
서로 뭘 하는지 모름
이 "새 Claude"에 전문 역할을 부여한 게 바로 Agent입니다.
한 Claude가 설계도 하고, 코드도 짜고, 자기가 짠 걸 자기가 평가까지 하면 두 가지 문제가 생깁니다:
self-preference bias — AI가 자기 출력을 선호하는 편향. 사람도 그렇죠)그래서 Agent입니다. 일을 쪼개 각 역할을 독립된 전문가 Claude에게 맡깁니다. 코드 짜는 Claude 따로, 평가하는 Claude 따로. 서로의 사고 과정을 모르니 평가가 객관적이죠. → 이 "역할을 나눠 독립 전문가에게 맡기는 것"이 Agent의 핵심입니다.
그럼 역할을 어떻게 지키게 할까요? "프롬프트로 부탁"이 아니라 "도구로 강제"합니다. "코드 고치지 마"라고 부탁하면 대화가 길어질 때 잊고 고쳐버려요. 그래서 아예 도구 자체를 빼버립니다. (이렇게 부탁이 아니라 도구·구조로 AI를 강제하는 장치들을 엮은 게 바로 3부의 하네스입니다.)
--- name: generator model: opus disallowedTools: Bash ---
Write/Edit는 있어 코드는 짜되, Bash(실행)는 막음. 직접 테스트 돌리지 말고 구현에만 집중. (주피터 편집 같은 미사용 도구도 함께 막지만 핵심만 표시)
--- name: evaluator model: opus disallowedTools: Write, Edit, Bash ---
Write, Edit를 막음. 평가자가 코드를 못 고치니 평가에만 집중할 수밖에 없습니다.
프롬프트로 "고치지 마" → 대화 길어지면 무시 가능 ❌ 도구에서 Write/Edit 제거 → 물리적으로 불가능 ✅
model: opus로 난이도에 맞는 모델도 지정)AI에게 "TS 파일 고치면 타입체크 꼭 해"라고 프롬프트로 한 번 말해두면? 역시 대화가 길어지면(컨텍스트가 많아지면) 잊습니다. 한 번 말한 규칙은 시간이 지나면 묻혀요.
그래서 Hook입니다. Hook은 특정 사건이 일어날 때마다 외부 코드(스크립트)를 자동 실행하는 장치예요. AI가 아니라 컴퓨터가 매번 끼어들어 챙기니까 절대 안 잊죠.
프롬프트에 한 번: "타입체크 해" → 대화 길어지면 잊음 ❌ Hook으로 매번: 파일 고칠 때마다 자동 실행 → 절대 안 잊음 ✅
Hook은 "언제(이벤트)"에 "무엇을(스크립트)" 실행할지 정해 거는 겁니다. 자주 쓰는 이벤트:
| 이벤트 | 언제 |
|---|---|
SessionStart | 세션 시작할 때 |
UserPromptSubmit | 내가 메시지를 보낼 때 |
PreToolUse | 도구 실행 직전 (여기서 막을 수 있음) |
PostToolUse | 도구 실행 직후 |
Stop | Claude가 답변을 끝냈을 때 |
여기서 "도구(tool)"는 Claude의 내장 도구를 말합니다 —Read·Write·Edit·Bash·Grep등. "Edit 직전에 끼어들기", "Bash 직후 검사하기" 식으로 겁니다.
동작은 단순합니다. 내가 hooks에 등록해 둔 스크립트가 stdin으로 정보(JSON)를 받아 결과를 돌려줍니다.
이벤트 발생 (예: Edit 도구 실행 직후)
↓
stdin 으로 JSON 전달 { "tool_name": "Edit", "tool_input": { "file_path": "a.ts" } }
↓
내가 등록한 스크립트 실행 (예: post-edit.sh)
↓
스크립트가 결과 2가지를 반환:
① exit code exit 0 = 통과 / exit 2 = 차단
② additionalContext Claude에게 추가로 넣어줄 한마디
이벤트 발생 → stdin으로 JSON이 옴 → (내가 hooks에 정의한) 스크립트가 그걸 받아 실행 → exit code와 additionalContext로 결과를 돌려줌.
Q. additionalContext란? 스크립트가 stdout으로 내보내는 출력 필드입니다. 여기 담긴 텍스트가 Claude의 다음 컨텍스트(입력)에 이어붙어, Claude가 그걸 읽고 행동에 반영해요. → 즉 작업을 막는(exit 2) 게 아니라, 다음 컨텍스트에 "참고로 이거 해"라고 정보를 얹어주는 통로입니다.
~/.claude/hooks/post-edit.sh)INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') # Edit/Write로 .ts/.tsx 파일을 고치면 if [ "$TOOL_NAME" = "Edit" ] || [ "$TOOL_NAME" = "Write" ]; then FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path') if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then echo "typecheck 권장: pnpm typecheck" >&2 # 알림 띄우기 fi fi
→ TS 파일을 고칠 때마다 "타입체크 권장" 알림이 자동으로 뜹니다. (PostToolUse 이벤트)
남이 만든 화려한 예시도 있습니다 (oh-my-claudecode). additionalContext는 크게 두 가지로 쓰입니다.
PreToolUse)// 도구 이름별로 다른 한마디를 준비 const messages = { Bash: "독립적 작업은 병렬로. 긴 작업은 백그라운드로", Edit: "수정 후 반드시 동작을 검증하라", Read: "여러 파일을 읽을 때는 병렬로 읽어라", }; console.log(JSON.stringify({ hookSpecificOutput: { additionalContext: messages[toolName], // ← 이게 Claude에게 주입됨 }, }));
→ Claude가 Bash를 쓰려 하면, 실행 직전에 "독립적 작업은 병렬로"라는 문장이 컨텍스트에 자동으로 끼어듭니다. 바로 이게 additionalContext예요.
UserPromptSubmit)// UserPromptSubmit hook — 내가 보낸 메시지(prompt)를 읽음
const prompt = readStdin();
if (prompt.includes("리뷰")) {
console.log(JSON.stringify({
hookSpecificOutput: {
additionalContext: "코드리뷰 워크플로우(/fe:review)를 실행하라",
},
}));
}
→ 내가 "리뷰해줘"라고만 치면 hook이 "리뷰" 키워드를 잡아 "코드리뷰 워크플로우를 실행하라"를 주입합니다. 나는 자연어로 편하게, 실행은 정해진 워크플로우로 정확하게 — 이걸 hook이 이어주는 거죠.
post-edit.sh는 제 개인 설정이고, 정작 fe-plugin(하네스) 자체는 hook을 안 씁니다. 같은 타입체크라도 하네스는 hook이 아니라 절차로 처리하거든요. 왜 그렇게 했는지는 3부에서 따져봅니다.지금까지의 부품을 정리하면:
| 부품 | 정의 |
|---|---|
| Skill | /이름으로 부르는 AI 명령어. 자동(description 매칭)·수동(/)으로 실행되며, 반복 지시·기준을 담는다 |
| Agent | 역할을 나눠 맡는 독립 전문가 Claude. 자기만의 컨텍스트를 갖고, 도구를 제한해 역할을 강제한다 |
| Hook | 특정 이벤트(도구 실행·세션 시작 등)에 자동 실행되는 외부 스크립트. Claude 동작을 막거나(차단) 메시지를 주입(교정)한다 |
| MCP | 외부 서비스(Slack·Notion·Calendar 등)를 Claude가 직접 읽고 쓰게 해주는 연동 방식 |
Plugin = 이 부품들을 하나로 묶어 배포하는 패키지입니다. 마켓플레이스에서 설치하면 그 안의 Skill/Agent/Hook이 한 번에 딸려옵니다.
참고로 모든 부품을 다 써야 하는 건 아닙니다. 제 fe-plugin은 이렇게 골라 썼어요:
| 부품 | fe-plugin에서 |
|---|---|
| Skill | O harness · fe-principles (자동화 진입점·코드 원칙) |
| Agent | O 3개 (planner · generator · evaluator) |
| Hook | X 안 씀 (절차로 대체) |
| MCP | X 안 씀 |
제가 실제로 만들어 쓰는 하네스(harness)입니다. Planner → Generator → Evaluator 단계를 따라가며, 요구사항 한 줄이 어떻게 자동으로 코드가 되는지 봅니다.
"로그인 페이지 만들어줘" 수준의 요구사항 하나로, 스펙 작성부터 구현·검증까지 자동으로 굴러갑니다.
구조는 단순합니다. 지휘자(Orchestrator)인 Skill 하나가 전문가(Agent) 셋에게 위임합니다.
| 구성 | 역할 |
|---|---|
| harness (Skill) | 지휘자(Orchestrator). 직접 코드 안 짜고 흐름만 제어 |
| planner (Agent) | 요구사항 → 스펙 + 작업 순서(Sprint) 분해 |
| generator (Agent) | 스펙대로 코드 구현 |
| evaluator (Agent) | 코드를 독립 평가 + 점수 |
핵심은 1·2부에서 본 원칙 그대로 — 지휘자(Skill)는 코드를 안 짜고, 실제 일은 도구가 제한된 전문가(Agent)들이 합니다.
지휘자인 harness는 코드를 한 줄도 안 짜고, Agent를 호출하며 흐름만 제어합니다 (1부 "Skill=지휘자" + 2부 "Agent=전문가"가 만나는 지점).
1 − Σ(명확도×가중), 목표40·제약30·성공30 → ≤ 0.2 통과. 초과(모호)면 2단계로 돌아가 다시 질문 ⟲| 단계 | 본다 | 가중 |
|---|---|---|
| A Contract | pass/fail | 60% |
| B 열린평가 | 컨벤션·복잡도·UX | 30% |
| C Contrarian | 약점 ≥ 1개 | 10% |
| D Contract 검토 | 기준 부실 점검 | —* |
* D의 "기준 자동 보강" 회로는 현재 미반영 — 개선 예정 (→ 3.8)
"AI는 판단을 못 한다"고 했죠. 그 판단의 첫 단추가 요구사항 해석입니다. "주문 목록 페이지 만들어줘" 한 마디엔 빈칸이 너무 많아요. planner는 이 빈칸을 5단계로 메웁니다 (위 그림 Phase 1 참고).
소크라테스식이란 답을 정해 주는 게 아니라, 되물어서 상대가 스스로 빈틈을 깨닫게 하는 방식입니다. planner는 "주문 목록 만들어줘"에 이렇게 되묻죠 — 기능 범위 / 데이터 / 권한 / 에러 / 데이터 구조. → 사용자가 미처 말 안 한(생각 못 한) 요구사항이 질문을 통해 드러납니다.
스펙이 충분히 명확한지 숫자로 판정합니다. 목표·제약·성공기준이 각각 얼마나 명확한지를 LLM이 0~1로 채점하고(판단), 가중 평균을 냅니다:
모호성 점수 = 1 − Σ(명확도ᵢ × 가중치ᵢ) 목표 명확도 40% · 제약 명확도 30% · 성공기준 명확도 30% 통과 조건: 모호성 점수 ≤ 0.2
→ "목표는 명확한데 성공 기준이 모호하다" 같은 진단으로, 모호한 채로 코드 짜는 걸 구조로 막습니다.
코드가 나오면 evaluator가 독립적으로(generator의 의도를 모른 채, 코드만 보고) 네 단계로 평가합니다.
| 단계 | 무엇을 보나 | 점수 반영 |
|---|---|---|
| A. Contract 기준 (닫힌 평가) | 합의 기준을 pass/fail로 검증. 하나라도 fail이면 미통과 | 60% |
| B. 열린 평가 (자유 판단) | 컨벤션 준수 · 불필요한 복잡도 · UX 기본 | 30% |
| C. Contrarian (반대 관점) | 일부러 약점을 찾는다 (최소 1개) | 10% |
| D. Contract 검토 | 기준 자체가 부실한지 점검 (누락·모호) | 미반영, 개선용 |
품질 점수 = ( Contract 통과율 × 0.6 ) + ( B × 0.3 ) + ( C × 0.1 ) × 10 · B = 이슈 없으면 1.0, 있으면 가장 심각한 1개의 값 · C = 약점 없으면 1.0, 있으면 가장 심각한 1개의 값
| 단계 | CRITICAL | 심각 | 중간 | 경미 |
|---|---|---|---|---|
| B. 열린 평가 | 0.5 (컨벤션 위반) | 0.5 | 0.7 | 0.85 |
| C. Contrarian | — | — | 0.6 | 0.8 |
Sprint 통과 = Contract 전부 pass AND 품질 점수 ≥ 9.0
CRITICAL 규칙: 컨벤션 위반 / any 사용 / 비즈니스 로직 누락 중 하나라도 있으면 무조건 9점 미만. 즉 "컨벤션 어기면 절대 통과 못 함."
여기가 2부에서 본 Generator ↔ Evaluator 분리가 빛나는 곳입니다. 짠 사람과 채점하는 사람이 다르고, 채점자는 Write/Edit도 없어서 — 자기선호편향(self-preference bias) 없이 코드만 냉정하게 봅니다.
9.0을 못 넘었다고 무한 반복할 순 없죠. 점수 추이로 다음 행동을 자동 결정합니다:
delta = 이번 점수 − 지난 점수 1. 이번 점수 < 5.0 → PIVOT (방향 자체가 틀림 → 사람 호출) 2. 라운드 ≥ 5 → HALT (재시도 한계 → 사람 호출) 3. delta ≥ 0.3 → RETRY (개선 중 → feedback 주고 재생성) 4. delta < 0.3 → 정체 +1 → 정체 2회면 HALT, 아니면 RETRY
정책: 9.0을 못 넘으면 자동 통과(ACCEPT)는 없습니다. 점수가 정체해도 기준을 못 넘었으면 사람이 확인합니다.
컨벤션 2층 (모든 Agent 프롬프트에 직접 박아 넣어, 누가 작업해도 같은 기준):
산출물 — 전 과정이 파일로 남아 추적 가능:
.ai/specs/{도메인}/{페이지}/
├── spec.md ← planner (스펙 + Sprint 분해)
├── sprint-{N}/
│ ├── contract.md ← 이번 Sprint 합의 기준
│ ├── eval-log-r{R}.md ← 라운드별 평가 기록
│ └── feedback-r{R}.md ← FAIL 시 generator에 전달
└── summary.md ← 전체 종합
주요 설정값: 품질 임계값 9.0 · 가중치 0.6 / 0.3 / 0.1 · Static Gate 재시도 3회 · 평가 재시도 5회 · 정체 허용 2회.
지금도 잘 돌지만, 더 다듬고 싶은 지점들이 있습니다:
다시 처음으로. AI 자동화는 재테크라고 했죠. fe-plugin은 제가 자는 동안에도 "요구사항 → 구현 → 평가 → 학습"을 굴리는 기계입니다.
단, "AI는 아직 판단을 못 한다"를 잊지 않았습니다. 그래서:
→ 부품(Skill·Agent·Hook)은 정해져 있지만, 그걸 어떻게 조합하고 발전시키느냐는 계속 진화합니다.