하나의 에이전트로 블로그 포스트 하나를 13개 언어로 번역하는 방법
단일 TypeScript 에이전트가 Claude API를 병렬로 호출해 영어 포스트를 90초 이내에 12개 언어로 번역합니다. 문체 보존을 위해 시스템 프롬프트를 두 부분으로 나눠야 합니다. 먼저 스타일 제약 조건, 그 다음 로케일별 노트. Haiku 기준 포스트당 비용은 약 $0.004–$0.02입니다. 제 사이트는 60일 이내에 국제 트래픽이 34% 증가했습니다.
매주 수요일. 28,400명+ 구독자. 핵심만.
✓ 받은편지함을 확인하세요 — 확인 링크를 클릭해 가입을 완료하세요.
✓ 구독이 완료되었습니다!
✓ 이미 목록에 있습니다.
목차
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명+ 구독자. 핵심만.
✓ 받은편지함을 확인하세요 — 확인 링크를 클릭해 가입을 완료하세요.
✓ 구독이 완료되었습니다!
✓ 이미 목록에 있습니다.
관련 게시물
Claude 스킬 vs. 슬래시 명령어 vs. 서브에이전트
스킬, 슬래시 명령어, 서브에이전트는 Claude에서 각기 다른 문제를 해결합니다. 상황에 맞는 도구를 고르기 위해 제가 쓰는 의사결정 프레임워크를 소개합니다.
AI Agents프로덕션 AI 에이전트를 위한 프롬프트 인젝션 방어: 실제로 효과가 있는 것들
2026년 업데이트. 에이전트가 페이스북 댓글, 이메일, 웹훅 페이로드를 읽기 시작하는 순간, 프롬프트 인젝션은 더 이상 이론적인 CTF 연습이 아니게 된다. 30개 이상의 프로덕션 에이전트에서 실제로 사용하는 다층 방어책과, 결국 눈속임에 불과했던 것들을 정리했다.
AI AgentsAI 에이전트를 위한 컨텍스트 엔지니어링: 컨텍스트 윈도우에 실제로 무엇이 들어가야 하는가
컨텍스트 엔지니어링은 에이전트가 무엇을 알아야 하는지 묻는다. 30개 이상의 프로덕션 에이전트에서 적용하는 예산 — 시스템 지침, 도구 정의, 검색된 데이터, 히스토리 — 그리고 윈도우가 꽉 찼을 때 가장 먼저 잘라내는 것을 소개한다.
AI 플레이북을 받아보세요
매주 수요일. 28,400명+ 구독자. 핵심만.
받은편지함을 확인하세요.
확인 이메일을 보냈습니다 — 링크를 클릭해 구독을 완료하세요. 1분 안에 보이지 않으면 스팸함을 확인하세요.
구독이 완료되었습니다.
환영합니다 — 다음 호가 곧 받은편지함에 도착합니다.
이미 목록에 있습니다 — 매주 수요일에 확인하세요.