국제화, 다국어 서비스 도입 (internationalization, I18N)

p_samename·2025년 9월 17일

글로벌 서비스를 위한 i18n 도입

운영 중인 닷컴 서비스에 **i18n(국제화)**을 도입을 하게 되었다. 단순히 번역 파일을 작성하는 것이 아닌, i18next-scanner로 번역 키를 자동 추출하고, 구글 스프레드시트와 연동하여 각 팀이 협업할 수 있는 워크플로우를 구축한 과정을 정리 해보려고한다.


1. 왜 i18n을 도입하게 되었는가?

서비스가 글로벌 유저를 타깃으로 확장되면서 걱정되었던 문제는 언어 지원이었다. 영어와 일본어,중국어등 최소5개 국어에 대한 언어를 도입하겠다는 계획에 번역 관리의 필요성에 대한 고민이 커졌다.

체감한 문제들:

  • 페이지 곳곳에 흩어져 있는 하드코딩 텍스트
  • 프로젝트가 커질수록 누적되는 불필요한 번역 키
  • 디자이너와 마케터와의 원활한 협업 불가

이에 i18next 도입을 결정했다.


2. i18next-scanner로 번역 키 자동화

수동으로 번역 키를 추출하고 JSON에 정리하는 방법은 곧 한계에 다다랐다. 그래서 i18next-scanner를 도입했다.

var fs = require('fs');
var chalk = require('chalk');
const path = require('path');

// JavaScript와 TypeScript 파일 확장자
const COMMON_EXTENSIONS = '/**/*.{js,jsx,ts,tsx}';

module.exports = {
  // 스캔 대상 입력 파일/디렉토리
  input: [`pages${COMMON_EXTENSIONS}`, `components${COMMON_EXTENSIONS}`],
  options: {
    defaultLng: 'ko', // 기본 언어 설정: 'ko-KR'
    createNamespace: true,
    lngs: ['ko', 'en', 'ja', 'zh'], // 지원 언어 목록
    trans: {
      component: 'Trans',
      i18nKey: 'i18nKey',
      defaultsKey: 'defaults',
      extensions: ['.js', '.jsx'],
      fallbackKey: function (ns, value) {
        return value;
      },
    },
    resource: {
      // 번역 파일 경로. {{lng}}, {{ns}}는 언어 및 네임스페이스로 대체
      loadPath: 'public/locales/{{lng}}/{{ns}}.json',
      savePath: 'public/locales/{{lng}}/{{ns}}.json',
      jsonIndent: 2,
      lineEnding: '\n',
    },
    ns: ['common', 'footer', 'event'],
    defaultNs: 'common',
    defaultValue(lng, ns, key) {
      return '';
    },
    nsSeparator: ':',
    keySeparator: false,
    removeUnusedKeys: true, // 사용하지 않는 키 자동 제거
  },
  transform: function customTransform(file, enc, done) {
    'use strict';
    const parser = this.parser;
    const content = fs.readFileSync(file.path, enc);
    let ns;
    const match = content.match(/useTranslation\(.+\)/);
    if (match) ns = match[0].split(/(\'|\")/)[2];
    let count = 0;
    parser.parseFuncFromString(content, { list: ['t'] }, function (key, options) {
      parser.set(
        key,
        Object.assign({}, options, {
          ns: ns ? ns : 'common',
          nsSeparator: ':',
          keySeparator: false,
          removeUnusedKeys: true, // 사용하지 않는 키 자동 제거
        }),
      );
      ++count;
    });
    if (count > 0) {
      console.log(`i18next-scanner: count=${chalk.cyan(count)}, file=${chalk.yellow(JSON.stringify(file.relative))}`);
    }

    done();
  },
};

상세 설명

  • input: pagescomponents 폴더 내의 모든 .js, .jsx, .ts, .tsx 파일을 스캔.
  • options: 기본 언어와 지원 언어, 네임스페이스, 사용하지 않는 키 자동 제거 설정 등..
  • transform: 파일을 읽고 t() 함수 호출을 파싱하여 namespace에 맞게 JSON에 반영.

이로써 코드 내 모든 번역 키를 자동으로 추출하고, namespace별로 정리할 수 있어 유지보수가 용이할 수 있도록 하였다.


3. 구글 스프레드시트와 연동

개발자가 아닌 팀원들도 번역을 편하게 수정할 수 있어야 했다. json파일에 쉽게 접근할 수 없기에 구글 스프레드시트 사용을 선택하였다.

3-1. 번역 업로드 (Uploader)

프로젝트 내 json형식으로 되어있는 번역 키 파일을 파싱하여 스프레드시트로 업로드해 팀원들이 수정할 수 있도록 동기화한다.

class I18nUploader {
  constructor(spreadsheetId, credentials) {
    this.spreadsheetId = spreadsheetId;
    this.credentials = credentials;
  }


  async upload(namespace, data) {
    const doc = new GoogleSpreadsheet(this.spreadsheetId);
    await doc.useServiceAccountAuth(this.credentials);
    await doc.loadInfo();


    const sheet = doc.sheetsByTitle[namespace];
    if (!sheet) return;
    // 데이터의 키와 값을 시트에 업데이트
    // 존재하지 않으면 새 행 추가
    
    ... 생략
  }
}

3-2. 번역 다운로드 (Downloader)

스프레드시트에서 최신 번역 데이터를 json 형식으로 파싱하여 프로젝트로 가져올 수 있도록 동기화한다.

const { GoogleSpreadsheet } = require('google-spreadsheet');
const creds = require('./credentials.json');
const SPREADSHEET_ID = '스프레드시트-ID';


class I18nDownloader {
  constructor(spreadsheetId, credentials) {
    this.spreadsheetId = spreadsheetId;
    this.credentials = credentials;
  }


  async download() {
    const doc = new GoogleSpreadsheet(this.spreadsheetId);
    await doc.useServiceAccountAuth(this.credentials);
    await doc.loadInfo();


    for (const sheet of doc.sheetsByIndex) {
      const rows = await sheet.getRows();
      // 각 시트의 데이터를 언어별 JSON로 변환 및 저장
      
      ... 생략
    }
  }
}

프로젝트에서 새로 추가된 번역 키를 스프레드시트에 반영.

스프레드시트 API를 통해 행 데이터를 가져오고, 언어별 JSON 파일을 생성.

비개발자인 팀원들이 스프레드시트에서 번역을 바로 수정할 수 있도록 함.

팀원들은 스프레드시트에서 바로 텍스트를 수정하고, 개발자는 스크립트 실행만으로 최신 번역을 반영할 수 있도록 하였다.


4. 번역 프로세스

번역 키 추출 (scan)

명령어:

"i18n:scan": "i18next-scanner --config i18next-scanner.config.js"

역할:
pages/**/*.js, ts, jsx, tsx 파일에서 t("...") 같은 번역 키를 자동으로 추출

결과:
i18n/locales/{{lng}}/{{ns}}.json 파일 업데이트 (키 추가)

구글 시트 업로드 (export)

명령어:

i18n:export: "node i18n/uploadSheets.js"

역할:
로컬 locales/ko/translation.json, locales/en/translation.json → Google Spreadsheet에 업로드

목적:
협업자가 구글 시트에서 번역을 쉽게 관리 가능

업로드 프로세스 (JSON → 구글 시트)

"i18n:export": "node i18n/uploadSheets.js"

flow:

translation.json (ko/en)
        ↓
  Node.js 스크립트
        ↓
  GoogleSpreadsheet 객체 생성
        ↓
  useServiceAccountAuth(creds)
        ↓
  loadInfo() → 시트 정보 로드
        ↓
  시트 clear & setHeaderRow(['Key','Korean','English'])
        ↓
  addRows(rows) → 구글 시트에 업로드
  • JSON에서 Key / Korean / English 매핑
  • 빈 값 처리 가능 (|| '')
  • CommonJS (require) 권장
  • creds 파일 로컬에서 읽어서 안전하게 사용
  • google sheet api v3 < 최신 버전 v4 이나 현재 프로젝트 commonJS 사용으로 v3 설치

다운로드 프로세스 (구글 시트 → JSON)

"i18n:import": "node i18n/downloadSheets.js"

flow:

Node.js 스크립트
        ↓
  GoogleSpreadsheet 객체 생성
        ↓
  useServiceAccountAuth(creds)
        ↓
  loadInfo() → 시트 정보 로드
        ↓
  sheet.getRows() → 모든 행 가져오기
        ↓
  각 행을 Ko / En 객체에 매핑
        ↓
  locales/ko/translation.json, locales/en/translation.json 저장

디렉토리 없으면 생성

JSON.stringify(..., null, 2)로 가독성 확보 (들여쓰기)

로컬 개발 환경에서 바로 실행 가능


최종 프로세스

  A --> [Source Code pages/**] -->|i18n:scan| --> [Locales JSON]
  B --> |i18n:import| google sheet --> 프로젝트 JSON 파싱
  C --> |i18n:export| 프로젝트 JSON --> google sheet
  D --> [Next.js Dev / Build]
  E --> [Production 서비스]
  1. google sheet에 있는 언어팩 파일을 json형식으로 파싱하여 프론트로 import
  2. 프론트 전체 설정 경로의 i18n key들을 추출하여 locales json 파일로 파싱
  3. json 으로 파싱된 프론트의 key들을 google sheet 로 excel 형식으로 파싱하여 export
  4. 프론트 서버 빌드
  5. production 실행

5. 실제 JSON 예시

예시 common 네임스페이스:

// public\locales\ko\common.json

{
  "welcomeMessage": "환영합니다!",
  "logoutButton": "로그아웃",
  "profileTitle": "프로필"
}

 
// public\locales\en\common.json

{
  "welcomeMessage": "welcome!",
  "logoutButton": "logout",
  "profileTitle": "profile"
}


-------------------


// public\locales\ko\footer.json
{
  "terms": "이용약관",
  "policy": "개인정보취급방침",
  "businessInfo": "사업자정보"
}


// public\locales\en\footer.json
{
  "terms": "Terms",
  "policy": "Policy",
  "businessInfo": "BusinessInfo"
}

...

스프레드시트에서 키와 번역 값을 관리하면 각 네임스페이스별('common.json', 'footer.json' ...등) 언어별(ko.json, en.json, ...등 ) JSON 파일이 자동 생성된다.


이번 i18n 도입은 팀 전체가 효율적으로 번역을 관리할 수 있는 시스템을 고민할 수 있는 경험이 되었다. 글로벌 서비스 준비에 있어 자동화와 협업 중심 접근은 필수적임을 확인했다. 또한 구글시트를 json 파싱할때에 생기는 실수 혹은 오류들에 대한 검증 프로세스도 준비해야 할 것 같다. 각 행에 대한 빈값이나, 허용되지 않는 문자 등.. 정책을 정하고 해당 정책에 대한 검증 로직을 추가 예정이다.

profile
@p_samename

0개의 댓글