Loop 엔지니어링 실습 — 메아리방 vs 거부 신호 루프¶
이 실습의 목적: concept-loop-engineering 의 핵심 한 문장 — "거부할 수 있는 무언가(테스트·타입체크·에러)가 없는 루프는 메아리방" — 을 직접 코드로 짜서 체험합니다. "루프를 작성한다"는 감각을 손에 익히는 게 핵심입니다.
문서 전체에서 쓰는 두 용어를 먼저 잡습니다. 메아리방(echo chamber)은 에이전트의 "다 됐어요" 자기 보고를 검증 없이 믿고 종료하는 루프를, 거부 신호(reject signal)는 테스트·타입체크·빌드처럼 실패를 객관적으로 돌려줘 그 종료를 막는 검사를 가리킵니다.
시간: 8분 (셋업 2분 + Before 메아리방 2분 + 거부 신호 추가 1분 + After 검증 루프 2분 + 실제 Claude 연결 1분)
진행 흐름: 1분 이론으로 "관찰(Observe)이 무엇이냐"의 두 갈래를 확인 → 비교 실험용 공통 재료 만들기(Step 1) → 거부 신호 없는 메아리방 체험(Step 2) → 종료 조건 교체(Step 3) → 검증 루프 체험(Step 4) → 차이 정리(Step 5) → 가짜 에이전트를 진짜 Claude로 교체(Step 6) → 토큰 비용 심화(Step 6.5).
언제 보면 좋은가: concept-loop-engineering 를 읽은 직후. guide-harness-demo(하네스 5분 데모)의 다음 단계 — 하네스가 "환경"을 설계했다면, 루프는 "메커니즘 자체"를 설계합니다.
전제: Node 18+ 설치. (Step 6의 실제 Claude 연결만 Claude Code 로그인 필요 — 나머지는 토큰 0으로 누구나 재현 가능)
✅ 실행 검증됨 (2026-06-29, Node v26): Step 1·2·4를 실제로 돌려 본문 수치를 확인했습니다 — 후보 (A)·(B)는 실패하고 (C)만 통과(결정적), Step 2 메아리방은 20회 중 13회(≈2/3)가 깨진 채 "완료" 종료, Step 4 검증 루프는 통과 시 정상 종료. "드물게 약 4%"는 이론값 (2/3)⁸=3.9%로 정확합니다. (Step 6은 토큰이 들어 본 검증에서 제외 — 명령 구문은 공식 헤드리스 docs 기준)
왜 이 실습인가 — 1분 이론¶
현대 에이전트 루프는 Princeton·Google의 ReAct(Reason + Act) 패턴에 뿌리를 둡니다 (arXiv 2210.03629):
여기서 관찰(Observe)이 무엇이냐가 루프의 운명을 가릅니다.
- 관찰이 에이전트 자기 보고("다 됐어요")면 → 모델이 자기 출력에 동의하는 메아리방입니다. Sonar의 표현대로 "두 낙관주의자가 서로 동의하는 것".
- 관찰이 객관적 거부 신호(테스트·타입체크·빌드)면 → 사실에 부딪혀 교정됩니다. "A failing build is a fact; an opinion is a starting point" (실패한 빌드는 사실이고, 의견은 출발점일 뿐).
이 실습은 같은 루프를 거부 신호 없이 / 있게 두 번 실행해 그 차이를 눈으로 봅니다.
Step 1 — 데모 디렉터리 + 검증 대상 (2분)¶
위 이론의 두 루프를 같은 조건에서 비교하려면 공통 재료가 필요합니다 — 코드를 내놓는 "에이전트"와, 그 코드를 채점하는 "검증자"입니다. 이 Step에서 그 둘을 만듭니다. 풀 문제는 회문(palindrome) 검사 함수 — 가짜 "에이전트"가 후보 구현을 내놓고, 테스트가 그것을 채점합니다.
실습 위치·실행
- 위치:
~/loop-demo(이 Step에서 새로 만드는 데모 디렉터리) - 만들 파일:
agent.js— 후보 구현을 무작위로 내놓는 가짜 에이전트 /test.js— pass/fail을 종료 코드로 돌려주는 거부 신호 - 실행: 아래 코드블록 3개를 차례로 붙여넣어 실행합니다.
먼저 실습 전용 디렉터리를 만들고 이동합니다:
가짜 코딩 에이전트 — 실제 LLM의 비결정성을 흉내 내, 후보 3개 중 하나를 무작위로 출력합니다 (1개만 정답):
cat > agent.js << 'EOF'
// 가짜 코딩 에이전트: isPalindrome 후보를 무작위로 하나 출력한다.
// 후보 3개 중 (C)만 모든 케이스를 통과한다 — 실제 에이전트의 "가끔 맞고 가끔 틀림"을 흉내.
const candidates = [
// (A) 정규화 없음 — 대소문자·공백·구두점이 있으면 틀림
"module.exports = (s) => s === [...s].reverse().join('');",
// (B) 소문자화만 — 공백·구두점이 있으면 여전히 틀림
"module.exports = (s) => { const t = s.toLowerCase(); return t === [...t].reverse().join(''); };",
// (C) 완전 정규화 — 통과
"module.exports = (s) => { const t = s.toLowerCase().replace(/[^a-z0-9]/g, ''); return t === [...t].reverse().join(''); };",
];
const pick = candidates[Math.floor(Math.random() * candidates.length)];
process.stdout.write(pick + "\n");
EOF
거부 신호 역할의 검증자 — 객관적으로 pass/fail 을 돌려줍니다. 채점 대상은 solution.js(에이전트가 내놓은 후보 구현을 저장할 파일 — Step 2에서 처음 생성)이고, 통과 못 하면 종료 코드 1:
cat > test.js << 'EOF'
const isPalindrome = require('./solution');
const cases = [
['racecar', true],
['A man, a plan, a canal: Panama', true], // 공백·구두점·대소문자
['No lemon, no melon', true], // 공백·구두점
['hello', false],
['', true],
];
let failed = 0;
for (const [input, expected] of cases) {
let got;
try { got = isPalindrome(input); } catch (e) { got = 'ERROR:' + e.message; }
if (got !== expected) {
console.error(` ❌ isPalindrome(${JSON.stringify(input)}) = ${got} (기대값 ${expected})`);
failed++;
}
}
if (failed > 0) { console.error(`FAIL — ${failed}개 케이스 실패`); process.exit(1); }
console.log('PASS — 5개 케이스 전부 통과');
EOF
후보 (A)·(B)는
"A man, a plan…"같은 케이스에서 깨지고, (C)만 전부 통과합니다. 즉 에이전트가 (C)를 뽑을 때까지가 "정답"입니다.
Step 2 — Before: 거부 신호 없는 루프 (메아리방) (2분)¶
재료가 준비됐으니 먼저 나쁜 쪽부터 체험합니다. 에이전트를 1회 호출하고, 검증 없이 그 말을 믿고 끝냅니다 — 종료 조건이 "에이전트의 자기 보고"인 루프입니다.
실습 위치·실행
- 위치:
~/loop-demo(Step 1에서 만든 디렉터리) - 만들 파일:
solution.js— 에이전트가 내놓은 후보 구현 (아래 명령이 자동 생성) - 실행: 아래 블록을 그대로 붙여넣고, 확률 차이가 보이도록 여러 번 반복 실행합니다.
아래 블록 한 덩이가 루프 1회분 전체입니다 — 코드를 생성하고, 자기 보고를 믿고, 검증 없이 끝냅니다:
cd ~/loop-demo
node agent.js > solution.js
echo "🤖 에이전트: '구현 완료했습니다 ✅'"
echo "→ 거부 신호가 없는 루프는 이 보고를 그대로 믿고 종료한다."
echo "--- 생성된 코드 ---"; cat solution.js
echo "--- 그런데 실제로 돌려보면? ---"
node test.js || echo "💥 깨져 있다 — 그러나 루프는 이미 '완료'라고 보고했다 (= 메아리방)"
여러 번 실행해 보면 약 2/3 확률 — 후보 3개 중 (A)·(B) 2개가 오답이므로 — 로 깨진 코드인데 "완료"로 종료됩니다. 문서 상단 실행 검증 노트의 "20회 중 13회"가 바로 이 확률의 실측값입니다. 종료 조건이 자기 보고이기 때문입니다 — concept-loop-engineering 의 "8번 전 실패를 기억 못 하고 같은 길을 가는" 나쁜 루프입니다.
Step 3 — 거부 신호를 루프에 넣기 (1분)¶
Step 2가 무너진 지점은 코드 생성이 아니라 종료 조건이었습니다. 그래서 바꿀 것도 단 하나입니다: 종료 조건을 "에이전트의 말" → "테스트의 종료 코드"로. node test.js 가 exit 0 이어야만 끝냅니다. 이게 ReAct 의 관찰(Observe) 을 객관화하는 것입니다.
Step 4 — After: 검증 루프 (통과까지 재시도) (2분)¶
Step 3의 한 줄짜리 결정을 실제 루프로 옮깁니다. 재료는 Step 1의 두 파일 그대로이고, 종료 조건만 테스트의 종료 코드로 바뀌었습니다.
실습 위치·실행
- 위치:
~/loop-demo(Step 1의agent.js·test.js그대로 사용) - 실행: 아래 for 루프를 붙여넣어 실행합니다.
Step 2와 골격은 같고, 종료만 node test.js의 종료 코드 0에 걸어 둔 루프입니다:
cd ~/loop-demo
for i in $(seq 1 8); do
echo "── 사이클 $i ──"
node agent.js > solution.js # Act: 에이전트가 코드 생성
if node test.js; then # Observe: 거부 신호(테스트)가 '사실'을 반환
echo "✅ 사이클 $i 에서 통과 — 종료 조건 충족, 루프 종료"
break
fi
echo "↻ 실패 — 거부 신호가 루프를 한 번 더 돌린다 (Reason → 다음 Act)"
done
이번엔 통과하는 구현이 나올 때까지 루프가 반복됩니다. 거부 신호(테스트의 exit code)가 사이클을 제어합니다.
가짜 에이전트가 무작위라, 드물게(약 4%) 8 사이클 안에 (C)가 안 나올 수 있습니다 — 사이클당 실패 확률 2/3이 8번 연속될 확률 (2/3)⁸ ≈ 3.9%입니다. 그땐 다시 실행하세요. 실제 에이전트라면 실패를 피드백받아 다음 시도가 개선됩니다 → Step 6.
seq 1 8 이라는 반복 상한(최대 8사이클)에 주목합니다. 업계 권고는 보통 15~25 스텝이며, 상한 없는 루프는 토큰·시간을 폭주시킵니다. 종료 조건은 ① 검증 통과(goal) ② 반복 상한(resource) 둘 다 있어야 합니다.
Step 5 — 차이 표 (직접 채워보기, 30초)¶
두 루프를 모두 실행해 봤으니, 관찰한 차이를 직접 채워 넣으며 정리합니다. 요약을 읽는 것보다 방금 본 것을 스스로 언어화해야 기억에 남습니다.
| Before (거부 신호 없음) | After (테스트 = 거부 신호) | |
|---|---|---|
| 깨진 코드로 종료될 수 있나 | __ | __ |
| 종료 조건의 정체 | __ (에이전트 자기 보고?) | __ (객관적 검증?) |
| 같은 실수를 반복하나 | __ | __ |
| 사람이 매번 확인해야 하나 | __ | __ |
한 줄 소감: ________
Step 6 — 실제 Claude로 (선택, 1분)¶
Step 4의 가짜 에이전트는 무작위라 실패해도 다음 시도가 나아지지 않고 주사위를 다시 던질 뿐입니다. 이제 가짜 에이전트를 진짜 Claude Code 헤드리스(headless) 호출로 바꿉니다 — 헤드리스는 대화 화면 없이 터미널 명령 한 줄로 Claude를 실행하고 결과만 돌려받는 방식입니다. 핵심은 실패한 테스트 출력을 stdin 으로 피드백해 다음 시도가 실제로 개선되게 하는 것입니다 (공식 패턴: cat … | claude -p "…").
실습 위치·실행
- 위치:
~/loop-demo(Step 1의test.js그대로 사용) - 만들 파일:
solution.js— 일부러 틀린 구현으로 덮어쓰고 루프가 고치게 합니다 - 실행: Claude Code 로그인 상태에서 아래 블록을 붙여넣어 실행합니다 (토큰이 소모됩니다).
루프 골격은 Step 4 그대로이고, 에이전트 호출 자리만 claude -p 파이프라인으로 바뀌었습니다:
cd ~/loop-demo
# 일부러 틀린 구현으로 시작 (정규화 없음 → 공백·구두점 케이스 실패)
echo "module.exports = (s) => s === [...s].reverse().join('');" > solution.js
for i in $(seq 1 5); do
echo "── 사이클 $i ──"
if node test.js > test.log 2>&1; then
echo "✅ 통과 — 종료"; cat test.log; break
fi
echo "↻ 실패 — 에러를 Claude 에 피드백해 수정 요청"
cat test.log | claude -p "solution.js 의 isPalindrome 구현이 아래 테스트에서 실패한다. 근본 원인을 찾아 solution.js 만 수정하라. 에러를 숨기지 말 것. 대소문자·공백·구두점은 무시해야 한다." \
--allowedTools "Read,Edit,Bash(node *)"
done
claude -p(=--print)는 비대화형으로 1회 실행 후 종료하므로for루프로 감싸기에 딱 맞습니다 (공식 docs).--allowedTools로 도구를 좁혀 자동 승인합니다 — 프롬프트 없이 무인 실행됩니다.- 이게 Reflexion·Self-Refine 의 핵심입니다: 실패 신호를 언어로 받아 다음 시도를 개선 (Reflexion · Self-Refine).
⚠️ 토큰 비용: 사이클마다 모델을 호출합니다. Addy Osmani 의 신중론 — "토큰 비용에 절대적으로 주의". 반드시 반복 상한(위 코드의
seq 1 5)과 검증 게이트를 두고, 무인 루프는 비용을 모니터링해야 합니다. src-copilot-token-pricing 의 종량제 전환과 같은 맥락입니다.
Step 6.5 — 토큰 비용 심화 (무인 루프의 진짜 리스크)¶
검증 루프의 장점("사람 없이 통과까지 돈다")은 그대로 비용 리스크입니다 — 약한 게이트 + 높은 상한이 만나면 루프가 헛돌며 토큰을 태웁니다. Osmani의 검증된 경고:
"Verification is still on you. A loop running unattended is also a loop making mistakes unattended." "you absolutely have to be careful about token costs (usage patterns can vary wildly if you are token rich or poor)."
봉투 뒷면 비용 모델¶
무인 루프 1회 비용 ≈ (사이클 수) × (사이클당 토큰). 사이클당 토큰을 키우는 3대 요인:
| 요인 | 폭증 형태 | 줄이는 법 |
|---|---|---|
| 컨텍스트 크기 | 매 사이클 전체 저장소·전체 로그를 다시 첨부 | 실패 diff·로그 꼬리만 전달 (이 실습의 cat test.log처럼) |
| 사이클 수 | 게이트가 약해 통과 판정이 안 나 무한 근접 | 하드 상한(max N) + 토큰 예산, K회 실패 시 사람 에스컬레이션 |
| 서브에이전트 | 사이클마다 추가 모델 호출 | "두 번째 의견이 값어치 할 때만" (Osmani) |
핵심 완화책 — 게이트를 모델 앞에 두기¶
이 실습 Step 6의 형태가 이미 정답을 담고 있습니다: 결정적 검증(node test.js)을 먼저 실행하고, 실패할 때만 모델을 호출합니다. 로컬 테스트·타입체크·린트는 토큰 0입니다.
# 비용 최적 패턴: 무료 게이트 통과면 모델을 아예 안 부른다
if node test.js > test.log 2>&1; then
echo "✅ 이미 통과 — 모델 호출 0회, 토큰 0"
else
cat test.log | claude -p "…수정…" --allowedTools "Read,Edit,Bash(node *)"
fi
→ "모델을 매 사이클 부릅니다"가 아니라 "무료 검증이 거부했을 때만 부릅니다". 거부 신호는 루프 품질만이 아니라 토큰 절약 장치이기도 합니다.
종료 조건은 ① 검증 통과(goal) ② 반복 상한(resource) ③ 토큰 예산(budget) 세 가지여야 합니다. 셋 중 하나라도 빠지면 무인 루프는 조용히 비용을 흘립니다 — Sonar: "a loop doesn't fail loudly, it fails quietly."
정리 (30초)¶
실습이 끝났으면 데모 디렉터리를 삭제합니다. 다시 해보고 싶으면 Step 1부터 2분이면 재구성됩니다.
좋은 루프가 답해야 할 체크리스트¶
concept-loop-engineering 의 4가지 설계 질문 + 외부 조사로 보강:
- 이 루프의 종료 조건은 무엇인가? verify(테스트·타입체크)로 표현되는가, 아니면 자기 보고인가?
- 루프 안에 거부할 수 있는 무언가가 있는가? (실패를 반환할 수 있는 객관적 검사)
- 반복 상한(max iterations)과 토큰 예산이 있는가? (업계 권고 15~25 스텝)
- 한 사이클이 8번 전 실패를 기억하는가, 아니면 같은 길을 다시 가는가? (피드백 전달)
- 재시도해도 안 될 때의 다음 행동(사람에게 에스컬레이션)은?
- 사이클 결과가 사람에게 어디서·어떻게 보고되는가?
같은 인사이트 패턴 — "거부 신호 없는 자동화는 폭주한다"¶
이 실습이 보여준 원리는 위키 전반에 반복됩니다 (concept-loop-engineering 에 누적):
| 영역 | 폭주 시나리오 | 거부 메커니즘 | 참조 |
|---|---|---|---|
| AI 루프 | 검증 없이 자기 출력에 동의 → 메아리방 | 테스트·타입체크를 루프 안에 (이 실습) | concept-loop-engineering |
| Hooks | 위험 명령 자유 실행 → 사고 | guard.sh exit 1 → 도구 차단 |
concept-claude-hooks |
| 멀티 에이전트 | 단일 에이전트 자기검증 통과 편향 | Critic 의 CONDITIONAL REJECT |
concept-multi-agent-pattern |
| 선언 층 | 부정 명령이 잊힘 | STOP 트리거 → 명시적 중단 조건 | concept-claude-md |
→ 공통 원리: 자동 사이클에는 반드시 밀어내는 신호(reject·timeout·exit code) 가 짝지어 있어야 합니다.
원본·외부 출처¶
개념·발화 (2026-06): concept-loop-engineering / src-loop-engineering (1차 출처 검증 2026-06-29)
- Addy Osmani Loop Engineering (2026-06-07, ✅용어 명명 1차 글) · Boris Cherny Acquired "write loops" (⚠️자구·날짜 매체별 편차) · Peter Steinberger X (⚠️402, "650만 조회"는 2차 주장)
이론 (1차 출처):
- ReAct — Yao et al. 2022, arXiv 2210.03629
- Reflexion — Shinn et al. 2023, arXiv 2303.11366
- Self-Refine — Madaan et al. 2023, arXiv 2303.17651
- "검증 없는 루프 = 단순 자동화" — Sonar 블로그
구현 (공식):
- Claude Code 헤드리스 모드 — code.claude.com/docs/headless
- 모범 사례 (검증 게이트·Stop 훅) — code.claude.com/docs/best-practices
관련 페이지¶
- concept-loop-engineering — 이 실습의 이론 (메커니즘 자체를 설계)
- guide-harness-demo — 직전 단계: 하네스 5분 데모 (환경 설계)
- concept-claude-hooks — back-pressure 가 "거부할 수 있는 무언가" 의 또 다른 구현
- concept-multi-agent-pattern — Critic 이 거부 메커니즘의 또 다른 구현
- concept-harness-engineering — 직전 패러다임 (환경)
- src-copilot-token-pricing — 루프의 토큰 비용 폭증 위험