하나의 에이전트로 블로그 포스트 하나를 13개 언어로 번역하는 방법
단일 TypeScript 에이전트가 Claude API를 병렬로 호출해 영어 포스트를 90초 이내에 12개 언어로 번역합니다. 문체 보존을 위해 시스템 프롬프트를 두 부분으로 나눠야 합니다. 먼저 스타일 제약 조건, 그 다음 로케일별 노트. Haiku 기준 포스트당 비용은 약 $0.004–$0.02입니다. 제 사이트는 60일 이내에 국제 트래픽이 34% 증가했습니다.
매주 수요일. 28,400명+ 구독자. 핵심만.
✓ Check your inbox — click the confirmation link to complete sign-up.
✓ You're subscribed!
✓ You're already on the list.
목차
2026년 5월 업데이트.
TL;DR: 단일 TypeScript 에이전트가 Claude API를 병렬로 호출해 영어 포스트를 90초 이내에 12개 언어로 번역합니다. 문체 보존을 위해 시스템 프롬프트를 두 부분으로 나눠야 합니다. 먼저 스타일 제약 조건, 그 다음 로케일별 노트. Haiku 기준 포스트당 비용은 약 $0.004–$0.02입니다. 제 사이트는 60일 이내에 국제 트래픽이 34% 증가했습니다.
[운영자 관점] 새 포스트를 게시할 때마다 이 에이전트를 실행합니다. 번역을 단 한 번도 수동으로 건드리지 않고 341개 포스트를 12개 언어로 처리했습니다. 정확히 어떻게 작동하는지 설명합니다.
번역가를 고용하는 대신 에이전트를 구축한 이유
다국어 SEO 관련 설명은 건너뛰겠습니다. 이미 중요하다는 걸 알고 있을 테니까요. 제가 겪은 문제는 워크플로였습니다. 포스트별로 번역가를 고용하면 비용이 많이 들고($40–$120/포스트 × 12언어 = $480–$1,440/기사), 느리며(납기 3–7일), 기존 341개 포스트를 따라잡으려면 배치 처리가 불가능합니다.
다른 사람들이 제안하는 방법은 Google Translate나 DeepL입니다. 둘 다 정확도는 괜찮지만 문체를 망가뜨립니다. 제 글쓰기 스타일은 직접적이고, 1인칭이며, 약간 반골적입니다. 기계 번역은 모든 것을 형식적이고 수동적으로 만드는 경향이 있습니다. 브랜드의 문체 일관성이 중요한 경우 이는 문제가 됩니다.
그래서 Claude 기반 TypeScript 에이전트를 구축했습니다. main에 매 머지마다 CI에서 실행되고, 번역을 병렬로 배포하고, 파일을 디스크에 다시 쓰고, 이미 파일이 있는 언어는 건너뜁니다. 새 포스트의 경우 전체가 90초 이내에 완료됩니다.
프로젝트 구조
에이전트는 scripts/agent/translate-worker.ts에 있습니다. 영어 포스트를 읽고, frontmatter를 추출하고, 언어별로 번역 작업을 디스패치하는 최상위 오케스트레이터에서 호출됩니다.
scripts/
agent/
translate-worker.ts # 로케일별 번역 로직
translate-all.ts # 오케스트레이터: EN 읽기, 12개 언어로 배포
lib/
frontmatter.ts # gray-matter frontmatter 파싱/직렬화
voice-prompt.ts # 공유 시스템 프롬프트 빌더오케스트레이터(translate-all.ts)는 Promise.allSettled를 사용하므로 하나의 로케일이 실패해도 나머지가 차단되지 않습니다.
시스템 프롬프트 설계
여기서 대부분의 사람들이 실수합니다. “이것을 프랑스어로 번역하고 저자의 문체를 유지해줘” 같은 한 줄짜리를 작성합니다. 그러면 평범한 결과가 나옵니다.
제 시스템 프롬프트에는 두 가지 필수 섹션이 있습니다.
섹션 1 — 스타일 제약 조건(범용, 모든 호출에 앞부분에 추가):
// scripts/agent/lib/voice-prompt.ts
export function buildSystemPrompt(targetLocale: string): string {
const styleConstraints = `
You are a professional translator working on blog posts written by Alejandro Rioja.
STYLE RULES — apply to every locale:
- Short paragraphs (1–3 sentences max). Do not merge them.
- First-person, direct voice. Never passive if active is natural.
- No filler phrases: no "In today's world", no "It is worth noting that".
- Preserve all markdown: headings, bold, italics, code blocks, links.
- Translate heading text but keep the ## / ### prefix exactly.
- Code blocks: translate comments only. Keep all variable names, strings, and syntax in English.
- Preserve frontmatter keys exactly. Only translate the VALUES for: title, ogTitle, description, tldr, imageAlt.
- Keep these frontmatter values UNCHANGED: pubDate, updatedDate, translation_key, tags, image, author, draft, lang (set lang to: ${targetLocale}).
`.trim();섹션 2 — 로케일별 노트(호출마다 추가):
const localeNotes: Record<string, string> = {
ar: "Arabic: use Modern Standard Arabic (MSA). RTL layout is handled by the CMS — do not add any RTL markup. Avoid overly formal Classical Arabic registers.",
de: "German: use informal 'du' not formal 'Sie'. Compound nouns are fine; don't over-hyphenate. Keep tech terms in English when that's the industry standard (e.g. 'Content Marketing', 'SEO').",
es: "Spanish: use neutral Latin American Spanish, not Castilian. Tuteo ('tú') over 'usted'. Keep anglicisms that are standard in tech (SEO, agente, prompt).",
fr: "French: use informal 'tu'. Avoid over-formalizing. Tech anglicisms are acceptable when widely used (SEO, agent, prompt).",
hi: "Hindi: use Devanagari script. Mix Hindi and English naturally for tech terms — this is standard in Indian tech writing. Don't force Hindi equivalents for words like 'agent', 'prompt', 'SEO'.",
it: "Italian: use 'tu' form. Keep English tech terms where they're standard in Italian digital marketing.",
ja: "Japanese: use です/ます (polite) style, not casual or keigo. Keep technical English terms in katakana where standard (e.g. エージェント, プロンプト, SEO).",
ko: "Korean: use 합쇼체 (formal polite). Tech terms in English or standard Korean loanwords. Keep SEO, agent, prompt as-is or standard loanwords.",
nl: "Dutch: use 'je/jij' (informal). Keep English tech terms standard in Dutch digital marketing.",
pt: "Portuguese: use Brazilian Portuguese (pt-BR). Informal 'você'. Keep tech anglicisms standard in Brazilian digital marketing.",
ru: "Russian: use modern, accessible Russian. Avoid overly bureaucratic phrasing. Tech terms can stay in English where that's the norm in Russian tech writing.",
zh: "Chinese: use Simplified Chinese (zh-CN). Modern, accessible tone. Tech terms can use standard Chinese equivalents or keep English where that's industry norm.",
};
return `${styleConstraints}\n\nLOCALE-SPECIFIC NOTES for ${targetLocale}:\n${localeNotes[targetLocale]}`;
}번역 워커
전체 워커입니다. EN 파일을 읽고, Claude를 호출하고, 출력을 디스크에 씁니다.
// scripts/agent/translate-worker.ts
import Anthropic from "@anthropic-ai/sdk";
import * as fs from "fs";
import * as path from "path";
import { buildSystemPrompt } from "./lib/voice-prompt";
const client = new Anthropic();
export interface TranslateJob {
enFilePath: string;
locale: string;
outputDir: string;
model?: "claude-haiku-4-5" | "claude-sonnet-4-5";
dryRun?: boolean;
}
export async function translatePost(job: TranslateJob): Promise<string> {
const { enFilePath, locale, outputDir, model = "claude-haiku-4-5", dryRun = false } = job;
// 멱등성: 번역이 이미 존재하면 건너뜀
const filename = path.basename(enFilePath);
const outPath = path.join(outputDir, locale, filename);
if (fs.existsSync(outPath)) {
console.log(`[${locale}] 이미 존재함 — 건너뜀: ${outPath}`);
return outPath;
}
const enContent = fs.readFileSync(enFilePath, "utf-8");
const systemPrompt = buildSystemPrompt(locale);
const message = await client.messages.create({
model,
max_tokens: 8192,
system: systemPrompt,
messages: [
{
role: "user",
content: `Translate the following blog post to ${locale}. Return ONLY the translated markdown file content — no explanation, no preamble, no code fences around the whole file.\n\n${enContent}`,
},
],
});
const translated = (message.content[0] as { type: string; text: string }).text;
if (!dryRun) {
fs.mkdirSync(path.join(outputDir, locale), { recursive: true });
fs.writeFileSync(outPath, translated, "utf-8");
console.log(`[${locale}] 작성됨: ${outPath}`);
}
return outPath;
}오케스트레이터
// scripts/agent/translate-all.ts
import * as path from "path";
import * as fs from "fs";
import { translatePost } from "./translate-worker";
const LOCALES = ["ar", "de", "es", "fr", "hi", "it", "ja", "ko", "nl", "pt", "ru", "zh"];
const POSTS_DIR = path.resolve("src/content/posts");
const MODEL = (process.env.TRANSLATE_MODEL as "claude-haiku-4-5" | "claude-sonnet-4-5") ?? "claude-haiku-4-5";
async function main() {
// 특정 파일을 받거나 모든 EN 포스트를 번역
const targetFile = process.argv[2];
const enFiles = targetFile
? [path.resolve(targetFile)]
: fs.readdirSync(path.join(POSTS_DIR, "en")).map((f) => path.join(POSTS_DIR, "en", f));
console.log(`${enFiles.length}개 포스트 × ${LOCALES.length}개 언어 번역 중. 모델: ${MODEL}`);
for (const enFile of enFiles) {
const results = await Promise.allSettled(
LOCALES.map((locale) =>
translatePost({
enFilePath: enFile,
locale,
outputDir: POSTS_DIR,
model: MODEL,
})
)
);
results.forEach((r, i) => {
if (r.status === "rejected") {
console.error(`[${LOCALES[i]}] 실패:`, r.reason);
}
});
}
console.log("완료.");
}
main();실행 방법:
# 새 포스트 하나 번역
npx ts-node scripts/agent/translate-all.ts src/content/posts/en/my-new-post.md
# 전체 번역(멱등 — 기존 항목 건너뜀)
npx ts-node scripts/agent/translate-all.ts비용 비교: Haiku vs Sonnet
제 실제 사용량을 기반으로 한 포스트당 실제 비용:
| 모델 | 입력 토큰(평균) | 출력 토큰(평균) | 언어당 비용 | × 12언어 비용 |
|---|---|---|---|---|
| claude-haiku-4-5 | ~2,400 | ~2,600 | ~$0.0004 | ~$0.005 |
| claude-sonnet-4-5 | ~2,400 | ~2,600 | ~$0.015 | ~$0.18 |
Haiku로 341개 포스트 × 12개 언어: 총 약 $1.70. 이것이 전체 백로그 비용입니다.
Sonnet이 관용적 표현이 약간 더 자연스럽지만, 대부분의 포스트에서 그 차이가 36배 높은 가격을 정당화하지 못합니다. 미묘한 설득력 있는 톤이 중요한 포스트(세일즈 페이지나 트래픽 높은 핵심 콘텐츠)에만 Sonnet을 사용합니다.
TRANSLATE_MODEL 환경 변수로 실행별 모델을 전환할 수 있습니다:
TRANSLATE_MODEL=claude-sonnet-4-5 npx ts-node scripts/agent/translate-all.ts src/content/posts/en/flagship-post.md실제 결과: 트래픽에 무슨 일이 있었나
2025년 12월에 전체 백로그 번역(341개 포스트)을 게시했습니다. 60일 이내:
- 유기 세션 +34% 사이트 전체(Google Search Console, 2026년 1–2월 vs 2025년 10–11월)
- 세션 수 기준 최고 신규 언어: 브라질 포르투갈어(pt) — 새로운 국제 트래픽의 11%
- 전환율 기준 최고 신규 언어: 독일어(de) — 컨설팅 예약률 2.1% vs 글로벌 평균 1.8%
- 최저 성과: 아랍어(ar) — 트래픽은 왔지만 전환은 제로. 예약 플로우가 포스트 콘텐츠 이상으로 로컬라이즈되지 않았기 때문인 것으로 추정합니다.
- 일본어(ja)와 한국어(ko): 의미 있는 트래픽 증가(각각 국제 세션의 8%와 6%)에 평균 이상의 참여도(페이지 체류 시간 영어 기준 대비 +40%)
일본어와 한국어 결과가 놀라웠습니다. 두 언어 모두 고품질 AI 커뮤니티를 보유하고 있고, 실용적인 오퍼레이터 콘텐츠에 대한 수요도 상당한 것 같습니다.
운영자의 결론
에이전트 1개, 설정 1시간, API 비용 $1.70. 341개 포스트를 12개 추가 언어로 검색 가능하게 만드는 데 필요한 전부였습니다. SEO 상승만으로도 첫 주에 컴퓨팅 비용이 회수됐습니다. 콘텐츠가 많은 사이트를 운영하면서 아직 이것을 구축하지 않았다면, 국제 트래픽을 놓치고 있는 겁니다. 위의 코드가 완전한 구현입니다. 포크하고, voice-prompt 노트를 바꿔서, 오늘 밤 백로그에 실행해 보세요.
매주 수요일. 28,400명+ 구독자. 핵심만.
✓ Check your inbox — click the confirmation link to complete sign-up.
✓ You're subscribed!
✓ You're already on the list.
AI 플레이북을 받아보세요
매주 수요일. 28,400명+ 구독자. 핵심만.
Check your inbox.
We sent you a confirmation email — click the link inside to complete your subscription. Check spam if you don't see it within a minute.
You're subscribed.
Welcome — the next edition lands in your inbox soon.
You're already on the list — look for it every Wednesday.