Claude Code의 모델 부분만 모킹하여 비용 없이 실행하는 방법 (내부 로직 확인 및 테스트)
요약
본 글은 Claude Code의 모델 부분만 모킹하여 비용 없이 테스트하고 내부 로직을 확인하는 방법을 안내합니다. 실제 Anthropic 서버 호출 대신, Node.js를 사용하여 미리 정의된 답변을 반환하는 가짜 서버(모크)를 구축할 수 있습니다.
핵심 포인트
- Claude Code는 CLI와 모델로 분리되어 작동하며, 통신은 Messages API를 통해 이루어집니다.
- 실제 테스트 시 발생하는 비용과 비재현성 문제를 모킹으로 해결할 수 있습니다.
- Node.js만 사용하여 Anthropic의 SSE 형식에 맞는 최소한의 모크 서버 구현이 가능합니다.
Claude Code를 사용하거나, Claude Code와 결합하여 작동하는 hooks, MCP 서버, 도구를 만들다 보면 이런 문제에 직면하지 않으신가요?
- hooks의 출력이나 MCP 서버의 설명,
/compact
의 요약이 Claude(모델)에게 어떤 형태로 전달되는지 보고 싶다. - 실제 모델로 테스트하면 비용(사용량)이 발생하고, 응답도 매번 다르기 때문에 동일한 상황을 재현하기 어렵다. - Claude Code와 결합하여 작동하는 것을 CI에서 자동 테스트하고 싶다.
이 글에서는 Claude Code 중 모델 부분만, 미리 정해진 답을 반환하는 작은 서버(모크)로 대체하여 비용 없이 실행하는 방법을 소개합니다. 이를 위해 먼저 Claude Code가 어떻게 분리되어 작동하는지 살펴봅니다.
Claude Code는 CLI와 모델로 나뉘어 있다
Claude Code는 두 부분으로 나누어 작동합니다.
- 로컬 컴퓨터에서 실행되는 프로그램(
claude명령어): 설정이나 CLAUDE.md를 읽고, 모델에 전달할 내용을 취합하며, 도구를 실제로 실행한다. - Anthropic 서버에서 작동하는 모델(Claude): 전달받은 것을 읽고, 다음에 무엇을 말해야 할지 또는 어떤 도구를 어떤 인수로 호출할지 결정한다.
혼동을 피하기 위해, 이후부터는 전자를 CLI로, 후자를 모델로 부르겠습니다. 'Claude Code'는 이 두 가지를 합친 제품의 이름으로 사용합니다.
이 둘을 연결하는 것이 Anthropic의 API(Messages API)로 보내는 HTTP 요청입니다. CLI는 다음 세 가지를 취합하여 보내며,
| 언제 | 수신된 요청 | 내용 |
|---|---|---|
| CLI 실행 시 | HEAD /api/hello | 내용 없는 문의 (무엇을 위한 것인지는 확실하지 않음). 404를 반환해도 그대로 진행함 |
| 그림 1 | POST /v1/messages?beta=true | "다음 답변을 만들어줘"라는 요청 |
| 그림 4 | POST /v1/messages?beta=true | 그림 1과 같은 요청의 대화에 Bash 호출과 그 결과를 추가한 것 |
/v1/messages는 Messages API의 '대화를 전달하여 다음 답변을 받는' 창구입니다 (?beta=true는 CLI가 붙이는 것이므로 모킹에서는 신경 쓰지 않아도 됩니다). 그림 2와 5의 답변은 별도의 요청으로 보내지는 것이 아니라, 1과 4의 요청에 대한 응답으로 돌아옵니다. 모크가 답변해야 하는 것은 이 창구뿐입니다.
답변은 SSE로 반환
요청에는 stream: true가 붙어 있으므로, 답변은 SSE(Server-Sent Events. 답변을 작은 이벤트로 나누어 조금씩 보내는 형식)로 반환합니다. 아래 모크가 반환하는 답변을 curl -N으로 보면 다음과 같이 흘러옵니다.
event: message_start
data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"test","content":[],"stop_reason":null,"usage":{"input_tokens":100,"output_tokens":0}}}\nevent: content_block_start
...
최소한의 모크
Node.js만으로 작성할 수 있습니다. 시나리오(steps) 순서대로 답변을 반환하고, 받은 요청은 req/에 저장합니다.
// 모델 대신 답변하는 모크 (Anthropic의 Messages API인 척). 받은 요청을 req/에 저장하고, 시나리오의 답변을 반환
import { createServer } from 'node:http';
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
...
핵심은 세 가지입니다.
- 몇 번째 답변인지는 요청의 대화에 포함된
assistant발언 수로 결정합니다. CLI는 매번 대화를 모두 보내오므로, 모크는 상태를 가질 필요가 없습니다 -
답변은 위의 SSE 이벤트 순서(message_start→content_block_start→content_block_delta→content_block_stop→message_delta→message_stop)로 반환합니다 -
도구를 호출하게 할 때는,tool_use블록을 반환하고 인수는input_json_delta로 보냅니다.stop_reason은tool_use로 합니다.
실행하기
모크를 시작한 후, 다른 터미널에서 CLI를 실행합니다.
node mock-api.mjs
# 다른 터미널에서
mkdir -p /tmp/claude-home /tmp/work && cd /tmp/work
...
echo 를 실행했습니다.
ANTHROPIC_API_KEY는 비어있지 않으면 어떤 값이든 상관없습니다. 모크가 확인하지 않으므로 실제 키는 필요 없습니다 -
HOME을 일회용 폴더로 사용하면, 평소의~/.claude(설정・CLAUDE.md・hooks)와 섞이지 않고 대화 로그도 그곳에 작성됩니다 -
env -i로 다른 환경 변수도 제외합니다. 평소의 환경 변수 (CLAUDE_CODE_로 시작하는 것 등)가 적용되면, CLI가 모크에 보내는 요청 내용이 바뀔 수 있기 때문입니다 -
req/는 모크를 실행한 폴더로 지정할 수 있습니다.
env -i로 HTTPS_PROXY 같은 것도 제외되므로, 보통은 프록시는 신경 쓰지 않아도 됩니다. 프록지를 통하지 않으면 외부로 나갈 수 없는 환경에서, HTTPS_PROXY
전달할 때는 NO_PROXY=127.0.0.1도 함께 전달합니다.
이것을 전달하지 않으면, 모크(mock)에 대한 요청까지 프록시로 전송됩니다.
내용 읽기
req/
에 저장된 요청에는 실제 모델이라면 전달될 모든 것이 들어 있습니다. jq로 읽어봅니다.
# 도착한 요청 목록
jq -r '"(.method) (.url)"' req/*.json
# 도구 이름 (그림 1의 요청)
...
HEAD /api/hello
POST /v1/messages?beta=true
POST /v1/messages?beta=true
...
그림 4의 대화에는 발언이 총 4개 들어 있었습니다.
- user: 'hello를 표시해' 등의 발언
- system: 작업 폴더 정보나 스킬 목록 등 CLI가 추가한 서문
- assistant: 그림 2에서 모크가 반환한 Bash 호출
- user: 그림 3에서 CLI가 실행한 결과
3과 4는 tool_use_id (toolu_1)
로 연결되어 있습니다.
{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_1", "name": "Bash", "input": {"command": "echo hello", "description": "인사"}}]}
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "hello", "is_error": false}]}
예시: hooks의 출력은 어디에 들어가는가
작업 폴더의 .claude/settings.json에 발언마다 문장을 내보내는 hooks(UserPromptSubmit)를 추가하여 다시 실행해 보았습니다.
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "echo '오늘은 금요일입니다 (표식: HOOK-OUTPUT)'" } ] } ] } }
표식은 위의 두 번째 발언(system) 안에 다음과 같이 들어 있었습니다.
UserPromptSubmit hook success: 오늘은 금요일입니다 (표식: HOOK-OUTPUT)
hooks의 출력은 발언 자체에 섞이는 것이 아니라, CLI가 추가하는 서문 속에 'UserPromptSubmit hook success:'라는 제목을 붙여 전달되고 있음을 알 수 있습니다. 이전 기사에서도 같은 방식으로 다음 사항들을 확인했습니다.
에러 반환하기
API가 혼잡할 때(HTTP 529)에도 CLI나, 자신의 hooks 및 도구가 어떻게 작동하는지 시험해 볼 수 있습니다. 모크에 몇 줄을 추가하여 정한 횟수만큼 529를 반환하도록 했습니다.
let failed = 0; // 파일 상단에 추가
// const body = JSON.parse(raw); 다음에 추가
// FAIL_TIMES 횟수만큼 혼잡하다는 (HTTP 529) 응답을 보냄
...
CLI는 529를 받으면 잠시 기다린 후 다시 보내줍니다. 재전송 횟수는 환경 변수 CLAUDE_CODE_MAX_RETRIES로 변경할 수 있습니다 (기본 횟수는 확인하지 못했습니다).
| 529 반환 횟수 | CLAUDE_CODE_MAX_RETRIES |
결과 |
|---|---|
| 2회 | 3 | 2번 재전송 후, 정상적으로 응답이 나옴 |
| 5회 | 2 | 처음 1회와 재전송 2회를 합쳐 총 3번 전송하고 포기하며 에러를 반환함 |
포기했을 때는 다음과 같이 나왔습니다.
API Error: Repeated 529 Overloaded errors. The API is at capacity — this is usually temporary. Try again in a moment. If it persists, check your inference gateway (127.0.0.1:8787).
테스트로 만들기
여기까지는 Node.js의 node:test로 테스트했습니다. 테스트에서는 모크(mock)와 CLI를 실행하여, CLI의 출력과 모크에 전달된 Bash 결과를 확인합니다.
// 실제 CLI를 모델 대체용 모크로 구동하는 테스트 (node --test로 실행)
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
...
$ node --test claude.test.mjs
ok 1 - Bash에서 echo를 실행하고 결과가 모델에 도달하는지 확인
# tests 1
...
모델의 답변을 기다리지 않기 때문에, 1회당 1~2초 만에 끝납니다. 비용도 발생하지 않아 CI에서도 여러 번 돌려도 문제가 없습니다.
CI에서 실행하기
CI에서는 CLI를 버전별로 등록해 두면, Claude Code가 업데이트되어 테스트가 갑자기 실패하는 것을 방지할 수 있습니다. 실제 API 키는 필요하지 않습니다. GitHub Actions의 경우, 예를 들어 다음과 같이 작성할 수 있습니다 (tanacode의 매일 확인 방식과 동일한 형태를 이 기사의 테스트용으로 축소한 것입니다. 이 형태로 실제로 구동하고 있지는 않습니다).
jobs:
test:
runs-on: ubuntu-latest
...
모크로는 확인할 수 없는 것들
모크로 대체하는 것은 모델 부분뿐이며, CLI는 실제입니다. 그럼에도 불구하고 다음 항목들은 실제와 다른지 여부를 확인할 수 없습니다.
모델의 판단 자체: 답변은 대본대로 나오기 때문에, 모델이 어떻게 생각하고 어떤 도구를 선택했는지 여부는 확인할 수 없습니다.
로그인이 필요한 것들: claude.ai 계정 로그인이 전제되는 기능(스마트폰 등에서 조작하는 Remote Control이나 /usage의 사용량 한도 등). API 키로 구동하기 때문에 사용할 수 없습니다.
API 키로 구동할 때 나오지 않는 도구: ToDo 도구(TodoWrite, TaskCreate)는 API 키로 실행하면 나타나지 않습니다 (tanacode 개발 과정에서 확인했으며, 이번 도구 목록에도 포함되지 않았습니다).
보너스: tanacode는 매일 이것으로 Claude Code의 새로운 버전을 확인하고 있습니다
저는
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기