Claude Code로 코드베이스를 다른 언어로 포팅하는 ralph 루프 패턴
Geoffrey Huntley가 공개한 ralph 루프 패턴으로 테스트 파일을 `/specs/*.md`에 파일별로 압축한 뒤, Claude를 루프로 돌려 코드베이스 전체를 다른 언어로 자동 포팅하는 방법을 단계별로 따라해볼 수 있어요.
Python으로 짠 서비스를 TypeScript로 옮겨야 하는 상황, 한 번쯤 있었을 거예요. 파일 하나씩 열어서 손으로 옮기다 보면 금방 지치고, 테스트가 통과하는지도 불확실해요. Geoffrey Huntley가 공개한 방법은 이 과정을 꽤 다르게 접근해요. 테스트 파일을 서브에이전트로 읽어서 /specs/ 디렉토리 안에 파일별 명세 문서(/specs/*.md)로 압축한 뒤, Claude를 루프로 돌려 언어 포팅을 자동화하는 구조예요. 스크립트 구조와 프롬프트 패턴이 구체적으로 공개되어 있어서 그대로 따라해볼 수 있어요.
준비물
- Claude Code (
claudeCLI) 설치 완료 - 포팅할 소스 코드베이스 (테스트 파일 포함)
ANTHROPIC_API_KEY환경변수 설정- bash 실행 환경 (macOS / Linux)
- 포팅 대상 언어 런타임 (예: Node.js, Go 등)
스텝 1: /specs/ 디렉토리 생성하기
이 단계가 핵심이에요. 테스트 파일을 그냥 Claude에 통째로 던지면 컨텍스트가 터지거나 노이즈가 너무 많아요. 대신 서브에이전트를 여러 개 띄워서 각 테스트 파일을 병렬로 읽고, 소스·테스트 쪽에 대응하는 명세를 파일 단위로 만들어요. Huntley 원문의 프롬프트가 /specs/*.md 글롭 패턴을 써서 여러 명세 파일을 만들고, 각 파일에서 구현 쪽 파일을 citation으로 링크하게 되어 있어요.
프로젝트 루트에서 아래 명령을 실행해요.
claude --print \
"study every file in tests/* using separate subagents and document in /specs/*.md \
and link the implementation as citations in the specification. \
Each subagent should read one test file and extract: \
1) what function/module is being tested \
2) input/output examples from the test cases \
3) edge cases and error conditions."
--print 플래그는 Claude Code를 비대화형 모드로 실행해요. 스크립트 안에서 루프를 돌릴 때 필요한 설정이에요.
실행하고 나면 프로젝트 루트에 /specs/ 디렉토리가 생기고, 그 안에 테스트 파일별 명세 마크다운이 여러 개 쌓여요. 열어보면 각 테스트 케이스에서 추출한 함수 시그니처, 입출력 예시, 엣지케이스, 그리고 원본 구현 파일로 걸린 citation이 정리되어 있어요. 이게 포팅의 실질적인 설계도가 돼요.
스텝 2: ralph 루프 스크립트 작성
명세가 준비됐으면 이제 Claude를 루프로 돌려요. 아래 스크립트를 ralph.sh로 저장하세요.
#!/bin/bash
SOURCE_LANG="python"
TARGET_LANG="typescript"
SPECS_DIR="/specs"
OUTPUT_DIR="./src-ts"
MAX_ITERATIONS=10
mkdir -p "$OUTPUT_DIR"
for i in $(seq 1 $MAX_ITERATIONS); do
echo "=== Iteration $i ==="
claude --print \
"Read every specification file in $SPECS_DIR/*.md. \
Port the described functionality from $SOURCE_LANG to $TARGET_LANG. \
Write all output files to $OUTPUT_DIR/. \
After writing files, run the $TARGET_LANG tests and report results. \
If tests pass, output exactly: DONE. \
If tests fail, fix the issues and continue."
# Claude가 DONE을 출력하면 루프 종료
LAST_OUTPUT=$(claude --print "Check if $OUTPUT_DIR tests pass. Output DONE if all pass, CONTINUE if not.")
if echo "$LAST_OUTPUT" | grep -q "DONE"; then
echo "포팅 완료. $i번 반복으로 끝났어요."
break
fi
done
실행 권한을 주고 돌려요.
chmod +x ralph.sh
./ralph.sh
루프 안에서 Claude는 명세를 읽고, 코드를 작성하고, 테스트를 돌리고, 실패하면 스스로 고쳐요. 사람이 중간에 개입할 필요가 없어요. 진짜로요.
스텝 3: 프롬프트 튜닝
기본 프롬프트로도 돌아가지만, 소스 언어의 관용적 패턴이 타겟 언어에 그대로 번역되는 경우가 있어요. 예를 들어 Python의 list comprehension이 TypeScript에서 .map().filter() 체인이 아니라 for 루프로 나오는 식이에요. 이럴 때 프롬프트에 한 줄 추가하면 돼요.
claude --print \
"Read every file in $SPECS_DIR/*.md. Port to $TARGET_LANG using idiomatic $TARGET_LANG patterns. \
Do not transliterate $SOURCE_LANG idioms. Use $TARGET_LANG conventions. \
Write to $OUTPUT_DIR/. Run tests. Output DONE if all pass."
idiomatic 키워드 하나가 출력 품질을 꽤 바꿔요.
확인 방법
/specs/ 디렉토리가 제대로 채워졌는지 확인하는 게 중요해요. 내용이 너무 얕으면 포팅 결과도 얕아요.
# 생성된 명세 파일 목록
ls /specs/
# 특정 명세 파일 내용 확인
cat /specs/<specific-file>.md | head -100
# 포팅된 파일 목록 확인
ls -la ./src-ts/
# TypeScript 예시: 테스트 직접 실행
cd src-ts && npx jest --verbose 2>&1 | tail -30
테스트가 전부 통과하면 끝이에요. 실패 케이스가 남아 있으면 루프 MAX_ITERATIONS를 늘리거나 프롬프트를 더 구체적으로 다듬으면 돼요.
응용
/specs/ 디렉토리를 한 번 만들어두면 여러 언어로 포팅하는 데 재사용할 수 있어요. TARGET_LANG만 바꿔서 ralph.sh를 다시 돌리면 Go 버전, Rust 버전을 따로 만들 수 있어요. 명세 파일 자체가 언어 독립적인 설계 문서가 되는 거예요.
테스트가 없는 코드베이스라면 tests/* 대신 src/*를 읽어서 명세를 만들어도 돼요. 정확도는 조금 떨어지지만 아예 없는 것보다는 낫더라고요.
트러블슈팅
서브에이전트가 파일을 못 찾는 경우: tests/* 경로가 실제 프로젝트 구조와 다를 수 있어요. find . -name "*_test*" -o -name "*.spec.*" 로 먼저 경로를 확인하고 프롬프트에 절대경로로 넣으세요.
루프가 DONE을 못 내고 계속 도는 경우: 타겟 언어 런타임이 설치 안 됐거나 테스트 실행 명령이 틀린 경우가 많아요. 프롬프트에 run tests using: npx jest 처럼 실행 명령을 명시적으로 적어주면 해결되는 경우가 많아요.
/specs/ 명세 파일들이 너무 짧게 나오는 경우: 테스트 파일이 assertion 위주가 아니라 fixture 위주로 구성된 경우예요. 이럴 땐 프롬프트에 also read src/* to understand the implementation 을 추가해서 소스도 같이 읽게 하면 돼요.
/specs/*.md를 한 번 제대로 만들어두면 그 이후 루프는 거의 자동으로 굴러가요. 손으로 파일 하나씩 옮기던 작업이 커피 한 잔 내리는 사이에 끝나는 경우도 있어요.