3 min read

프로젝트에 다국어 지원이 필요한데.. i18n 을 안썼습니다.

i18n 라이브러리 말고 그냥 만들어보자

왜 vue-i18n 같은거 안쓰고..

vue-i18n 같은 라이브러리를 쓰면 번역 파일이 프로젝트 안에 들어갑니다. 번역을 수정하려면 코드를 고치고 배포해야 해요.

우리 상황에서는 이게 맞지 않았습니다

  • 번역은 기획자/번역가가 직접 관리해야 함 (개발자 병목 제거)
  • 번역 수정이 코드 배포와 독립적이어야 함
  • 라이브 중간에 빌드 및 새 배포 없이 즉각 수정해서 언어팩만 교체해야 했음
  • 도메인별로 시트를 나눠서 관리하고 싶음

그래서 그냥.. 직접 만들었습니다.

구조

Google Sheets (번역 원본)
    ↓ Lambda 트리거
    ↓ Google Sheets API로 시트별 데이터 조회
    ↓ 언어별 JSON으로 변환
    ↓ S3 업로드 (CDN 배포)
    ↓
프론트 앱 초기화 시 CDN에서 JSON fetch
    ↓
$t(key, sheetName) 으로 사용

구글 시트 구조

시트 하나가 하나의 도메인입니다. 시트 이름이 곧 sheetName.

KEY KO EN JA
member.table.name 이름 Name 名前
member.table.email 이메일 Email メール
member.role.admin 관리자 Admin 管理者

비개발자가 구글 시트에서 직접 번역을 추가/수정합니다. 개발자는 코드에서 키만 참조하면 돼요.

프론트에서는 이런 식으로

class LanguageHelper {
  static languagePack: Record<string, Record<string, string>> = {};

  static $t(
    key: string,
    sheetName = 'global',
    { replace = [] }: { replace?: string[] } = {},
  ): string {
    const sheet = this.languagePack[sheetName] || {};
    let str = sheet[key] || `#${key}`;
    replace.forEach((item, index) => {
      str = str.replace(new RegExp(`\\{${index}\\}`, 'g'), item);
    });
    return str;
  }

  static async fetchLanguagePack() {
    const res = await fetch(`https://cdn.example.com/locale/${env}/ko.json`);
    this.languagePack = await res.json();
  }
}

앱이 초기화될 때 CDN에서 언어팩 JSON을 fetch해서 메모리에 올립니다. 이후 $t('member.table.name', 'organization') 이런 식으로 사용.

{0}, {1} 같은 플레이스홀더도 지원해서, "총 {0}명의 멤버" 같은 동적 텍스트도 처리됩니다.

Lambda: 시트 → S3 동기화

const getLocales = (data) => {
  const wrapperData = {};

  for (const sheetKey in data) {
    const headers = data[sheetKey][0]; // 첫 행이 헤더 (KEY, KO, EN, JA...)
    const keyIndex = headers.indexOf('KEY');

    supportedLanguages.forEach(langCode => {
      if (!headers.includes(langCode)) return;

      const langIndex = headers.indexOf(langCode);
      const localeData = {};

      for (const row of data[sheetKey].slice(1)) {
        const key = row[keyIndex];
        const value = row[langIndex];
        if (value) localeData[key] = value;
      }

      wrapperData[langCode] = wrapperData[langCode] || {};
      wrapperData[langCode][sheetKey] = localeData;
    });
  }
  return wrapperData;
};

Google Sheets API로 시트를 5개씩 배치로 조회하고 (API rate limit 회피를 위해 지수 백오프 적용), 언어별로 JSON을 만들어서 S3에 업로드합니다.

S3에 올라간 JSON은 CDN을 통해 서빙되니까, 결국 번역 업데이트가 코드 배포 없이 반영됩니다.


마지막으로

번역 관리는 기획자가 시트에 쓰고 Slack에서 동기화하면 끝. 배포도 Slack에서 30초면 되니까, "배포 좀 해주세요" 같은 요청이 없어졌습니다.

i18n 라이브러리를 안 쓴 게 정답이었냐고 하면 — 상황에 따라 다릅니다. 번역이 코드와 독립적으로 관리되어야 하고, 비개발자가 직접 수정해야 하는 환경이라면 이 방식이 맞았어요. 소규모 프로젝트에서 개발자가 직접 번역 파일 관리하는 거라면 vue-i18n이 훨씬 간단했을 겁니다.