루프를 직접 돌려보기
여기서는 손으로 해봅니다. 2장에서 만든 글쓰기실습/ 폴더를 그대로 다시 열어서, 그 위에 루프를 한 겹 얹습니다. 폴더를 지우셨다면 2-2를 보고 다섯 파일을 다시 만들면 됩니다. 새로 만드는 파일은 세 개뿐이고, 나머지는 이미 있는 것들을 이어 붙이는 일입니다.
1. 먼저 판정 가능한 자리를 만든다
루프에서 가장 먼저 해야 할 일은 8-1의 첫 번째 부품, 기계가 판정할 수 있는 멈춤 조건을 만드는 것입니다. 그런데 2장의 SKILL.md에 적힌 품질 기준은 지금 이런 모양입니다.
## 품질 기준
- 한 절 분량이 기본값(약 200줄) ±20% 범위 안.
- 본문 안에 예시 코드 또는 비교표가 최소 1개.
- 첫 문단에 "이 절이 무엇을 다루는지" 한 줄 요약.
- 마지막 문단에 "다음에 무엇이 오는지" 한 줄 안내.
사람이 읽기에는 충분한데, 루프가 쓰기에는 부족합니다. "한 줄 요약이 있는지"를 매 바퀴 모델에게 물어보면 판정이 흔들리고 토큰도 씁니다. 판정할 수 있는 항목은 스크립트로 내리고, 판정할 수 없는 항목만 모델에게 남깁니다.
폴더 루트에 check.sh를 만듭니다.
#!/bin/bash
# 사용법: bash check.sh artifacts/draft.md
FILE="${1:-artifacts/draft.md}"
FAIL=0
if [ ! -f "$FILE" ]; then
echo "FAIL: 파일이 없습니다 ($FILE)"
exit 1
fi
# 1. 분량: 180줄 이상
LINES=$(wc -l < "$FILE")
if [ "$LINES" -lt 180 ]; then
echo "FAIL: 분량 $LINES줄 (180줄 이상 필요)"
FAIL=1
else
echo "PASS: 분량 $LINES줄"
fi
# 2. 코드 블록 또는 표가 최소 1개
BLOCKS=$(grep -c '^```' "$FILE")
TABLES=$(grep -c '^|' "$FILE")
if [ "$BLOCKS" -lt 2 ] && [ "$TABLES" -lt 2 ]; then
echo "FAIL: 예시 코드도 비교표도 없습니다"
FAIL=1
else
echo "PASS: 코드 블록 $((BLOCKS / 2))개, 표 줄 $TABLES개"
fi
# 3. 금지 표현
for WORD in "매우" "굉장히" "정말정말"; do
if grep -q "$WORD" "$FILE"; then
echo "FAIL: 금지 표현 '$WORD' 사용"
FAIL=1
fi
done
# 4. 명령어 예시가 있으면 Windows 안내도 있어야 함
if grep -q '^\$ ' "$FILE" && ! grep -qi 'windows' "$FILE"; then
echo "FAIL: 명령어 예시는 있는데 Windows 안내가 없습니다"
FAIL=1
fi
[ "$FAIL" -eq 0 ] && echo "ALL PASS" || echo "검사 실패"
exit $FAIL
Windows에서는 Git Bash에서 그대로 돌아갑니다. 한 번 직접 돌려보세요.
bash check.sh artifacts/draft.md
이 스크립트가 대단해 보일 필요는 없습니다. 지금은 네 가지만 봅니다. 글로 적힌 규칙 중 기계가 볼 수 있는 것을 스크립트로 옮겼다는 사실 자체가 중요합니다. 규칙이 늘어나면 이 파일에 한 줄씩 더하면 됩니다.
2. 실습 1, 조건이 충족될 때까지 밀어붙이기
이제 루프를 한 번 돌려봅니다. Claude Code에서 글쓰기실습/ 폴더를 열고 다음을 입력합니다.
/goal artifacts/draft.md가 `bash check.sh artifacts/draft.md`에서 ALL PASS로 끝난다.
주제는 "JavaScript의 var, let, const 차이"이고, harness 스킬의 순서를 따른다.
검사 출력은 매번 대화에 남긴다. check.sh는 수정하지 않는다.
12턴 안에 못 끝내면 중단하고 무엇이 막혔는지 보고한다.
입력하면 곧바로 첫 턴이 시작됩니다. 그리고 이런 순환이 도는 것을 볼 수 있습니다.
[턴 1] outliner를 불러 목차를 짜고, writer로 본문 초안 작성 → artifacts/draft.md 저장
bash check.sh 실행
FAIL: 분량 152줄 (180줄 이상 필요)
PASS: 코드 블록 3개, 표 줄 6개
[평가] 아직 아님 — 분량 미달
[턴 2] 3절과 4절에 예시를 보강해 분량을 늘림
bash check.sh 실행
PASS: 분량 191줄
FAIL: 금지 표현 '매우' 사용
[평가] 아직 아님 — 금지 표현 잔존
[턴 3] '매우'를 걷어내고 다시 검사
ALL PASS
[평가] 충족 — 목표 해제
여기서 눈여겨볼 자리가 세 군데입니다.
- 사람이 아무 말도 하지 않았습니다. 턴 1과 턴 2 사이에 "분량 늘려줘"라고 말한 사람이 없습니다. 실패 메시지가 그 역할을 했습니다.
- 판정은 별도 모델이 했습니다. 본문을 쓴 쪽과 "아직 아님"을 판정한 쪽이 다릅니다.
- 실패 메시지가 곧 다음 지시문입니다. 그래서
check.sh의 실패 메시지를 "Error"가 아니라 "분량 152줄(180줄 이상 필요)"처럼 쓴 것이 중요했습니다. 5장에서 본 "실패 메시지가 의미 있게"라는 도구 설계 원칙이 루프에서 훨씬 크게 작동합니다.
2.1 중간에 상태 보기
루프가 도는 동안 /goal을 단독으로 입력하면 지금 어디쯤인지 볼 수 있습니다.
/goal
조건, 경과 시간, 평가된 턴 수, 지금까지 쓴 토큰, 평가 모델의 최근 판정 이유가 나옵니다. 멈추고 싶으면 /goal clear입니다.
2.2 조건을 잘못 쓰면 어떻게 되는지도 한 번
일부러 나쁜 조건으로 한 번 돌려보면 배우는 게 많습니다.
/goal 글이 읽기 좋아진다
이 조건은 두 가지 방식으로 실패합니다. 운이 좋으면 평가 모델이 첫 턴에 "충족"을 내버려서 루프가 즉시 끝나고, 운이 나쁘면 계속 "아직 아님"이 나오면서 같은 자리를 맴돕니다. 조건이 모호하면 루프는 멈추지 못하거나, 아무 때나 멈춥니다. 8-1의 첫 번째 부품이 왜 첫 번째인지가 여기서 분명해집니다.
3. 실습 2, 턴마다 자동으로 검사하기
실습 1에서는 Claude가 스스로 check.sh를 돌렸습니다. "돌려야지"를 잊으면 검사가 빠집니다. 이걸 잊을 수 없게 만들려면 hook으로 내립니다.
.claude/settings.json을 만들고 다음을 적습니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash",
"args": ["check.sh", "artifacts/draft.md"],
"statusMessage": "원고 검사 중..."
}
]
}
]
}
}
이제 artifacts/draft.md를 고칠 때마다 검사가 자동으로 돌고, 결과가 대화에 들어옵니다. 실습 1을 다시 돌려보면 차이가 보입니다. Claude가 검사를 "기억해서" 돌리는 게 아니라, 고치는 순간 결과가 따라옵니다.
규칙을 글로 적어두는 것과 실행 경로에 박아두는 것의 차이가 이 실습의 요점입니다. SKILL.md에 적힌 품질 기준은 지켜지지 않을 수 있지만, hook은 빠질 수 없습니다. 1장의 Mitchell Hashimoto가 말한 "그 실수를 다시는 못 하게 환경에 박아 넣는다"가 이 모양입니다.
4. 실습 3, 검사하는 사수를 따로 두기
check.sh가 못 보는 자리가 있습니다. 톤이 우리 책 톤인지, 비유가 억지스럽지 않은지, 설명 순서가 자연스러운지. 이건 사람이 보거나 모델이 봐야 합니다. 그런데 본문을 쓴 그 모델에게 물어보면 자기 글에 후합니다.
2장에서 사수 카드를 두 장 만들었으니, 세 번째 카드를 한 장 더 만듭니다. .claude/agents/reviewer.md입니다.
---
name: reviewer
description: writer가 만든 본문 초안을 CLAUDE.md의 톤 규칙과 대조해 검수하는 사수.
본문을 직접 고치지 않고 지적만 한다.
---
# Reviewer
## 책임
- `artifacts/draft.md`를 읽고 `CLAUDE.md`의 톤·독자 규칙과 대조한다
- 어긋난 자리를 줄 번호와 함께 지적한다
- 지적마다 "왜 어긋났는지"를 한 줄로 적는다
## 판정
마지막 줄에 반드시 다음 중 하나를 적는다.
- `REVIEW: PASS`
- `REVIEW: FAIL` (뒤에 지적 개수)
## 하지 말아야 할 일
- 본문을 직접 수정하는 일 (그건 writer의 책임)
- "전반적으로 괜찮습니다" 같은 뭉뚱그린 평가
- check.sh가 이미 보는 항목(분량, 코드 블록 수, 금지 표현) 재확인
마지막 항목이 중요합니다. 기계가 보는 자리와 모델이 보는 자리를 겹치지 않게 나눕니다. 겹치면 토큰만 쓰고 판정이 두 벌 나옵니다.
이제 조건을 두 겹으로 만듭니다.
/goal artifacts/draft.md가 `bash check.sh artifacts/draft.md`에서 ALL PASS이고,
reviewer 서브 에이전트의 검수 결과가 REVIEW: PASS다.
두 결과 모두 대화에 남긴다. 15턴 안에 못 끝내면 중단하고 보고한다.
이러면 루프가 이렇게 돕니다.
5장의 Evaluator-Optimizer 패턴이 실제로 도는 모양입니다. 다만 평가자가 둘로 나뉘어서, 싼 것(스크립트)이 먼저 걸러내고 비싼 것(모델)이 나중에 봅니다.
5. 실습 4, 바퀴 사이를 잇는 상태 파일
지금까지는 절 하나였습니다. 절 다섯 개를 이어서 쓰려면 8-1의 다섯 번째 부품, 바퀴 사이를 잇는 상태가 필요합니다.
폴더 루트에 progress.md를 만들고 대기열을 적습니다.
# 진행 상황
## 대기열
- [ ] 01. var, let, const 차이
- [ ] 02. 함수 선언식과 화살표 함수
- [ ] 03. 배열 고차 함수 map, filter, reduce
- [ ] 04. 비동기, 콜백에서 async/await까지
- [ ] 05. 모듈 시스템 import, export
## 완료
(아직 없음)
## 막힌 자리
(아직 없음)
그리고 조건을 대기열 기준으로 씁니다.
/goal progress.md의 대기열이 전부 완료로 옮겨진다.
한 항목마다: harness 스킬로 원고를 만들고, artifacts/{번호}-{슬러그}.md로 저장하고,
check.sh와 reviewer를 통과시킨 뒤 progress.md를 갱신한다.
같은 항목에서 3턴 연속 실패하면 그 항목을 "막힌 자리"로 옮기고 다음 항목으로 넘어간다.
마지막 줄이 8-1의 세 번째 부품, 진전 없음 감지와 탈출구입니다. 한 항목에서 막혔다고 루프 전체가 멈추면 나머지 네 항목이 밤새 놀게 됩니다. 막힌 것은 옆으로 치워두고 계속 갑니다. 아침에 사람이 볼 자리는 "막힌 자리" 목록 하나입니다.
progress.md를 세션 시작 때마다 자동으로 읽히게 하려면 hook을 하나 더 붙입니다.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "cat",
"args": ["progress.md"]
}
]
}
]
}
}
이제 세션을 새로 열어도, 컨텍스트를 비워도, 다음 바퀴는 "지금 어디까지 왔는지"를 알고 시작합니다. 3장에서 본 "작업이 끝나면 새 대화로 끊기"를 루프에서도 그대로 쓸 수 있게 만드는 장치입니다.
6. 실습 5, 터미널을 닫고 자는 동안
여기까지는 창을 열어둔 채였습니다. 이번에는 한 번의 명령으로 끝까지 돌리고 끝나게 합니다.
claude -p "/goal progress.md의 대기열이 전부 완료로 옮겨진다. 한 항목마다 harness 스킬로 원고를 만들고 check.sh와 reviewer를 통과시킨 뒤 progress.md를 갱신한다. 같은 항목에서 3턴 연속 실패하면 막힌 자리로 옮기고 다음 항목으로 간다." \
--output-format stream-json --verbose \
--max-budget-usd 5 \
--permission-mode acceptEdits \
--allowedTools "Read Write Edit Bash(bash check.sh*)" \
> run.log
옵션 하나하나가 8-1의 부품과 대응합니다.
| 옵션 | 하는 일 | 대응하는 부품 |
|---|---|---|
-p "/goal ..." | 조건이 충족될 때까지 한 번에 돌림 | 멈춤 조건 |
--max-budget-usd 5 | 5달러를 넘기면 중단 | 예산 상한 |
--permission-mode acceptEdits | 파일 편집은 매번 묻지 않음 | 자율성 |
--allowedTools | 허용 도구를 명시적으로 좁힘 | 가드레일 |
--output-format stream-json | 진행 상황을 실시간으로 기록 | 관찰성 |
> run.log | 나중에 무슨 일이 있었는지 볼 수 있게 | 관찰성 |
--max-budget-usd와 --allowedTools는 처음 돌릴 때 반드시 같이 씁니다. 특히 --allowedTools를 좁게 잡는 습관이 중요합니다. 사람이 지켜보지 않는 루프에 넓은 권한을 주는 것은, 5장에서 본 "잘못된 호출의 비용이 글자에서 행동으로 바뀐다"를 밤새 반복하겠다는 뜻입니다.
아침에 run.log와 progress.md를 열어봅니다. 완료 목록, 막힌 자리, 그리고 각 원고가 artifacts/에 쌓여 있습니다. 여기서 사람이 하는 일은 결과를 판정하는 것 하나입니다.
7. 실습 6, 주기적으로 청소 도는 루프
마지막 실습은 성격이 다릅니다. 끝나는 지점이 없고, 계속 돌면서 어긋난 자리를 치우는 루프입니다. 1장에서 본 가비지 컬렉션 아이디어입니다.
.claude/loop.md를 만듭니다.
`artifacts/` 아래 원고를 전부 `bash check.sh`로 검사한다.
실패한 파일이 있으면 어느 규칙을 어겼는지 확인하고 최소한으로 고친다.
그다음 다음 세 가지를 살핀다.
- `CLAUDE.md`의 톤 규칙과 실제 원고들의 톤이 어긋난 자리
- 여러 원고에서 반복되는 같은 지적 (규칙으로 승격할 후보)
- `progress.md`의 "막힌 자리"에 새로 들어온 항목
고칠 게 없으면 "이상 없음" 한 줄로 답한다.
반복되는 지적을 발견하면 고치지 말고 `CLAUDE.md`에 추가할 규칙 문구를 제안만 한다.
그리고 돌립니다.
/loop 30m
30분마다 이 프롬프트가 실행됩니다. 마지막 두 줄이 이 루프의 핵심입니다. 같은 지적이 반복되면 그건 원고 문제가 아니라 규칙이 빠진 것이고, 그 판단은 사람이 해야 하니 제안만 하고 멈춥니다. 2장에서 "고치는 일이 곧 다음 글을 위한 학습"이라고 했던 자리를, 루프가 대신 찾아주는 셈입니다.
간격 없이 /loop만 입력하면 Claude가 매 바퀴 다음 간격을 스스로 정합니다. 고칠 게 많으면 짧게, "이상 없음"이 이어지면 길게 잡습니다.
8. 여섯 실습을 한 그림으로
방금 만든 것들이 어떻게 맞물리는지 한 번에 보면 이렇습니다.
파란 칸이 사람 자리입니다. 8-1의 첫 그림에서 사람 자리가 두 칸이었는데, 지금은 아래쪽 한 칸만 남았습니다. 그리고 그 칸에서 사람이 하는 일이 "다시 해줘"가 아니라 "이 판단을 규칙으로 승격할까" 로 바뀌었습니다.
이게 루프 엔지니어링이 실제로 바꾸는 자리입니다. 일이 줄어드는 게 아니라, 일의 종류가 바뀝니다.
9. 우리 하네스로 옮기려면
7장까지 만든 랜딩 하네스에 같은 것을 얹는다면 갈아 끼울 자리는 세 군데뿐입니다.
| 실습에서 | 랜딩 하네스에서 |
|---|---|
check.sh | 7장의 디자인 시스템 검사 스크립트 그대로 |
reviewer.md | brand.md의 카피 톤을 검수하는 사수 |
progress.md의 대기열 | 이번 주에 만들 페이지 브리프 목록 |
나머지(/goal 조건문의 모양, hook 설정, 헤드리스 명령의 옵션)는 거의 그대로 씁니다. 7장에서 "갈아 끼우지 않는 자리"를 정리했던 것과 같은 구조입니다.
이어서 이 루프를 실제로 운영에 둘 때 반드시 챙겨야 하는 안전장치와, 루프가 잘 도는 조직에서 오히려 생기는 새로운 위험을 봅니다.