Claude Code 스킬로 "이슈 → 커밋 → PR → 리뷰 → 리뷰 반영" 자동화해 보기
버그 하나를 고치는 데 필요한 GitHub 작업을 Claude Code 프로젝트 스킬 여섯 개로 나눠 만들고, 실제 저장소에서 처음부터 끝까지 돌려 봤습니다. 만드는 법, 돌리면서 막힌 곳, 그리고 배운 점을 정리합니다.
- 실습 저장소: goodsosbva/task-keeper
- 전체 코드: Harness_Engineering/skills-example
- 환경: Windows 11, Claude Code,
ghCLI, Python(FastAPI + SQLAlchemy)
들어가며: 왜 스킬인가
Claude Code에 "이슈 만들어 줘", "커밋해 줘", "PR 올려 줘"라고 말하면 대체로 잘 해 줍니다. 문제는 매번 결과가 조금씩 다르다는 점입니다.
- 이번에는 제목에
[Bug]를 붙였는데 다음에는 빠집니다. - PR 본문에 관련 이슈 연결을 잊습니다.
- 확인도 없이 push해 버립니다.
매번 프롬프트에 규칙을 길게 적을 수도 있지만, 그러면 사람이 같은 말을 반복해야 합니다. 스킬은 이 반복을 파일로 고정하는 방법입니다. 한 번 적어 두면 Claude가 상황에 맞을 때 스스로 꺼내 쓰고, /스킬이름으로 직접 부를 수도 있습니다.
이번 실습의 목표는 간단합니다.
작은 API 서버의 성능 버그 하나를 두고, 이슈 등록부터 리뷰 반영까지 전 과정을 스킬로 처리한다.
실습 대상: task-keeper
task-keeper는 할 일(Task)과 태그(Tag)를 다대다로 관리하는 작은 FastAPI 앱입니다. 여기에 일부러 심어 둔 버그가 하나 있습니다.
# app/services/task_service.py
def list_tasks(db: Session) -> list[Task]:
tasks = db.query(Task).all()
return tasks
# app/models.py
tags = relationship("Tag", secondary=task_tags, lazy="select")
GET /tasks는 할 일 목록을 한 번에 가져오지만, 응답을 JSON으로 만들 때 할 일마다 task.tags에 접근합니다. 관계가 lazy 로딩이라서 접근할 때마다 태그 조회 쿼리가 하나씩 더 나갑니다. 할 일이 N개면 쿼리는 1 + N번입니다. 전형적인 N+1 문제입니다.
1. 스킬의 기본 구조
스킬은 프로젝트의 .claude/skills/<스킬이름>/SKILL.md 파일 하나로 시작합니다.
.claude/skills/
└── create-issue/
├── SKILL.md # 무엇을, 어떤 순서로 할지
└── resources/
└── issue_templates.md # 결과물의 모양 (템플릿)SKILL.md의 맨 위에는 frontmatter가 들어갑니다.
---
name: create-issue
description: 깃헙 이슈를 생성합니다. 사용자가 버그 리포트, 기능 요청, 개선 사항 등을
이슈로 등록하고 싶을 때 사용합니다. "이슈 만들어줘", "버그 리포트 작성해줘",
"기능 요청 이슈 올려줘" 등의 요청에 반응합니다.
---
여기서 가장 중요한 것은 description 입니다. Claude는 이 설명을 보고 지금 이 스킬을 쓸지 판단합니다. 그래서 "무엇을 하는지"와 함께 "어떤 말을 들으면 반응하는지" 를 예시로 적어 두는 게 좋습니다.
본문에는 보통 아래 세 가지를 적습니다.
| 섹션 | 내용 |
|---|---|
| Goal | 이 스킬이 끝나면 무엇이 만들어지는가 |
| Instructions | 어떤 명령을 어떤 순서로 실행하는가 |
| Constraints | 하면 안 되는 것, 반드시 사람에게 물어야 하는 지점 |
이번에 만든 스킬 여섯 개
| 스킬 | 하는 일 | 템플릿 |
|---|---|---|
create-issue |
요청을 분석해 [Bug]/[Feature]/[Improvement] 이슈 생성 |
issue_templates.md |
git-commit |
<type>(<scope>): <subject> 형식의 한국어 커밋 |
— |
create-pr |
브랜치의 모든 커밋을 분석해 PR 생성, 이슈 연결 | pr_template.md |
review-pr |
체크리스트로 PR 리뷰 (품질·보안·성능·테스트) | review_checklist.md |
apply-pr-review |
리뷰 코멘트를 분류해 반영하고 코멘트마다 답글 | reply_templates.md |
commit-and-pr |
git-commit과 create-pr을 차례로 호출 |
두 스킬 것을 사용 |
create-issue와 git-commit은 미리 준비된 예제이고, 나머지 네 개는 이번에 직접 만들었습니다.
2. 스킬을 만들 때 정한 원칙 네 가지
만들다 보니 스킬마다 반복해서 고민하게 되는 지점이 있었습니다. 다음 네 가지로 정리했습니다.
① 절차와 형식을 분리한다
SKILL.md에는 절차만 적고, 결과물의 모양은 resources/의 템플릿으로 뺐습니다.
4. **템플릿 참조**: `resources/pr_template.md`에서 제목 형식과 본문 템플릿을 읽습니다.
PR 본문에 섹션을 하나 추가하고 싶으면 템플릿 파일만 고치면 됩니다. 절차는 건드리지 않아도 됩니다.
② 스킬끼리는 규칙을 복사하지 말고 호출한다
commit-and-pr을 만들 때, 커밋 규칙과 PR 규칙을 다시 적고 싶은 유혹이 있었습니다. 하지만 그러면 규칙이 두 곳에 생기고, 한쪽만 고치는 순간 어긋납니다. 그래서 이 스킬은 두 스킬을 부르기만 하도록 만들었습니다.
4. **커밋**: `git-commit` 스킬을 호출하여 그 절차대로 커밋합니다.
5. **PR 생성**: 이어서 `create-pr` 스킬을 호출하여 그 절차대로 push하고 PR을 만듭니다.
③ 되돌리기 어려운 일 앞에서 멈춘다
이슈 생성, PR 생성, GitHub에 리뷰 게시, push는 지워도 알림과 기록이 남습니다. 이런 단계 앞에는 반드시 사람의 승인을 받도록 적었습니다.
## Constraints
- 사용자 승인 없이 PR을 생성하지 않습니다.
- 기본 브랜치(`main`)에 직접 push하지 않습니다.
- `git push --force`는 사용자가 명시적으로 요청한 경우에만 사용합니다.
반대로 로컬 파일을 읽거나 분석하는 단계에는 승인을 두지 않았습니다. 모든 단계에서 물어보면 흐름이 끊기기 때문입니다.
④ 확인하지 못한 것을 적을 자리를 만든다
에이전트가 가장 위험할 때는 확인하지 않은 것을 확인한 것처럼 말할 때입니다. 그래서 템플릿 곳곳에 "모르면 모른다고 쓰는 자리"를 만들었습니다.
- 이슈 템플릿: 모르는 정보는
TODO: 추가 정보 필요 - PR 템플릿: 테스트 항목은 체크박스. 실행하지 않았으면 체크하지 않음
- 리뷰: 추측과 확인한 사실을 구분하고, 확인 못 한 것은 "확인 필요"
- 리뷰 반영: "실행하지 않은 테스트를 통과했다고 쓰지 않는다"
이 원칙은 실습 중에 실제로 크게 도움이 되었습니다. 뒤에서 다시 나옵니다.
3. 실제로 돌려 보기
Step 1. /create-issue: 한 줄짜리 요청을 이슈로
/create-issue 할 일 목록 조회가 느림. 할 일이 많을 때 GET /tasks 응답이 오래 걸림흥미로웠던 점은, Claude가 이 문장만으로 이슈를 쓰지 않았다는 것입니다. 먼저 GET /tasks 코드를 따라가 읽고, list_tasks()와 lazy="select"에서 N+1 원인을 찾은 뒤 이슈를 썼습니다.
생성된 이슈는 다음과 같습니다.
- 제목:
[Bug] 할 일이 많을 때 GET /tasks 응답이 느림 (태그 N+1 쿼리) - 본문: 원인, 재현 방법(
SQLALCHEMY_ECHO=1로 쿼리 로그 보기), 기대 동작과 실제 동작, 수정 방향 - 측정값처럼 모르는 정보는
TODO: 추가 정보 필요로 남김
막힌 곳
| 문제 | 해결 |
|---|---|
needs-triage 라벨이 저장소에 없음 |
스킬에 적힌 대로 gh label create로 먼저 생성 |
the repository has disabled issues |
저장소 설정을 바꾸는 일이라 Claude가 멈추고 물어봄 → 승인 후 gh repo edit --enable-issues |
두 번째 경우는 스킬에 적혀 있지 않은 상황이었습니다. 그런데도 "되돌리기 어려운 일은 묻는다"는 원칙대로 멈춘 것이 좋았습니다.
Step 2. /git-commit: 성격별로 커밋 나누기
스킬 파일과 문서를 커밋할 때, Claude는 이를 chore(skills)와 docs(wiki) 두 커밋으로 나눴습니다.
여기서 Author identity unknown 오류가 났습니다. 새 PC라 git 사용자 정보가 설정되어 있지 않았던 것입니다. 전역 설정을 함부로 바꾸지 않고, 해당 커밋에만 작성자를 지정했습니다.
git -c user.name=<이름> -c user.email=<이메일> commit -m "..."
Step 3. /create-pr: 요청과 실제 상태가 다를 때
/create-pr 방금 커밋한 n+1 수정을 pr로 올려라이번 실습에서 가장 인상적인 장면이었습니다. create-pr 스킬에는 이런 단계가 있습니다.
2. **변경사항 분석**: base 브랜치 대비 이 브랜치에 들어간 내용을 모두 읽습니다.
- 최신 커밋 하나만 보지 말고, 브랜치의 **모든 커밋**을 기준으로 작성합니다.
이 단계를 실행해 보니, 브랜치에 N+1 수정 커밋이 아예 없었습니다. 앞에서 커밋한 것은 스킬 파일과 문서뿐이었습니다. 요청을 그대로 따랐다면 버그 수정이 없는 "fix" PR이 올라갔을 것입니다.
Claude는 이 사실을 먼저 알리고, 수정을 만든 뒤 PR을 올렸습니다.
from sqlalchemy.orm import Session, selectinload
def list_tasks(db: Session) -> list[Task]:
# 태그를 IN 쿼리 한 번으로 미리 불러와 N+1을 막습니다.
return db.query(Task).options(selectinload(Task.tags)).all()
selectinload는 할 일 목록을 가져온 뒤, 그 할 일들의 태그를 WHERE task_id IN (...) 쿼리 한 번으로 몰아서 가져옵니다. 할 일이 몇 개든 쿼리는 2번으로 고정됩니다.
회귀 테스트도 하나 추가했습니다. SQLAlchemy 이벤트로 실제 실행된 쿼리 수를 세는 방식입니다.
def test_list_tasks_query_count_is_constant(client, db_session):
for i in range(5):
client.post("/tasks", json={"title": f"할 일 {i}", "tags": [f"태그{i}"]})
queries = []
engine = db_session.get_bind()
listener = lambda *args: queries.append(args[2])
event.listen(engine, "before_cursor_execute", listener)
try:
res = client.get("/tasks")
finally:
event.remove(engine, "before_cursor_execute", listener)
assert len(queries) == 2 # 할 일 1회 + 태그 1회
그런데 테스트가 안 돌았습니다
uv run pytest를 실행하자마자 pydantic 내부에서 AssertionError가 났습니다. 원인은 로컬 Python이 3.14.0rc2, 즉 정식 출시 전 버전이었기 때문입니다. pydantic이 쓰는 typing 내부 API가 rc 버전과 맞지 않았습니다. 수정 전 코드에서도 똑같이 실패했으니 이번 변경과는 관계가 없었습니다.
정식판 Python을 받으려 했지만 실습 환경에서 다운로드가 막혀 있었습니다. 그래서 pydantic을 거치지 않고 SQLAlchemy만으로 쿼리 수를 직접 세 봤습니다.
| 할 일 50개일 때 쿼리 수 | |
|---|---|
| 수정 전 | 51회 |
| 수정 후 | 2회 |
그리고 pytest를 돌리지 못했다는 사실은 PR 본문의 테스트 항목에 체크하지 않은 채로 남겼습니다. 원칙 ④가 쓰인 순간입니다.
## 테스트
- [x] SQLAlchemy로 쿼리 수 측정: 할 일 50개 기준 51회 → 2회
- [ ] `uv run pytest` — 로컬 Python 3.14.0rc2에서 pydantic 임포트 실패로 미실행
PR 본문에 Closes #1을 넣어 이슈와 연결했습니다. 이렇게 하면 PR이 머지될 때 이슈가 자동으로 닫힙니다.
Step 4. /review-pr: 체크리스트로 리뷰하기
review-pr 스킬은 gh pr view --json으로 메타데이터를, gh pr diff로 변경 내용을 읽은 뒤 체크리스트를 하나씩 적용합니다. 체크리스트 일부를 소개하면 다음과 같습니다.
## 3. 성능
- [ ] N+1 쿼리가 없는가 (관계 접근 시 `selectinload`, `joinedload` 사용)
- [ ] 반복문 안에서 DB 쿼리나 네트워크 호출을 하지 않는가
- [ ] 목록 조회에 페이지네이션이나 개수 제한이 필요하지 않은가
## 4. 테스트
- [ ] 버그 수정이라면, 수정 전 코드에서 실패하는 테스트인가
지적 사항은 심각도로 나눕니다.
- 🔴 Blocker: 머지 전에 반드시 수정 (버그, 보안 취약점)
- 🟡 Major: 강하게 권장 (성능, 테스트 누락)
- 🟢 Minor: 선택 (가독성, 스타일)
방금 만든 PR을 리뷰해 보니, Blocker는 없었지만 Major가 세 개 나왔습니다.
| 심각도 | 지적 |
|---|---|
| 🟡 | 새 테스트를 한 번도 실행하지 못했다 |
| 🟡 | 테스트가 같은 DB 세션을 공유해서, 수정 전 코드로 돌려도 1+N이 아니라 1+(N-1)로 셀 수 있다 → 측정 직전에 db_session.expire_all() 필요 |
| 🟡 | PR에 버그 수정과 관계없는 커밋(스킬, 문서)이 섞여 있다 |
두 번째 지적이 특히 좋았습니다. 테스트가 통과하는지만 볼 게 아니라, "이 테스트가 정말 버그를 잡아내는가" 를 따진 것입니다. 앞선 POST 요청에서 마지막 할 일의 태그가 세션에 이미 올라와 있으므로, 측정값이 버그 모양과 정확히 맞지 않는다는 분석입니다.
참고로 자기 PR에는 GitHub에서 Approve나 Request changes를 할 수 없습니다. 그래서 스킬에 --comment로 남기라고 적어 두었습니다.
Step 5. /apply-pr-review: 리뷰를 무조건 따르지 않기
리뷰를 반영하는 스킬에서 가장 신경 쓴 부분은 이것입니다.
리뷰어 의견을 무조건 따르지 않습니다. 지적이 맞는지 코드를 직접 읽고 확인합니다.
지적마다 네 가지 중 하나로 분류합니다.
| 분류 | 의미 | 답글 |
|---|---|---|
| ✅ 반영 | 타당하고 이 PR 범위 안 | "반영했습니다. (커밋 해시)" |
| 💬 질문 답변 | 코드 변경 없이 설명 필요 | 답변 |
| 🙅 반영 안 함 | 근거가 틀렸거나 범위 밖 | 이유와 대안 |
| ⏸️ 보류 | 사람이 결정해야 함 | 결정 후 다시 답변 |
GitHub의 리뷰 코멘트는 종류가 세 가지라서 가져오는 방법도 다릅니다. 이 부분은 미리 알아 두면 좋습니다.
| 종류 | 가져오기 | 답하기 |
|---|---|---|
| 코드 줄에 단 인라인 코멘트 | GraphQL reviewThreads (해결 여부 포함) |
gh api .../pulls/<n>/comments/<id>/replies |
| 리뷰 본문 | gh pr view --json reviews |
답글 기능 없음 → gh pr comment로 종합 |
| 일반 PR 코멘트 | gh pr view --json comments |
답글 기능 없음 → gh pr comment로 종합 |
실제로 돌려 보니 PR에 코멘트가 하나도 없었습니다. 앞 단계의 리뷰가 로컬 파일로만 있었기 때문입니다. 스킬은 그대로 끝내지 않고 "리뷰를 먼저 게시할까요?"라고 물었고, 게시한 뒤 지적 7건을 분류했습니다.
- ✅ 반영 4건:
expire_all()추가, import 위치 정리, README와 주석 갱신 - 💬 답변 1건: 테스트 미실행. 이 환경에서는 해결할 수 없음
- 🙅 반영 안 함 2건: PR 분리(force push 필요), 페이지네이션(이번 PR 범위 밖)
PR 분리를 "반영 안 함"으로 둔 이유가 좋았습니다. 리뷰 지적은 맞지만, 고치려면 브랜치 이력을 다시 쓰고 force push를 해야 합니다. 스킬의 제약("force push는 명시적 요청이 있을 때만")과 부딪히므로, 사람에게 판단을 넘긴 것입니다.
4. 번외: Claude Code가 아예 안 켜졌던 이야기
사실 이 실습은 Claude Code가 실행되지 않는 문제로 시작했습니다. Windows에서 npm으로 Claude Code를 쓴다면 겪을 수 있는 일이라 함께 적어 둡니다.
$ claude
/c/Users/admin/.npm-global/claude: line 12: .../claude-code/bin/claude.exe: No such file or directorynpm uninstall -g, npm install -g로 다시 설치해도 그대로였습니다.
원인: claude가 두 군데 설치되어 있었다
| 위치 | 버전 | 상태 |
|---|---|---|
~/.npm-global (예전 npm 설치 위치) |
2.1.291 | 망가짐. 셸이 PATH에서 이걸 먼저 찾음 |
C:\Program Files\nodejs (nvm, 현재 npm 설치 위치) |
2.1.319 | 정상 |
망가지는 과정은 이랬습니다.
~/.npm-global의 claude가 자동 업데이트를 시작합니다.- Windows는 실행 중인 exe를 덮어쓸 수 없어서, 업데이터가 기존 파일 이름을
claude.exe.old.*로 바꿔 둡니다. - 새 버전은
npm install -g로 설치되는데, 이 명령은 현재 npm 설정의 위치(nvm 쪽) 에 설치합니다. - 결국
~/.npm-global에는.old파일만 남고, 셸은 계속 거기서 claude를 찾다가 실패합니다.
사용자가 직접 한 재설치도 모두 nvm 쪽에만 설치되었기 때문에, 망가진 쪽은 한 번도 고쳐지지 않았던 것입니다.
헛짚었던 것들
- 처음에는 npm의
ignore-scripts설정 때문에 설치 스크립트가 안 돈 줄 알았습니다. 확인해 보니 처음부터false였습니다. - exe를 수동으로 다시 만드는 임시 복구를 했지만, 16분 뒤 자동 업데이트가 돌면서 똑같이 다시 망가졌습니다.
로그를 끝까지 읽고 나서야 "설치 위치가 두 군데"라는 근본 원인이 보였습니다.
해결
오래된 설치본을 지우는 대신 백업 폴더로 옮겼습니다. 혹시 문제가 생기면 되돌릴 수 있게 하기 위해서입니다.
mkdir -p ~/.npm-global/_claude-backup
mv ~/.npm-global/node_modules/@anthropic-ai/claude-code \
~/.npm-global/claude ~/.npm-global/claude.cmd ~/.npm-global/claude.ps1 \
~/.npm-global/_claude-backup/
hash -r
$ which -a claude
/c/Program Files/nodejs/claude
$ claude --version
2.1.319 (Claude Code)
💡
hash -r은 Git Bash(bash)용 명령입니다. bash는 한 번 찾은 실행 파일의 경로를 기억해 두기 때문에, 파일을 옮긴 뒤에는 이 명령으로 기억을 지워야 합니다. PowerShell에는hash명령이 없고 경로를 기억해 두지도 않으므로 필요 없습니다. 창을 새로 열어도 됩니다.
비슷한 증상을 겪는다면 which -a claude(bash) 또는 where.exe claude(PowerShell)로 claude가 몇 군데 있는지부터 확인해 보세요.
5. 배운 점
스킬은 "요청"보다 "실제 상태"를 먼저 확인하게 만들어야 한다
이번 실습에서 요청에 담긴 전제가 틀린 경우가 세 번 있었습니다.
- "방금 커밋한 수정" → 브랜치에 수정 커밋이 없었음
- "리뷰 반영해 줘" → PR에 리뷰 코멘트가 없었음
- "재설치했는데 안 됨" → 재설치가 엉뚱한 위치에 되고 있었음
세 경우 모두 상태를 먼저 확인하는 단계 덕분에 잡아냈습니다. 스킬의 첫 단계는 거의 항상 "현재 상태 확인"이어야 한다고 느꼈습니다.
스킬이 모든 예외를 다룰 수는 없다. 대신 "멈추고 알린다"를 기본으로
Issues 기능 꺼짐, git 사용자 정보 없음, rc 버전 Python은 모두 스킬에 적혀 있지 않은 상황이었습니다. 이런 일을 하나하나 스킬에 적을 수는 없습니다. 대신 "되돌리기 어려운 일 앞에서는 멈춘다" 는 원칙을 Constraints에 넣어 두니, 처음 보는 상황에서도 안전하게 행동했습니다.
검증하지 못한 것을 기록할 자리가 신뢰를 만든다
pytest를 돌리지 못한 사실이 PR 본문, 리뷰, 리뷰 반영 계획에 일관되게 남았습니다. 템플릿에 체크박스와 "확인 필요" 자리를 만들어 둔 덕분입니다. 이게 없었다면 테스트가 통과한 것처럼 보였을 것입니다.
에이전트에게 다른 저장소에 올리라고 할 때는 경로를 명확히
실습 결과를 별도 저장소에 올리는 과정에서, "올려 줘"라고만 했더니 Claude가 폴더 위치를 추측해서 두 번이나 엉뚱한 곳에 올렸습니다. 사람에게는 당연한 "기존 폴더들과 같은 위치"가 에이전트에게는 당연하지 않았습니다. 대상 경로는 처음부터 명시하거나, 올리기 전에 확인하게 하는 게 좋습니다.
스킬 조합은 복사가 아니라 호출로
commit-and-pr을 두 스킬의 호출로만 만드니, 커밋 규칙을 바꿀 때 git-commit 하나만 고치면 됩니다. 코드에서 함수를 재사용하는 것과 같은 원리입니다.
마치며
| 단계 | 결과 |
|---|---|
| 이슈 | task-keeper#1 |
| 수정 | selectinload 한 줄, 쿼리 51회 → 2회 |
| PR | task-keeper#2, 이슈 자동 연결 |
| 리뷰 | Blocker 0 / Major 3 / Minor 4 |
| 리뷰 반영 | 7건 분류 (반영 4, 답변 1, 반영 안 함 2) |
스킬을 만들기 전에는 "Claude에게 일을 시킨다"고 생각했다면, 만들고 나서는 "Claude가 일하는 방식을 설계한다" 는 느낌에 가까웠습니다. 무엇을 할지보다 어디서 멈출지, 무엇을 기록할지 를 정하는 데 더 많은 시간을 썼고, 실제로 그 부분이 가장 큰 차이를 만들었습니다.
아직 남은 것
- 정식판 Python에서
pytest실행해 보기 - 리뷰 반영을 끝까지 실행하고, 인라인 코멘트 답글·스레드 해결 경로 검증하기
commit-and-pr실제로 돌려 보기
전체 스킬 파일은 Harness_Engineering/skills-example/task-keeper에서 볼 수 있습니다.
'인공지능' 카테고리의 다른 글
| 하네스 엔지니어링이란? - (end) (0) | 2026.11.04 |
|---|---|
| 하네스 엔지니어링이란? - (3) (0) | 2026.11.03 |
| 하네스 엔지니어링이란? - (2) (0) | 2026.11.02 |
| 하네스 엔지니어링이란? - (1) (0) | 2026.11.01 |
