2026.02.09AI 개발 패턴
claude-codelibrarycode-reusepythonnodejsintegration-testing

Claude Code 공용 라이브러리 만들기 — Node.js + Python 래퍼 개발과 통합 검증

배경: 프로젝트마다 반복되는 CLI 호출 코드

Claude Code CLI를 여러 프로젝트에서 사용하다 보면, 거의 동일한 코드가 반복됩니다. child_process.spawn으로 프로세스를 실행하고, stdout을 수집하고, JSON을 파싱하고, 타임아웃을 걸고, rate limit을 감지하는 로직이 프로젝트마다 미묘하게 다른 형태로 존재하게 됩니다.

블로그 자동 생성 시스템, 노트 편집기, 주식 분석 등 여러 프로젝트가 Claude Code CLI를 사용하는 상황에서, 이 중복은 점점 부담이 됩니다. CLI 업데이트로 출력 형식이 바뀌면 각 프로젝트를 개별 수정해야 하고, 에러 처리 방식도 제각각이 됩니다.

이 문제를 해결하기 위해 claude-lib라는 공용 라이브러리를 만들었습니다. Node.js 코어 라이브러리와 Python 래퍼로 구성됩니다.

설계 결정: HTTP 게이트웨이 vs 로컬 라이브러리

처음에는 HTTP 게이트웨이 서비스를 구상했습니다. 중앙 서버가 Claude Code CLI 세션을 관리하고, 각 프로젝트는 HTTP 요청으로 호출하는 방식입니다.

초기 구상 (HTTP 게이트웨이)

[Blog] ──HTTP──▶ [Claude Gateway :5000] ──spawn──▶ [Claude CLI] [Note] ──HTTP──▶ [Claude Gateway :5000] [Stock] ──HTTP──▶ [Claude Gateway :5000]

하지만 검토해보니 단점이 더 많았습니다.

  • 같은 서버 내 통신인데 HTTP 레이어를 추가하는 것은 불필요한 오버헤드
  • 게이트웨이 장애 시 모든 프로젝트가 영향을 받는 단일 장애점
  • 관리할 PM2 프로세스가 하나 더 늘어남
  • 대형 프롬프트 전달 시 HTTP 바디 크기 제한 문제

결론적으로, 로컬 라이브러리 방식을 선택했습니다.

최종 구조 (로컬 라이브러리)

[Blog] ──import──▶ [claude-lib] ──spawn──▶ [Claude CLI] [Note] ──import──▶ [claude-lib] [Stock] ──import──▶ [claude-lib-python] ──subprocess──▶ [Claude CLI]

라이브러리 구조: 6개 모듈로 분리

claude-lib의 모듈 구성은 다음과 같습니다.

claude-lib/src/ ├── index.ts # 진입점 (re-export) ├── types.ts # 타입 정의 ├── executor.ts # CLI 실행 엔진 (핵심) ├── session.ts # 세션 관리 ├── stream-parser.ts # 스트림 파싱 ├── logger.ts # 로깅 및 통계 └── config.ts # 설정 관리

외부 의존성은 제로입니다. TypeScript와 @types/node만 devDependencies로 사용하고, 런타임 의존성이 없습니다.

핵심 구현: 3가지 실행 모드

1. 일괄 응답 모드

가장 기본적인 모드입니다. CLI를 실행하고, stdout 전체를 수집한 뒤 JSON으로 파싱합니다.

typescript import { executeOneShot } from 'claude-lib';

const result = await executeOneShot({ prompt: '1+1은?', caller: 'blog', }); console.log(result.result); // "2"

내부적으로는 spawn으로 프로세스를 실행하고, --output-format json 플래그를 전달합니다.

2. 스트리밍 모드

긴 응답을 점진적으로 받아야 할 때 사용합니다. AsyncIterable을 반환합니다.

typescript const stream = await executeOneShot({ prompt: '긴 설명을 해줘', stream: true, caller: 'blog', });

for await (const event of stream) { if (event.type === 'text') { process.stdout.write(event.content); } }

내부적으로 readline 모듈을 사용해 stdout을 줄 단위로 읽고, 이벤트 큐 기반으로 비동기 이터레이터를 구현합니다.

3. 세션 모드

멀티턴 대화가 필요할 때 사용합니다. 첫 요청에서 sessionId를 추출하고, 이후 요청에 --resume 플래그를 전달합니다.

typescript import { createSession } from 'claude-lib';

const session = await createSession({ prompt: '너의 이름을 테스트봇이라고 기억해줘.', caller: 'blog', });

const reply = await session.send({ prompt: '방금 기억한 이름이 뭐야?', }); console.log(reply.result); // "테스트봇"

await session.end();

스트림 파서: CLI 출력을 표준 이벤트로

Claude Code CLI의 stream-json 출력 형식은 줄 단위 JSON입니다. 하지만 이벤트 타입이 다양하므로 이를 5가지 표준 이벤트로 매핑합니다.

typescript // CLI 출력 타입 → 라이브러리 이벤트 타입 // 'assistant' → 'text' (전체 텍스트) // 'content_block_delta' → 'text' (스트리밍 델타) // 'tool_use' → 'tool_use' // 'tool_result' → 'tool_result' // 'result' → 'done' (세션ID 포함) // 'error' → 'error'

특히 assistant 타입은 message.content 배열에서 type: 'text'인 블록만 추출해서 합칩니다. content_block_deltadelta.type === 'text_delta'인 경우만 처리합니다.

typescript case 'content_block_delta': if (data.delta && typeof data.delta === 'object') { const delta = data.delta as Record<string, unknown>; if (delta.type === 'text_delta' && typeof delta.text === 'string') { return { type: 'text', content: delta.text }; } } return null;

Python 래퍼: 동일 인터페이스, 동일 설정

Python 래퍼의 핵심 설계 원칙은 Node.js 버전과 완벽한 호환성입니다.

공유하는 것들

항목경로/형식
설정 파일~/.claude-lib/config.json
로그 디렉토리~/.claude-lib/logs/
로그 포맷camelCase JSON Lines
CLI 인자 구성동일한 플래그 순서
이벤트 타입동일한 매핑 로직
에러 분류timeout, rate_limit, cli_error, process_error

Python에서의 사용법은 거의 동일합니다.

python from claude_lib import execute_one_shot, create_session

일괄 응답

result = execute_one_shot( prompt='1+1은?', caller='stock' ) print(result['result']) # "2"

스트리밍 (async)

import asyncio

async def main(): stream = execute_one_shot( prompt='설명해줘', stream=True, caller='stock' ) async for event in stream: if event['type'] == 'text': print(event['content'], end='')

asyncio.run(main())

Node.js와 Python의 구현 차이

기능Node.jsPython
프로세스 실행child_process.spawnsubprocess.run (batch) / asyncio.create_subprocess_exec (stream)
스트림 읽기readline + 이벤트 큐asyncio.subprocess + readline()
일괄 응답async (Promise)동기 (subprocess.run)
타임아웃setTimeout + SIGTERM/SIGKILLtime.monotonic() 데드라인 체크
설정 키camelCasesnake_case (로드 시 자동 변환)

가장 큰 차이는 일괄 응답 모드입니다. Node.js는 async 함수지만, Python은 동기 함수로 구현했습니다. subprocess.run이 블로킹 호출이므로 async로 감쌀 이유가 없었습니다.

팀 에이전트 병렬 개발

Node.js 코어와 Python 래퍼는 Claude Code의 팀 에이전트 기능으로 병렬 개발했습니다.

[Team Lead] ── 설계 문서 작성, 태스크 분배 ├── [Worker 1] Node.js 프로젝트 초기화 + 핵심 모듈 개발 ├── [Worker 2] Python 래퍼 프로젝트 개발 └── [Worker 3] 통합 검증 및 검수

Worker 1이 Node.js 코어를 만드는 동안 Worker 2가 Python 래퍼를 개발하고, 두 Worker 작업이 끝난 후 Worker 3이 통합 검증을 수행하는 구조입니다.

통합 검증에서 발견한 스트리밍 파싱 버그

통합 검증 단계에서 중요한 버그를 발견했습니다. Node.js 스트리밍 테스트에서 stream-json 출력에 --verbose 플래그가 누락되면 이벤트가 제대로 파싱되지 않는 문제였습니다.

Claude Code CLI는 --output-format stream-json 사용 시 반드시 --verbose 플래그를 함께 전달해야 상세 이벤트(content_block_delta, tool_use 등)를 출력합니다. 이것 없이는 최종 result 이벤트만 나옵니다.

typescript // executor.ts - stream-json 시 --verbose 필수 const args: string[] = [ '-p', options.prompt, '--output-format', options.stream ? 'stream-json' : 'json', ];

if (options.stream) { args.push('--verbose'); }

개별 프로젝트에서 CLI를 직접 호출할 때는 시행착오로 이 사실을 알고 있었지만, 라이브러리 코드에는 초기에 누락되었습니다. 통합 테스트에서 스트리밍 응답이 text 이벤트 없이 done 이벤트만 반환되는 것을 확인하고, 원인을 추적하여 --verbose 플래그를 추가했습니다.

이 버그가 개별 프로젝트 레벨이 아닌 공용 라이브러리에서 수정되었기 때문에, 향후 모든 프로젝트가 동일한 수정 혜택을 받게 됩니다.

에러 처리 전략: 5가지 유형 분류

claude-lib는 모든 에러를 ClaudeError 클래스로 래핑하고, errorType으로 원인을 구분합니다.

typescript type ClaudeErrorType = | 'timeout' // 타임아웃 초과 → SIGTERM → 3초 대기 → SIGKILL | 'rate_limit' // stderr에서 패턴 매칭 (429, overloaded 등) | 'cli_error' // exit code !== 0 | 'process_error' // spawn 자체 실패 | 'parse_error'; // JSON 파싱 실패

특히 프로세스 종료는 2단계입니다. 먼저 SIGTERM을 보내고, 3초 후에도 살아있으면 SIGKILL을 보냅니다.

typescript function killProcess(proc): Promise<void> { return new Promise((resolve) => { proc.kill('SIGTERM'); const killTimer = setTimeout(() => { if (proc.exitCode === null && !proc.killed) { proc.kill('SIGKILL'); } resolve(); }, 3000); proc.on('exit', () => { clearTimeout(killTimer); resolve(); }); }); }

로깅: Node.js와 Python이 같은 로그 파일 사용

두 라이브러리 모두 ~/.claude-lib/logs/ 디렉토리에 일별 JSON Lines 파일로 로그를 기록합니다.

{"timestamp":"2026-02-09T15:30:00+09:00","caller":"blog","type":"one-shot","prompt":"블로그 포스트를 작성해...","stream":false,"status":"success","durationMs":12345}

Python에서 기록한 로그도 camelCase 키를 사용합니다. 이렇게 하면 getUsage() 통계 조회 시 Node.js든 Python이든 구분 없이 통합 집계가 가능합니다.

핵심 정리

  1. HTTP 게이트웨이보다 로컬 라이브러리가 나은 경우가 있습니다. 같은 서버 내 프로젝트 간 공유라면, 네트워크 오버헤드 없는 라이브러리 import가 더 단순합니다.

  2. Node.js와 Python 래퍼는 설정/로그를 공유하도록 설계하세요. 동일한 ~/.claude-lib/config.json, 동일한 로그 포맷을 사용하면 언어가 달라도 통합 관리가 가능합니다.

  3. 통합 검증은 반드시 수행하세요. 개별 모듈이 정상 동작해도 통합 시점에 드러나는 버그가 있습니다. 스트리밍 파싱에서 --verbose 플래그 누락은 단위 테스트로는 잡기 어려운 종류의 버그입니다.

  4. CLI 래퍼 라이브러리의 핵심은 에러 처리입니다. 타임아웃, rate limit, 비정상 종료 등 다양한 실패 시나리오를 분류하고 적절히 처리하는 것이 라이브러리의 가치입니다.

  5. 팀 에이전트 병렬 개발은 언어가 다른 모듈 개발에 효과적입니다. Node.js 코어와 Python 래퍼를 동시에 작업하고, 마지막에 통합 검증 에이전트가 전체를 점검하는 워크플로우가 잘 동작했습니다.