윤오영의 수필 「방망이 깎던 노인」에는 다듬이 방망이 하나를 사면서 애가 타는 손님이 나온다.1 차 시간은 다가오는데 노인은 대충 깎아줄 생각이 없다. 다 깎은 것 같은데도 이리 돌려 보고 저리 돌려 보며 굼뜨게 손을 놀린다. 실랑이 끝에 방망이를 받아 든 손님은 투덜거리며 떠나지만, 집에 와서야 알게 된다. 그렇게 깎은 방망이가 다르다는 것을.
요즘 개발자들 사이에도 뭔가를 깎는 사람들이 있다. 코딩 에이전트에 물려줄 스킬을 깎는 사람들이다. 그런데 방망이 가게 손님과 같은 질문이 나올 법하다. 마켓플레이스에 잘 깎인 스킬이 쌓여 있는데, 굳이 손수 깎아야 하는가? 스타를 몇만 개씩 받은 스킬을 설치하면 끝나는 일 아닌가?
이 글은 그 질문에 답해보려는 시도다. 왜 남의 스킬만으로는 부족한지, 자기 스킬은 어떻게 깎는지, 그리고 잘 깎였는지는 어떻게 재는지까지 다룬다. 스킬이 처음인 사람도 따라올 수 있게 바닥부터 시작한다.
스킬이라는 물건
먼저 용어부터 정리하자. 코딩 에이전트Coding Agent는 Claude Code, Codex CLI, Cursor처럼 LLM이 직접 코드를 읽고, 고치고, 명령을 실행하며 개발 작업을 수행하는 도구를 말한다. 예전의 코드 자동완성과 달리 에이전트는 "이 버그 고쳐줘" 같은 요청을 받아 파일을 뒤지고 테스트를 돌리는 일련의 작업을 스스로 해낸다.
에이전트를 쓰다 보면 누구나 같은 벽에 부딪힌다. 분명 지난주에 말했는데 또 말해야 한다. "커밋 메시지는 한국어로 써줘", "에러 응답은 우리 포맷으로 맞춰줘", "마이그레이션 파일은 절대 수정하지 말고 새로 만들어줘". 에이전트는 세션이 끝나면 대화를 잊는다. 그래서 매번 같은 지시를 복사해 붙여넣게 되는데, 이 반복되는 지시를 파일로 만들어 에이전트가 필요할 때 꺼내 읽게 한 것이 스킬Skill이다.
실체는 허무할 만큼 단순하다. SKILL.md라는 이름의 마크다운 파일 하나다.
---
name: api-error-convention
description: API 엔드포인트를 새로 만들거나 에러 응답을 수정할 때 사용한다.
---
이 저장소의 API 에러 응답은 항상 다음 형태를 따른다.
```json
{ "error": { "code": "ORDER_NOT_FOUND", "message": "...", "details": [] } }
```
- code는 대문자 스네이크 케이스로 쓰고 도메인 접두사를 붙인다 (ORDER_, COUPON_)
- message는 최종 사용자가 아니라 클라이언트 개발자를 위한 문장이다
- HTTP 상태 코드만으로 구분하지 말고 code를 반드시 채운다위쪽 --- 사이의 영역을 프런트매터frontmatter라고 부르는데, 스킬의 이름과 "언제 쓰는 물건인지"를 적는 메타데이터다. 그 아래 본문에는 에이전트가 따라야 할 지시를 적는다. 이 파일을 정해진 디렉터리(Claude Code라면 ~/.claude/skills/ 또는 프로젝트의 .claude/skills/)에 넣어두면 끝이다. 이 형식은 Agent Skills라는 오픈 표준으로 정리되어 있어서, 한 번 써둔 스킬이 여러 에이전트에서 통용된다.2
미리 적어두고 싶은 것이 하나 있다. 이 파일을 읽는 주체는 마크다운 파서가 아니라 LLM이다. 스킬은 문서의 모양을 하고 있지만 실체는 프롬프트다. 지금은 당연한 말로 들리겠지만, 이 사실이 뒤에서 스킬을 재는 법을 통째로 바꿔놓는다.
스킬은 두 번 읽힌다
스킬이 프롬프트 복붙이나 CLAUDE.md 같은 상시 지침 파일과 무엇이 다른지는 로딩 방식에 있다. 에이전트는 세션을 시작할 때 설치된 모든 스킬의 이름과 description만 목차처럼 들고 있는다. 본문은 이 목차와 들어맞는 요청이 왔을 때만 컨텍스트로 로드된다. 이 방식을 점진적 공개Progressive Disclosure라고 부른다.
이 구조가 중요한 이유는 LLM의 컨텍스트 창(모델이 한 번에 읽을 수 있는 텍스트의 총량)이 유한하고, 유료이기 때문이다. CLAUDE.md에 적은 내용은 모든 요청에 끼어들어 매번 토큰을 소비하지만, 스킬 본문은 필요한 순간에만 값을 치른다. 그래서 상시 지침 파일에는 짧은 사실만 남기고, 절차나 긴 참고 자료는 스킬로 빼는 것이 정석이다.3
이 두 단계 구조에서 실용적인 결론이 하나 나온다. description은 항상 내는 비용이고, 본문은 쓸 때만 내는 비용이다. 그러니 description은 한두 문장으로 벼리고, 본문은 필요한 만큼 쓰되 무한정 길어지면 안 된다. 이 감각은 뒤에서 스킬을 직접 깎을 때 다시 쓴다.
잘 깎인 방망이를 두고 왜 직접 깎는가
이제 첫 질문으로 가자. 답은 두 겹이다. 하나는 금방 끝나고, 다른 하나가 이 장의 진짜 주제다.
마켓플레이스에는 우리 팀이 없다
금방 끝나는 쪽부터. 스킬에 담기는 내용의 상당수는 사상이 아니라 사실이다. 앞에서 본 api-error-convention 스킬이 그렇다. 에러 code에 도메인 접두사를 붙인다는 규칙은 소프트웨어 공학의 원리가 아니라, 언젠가 클라이언트 개발자들이 에러 코드만 보고는 어느 도메인의 문제인지 몰라 헤맸던 이 팀의 역사가 남긴 규칙이다.
워크플로우로 가면 간극은 더 벌어진다. 금요일에는 배포하지 않는 팀이 있고 커밋마다 자동 배포하는 팀이 있다. 마이그레이션 한 번에 DBA 승인이 필요한 회사가 있고 개발자가 알아서 미는 회사가 있다. 커밋 제목에 티켓 번호를 박아야 하는 조직도, 릴리스 노트를 고객의 언어로 다시 쓰는 조직도 있다. 같은 언어와 같은 프레임워크를 쓰더라도 일하는 절차는 회사마다 다르고, 같은 회사의 옆 팀과도 다르다. 겪어온 장애가 다르고, 규모가 다르고, 받는 규제가 다르기 때문이다.
에이전트에게 이것은 능력의 문제가 아니라 정보의 문제다. LLM의 학습 데이터는 공개된 세계다. 우리 팀의 배포 체크리스트와 장애 대응 절차와 사내 CLI 사용법은 거기에 없다. 아무리 똑똑한 모델도 모르는 절차를 맞힐 수는 없고, 아무리 잘 깎인 마켓플레이스 스킬도 남의 회사 사정까지 담을 수는 없다. 그러니 이런 스킬은 품질을 따질 단계 이전에 존재의 문제다. 우리가 깎지 않으면 세상에 없다.
세상에서 가장 유명한 방망이
여기까지는 논쟁의 여지가 없어서 시시하다. 흥미로운 것은 나머지 절반이다. 커밋 메시지 쓰는 법이나 과잉 설계를 피하는 법처럼 어느 회사에서나 통하는 범용 영역에는 잘 깎인 기성품이 정말로 쌓여 있다. 그것으로 충분하지 않은가? 대표적인 예로 ponytail이라는 프로젝트를 보자. 스타 7만 개를 넘긴, 아마 세상에서 가장 유명한 에이전트 스킬이다.
ponytail의 사상은 첫 문장에 압축되어 있다.
You are a lazy senior developer. Lazy means efficient, not careless.
게으른 시니어 개발자가 되어라. 최고의 코드는 작성하지 않은 코드이니, 코드를 만들기 전에 이 사다리를 위에서부터 밟아 내려가라.
- 애초에 이게 필요한가? 필요 없으면 만들지 마라 (YAGNI)
- 코드베이스에 이미 있는가? 재사용해라
- 표준 라이브러리가 해주는가? 써라
- 플랫폼 네이티브 기능이 있는가? 써라
- 이미 설치된 의존성이 해결하는가? 써라
- 한 줄로 되는가? 한 줄로 써라
- 그제서야, 동작하는 최소한의 코드를 써라
효과도 실측되어 있다. 실제 FastAPI + React 프로젝트의 기능 작업 12개로 측정한 결과, 생성되는 코드가 평균 54% 줄었고 토큰은 22%, 비용은 20% 줄었다. 검증·보안·접근성 관련 코드는 건드리지 않았는데도 그렇다. 훌륭한 물건이다. 설치하면 오늘부터 에이전트가 과잉 설계를 덜 한다.
그런데 이 사다리를 가만히 들여다보면 물음이 생긴다. 6번, "한 줄로 되는가? 한 줄로 써라." 정말인가?
사상은 충돌한다
결제 정산 모듈을 관리하는 개발자를 떠올려보자. 이 사람에게 한 줄짜리 영리한 코드는 미덕이 아니다. reduce와 삼항 연산자를 겹쳐 한 줄로 눌러 담은 수수료 계산식보다, 중간 변수에 이름을 붙여 네 줄로 풀어 쓴 코드가 낫다고 믿는다. 새벽 두 시에 정산 금액이 안 맞는다는 전화를 받고 그 코드를 열어본 경험이 그렇게 만들었다. 이 사람의 사다리에서는 "읽는 사람이 멈칫하는가?"가 "한 줄로 되는가?"보다 위에 있다.
반대편에는 ponytail의 저자 같은 사람이 있다. 코드가 짧을수록 버그가 숨을 자리도 줄어든다고 믿고, 실제로 그 믿음으로 코드 절반을 지워본 사람이다. 어느 쪽이 옳은가?
둘 다 옳다는 것이 곤란한 진실이다. 소프트웨어 설계에서 수학의 공리처럼 반박 불가능하게 옳은 명제는 거의 없다. 대부분의 선택은 트레이드오프이고, 그 결과는 미리 실측할 수 없다. 지금 만든 추상화가 3개월 뒤 요구사항 변경에서 이득일지 족쇄일지는 3개월이 지나야 안다. 그 실측 불가능한 간극을 메우는 것이 각자의 직관이고, 직관은 결국 축적된 경험의 압축이다. 겪어온 장애가 다르고 만들어온 제품이 다르니 사람마다 직관이 다른 것은 당연하다. 코드 리뷰에서 벌어지는 그 지난한 논쟁들, DRY냐 명시성이냐, 이른 추상화냐 중복 허용이냐가 수십 년째 결론 나지 않는 이유이기도 하다. 그리고 그 충돌이 나쁜 것도 아니다. 서로 다른 직관이 부딪히고 검증되면서 분야 전체가 조금씩 나아간다.
에이전트는 직관으로 움직인다
이 이야기가 스킬과 무슨 상관인가. LLM의 동작 원리를 한 꺼풀 벗겨보면 연결된다.
LLM은 방대한 텍스트에서 학습한 패턴으로 "다음에 올 그럴듯한 토큰"을 확률적으로 골라내는 기계다. 사람으로 치면 숙고 없이 즉각 반응하는 시스템 1, 즉 직관에 가깝게 동작한다.4 에이전트가 코드를 쓸 때 가능한 설계안을 전부 나열하고 트레이드오프를 하나하나 저울질하는 일은 일어나지 않는다. 학습 데이터에서 비슷한 상황에 가장 흔했던 패턴, 말하자면 인터넷의 평균 직관을 따른다.
평균은 나쁘지 않다. 대부분의 상황에서 무난하다. 문제는 평균이 누구의 것도 아니라는 점이다. 결제 정산 개발자의 "풀어 써라"도, ponytail의 "지워라"도 아닌 어중간한 지점. 에이전트가 만든 코드를 보며 "틀리진 않았는데 우리 같지 않다"고 느꼈다면 정확히 이 간극을 느낀 것이다.
여기서 스킬의 본질이 나온다. 스킬은 에이전트의 평균 직관 위에 자신의 직관을 덮어쓰는 패치다. 앞에서 본 워크플로우 스킬이 에이전트가 모르는 사실을 채우는 패치라면, 이쪽은 이미 아는 판단을 교정하는 패치다. ponytail을 설치하는 것은 ponytail 저자의 직관을 이식받는 것이다. 그 직관이 자신과 맞으면 좋은 거래다. 하지만 어딘가는 반드시 어긋난다. 그 어긋남이 사소하면 그냥 쓰면 되고, 매일 거슬리면 그때가 직접 깎을 때다. 남의 방망이로 다듬이질을 못 하는 것은 아니지만, 손에 맞는 방망이는 결국 손에 맞춰 깎은 방망이다.
덤도 있다. 직관을 언어로 옮기다 보면 자신이 무엇을 중요하게 여기는지 처음으로 명시적으로 마주하게 된다. 스킬을 깎는 일은 에이전트 설정 작업이면서 동시에 자기 기준의 문서화다. 뒤에서 팀 스킬의 PR 리뷰를 다룰 때 이 부산물이 다시 등장한다.
직관을 언어로 깎는 법
그래서 깎기로 했다고 하자. 에디터를 열고 빈 SKILL.md를 마주하면 곧바로 난관이 온다. 분명히 아는데 쓸 수가 없다.
이것은 능력 부족이 아니라 직관의 본래 성질이다. 철학자 마이클 폴라니는 "우리는 말할 수 있는 것보다 많이 안다"는 말로 이를 정리했다.5 자전거를 탈 줄 알아도 균형 잡는 법을 말로 설명하지 못하는 것처럼, 코드를 보고 0.5초 만에 "이건 아닌데"를 느끼는 사람도 그 판단의 근거를 즉석에서 늘어놓지는 못한다. 직관은 불현듯 떠오르는 것처럼 보이지만 실제로는 축적된 지식과 경험이 보이지 않는 절차를 거쳐 나온 출력이다. 중요한 것은, 절차가 안 보일 뿐 절차가 있다는 사실이다. 절차가 있다면 구조화할 수 있고, 구조화할 수 있다면 언어로 옮길 수 있다. 스킬 깎기의 핵심은 결국 이 보이지 않는 절차를 꺼내 말로 박제하는 일이다.
정답이 있는 작업은 아니지만, 해볼 만한 순서는 제시할 수 있다.
재료 모으기: 직관의 화석을 찾아라
직관을 정면으로 마주 보고 "너는 무엇이냐"고 물으면 답이 안 나온다. 대신 직관이 지나간 자리에 남은 흔적을 모으는 편이 빠르다. 세 군데를 뒤지면 된다.
첫째, 코드 리뷰에서 반복해서 남긴 코멘트. 최근 리뷰 이력을 훑어보면 놀랄 만큼 같은 말을 하고 있다. "이 매직 넘버는 상수로 빼주세요", "실패 케이스 로그에 주문 ID를 남겨주세요". 세 번 이상 쓴 코멘트는 전부 스킬 후보다.
둘째, 에이전트의 출력에서 반복해서 고친 부분. 에이전트가 만든 PR을 머지하기 전에 손댄 곳들을 관찰하자. 커밋 메시지를 매번 다시 쓰고 있다면, 테스트 이름을 매번 한국어로 고치고 있다면, 그것이 평균 직관과 자기 직관이 어긋나는 지점의 목록이다.
셋째, "이건 아니지" 싶었던 순간에 왜냐고 되묻기. 리뷰하다 멈칫한 코드를 붙잡고 왜 거슬리는지 다섯 번쯤 파고들면 바닥에서 기준이 나온다. "N+1 쿼리라서" → "왜 문제인가" → "주문 목록은 트래픽이 몰리는 화면이라서" → 아, 화면의 트래픽 등급에 따라 허용하는 쿼리 패턴이 다르다는 기준을 갖고 있었구나. 이 바닥의 문장이 스킬의 재료다.
뼈대 세우기: 다섯 부위
재료가 모였으면 구조에 부어야 한다. 잘 깎인 스킬들을 해부해보면 대체로 다섯 부위로 이루어져 있다. 앞서 본 ponytail이 좋은 표본이니 옆에 두고 보자.
---
name: <스킬 이름>
description: <무엇을 할 때 쓰는가. 트리거가 될 상황을 구체적으로>
---
# 정체성 — 한 줄
너는 ~다. (이 스킬이 세상을 보는 관점)
# 판단 사다리 — 충돌이 났을 때 무엇이 이기는가
1. 가장 먼저 검사할 질문 → 결론
2. 다음 질문 → 결론
...
# 규칙 — "하지 마라" 형태로, 짧게
- 규칙마다 어길 때의 모습을 한 줄씩
# 예시 — 좋은 출력 하나, 나쁜 출력 하나
# 예외 — 이 스킬을 따르면 안 되는 상황정체성은 한 줄이면 된다. ponytail의 "너는 게으른 시니어 개발자다"가 전형이다. LLM은 부여받은 역할에 따라 출력 분포가 크게 달라지므로, 세부 규칙이 커버하지 못하는 회색 지대에서 이 한 줄이 방향을 잡아준다. 규칙은 유한하지만 상황은 무한하기 때문에, 규칙 사이의 빈틈은 결국 관점이 메운다.
판단 사다리가 심장이다. 직관의 본질은 지식의 목록이 아니라 우선순위이기 때문이다. "버그도 고치고 리팩터링도 한 커밋의 타입은?"처럼 가치가 충돌하는 상황에서 무엇이 이기는지가 곧 그 사람의 직관이다. ponytail의 7단 사다리도, 결제 정산 개발자의 "읽는 사람이 멈칫하는가가 한 줄로 되는가보다 위"도 전부 사다리다. 순서 없는 규칙 목록은 충돌 앞에서 침묵하지만 사다리는 답을 낸다.
규칙은 부정형이 힘이 세다. "깔끔하게 작성하라"는 판단 기준이 아니라 기분이다. "제목에 파일명을 나열하지 마라"처럼 어겼는지 아닌지 판별 가능한 문장으로 써야 한다. 형용사가 들어간 규칙은 대부분 덜 깎인 규칙이다.
예시는 규칙 열 줄보다 잘 동작할 때가 많다. LLM은 패턴 기계라서 좋은 출력의 실물 하나가 강력한 앵커가 된다. 나쁜 예시에는 왜 나쁜지 한 줄을 붙인다.
예외는 직관의 성숙도다. 어떤 직관이든 자기가 통하지 않는 영역을 안다. ponytail도 "When NOT to be lazy"라는 절에서 신뢰 경계의 입력 검증, 데이터 유실을 막는 에러 처리, 보안, 접근성은 게으름의 대상이 아니라고 못 박는다. 예외 없는 스킬은 예외 상황에서 이상한 고집을 부리는 에이전트를 만든다.
이 틀에 맞춰 실제로 하나 깎아보자. 에이전트가 쓴 커밋 메시지를 매번 고쳐 쓰는 것이 지겨워졌다고 하자. 고치는 손을 관찰해보니 형식보다 타입 판정이 문제였다. 버그 수정에 리팩터링이 섞인 diff를 에이전트는 자꾸 refactor로 분류하는데, 자신의 기준으로 그것은 fix다. 이 직관을 사다리로 박제하면 이렇게 된다.
---
name: commit-message
description: 커밋 메시지를 작성하거나 다듬을 때 사용한다.
diff를 보고 커밋 메시지를 만들어달라는 요청이면 이 스킬을 따른다.
---
너는 이 저장소의 커밋 메시지를 관리한다. 아래 형식과 판정 순서를 따른다.
## 형식
- 제목: `<타입>: <요약>` — 전체 50자 이내, 한국어, 마침표 금지
- 제목과 본문 사이에 빈 줄 하나
- 본문: 1~3문장. 무엇을 바꿨는지가 아니라 왜 바꿨는지를 쓴다
## 타입 판정 사다리
위에서부터 검사해서 처음 걸리는 것 하나만 쓴다.
diff에 여러 성격이 섞여 있어도 타입은 하나다.
1. 사용자가 겪던 잘못된 동작이 사라지는가? → fix
2. 사용자에게 보이는 동작이 새로 생기는가? → feat
3. 측정 가능한 성능 개선이 목적인가? → perf
4. 동작은 같고 코드 구조만 바뀌는가? → refactor
5. 테스트만 바뀌는가? → test
6. 문서만 바뀌는가? → docs
7. 어디에도 해당하지 않는가? → chore
## 규칙
- 제목에 파일명이나 함수명을 나열하지 않는다
- 본문에서 diff 내용을 다시 서술하지 않는다. diff는 코드가 이미 말해준다
- 버그 수정에 리팩터링이 섞여 있어도 타입은 fix다. 사다리에서 먼저 걸리기 때문이다
- 기능에 테스트가 딸려 있어도 타입은 feat다. 같은 이유다
## 예시
fix: 재고 0인 상품이 장바구니에 담기던 문제 수정
품절 검사가 캐시된 재고를 보고 있어 실제 재고와 어긋났다.
주문 시점에 재고를 다시 확인하도록 바꿨다.버그 수정과 함수 추출 리팩터링이 섞인 diff에 이 사다리를 적용하면 판정은 이렇게 진행된다.
description 깎기
본문을 다 깎아도 description이 무디면 스킬은 발동하지 않는다. 앞서 본 것처럼 에이전트는 description만 보고 이 스킬을 꺼낼지 결정하기 때문이다.
요령은 하나다. 무엇을 하는 스킬인지보다 언제 쓰는 스킬인지를 써라. 에이전트는 사용자의 요청 문장과 description을 대조한다. 그러니 사용자가 실제로 칠 법한 말이 들어 있어야 한다.
# 무딘 description — 스킬의 내용을 요약했다
description: 커밋 메시지 컨벤션
# 벼린 description — 발동할 상황을 적었다
description: 커밋 메시지를 작성하거나 다듬을 때 사용한다.
diff를 보고 커밋 메시지를 만들어달라는 요청이면 이 스킬을 따른다.이렇게 깎으면 망한다
거꾸로 자주 보이는 실패 유형도 짚어두자.
가장 흔한 것은 백과사전형이다. 아는 것을 전부 담아 본문이 수백 줄이 된다. 본문은 발동하면 통째로 컨텍스트에 들어가 이후 대화 내내 자리를 차지한다. 게다가 지시가 많아질수록 개별 지시의 준수율은 떨어진다. 긴 참고 자료는 별도 파일로 빼서 링크만 걸고, 본문은 판단에 필요한 것만 남기자.6
에세이형도 자주 보인다. 규칙마다 배경과 역사와 심정을 서술하는 유형이다. 사람에게는 "왜"가 설득의 핵심이지만, 스킬 본문에서의 긴 "왜"는 매번 지불하는 토큰 비용이다. 왜는 한 줄로 족하다.
만능칼형은 커밋 메시지, 리뷰 톤, 테스트 작성법을 한 스킬에 담는다. 발동 조건이 흐려져 엉뚱한 때에 로드되고, 필요한 순간에 안 로드된다. 스킬 하나에 관심사 하나가 원칙이다.
가장 교묘한 것은 형용사형이다. "적절히", "깔끔하게", "충분히"로 채워진 스킬인데, 에이전트의 "적절히"는 평균 직관의 적절히다. 그 평균을 고치려고 스킬을 깎는데 스킬이 다시 평균에 판단을 위임하면 아무것도 바뀌지 않는다.
깎았으면 재봐야 한다
스킬을 설치하고 며칠 쓰다 보면 에이전트가 확실히 나아진 것 같은 기분이 든다. 이 기분은 믿을 것이 못 된다. 스킬이 발동하는 장면을 봤다는 것과 스킬이 출력을 좋게 만들었다는 것은 다른 명제다. 재봐야 안다.
다만 재는 법을 단위 테스트에서 그대로 가져오면 곤란하다. 스킬은 코드가 아니기 때문이다.
재질이 다르면 재는 법도 다르다
함수 테스트에는 암묵적인 믿음이 두 개 깔려 있다. 잘 고른 입력 몇 개가 전체를 대변한다는 믿음, 그리고 같은 입력에는 언제나 같은 출력이 나온다는 믿음이다. 이 둘 위에서 assert add(2, 3) == 5 한 줄이 성립한다. 한 번 통과한 테스트는 코드가 바뀌기 전까지 계속 통과하고, 실패에는 원인이 있으며, 원인을 고치면 실패는 재발하지 않는다.
스킬은 이 두 믿음이 모두 깨진 세계에 산다. 앞서 말했듯 스킬의 실체는 프롬프트, 확률 기계에게 먹이는 자연어 지시문이기 때문이다.
먼저 입력이 비정형이다. 함수 테스트도 사실 전수 검사는 아니다. add에 모든 정수 쌍을 넣어보는 사람은 없다. 그런데도 표본 두어 개로 충분한 것은 코드에 동치류equivalence class와 경계값이 있어서다. 2+3이 맞으면 7+4도 맞으리라고 믿을 근거가 코드의 구조에 있다. 스킬이 만나는 입력은 다르다. "커밋 메시지 좀 써줘"부터 "방금 고친 거 정리해서 올려줘"까지 무한히 변주되는 자연어 요청과, 어떻게 생겼을지 모를 diff에는 그런 동치류가 없다. 두 요청이 같은 케이스인지 다른 케이스인지 미리 알 길이 없다. 그래서 검증은 대표성이 보장되지 않는 표본 조사가 되고, 어떤 입력을 표본으로 고르느냐가 검증의 품질을 좌우한다.
다음으로 출력이 비결정론적이다. LLM은 다음 토큰을 확률 분포에서 추첨한다. 같은 스킬에 같은 diff를 넣어도 돌릴 때마다 다른 메시지가 나온다.7 그러니 한 번 돌려서 잘 나왔다는 것은 증명이 아니라 표본 하나다. 동전을 한 번 던져 앞면을 보고 "앞면만 나오는 동전"이라고 결론 내릴 수 없는 것과 같다.
이 두 성질이 검증의 문법을 바꾼다. 실행은 반복해야 하고, 결과는 통과/실패의 이진값이 아니라 통과율로 읽어야 하며, assert는 "5회 중 4회 이상" 같은 임계값이 된다.
개선의 문법도 바뀐다. 버그는 원인을 찾아 고치면 그 자리에서 사라지지만, 스킬의 문장을 고치는 일은 출력의 분포를 옮기는 일이라 효과가 고친 자리에만 머문다는 보장이 없다. 한 판정을 바로잡으려 넣은 문장이 멀쩡하던 다른 판정을 흔들 수 있다. 코드도 한 줄 고치면 전체 테스트를 다시 돌리지 않느냐고 할 수 있는데, 코드에는 호출 그래프와 모듈 경계가 있어 "이 수정은 저 코드에 닿지 않는다"를 논증할 수라도 있다. 프롬프트의 문장 사이에는 그런 격리가 없다. 발동하면 본문 전체가 통째로 모델 앞에 놓이고, 모든 문장이 모든 판정에 관여한다. 그래서 한 줄을 고쳐도 전부를 다시 재야 한다.
요컨대 스킬 검증은 단위 테스트보다 임상시험에 가깝다. 같은 약도 사람마다, 같은 사람도 그날그날 반응이 다르므로, 여러 환자에게 여러 번 투여하고 통계로 판단하는 그 문법이다.
두 팔로는 부족하다
가장 먼저 떠오르는 실험은 스킬을 켠 것과 끈 것의 비교다. 임상시험의 말을 빌리면 비교하려는 조건 하나하나가 팔arm이니, 팔이 두 개인 실험이다. 그런데 이 두 팔 비교에는 함정이 있다. 스킬을 켠 쪽이 좋아졌다면, 그것이 공들여 깎은 사다리와 규칙 덕분인지, 아니면 그냥 "뭐라도 지시가 있어서"인지 구분할 수 없다. LLM은 지시에 민감해서 대충 쓴 한 문단짜리 요청만으로도 출력이 꽤 달라진다. 열 시간 들여 깎은 스킬이 10분 만에 쓴 문단과 같은 효과라면, 그 아홉 시간 오십 분은 어디로 갔는가.
그래서 팔이 세 개 필요하다. 신약을 위약placebo과 무처치 둘 다에 견주는 구도다.
- A안, 맨몸: 스킬 없이 과제만 준다
- B안, 산문: 스킬의 취지를 대충 한 문단으로 요약해 붙인다
- C안, 스킬: 깎은 스킬 전문을 붙인다
A와 B의 차이는 "지시의 존재"가 주는 효과, B와 C의 차이가 바로 "깎음"이 주는 효과다. ponytail도 정확히 이 구조로 자신을 증명한다. 앞서 본 실제 프로젝트 측정과는 별개로 상시 벤치마크를 두고 있는데, 스킬 없음, caveman이라 부르는 압축 산문, ponytail 세 팔을 모델 3종과 과제 5종에 대해 각 10회씩 돌리고 중앙값을 보고한다. 측정 지표도 코드 줄 수, 생성 코드의 실행 정확성, 호출당 비용, 지연 시간으로 명확하다.8
직접 재보기
말로만 하면 재미없으니 위에서 깎은 commit-message 스킬을 실제로 재보자. 실험 설계는 이렇다.
- 과제: 커머스 백엔드에서 나올 법한 diff 5개에 대해 커밋 메시지 작성. 이 중 2번(버그 수정 + 함수 추출 리팩터링)과 4번(N+1 쿼리 제거)은 일부러 성격이 섞인 충돌 케이스로 만들었다. 스킬의 사다리대로면 각각 fix와 perf가 정답이다.
- 팔 3개: 맨몸 / 산문 한 문단("타입 프리픽스 붙이고, 제목 50자 이내, 본문에 왜를 설명, 한국어로") / 스킬 전문
- 실행: 팔마다 과제당 5회 반복, 총 75회. 모델은 Claude Haiku 4.5를 헤드리스 모드(대화창 없이 프롬프트를 넣고 응답만 받아오는 실행 방식)로 호출했다.
- 채점: 사람 개입 없이 기계 채점. 타입 프리픽스 유무, 기대 타입 일치, 제목 50자 이내, 제목 마침표 없음, 빈 줄 뒤 본문 존재의 5개 항목이다.
하네스는 셸 스크립트와 채점 스크립트 각 하나면 된다. 뼈대만 보면 이렇다.
# run.sh — 팔 × 과제 × 반복을 전부 실행한다
for arm in none prose skill; do
for task in 1 2 3 4 5; do
for rep in 1 2 3 4 5; do
claude -p --model "$MODEL" \
< "prompts/${arm}-task${task}.txt" \
> "results/${arm}-task${task}-r${rep}.txt"
done
done
done// grade.mjs — 결과를 기계적으로 검사한다
function grade(text) {
const lines = strip(text).split("\n");
const subject = lines[0].trim();
const type = subject.match(/^([a-z]+):\s/)?.[1] ?? null;
return {
hasPrefix: TYPES.includes(type),
typeOk: type === EXPECTED_TYPE[task],
subjectOk: [...subject].length <= 50,
noPeriod: !subject.endsWith("."),
hasBody: lines[1] === "" && lines.slice(2).join("").trim().length > 0,
};
}결과를 보기 전에, 이 실험이 두 번 엎어진 이야기를 해야겠다. 벤치마크에서 어려운 것은 하네스가 아니라 과제와 정답을 만드는 일이라는 것을 몸으로 배웠기 때문이다. 검증이 표본 조사인 이상 과제가 곧 표본이고, 표본이 오염되면 그 위에서 나온 숫자는 전부 허수다.
첫 번째 판. 세 팔 모두 타입 판정이 만점에 가까웠다. 스킬이 훌륭해서가 아니었다. 충돌 케이스의 diff에 // 기존에는 부가세가 중복 계산되는 버그가 있었다 같은 친절한 주석이 붙어 있어서, 주석이 "이건 버그 수정이다"라고 정답을 대신 말해주고 있었다. 사다리가 필요 없는 과제였던 것이다. 벤치마크를 만들 때 흔히 겪는 정답 누출leakage이다.
두 번째 판. 힌트 주석을 지우고 다시 돌렸더니 이번에는 산문도 스킬도 2번 과제를 전원 refactor로 판정했다. 스킬의 사다리가 무력한가 싶어 diff를 다시 뜯어봤는데, 민망한 사실이 나왔다. 주석을 지우고 나니 코드 어디에도 버그가 없었다. 구코드도 새로 추가된 테스트를 통과하는, 판정하자면 진짜로 리팩터링인 diff였다. 모델이 틀린 게 아니라 기대 정답 라벨이 틀렸던 것이다. 부가세를 주문 전체에 곱하는 명백한 버그가 코드에 보이도록 diff를 고치고 나서야 실험이 성립했다.
세 번째 판의 결과가 아래다.
| 검사 항목 | 맨몸 | 산문 | 스킬 |
|---|---|---|---|
| 타입 프리픽스 존재 | 6/25 | 25/25 | 25/25 |
| 기대 타입 일치 | 4/25 | 24/25 | 24/25 |
| 제목 50자 이내 | 15/25 | 25/25 | 25/25 |
| 제목에 마침표 없음 | 25/25 | 25/25 | 25/25 |
| 빈 줄 뒤 본문 존재 | 9/25 | 25/25 | 25/25 |
| 5항목 모두 통과 | 0/25 | 24/25 | 24/25 |
숫자가 말해주는 것을 순서대로 읽어보자.
가장 큰 개선은 스킬이 아니라 "지시가 있다"는 사실에서 왔다. 맨몸의 전항목 통과가 0/25인데 산문 한 문단만 붙여도 24/25가 된다. 만약 두 팔(맨몸 vs 스킬)로만 실험했다면 이 24점 전부를 공들여 깎은 스킬의 공으로 착각했을 것이다. 가운데 팔은 정확히 이 착각을 막으라고 있다.
총점만 보면 산문과 스킬은 24/25로 동점이다. 그런데 실점의 위치가 다르다. 산문은 버그 수정과 리팩터링이 섞인 2번 과제에서 다섯 번 중 한 번을 refactor로 판정했다. 다섯 번 중 네 번은 맞았다는 것이 오히려 무섭다. 같은 diff가 요일에 따라 다른 타입을 받는다는 뜻이기 때문이다. 앞에서 말한 비결정론이 정확히 이런 얼굴로 나타난다. 스킬은 같은 과제에서 5/5로 흔들리지 않았다. 물론 5/5 역시 표본일 뿐이지만, 적어도 이 표본에서는 "섞여 있어도 사다리에서 먼저 걸리는 것 하나"라는 명시된 규칙이 분산을 지워버렸다. 스킬은 평균을 올리는 도구이기 이전에 분포를 좁히는 도구이고, 이 차이는 팔마다 한 번씩만 돌려서는 절대 보이지 않는다. 반복 실행이 사치가 아니라 검증의 전제인 이유다.
스킬의 실점은 스킬의 모호함을 폭로했다. 스킬이 잃은 1점은 5번 과제(CI 설정과 README 수정, 기대 타입 chore)에서 나왔다. 다섯 번 중 한 번 perf로 판정했는데, 들여다보니 모델 탓을 하기 어려웠다. CI에 npm 캐시를 추가하는 것은 분명 무언가를 빠르게 하는 일이고, 사다리 3번 "측정 가능한 성능 개선이 목적인가?"는 그것이 제품의 성능인지 빌드의 속도인지 말해주지 않는다. 직관 속에서는 당연히 구분되던 것("CI는 chore지")이 언어에는 담기지 않았던 것이다. 벤치마크가 다음에 깎을 지점을 짚어준 셈이다.
그래서 한 번 더 깎았다. 사다리 3번을 "제품 코드의 측정 가능한 성능 개선이 목적인가? (빌드·CI 속도는 해당하지 않는다)"로 바꾸고 스킬 팔을 다시 돌렸다. 이때 5번 과제만 확인하고 끝내지 않고 다섯 과제 전부를 다시 쟀다. 앞서 말한 이유에서다. 문장 하나를 고치는 일은 출력 분포 전체를 건드리는 일이므로, 고친 자리 밖에서 회귀가 나지 않았는지 함께 봐야 한다.
결과는 전항목 25/25. 잃었던 1점이 돌아왔고 다른 과제의 판정은 흔들리지 않았다. 산문 팔은 여전히 24/25다. 괄호 하나 포함해 서른 글자쯤 고친 것이 전부인데, 이 서른 글자는 감으로는 결코 쓸 수 없었다. 측정이 모호함을 짚어주고, 깎고, 다시 재서 확인하는 루프가 한 바퀴 돈 것이다. 방망이를 이리 돌려 보고 저리 돌려 보던 노인의 손놀림이 대충 이런 것이었겠다.
물론 이 실험의 한계도 분명히 해두자. 팔당 25회(과제 5개 × 반복 5회), 모델 하나(Haiku 4.5), 기계 채점이 가능한 항목만 쟀다. 통계적으로 엄밀한 검정이 아니라 방향을 보는 실험이다. 그리고 v2가 이 다섯 과제에 과적합되었을 가능성도 있다. "빌드·CI 속도는 해당하지 않는다"는 조항은 5번 과제를 정확히 겨냥한 문장이기 때문이다. 이것이 남의 벤치마크 숫자를 그대로 믿으면 안 되는 이유이자, 과제를 계속 늘려가며 상시로 재야 하는 이유다. 마침 다음 절의 주제다.
트리거도 재야 한다
위 실험은 스킬 본문을 프롬프트에 직접 넣어 "발동했을 때 출력이 좋은가"만 쟀다. 그런데 실전에서는 그 앞 단계, "써야 할 때 발동하는가"가 먼저다. 아무리 잘 깎아도 에이전트가 꺼내 들지 않으면 없는 스킬이다.
이것은 description의 문제이고, 마찬가지로 잴 수 있다. 발동해야 하는 요청 문장 열 개와 발동하면 안 되는 문장 열 개를 만들어 각각 새 세션에서 던져보고 적중률을 세면 된다. "커밋 메시지 좀 써줘"에 발동하고 "커밋 히스토리 보여줘"에 발동하지 않아야 한다. Claude Code 생태계에는 이 루프를 자동화해주는 skill-creator라는 공식 플러그인도 있다. 테스트 케이스 작성, 격리 세션 실행, 채점, 스킬 켬/끔 비교, 두 버전의 블라인드 A/B까지 해준다.9
한 가지 주의할 점은, 기계 채점이 가능한 것만 재고 싶어지는 유혹이다. 커밋 메시지의 형식은 정규식으로 재지만 "본문이 정말 왜를 설명하는가"는 못 잰다. 이런 항목은 채점 기준을 문장으로 적어두고 다른 LLM에게 채점시키는 방법(LLM-as-a-judge)으로 보완한다. 완벽하지 않지만, 아예 안 재는 것보다 훨씬 낫다.
스킬에도 CI/CD가 필요하다
여기까지 하면 잘 깎인, 잘 드는 스킬이 하나 생겼다. 이제 이것을 일회성 파일이 아니라 계속 관리되는 자산으로 만드는 마지막 단계가 남았다. 방법의 절반은 이미 아는 것이다. 스킬은 텍스트 파일이므로 버전 관리와 리뷰는 코드에 하던 것을 그대로 하면 된다. 나머지 절반인 테스트는 앞 장의 문법을 따른다. 반복해서 돌리고, 통과율로 읽는다.
버전 관리와 리뷰. 개인 스킬은 dotfiles 저장소에, 팀 스킬은 프로젝트의 .claude/skills/에 커밋한다. 팀 스킬의 변경이 PR로 오가기 시작하면 재미있는 일이 생기는데, 스킬 diff 위에서 팀의 사상 논쟁이 벌어진다. "판단 사다리에서 성능이 가독성보다 위인 게 맞나요?" 같은 코멘트가 달린다. 예전에는 개별 코드 리뷰에서 산발적으로 반복되던 논쟁이 이제 한 파일에 수렴하고, 결론은 스킬에 박제되어 다음 논쟁부터는 링크 하나로 끝난다. 팀의 직관을 합의하는 장소가 생기는 셈이다.
회귀 테스트. 파일은 그대로여도 스킬은 조용히 낡는다. 함수의 런타임은 명세가 고정된 물건이지만 스킬의 런타임은 모델이고, 모델은 몇 달마다 갈리기 때문이다. 평균 직관이 개선되면서 스킬의 어떤 조항은 불필요해지고, 드물게는 새 모델과 궁합이 나빠지기도 한다. 그래서 벤치마크를 한 번 쓰고 버리지 말고 CI에 걸어둔다. ponytail이 쓰는 promptfoo 같은 LLM 평가 도구를 쓰면 위에서 만든 3-arm 하네스를 설정 파일로 정리할 수 있다.10
# .github/workflows/skill-regression.yml
name: skill-regression
on:
pull_request:
paths: [".claude/skills/**"] # 스킬을 고치면 재측정
schedule:
- cron: "0 0 * * 1" # 모델은 조용히 바뀌므로 매주 한 번 재측정
jobs:
bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml --repeat 5
- run: node benchmarks/assert-threshold.mjs # 통과율이 기준 아래면 실패PR 트리거는 "이번 수정이 스킬을 더 좋게 만들었는가"를, 스케줄 트리거는 "모델이 바뀌어도 스킬이 여전히 유효한가"를 지킨다. 벤치마크 결과를 PR 코멘트로 남겨두면 스킬 diff 리뷰가 숫자 위에서 이루어진다.
배포. 스킬의 유통 단계는 대체로 개인 → 프로젝트 → 플러그인 순으로 넓어진다. 혼자 쓰던 ~/.claude/skills/의 스킬이 검증되면 프로젝트 저장소에 커밋해 팀에 퍼지고, 더 일반화되면 플러그인으로 묶어 마켓플레이스에 올린다. 이 단계까지 가면 다른 에이전트용 포맷 변환이라는 숙제가 생긴다. ponytail은 Claude Code 플러그인부터 Cursor 규칙 파일까지 16개 도구를 지원하는데, 같은 사상이 여러 포맷으로 복제되어 있으니 포맷 간 내용이 어긋나지 않는지 검사하는 테스트를 CI에 두고 있다. 스킬 저장소의 단위 테스트인 셈이다.
마지막 숙제는 재고 관리다. 스킬이 늘어나면 새 비용이 생긴다. 모든 스킬의 description이 상주하는 목차 비용이다. 안 쓰는 스킬은 목차를 오염시켜 다른 스킬의 발동 정확도까지 끌어내린다. 분기에 한 번쯤 발동 이력을 보고 안 쓰는 스킬을 지우거나 통합하자.
마치며
정리하자. 스킬을 직접 깎을 이유는 두 겹이다. 우리 팀의 워크플로우는 마켓플레이스에 없으므로 깎지 않으면 세상에 없고, 범용 영역에서도 에이전트는 인터넷의 평균 직관으로 움직이므로 남의 스킬과는 어딘가에서 반드시 어긋난다. 스킬은 그 평균 위에 자신의 사실과 직관을 덮어쓰는 패치이고, 매일 거슬리는 어긋남이 있다면 그때가 깎을 때다. 깎는 법의 핵심은 직관이 지나간 흔적(반복된 리뷰 코멘트, 반복해서 고친 출력)에서 재료를 모아 정체성, 판단 사다리, 규칙, 예시, 예외의 다섯 부위로 구조화하는 것이다. 그리고 깎았으면 재야 하는데, 스킬의 실체는 비정형 입력을 받아 비결정론적 출력을 내는 프롬프트이므로 한 번의 성공이 아니라 반복 실행의 통과율로, 두 팔이 아니라 세 팔로 잰다. 쟀으면 CI에 걸어 계속 잰다.
처음의 노인에게 돌아가자. 수필 속 손님이 끝내 이해하게 된 것은 방망이의 품질만이 아니었다. 깎는 시간을 아까워하지 않는 태도, 물건이 손에 맞아야 한다는 고집이었다. 스킬도 같다. 마켓플레이스의 스킬을 사다 쓰는 것은 전혀 부끄러운 일이 아니고 대부분의 경우 옳다. 다만 매일 쓰는 도구가 손에 안 맞는 채로 몇 달을 참고 있다면, 한 번쯤 직접 깎아볼 일이다. 깎다 보면 도구가 손에 맞아지고, 그보다 먼저 자신의 손이 어떻게 생겼는지를 알게 된다.
Footnotes
-
윤오영(1907~1976)의 수필로, 고등학교 국어 교과서에 실려 널리 알려졌다. 동대문 옆에서 방망이를 깎던 노인과의 실랑이를 통해 장인의 태도를 그린다. ↩
-
agentskills.io에 명세가 공개되어 있다. Claude Code, Codex CLI 등 여러 코딩 에이전트가 이 표준을 따르거나 호환 형식을 지원한다. ↩
-
Claude Code 기준으로 스킬 목차에 올라가는 description은 스킬당 1,536자에서 잘리고, 목차 전체에는 컨텍스트 창의 1% 수준의 예산이 있다. 스킬이 아주 많아지면 덜 쓰는 스킬의 description부터 잘려나간다. ↩
-
심리학자 대니얼 카너먼이 『생각에 관한 생각』에서 제시한 구분이다. 시스템 1은 즉각적·자동적 판단, 시스템 2는 느리고 의식적인 추론을 맡는다. LLM에 추론 과정을 강제로 밟게 하는 기법들이 시스템 2를 흉내 내려는 시도에 해당한다. ↩
-
Michael Polanyi, 『The Tacit Dimension』(1966). "We can know more than we can tell"이라는 문장으로 암묵지(tacit knowledge) 개념을 정리했다. ↩
-
Claude Code 문서는 SKILL.md 본문을 500줄 이하로 유지하고 상세 자료는 스킬 디렉터리의 별도 파일로 분리할 것을 권한다. 본문에서 파일을 링크해두면 에이전트가 필요할 때만 읽는다. ↩
-
temperature는 토큰 추첨의 무작위성을 조절하는 파라미터다. 0으로 내리면 출력이 상당히 안정되지만, 서빙 인프라의 병렬 연산과 부동소수점 특성 때문에 완전한 결정론이 보장되지는 않는다. 무엇보다 코딩 에이전트에서는 대개 사용자가 temperature를 만질 수 없고, 입력 쪽의 비정형성은 temperature와 무관하게 남는다. ↩
-
벤치마크 하네스와 결과가 저장소의
benchmarks/디렉터리에 공개되어 있어서 방법론을 직접 뜯어볼 수 있다. 재현 명령은npx promptfoo eval -c promptfooconfig.yaml --repeat 10이다. ↩ -
skill-creator 플러그인은 테스트 케이스를
evals/evals.json으로 관리하고, 발동해야 할 문장과 발동하면 안 되는 문장을 생성해 description 적중률을 재는 기능도 있다. ↩ -
promptfoo는 프롬프트·모델·테스트 케이스의 조합을 YAML로 선언하면 병렬 실행과 채점, 결과 비교 UI까지 제공하는 오픈소스 도구다. 이 글의 수제 하네스도 본질적으로 같은 일을 한다. ↩