인게임 텍스트 20개 국어 자동 번역 파이프라인 구축

번역 가능한 원문을 설계하고 추출·번역·검수·배포를 자동화하는 방법을 Unity 예제로 정리합니다. 런타임 번역을 피하면서도 재번역 비용과 누락을 줄이는 데 초점을 둡니다.

✓ 복사 완료!
챕터가격 정책 & 현지화
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
인게임 텍스트 20개 국어 자동 번역 파이프라인 구축

목표: 런타임 번역이 아닌 빌드 전 번역

인게임 텍스트를 20개 언어로 지원할 때 핵심은 번역 API를 게임 실행 중에 호출하는 일이 아닙니다. 네트워크 지연, 비용, API 장애, 문맥 손실 때문에 런타임 자동 번역은 플레이 경험에 적합하지 않습니다.

대신 원문을 안정적인 키로 관리하고 변경된 항목만 번역 서비스에 보내며 결과를 검수 가능한 형식으로 저장한 뒤 빌드에 포함합니다. 게임 클라이언트는 언제나 로컬에 포함된 번역 테이블만 읽습니다.

flowchart LR
    A[원문 작성] --> B[키와 문맥 추출]
    B --> C[변경 항목 판별]
    C --> D[번역 API 또는 번역 관리 도구]
    D --> E[형식 검증]
    E --> F[검수 큐]
    F --> G[번역 테이블 반영]
    G --> H[게임 빌드]

이 구조에서 자동화의 목적은 번역가를 완전히 없애는 것이 아니라 반복 작업과 누락을 줄이고 사람이 문맥과 품질에 집중하게 만드는 데 있습니다.

1. 텍스트를 키와 문맥으로 분리하기

번역 테이블의 키는 화면에 표시되는 문장이 아니라 기능을 설명하는 식별자여야 합니다. 예를 들어 버튼에 현재 시작이 표시된다고 해서 키를 START처럼 정할 수는 있지만 같은 단어가 게임 시작과 퀘스트 시작에 모두 쓰인다면 곧 충돌합니다.

{
  "menu.play_button": {
    "source": "게임 시작",
    "context": "타이틀 화면에서 새 게임을 시작하는 버튼"
  },
  "quest.start_action": {
    "source": "시작",
    "context": "선택한 퀘스트를 추적 상태로 바꾸는 동작"
  }
}

키는 한 번 배포한 뒤 가능하면 바꾸지 않습니다. 키를 변경하면 번역 메모리의 연결이 끊기고 기존 번역을 다시 찾거나 검수해야 합니다. 원문을 고칠 때는 키를 유지하고 원문과 문맥을 업데이트하는 편이 낫습니다.

문맥에는 적어도 화면 이름, UI 요소 종류, 글자 수 제약, 변수 의미를 넣습니다. 자동 번역 모델은 정보가 부족하면 존댓말 수준, 성별, 단복수, 버튼 문구 여부를 안정적으로 판단하기 어렵습니다.

2. 문자열 규칙을 먼저 고정하기

번역 자동화에서 자주 발생하는 오류는 번역 품질보다 서식 파손입니다. 플레이어 이름, 수치, 색상 태그, 줄바꿈 같은 토큰은 번역 대상이 아니며 결과에서도 그대로 보존되어야 합니다.

Unity의 Smart String 또는 자체 포맷 방식을 쓴다면 원문 규칙을 하나로 통일합니다.

{playerName}님이 레벨 {level}에 도달했습니다!

<color=#FFD54F>{itemName}</color>을 획득했습니다.

번역 요청에는 다음과 같은 제약을 함께 전달합니다.

  • {playerName}, {level}, {itemName} 같은 플레이스홀더를 변경하지 않는다.
  • Unity Rich Text 태그를 유지한다.
  • 줄바꿈 수를 임의로 늘리거나 줄이지 않는다.
  • 키, 원문, 문맥을 함께 참고하되 키 자체는 번역하지 않는다.

번역 결과를 받았을 때도 원문의 토큰 집합과 결과의 토큰 집합이 같은지 검사해야 합니다. 단순하지만 비용이 큰 UI 오류를 초기에 막아 줍니다.

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

public static class LocalizationValidation
{
    private static readonly Regex TokenPattern =
        new(@"\{[A-Za-z0-9_]+\}|<[^>]+>");

    public static bool HasSameTokens(string source, string translated)
    {
        var sourceTokens = new HashSet<string>(TokenPattern.Matches(source)
            .Select(match => match.Value));
        var translatedTokens = new HashSet<string>(TokenPattern.Matches(translated)
            .Select(match => match.Value));

        return sourceTokens.SetEquals(translatedTokens);
    }
}

태그의 순서까지 검증해야 하는 프로젝트라면 집합 비교 대신 토큰 시퀀스를 비교하거나 태그만 별도로 파싱하는 검증기를 두는 것이 안전합니다.

3. 변경분만 번역 요청으로 만들기

매번 모든 문자열을 20개 언어로 번역하면 비용과 검수량이 빠르게 커집니다. 각 항목의 원문과 문맥을 정규화한 뒤 해시를 저장하고 이전 해시와 달라진 키만 작업 대상으로 선정합니다.

using System;
using System.Security.Cryptography;
using System.Text;

public static string CreateSourceHash(string source, string context)
{
    var normalized = $"{source.Trim()}\n{context.Trim()}";
    using (var sha256 = SHA256.Create())
    {
        var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(normalized));
        return BitConverter.ToString(bytes).Replace("-", "");
    }
}

해시에는 원문뿐 아니라 번역에 영향을 주는 문맥도 넣습니다. 원문이 같더라도 버튼에서 대사로 용도가 바뀌었다면 재번역 또는 최소한 재검수가 필요하기 때문입니다.

번역 작업 파일에는 대상 언어별 상태를 둡니다. translated, needs_review, approved, invalid처럼 상태를 분리하면 자동 생성된 번역과 사람이 승인한 번역을 구별할 수 있습니다.

{
  "key": "battle.victory_message",
  "source": "승리했습니다!",
  "sourceHash": "A1B2C3...",
  "translations": {
    "en": { "text": "Victory!", "status": "approved" },
    "ja": { "text": "勝利しました!", "status": "needs_review" }
  }
}

4. 번역 요청은 작은 단위로 재시도는 중복없이

번역 API는 언어별 요청 한도와 일시적인 실패를 고려해야 합니다. 한 요청에 너무 많은 문장을 넣으면 실패 시 재처리 범위가 커지고 너무 적게 보내면 호출 횟수가 늘어납니다. 프로젝트에 맞는 묶음 크기를 정하되 응답을 키 단위로 다시 대응할 수 있게 요청 본문에 식별자를 포함합니다.

번역 결과를 바로 원본 파일에 덮어쓰지 않는 것도 중요합니다. 먼저 임시 결과 파일에 저장하고 검증을 통과한 항목만 병합합니다. 작업이 중단되어도 같은 입력 해시와 대상 언어라면 이미 완료된 결과를 재사용할 수 있어야 합니다.

번역 모델에는 게임 장르와 용어집을 제공합니다. 예를 들어 RPG에서 party는 파티 모임이 아니라 플레이어 일행일 수 있고 save는 구원이 아니라 저장일 수 있습니다. 고유명사, 아이템명, 기술명, 금칙어를 용어집으로 관리하면 언어 사이의 표기가 훨씬 일관됩니다.

5. Unity에서 로컬 번역 테이블 읽기

Unity Localization 패키지를 사용한다면 추출한 키와 번역 결과를 String Table에 반영하는 편이 자연스럽습니다. 별도 구현을 선택했다면 빌드 시 언어별 JSON을 포함하고 런타임에는 현재 로케일의 딕셔너리만 메모리에 올리는 방식도 충분합니다.

아래 예제는 간단한 런타임 조회 형태입니다. 실제 프로젝트에서는 누락 키를 수집해 CI 로그나 분석 도구로 보내는 처리를 추가하는 것이 좋습니다.

public sealed class TextCatalog
{
    private readonly IReadOnlyDictionary<string, string> _entries;

    public TextCatalog(IReadOnlyDictionary<string, string> entries)
    {
        _entries = entries;
    }

    public string Get(string key)
    {
        if (_entries.TryGetValue(key, out var value))
            return value;

        UnityEngine.Debug.LogWarning($"Missing localization key: {key}");
        return $"[{key}]";
    }
}

대체 언어 정책도 명시해야 합니다. 예를 들어 프랑스어 번역이 없을 때 영어를 보여 줄지, 원문인 한국어를 보여 줄지, 키를 노출해 테스트에서 즉시 발견하게 할지를 환경별로 정합니다. 개발 빌드에서는 키 노출이 유용하지만 출시 빌드에서는 사용자에게 읽을 수 있는 대체 언어를 보여 주는 편이 낫습니다.

6. CI에서 막아야 할 실패 유형

번역 파이프라인은 코드 빌드와 같은 수준으로 검증하는 편이 좋습니다. 다음 항목은 자동 검사 대상으로 적합합니다.

  • 모든 활성 키가 기본 언어에 존재하는지 확인한다.
  • 대상 언어별 필수 키의 누락 여부를 확인한다.
  • 플레이스홀더와 Rich Text 태그가 원문과 일치하는지 확인한다.
  • 빈 문자열과 중복 키를 막는다.
  • 승인되지 않은 자동 번역이 출시 대상에 포함되지 않도록 막는다.
  • UI 글자 수 제한이 있는 항목을 별도 목록으로 검사한다.

특히 글자 수는 단순 문자열 길이만으로 판단하기 어렵습니다. 언어마다 단어 길이와 줄바꿈 위치가 달라서, 제한이 엄격한 버튼·탭·HUD 항목은 실제 해상도와 폰트에서 스크린샷 테스트를 하는 것이 가장 확실합니다.

마무리

20개 국어 지원은 번역 호출 횟수보다 데이터 수명 주기를 설계하는 문제에 가깝습니다. 안정적인 키, 충분한 문맥, 토큰 검증, 변경분 처리, 승인 상태를 갖추면 새 텍스트가 추가되어도 작업량을 예측할 수 있습니다.

처음부터 모든 언어와 모든 화면을 자동화하려 하기보다 메뉴와 시스템 메시지처럼 구조가 단순한 영역에 파이프라인을 먼저 적용해 보세요. 누락 키와 서식 파손을 잡는 검증부터 안정화하면 대사, 퀘스트, 라이브 이벤트 텍스트로 확장하기가 훨씬 수월합니다.