Alejandro Rioja.
AI Agents SEO

하나의 에이전트로 블로그 포스트 하나를 13개 언어로 번역하는 방법

Alejandro Rioja
Alejandro Rioja
6 분 읽기
TL;DR

단일 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를 추출하고, 언어별로 번역 작업을 디스패치하는 최상위 오케스트레이터에서 호출됩니다.

code
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 — 스타일 제약 조건(범용, 모든 호출에 앞부분에 추가):

typescript
// 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 — 로케일별 노트(호출마다 추가):

typescript
  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를 호출하고, 출력을 디스크에 씁니다.

typescript
// 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;
}

오케스트레이터

typescript
// 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();

실행 방법:

sh
# 새 포스트 하나 번역
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 환경 변수로 실행별 모델을 전환할 수 있습니다:

sh
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일 이내:

일본어와 한국어 결과가 놀라웠습니다. 두 언어 모두 고품질 AI 커뮤니티를 보유하고 있고, 실용적인 오퍼레이터 콘텐츠에 대한 수요도 상당한 것 같습니다.

운영자의 결론

에이전트 1개, 설정 1시간, API 비용 $1.70. 341개 포스트를 12개 추가 언어로 검색 가능하게 만드는 데 필요한 전부였습니다. SEO 상승만으로도 첫 주에 컴퓨팅 비용이 회수됐습니다. 콘텐츠가 많은 사이트를 운영하면서 아직 이것을 구축하지 않았다면, 국제 트래픽을 놓치고 있는 겁니다. 위의 코드가 완전한 구현입니다. 포크하고, voice-prompt 노트를 바꿔서, 오늘 밤 백로그에 실행해 보세요.

계속 읽기

AI 플레이북을 받아보세요

매주 수요일. 28,400명+ 구독자. 핵심만.

↵ 전체 결과 보기 esc esc 닫기