배경
기술 블로그 운영에 있어 콘텐츠의 도달률은 주요한 고려사항이다. 특히 한국어로만 작성된 콘텐츠는 그 범위가 제한적일 수밖에 없으며, 이는 전 세계 개발자들과의 지식 공유를 제한하는 요소로 작용한다. 실제 개발자 인구 통계에 따르면 한국은 상위 15개국 중 하위권에 속해있어, 다국어 지원의 필요성이 더욱 부각된다.
(참조: 전문 개발자 수 기준 상위 15개국)
자동화의 필요성
최근의 AI 번역이나 전통적인 번역 도구들은 상당히 정확한 번역을 제공한다. 하지만 새 글을 작성할 때마다 수동으로 번역하고 업로드하는 작업은 비효율적이다. 특히 영어뿐만 아니라 개발자 수가 많은 중국어, 일본어 버전까지 제공하고 싶다면 수작업으로는 현실적으로 불가능하다.
이러한 문제를 해결하기 위해 DeepL API를 활용한 자동 번역 스크립트를 구현해보았다.
개발 환경 구성
Next.js 환경에서 Node.js 스크립트를 실행하기 위해 필요한 패키지들을 설치했다.
pnpm install deepl-node dotenv tsx
처음에는 ts-node
를 사용하려 했으나, Next.js 환경과의 설정 충돌 문제가 있었다. 대신 tsx
라이브러리를 사용하여 독립적인 실행 환경을 구성했다.
프로젝트 구조
우선 내 프로젝트는 대략 다음과 같은 구조를 가지고 있다.
.
├── src/
│ └── app/
│ └── posts/
│ └── [slug]/
│ └── page.tsx
├── posts/
│ └── post1.mdx
└── package.json
그리고 다음과 같이 script를 생성했다.
import fs from "fs/promises";
import path from "path";
import * as deepl from "deepl-node";
import matter from "gray-matter";
import dotenv from "dotenv";
dotenv.config();
const DEEPL_API_KEY = process.env.DEEPL_API_KEY!;
const translator = new deepl.Translator(DEEPL_API_KEY);
const SOURCE_DIR = "src/posts";
const TARGET_DIR = "src/posts/en";
interface PostContent {
content: string;
data: {
title: string;
description: string;
[key: string]: string;
};
}
async function translatePost(content: {
data: { [p: string]: string };
content: string;
}): Promise<PostContent> {
const translatedTitle = await translator.translateText(
content.data.title,
"ko",
"en-US",
);
const translatedDescription = await translator.translateText(
content.data.description,
"ko",
"en-US",
);
const translatedContent = await translator.translateText(
content.content,
"ko",
"en-US",
);
return {
content: translatedContent.text,
data: {
...content.data,
title: translatedTitle.text,
description: translatedDescription?.text,
originalLang: "ko",
},
};
}
async function processFile(filename: string) {
try {
const sourcePath = path.join(SOURCE_DIR, filename);
const targetPath = path.join(TARGET_DIR, filename);
// 파일 존재 여부 확인
try {
await fs.access(sourcePath);
} catch (error) {
throw new Error(`파일을 찾을 수 없습니다: ${filename}`);
}
// MDX 파일 읽기
const fileContent = await fs.readFile(sourcePath, "utf-8");
const { data, content } = matter(fileContent);
// 번역 실행
console.log(`${filename} 번역 중...`);
const translated = await translatePost({ data, content });
// 번역된 MDX 파일 생성
const translatedFileContent = matter.stringify(
translated.content,
translated.data,
);
await fs.mkdir(TARGET_DIR, { recursive: true });
await fs.writeFile(targetPath, translatedFileContent);
console.log(`${filename} 번역 완료!`);
} catch (error) {
console.error(`Error:`, error);
process.exit(1);
}
}
// 명령줄 인자에서 파일명 가져오기
const filename = process.argv[2];
if (!filename) {
process.exit(1);
}
// 파일 확장자 확인
if (!filename.endsWith(".mdx")) {
console.error("Error: MDX 파일만 지원됩니다.");
process.exit(1);
}
processFile(filename);
package.json
의 script도 추가해준다.
{
"scripts": {
"translate": "tsx scripts/translate-posts.ts"
}
}
실행 결과
이제 터미널에서 명령어를 입력하면..
번역된 mdx 파일이 생성된다.
문제점
초기 구현에서는 MDX 파일의 텍스트를 직접 DeepL API로 전송하는 방식을 채택하였으나, 다음과 같은 문제점들이 발견되었다:
- 마크다운 문법 훼손
- 코드 블록 불필요 번역
- 이미지 태그 및 링크 구조 왜곡
- 원본
- 일본어 번역본
해결 방법
어떻게 할지 고민 중 다음과 같은 아이디어를 얻었다. 우선, DeepL API 문서에서 HTML handling이라는 파트를 보았고, 텍스트를 HTML로 보내면 양식을 깨지 않고 잘 다뤄주는 것 같았다.
따라서 위에서 언급한 문제점을 해결하기 위해 다음과 같은 개선된 프로세스를 도입하였다.
- MDX → HTML 변환
- HTML 태그 보존 옵션을 활용한 DeepL API 전송
- 번역된 HTML → MDX 재변환
const convertMDXToHtml = async (markdown: string) => {
try {
const html = await unified()
.use(remarkParse)
.use(remarkHtml)
.process(markdown);
return html.toString();
} catch (err) {
console.error("MD => HTML 변환 중 오류가 발생했습니다: ");
return "error";
}
};
const convertHtmlToMDX = async (html: string) => {
try {
const markdown = await unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkStringify)
.process(html);
return markdown.toString();
} catch (err) {
console.error("HTML => MD 변환 중 오류가 발생했습니다: ");
return "error";
}
};
async function translatePost(
content: { data: { [p: string]: string }; content: string },
targetLang: TargetLanguageCode,
): Promise<PostContent> {
// 중략...
// md => html
const html = await convertMDXToHtml(content.content);
// html => translated html
const translatedContent = await translator.translateText(
html,
"ko",
targetLang,
{
tagHandling: "html",
},
);
// translated html => md
const mdx = await convertHtmlToMDX(translatedContent.text);
return {
content: mdx,
// ...
};
}
이제 양식도 제대로 맞춰서 들어오게 됐다!
마치며
본 자동화 구현을 통해 어렵지 않게 블로그 포스트의 다국어 버전 배포 프로세스가 완성 되었다. 번역된 게시글이 내가 의도한대로 번역이 되었는지는 계속해서 검증해 봐야겠지만, 우선 여러 언어로 정적 파일을 만들어 SEO 향상이 기대되는 만큼 실제로 유입이 더 되는지 지켜봐야겠다!