Claude Code로 블로그 75편을 60분 만에 영문 번역했다 — API 비용 0원
발단은 색인 0이었습니다
일요일 오후, 블로그의 구글 색인 상태를 확인하다가 숫자 하나를 봤습니다. 0. 75편을 써놓은 블로그가 구글 검색에 한 페이지도 안 잡혀 있었습니다. 색인 문제야 서치콘솔에 등록하면 풀리는 일인데, 그 과정에서 다른 생각이 하나 붙었습니다. 어차피 검색 노출을 손보는 김에, 영어권 검색까지 열면 어떨까.
브라우저 번역 기능이 있으니 한국어 글도 읽히긴 합니다. 문제는 읽기가 아니라 발견입니다. 구글은 페이지에 적힌 언어로 색인합니다. 영어 사용자가 "docker to podman migration"을 검색하면 한국어 글은 후보에도 못 듭니다. 영어 페이지가 실제로 존재해야 영어 검색에 잡힙니다.
그래서 75편 전부를 영어로 옮기기로 했습니다. 일요일 오후에 내리기엔 좀 무거운 결정이었는데, 견적을 뽑아보니 생각이 달라졌습니다.
옵션 세 개의 견적
번역 방법은 세 가지가 있었습니다.
전문 번역 외주. 기술 문서 번역 단가가 장당 3~5만 원 선입니다. 평균 6천 자짜리 글 75편이면 어림잡아 300만 원. 품질은 제일 좋겠지만 개인 블로그에 쓸 돈은 아닙니다.
Anthropic API. Opus 모델로 편당 입력 8천 토큰, 출력 5천 토큰 정도 잡으면 편당 500700원. 75편이면 47만 원 선입니다. 나쁘지 않은데, 이미 매달 내는 돈이 하나 있다는 게 걸렸습니다.
Claude Max 구독. 저는 Claude Code를 구독으로 쓰고 있습니다. 그리고 Claude Code에는 -p 플래그가 있습니다. 대화형이 아니라 스크립트에서 부를 수 있는 비대화형 모드. 구독 한도 안에서 돌아가니까 추가 비용이 0원입니다.
같은 모델, 같은 품질, 비용만 다릅니다. 답은 정해져 있었습니다.
Claude Code를 API처럼 쓰기
-p 플래그의 동작은 단순합니다. 프롬프트를 주면 응답을 출력하고 종료합니다.
echo "1+1은?" | claude -p --output-format json | jq -r .result
--output-format json을 붙이면 응답이 구조화되어 나옵니다. 파싱해서 파이프라인에 끼우면 그대로 API 대용이 됩니다. --append-system-prompt로 시스템 프롬프트도 박을 수 있습니다. 번역 규칙은 전부 여기에 넣었습니다. 코드 블록과 이미지 URL은 건드리지 말 것, 문장 호흡을 유지할 것, 마케팅 톤으로 매끈하게 다듬지 말 것.
Node 스크립트에서는 spawn으로 부릅니다. 긴 본문은 인자로 넘기면 argv 길이 한도에 걸리니까 stdin으로 흘려 넣습니다.
const child = spawn("claude", [
"-p", "--model", "opus",
"--output-format", "json",
"--append-system-prompt", SYSTEM,
], { stdio: ["pipe", "pipe", "pipe"] });
child.stdin.write(prompt);
child.stdin.end();
여기까지는 문서만 봐도 되는 얘기입니다. 문제는 문서에 없는 데서 터졌습니다.
첫 번째 삽질: 영문도 모를 401
첫 실행에서 401 인증 오류가 났습니다. 구독 로그인은 멀쩡히 되어 있는데 자격 증명이 틀렸다고 합니다.
한참 들여다보다가 원인을 찾았습니다. 환경변수였습니다. 셸에 ANTHROPIC_API_KEY가 설정되어 있으면 Claude Code는 구독 인증 대신 그 키를 우선합니다. 제 환경에는 다른 프로젝트에서 쓰던 키와 관련 변수 몇 개가 박혀 있었고, spawn된 자식 프로세스가 그걸 전부 상속받은 겁니다. 구독으로 인증하려던 프로세스가 엉뚱한 키를 들고 API 문을 두드리고 있었습니다.
해결은 spawn 시점에 관련 변수를 걷어내는 것이었습니다.
const env = {};
for (const [k, v] of Object.entries(process.env)) {
if (k.startsWith("ANTHROPIC_")) continue;
env[k] = v;
}
const child = spawn("claude", args, { env });
환경변수 상속은 유닉스에서 수십 년 된 동작인데, 알면서도 매번 여기서 넘어집니다. 자식 프로세스는 부모의 사정을 너무 많이 물려받습니다.
파이프라인: 메타 따로, 본문 따로
번역은 글마다 두 번 호출합니다. 제목·설명·태그 같은 메타데이터를 JSON으로 한 번, 본문 마크다운을 한 번. 한 번에 묶으면 편하겠지만, 긴 본문 번역 끝에 JSON 구조까지 정확히 맞춰 나오길 기대하는 건 확률 낮은 도박입니다. 쪼개면 각 호출의 실패 지점이 분리됩니다.
재실행 대비도 넣었습니다. 번역본 프런트매터에 원문 날짜를 sourceDate로 박아두고, 다시 돌릴 때 원문이 안 바뀌었으면 건너뜁니다. 중간에 끊겨도 이어서 돌리면 되고, 나중에 원문을 고치면 그 글만 다시 번역됩니다.
원문 75편 (content/posts/)
→ 메타 번역 (JSON)
→ 본문 번역 (마크다운)
→ content/posts-en/에 저장, 같은 slug 유지
→ /en/post/[slug] 페이지 + hreflang으로 원문과 연결
slug를 그대로 쓰는 게 중요합니다. 한국어 원문과 영어 번역본이 같은 주소 체계로 짝을 이루면, hreflang 선언 하나로 구글이 검색자 언어에 맞는 쪽을 알아서 보여줍니다.
숫자
| 항목 | 결과 |
|---|---|
| 번역한 글 | 75편 |
| 소요 시간 | 60분 |
| 실패 | 0편 |
| 편당 처리 | 26초 ~ 104초 |
| 추가 비용 | 0원 |
| API로 했다면 | 약 4~7만 원 |
| 외주였다면 | 약 300만 원 |
돌려놓고 다른 작업을 하다가 로그를 봤는데, 실패 0이라는 줄이 제일 어색했습니다. 이런 배치 작업은 꼭 두세 개쯤 알 수 없는 이유로 죽어 있어야 정상인데. 아마 호출을 쪼개고 stdin으로 넘긴 덕이 컸을 겁니다. 실패 지점을 미리 줄여놓으면 실패가 줄어듭니다. 당연한 말인데 매번 까먹는 말이기도 합니다.
품질은 스팟체크로 확인했습니다. 몇 편을 열어 원문과 대조해보니 문장 호흡이 살아 있었습니다. 짧은 문장은 짧게, 자조는 자조로. 시스템 프롬프트에 "마케팅 카피처럼 다듬지 말라"고 박아둔 게 먹힌 것 같습니다. 번역기 특유의 매끈하고 밋밋한 문장이 아니라, 쓴 사람이 있는 글처럼 읽혔습니다.
한계도 적어둡니다
공짜처럼 말했지만 정확히는 이미 낸 구독료 안에서 소화한 것입니다. Max 구독 한도를 이만큼 쓴 거라, 같은 시간에 다른 작업을 병행했다면 한도 압박이 있었을 겁니다. 수백 편 규모라면 하루에 다 못 돌리고 나눠 돌려야 합니다.
그리고 이 방식은 대화형 도구를 배치 파이프라인에 끼운 것이라, 서비스 백엔드처럼 상시로 돌리는 용도에는 맞지 않습니다. 그건 그냥 API를 쓰는 게 맞습니다. 도구마다 자리가 있습니다.
번역 품질도 전수 검수한 건 아닙니다. 75편 스팟체크로 넘어갔고, 어딘가에 어색한 문장이 남아 있을 겁니다. 발견되는 대로 고치는 쪽을 택했습니다. 완벽한 검수를 기다렸으면 이 글도 번역본도 아직 없었을 테니까.
60분이 바꾼 것
일요일 오후에 시작한 일이 저녁 전에 끝났습니다. 블로그에는 한국어 75편과 영어 75편이 있고, 구글은 이제 언어별로 다른 페이지를 보여줄 수 있게 됐습니다. 영어권 검색에서 실제로 유입이 생길지는 몇 달 지나봐야 압니다. 색인이라는 게 원래 그렇게 느립니다.
확실해진 건 하나입니다. 개인이 혼자 굴리는 블로그에서 "전체 영문판"은 예전엔 선택지가 아니었습니다. 돈이나 시간 중 하나를 크게 태워야 했으니까. 이제는 일요일 오후에 끝나는 일이 됐습니다. 이런 일이 요즘 자꾸 늘어납니다. 안 되던 게 되는 게 아니라, 비쌌던 게 싸지는 방식으로.
남은 걱정은 하나입니다. 이렇게 만든 영문판을 영어권 독자가 읽고 "번역 티가 난다"고 하면 어쩌나. 뭐, 그때 가서 고치겠습니다.
이 글이 도움이 되셨나요?
버튼 하나가 다음 글을 쓰는 힘이 됩니다