Google Sheets로 모두를 위한 국제화 구현하기

작성일: Sun Feb 04 2024

들어가며

위키백과에 따르면, 전세계에는 약 7,139 개의 언어가 있다고 합니다.

다양한 언어를 사용하는 유저들을 타겟으로 하는 서비스라면, 국제화는 빼놓을 수 없는 요소입니다.

그리고 그런 서비스가 성장할수록, 서비스 내에서 제공해야 하는 다국어 텍스트도 많아질 수밖에 없는데요.

점점 많아지는 다국어 텍스트가 사용자 경험과 개발자 경험에 어떤 영향을 미칠 수 있는지를 알아보고,

다국어 텍스트를 효율적으로 관리할 수 있는 방법을 소개합니다.


TL:DR

  • 자주 추가, 수정될 여지가 있는 다국어 텍스트는 소스 코드와 별개의 장소에서 관리하자. (for 개발자 경험)
  • 그러나 웹앱이 실행될 때에는 실행 환경과 가까운 곳에 위치해야 한다. (for 사용자 경험)
  • 따라서 웹앱의 빌드 타임에 다국어 텍스트를 불러와 소스 코드에 위치하게 하자.

대상 독자

이 글은 다음과 같은 사전지식이 있는 독자를 대상으로 합니다.

  • JavaScript 문법에 대한 기본적인 이해
  • Git 에 대한 기본적인 이해
  • 액셀의 행과 열, 셀에 대한 용어 이해

소스 코드에서 다국어 텍스트를 관리하는 것의 불편함

웹앱에 다국어 텍스트를 적용하려면 우선 다국어 텍스트를 어디에 저장할지를 정해야 합니다.

가장 먼저 떠올릴 수 있는 건 소스 코드에 다국어 텍스트를 저장하는 것인데요.

예를 들면 다음과 같이 언어별 파일을 두고 텍스트를 관리할 수 있습니다.

// ko-KR.json

{
  "hello": "안녕하세요",
     "contact": "연락처",
  ...
}
// en-US.json

{
  "hello": "Hello",
     "contact": "contact",
  ...
}
// ja-JP.json

{
  "hello": "こんにちは",
     "contact": "連絡先情報",
  ...
}

이렇게 소스 코드에 저장된 다국어 텍스트는 추가, 수정이 필요할 때마다 다음과 같은 과정을 거치게 됩니다.

  1. 기획 단계에서의 텍스트 변경 요청
  2. 소스 코드의 다국어 텍스트 파일 변경
  3. 변경사항 커밋
  4. 변경사항 리뷰 및 메인 브랜치 병합
  5. 배포

간단한 텍스트 하나를 수정하여 반영하는 경우에도 위와 같은 과정을 반복적으로 거쳐야 한다는 불편함이 있습니다.

만약 다국어 텍스트를 소스 코드 외부에 두고 관리하면 어떨까요?

개발자는 다국어 텍스트 파일을 직접 수정하지 않고도 웹앱에 변경 사항을 반영할 수 있습니다.

그렇다면 어떻게 다국어 텍스트를 소스 코드 외부에 두고 관리하면서도, 웹앱에 이를 적용할 수 있을지 알아보겠습니다.


다국어 텍스트 관리 툴 선택

다국어 텍스트를 관리할 수 있는 툴의 선택지는 매우 다양합니다.

선택지가 많을수록 요구사항을 분명히 해야 시간 낭비를 줄일 수 있습니다.

제가 다국어 텍스트 관리 툴에 원하는 바는 다음과 같았습니다.

  • 텍스트 수정 과정이 간편한가?
  • 변경내역을 조회할 수 있는가? (텍스트 수정시 발생할 수 있는 실수를 돌리기 위해)
  • 외부에서 텍스트를 조회할 수 있는 API 를 제공하는가?

다국어 텍스트 관리에만 집중한 솔루션은 많았지만, (Tolgee, SimpleLocalize 등)

해당 솔루션들은 무료가 아님에도 사용이 불편한 지점들이 하나씩 있었습니다.

결국 저의 요구사항을 모두 만족하는 도구는 Google Sheets 가 유일했습니다.

Google Sheets 는 액셀 시트를 클라우드 환경에서 사용할 수 있는 Google 의 제품으로,

기존에 액셀을 사용해본 사람이라면 누구나 손쉽게 수정, 관리할 수 있다는 점이 특장점으로 다가왔습니다.


Google Sheets 에 다국어 텍스트 작성하기

Google Sheets 는 Google 계정만 있으면 손쉽게 시작할 수 있습니다.

Google Sheets 에서 새 스프레드 시트를 만든 뒤, 다국어 텍스트를 다음과 같이 작성해볼 수 있습니다.

i18n_sheets_001.png

1열은 각 다국어 텍스트를 구분할 키를 작성해두고, 다른 열에는 언어별 텍스트를 작성합니다.


다국어 텍스트 불러오기 & 저장하기 구현

google-spreadsheet 라이브러리를 사용하면 Google Sheets 의 각 시트 정보를 불러오고, 수정하는 것도 가능합니다.

이를 위해서 아래와 같은 라이브러리들을 설치해줍니다.

const dotenv = require('dotenv');
const fs = require('fs');
const { JWT } = require('google-auth-library');
const { GoogleSpreadsheet } = require('google-spreadsheet');

Google Sheets 에 대한 EMAIL_ID 와 PRIVATE_KEY 등은 google-spreadsheet 공식문서의 인증 파트 를 참고하면 얻을 수 있습니다.

  const authorize = new JWT({
    email: process.env.GOOGLE_SHEET_API_EMAIL_ID,
    key: process.env.GOOGLE_SHEET_API_PRIAVTE_KEY,
    scopes: [
      'https://www.googleapis.com/auth/spreadsheets'
    ]
  });

Google Sheets 에 대한 액세스 권한 등을 얻었다면, 다국어 텍스트가 저장된 시트 정보를 불러오는 스크립트를 작성합니다.

const GOOGLE_SPREAD_SHEET_ID = 'your_google_sheets_id';
const TRANSLATION_NAMESPACE_SHEET_GID = 'yout_spread_sheet_tab_gid'

const doc = new GoogleSpreadsheet(GOOGLE_SPREAD_SHEET_ID, authorize);
await doc.loadInfo();
const sheet = doc.sheetsById[`${TRANSLATION_NAMESPACE_SHEET_GID}`];
// 불러올 셀의 범위는 각 행의 라벨을 제외하고, 다국어 텍스트 키와 값만 해당되는 범위로 설정합니다.
const cells = await sheet.getCellsInRange('A2:D');

cells 에는 이제 Google Sheets 에서 불러온 다국어 텍스트가 배열의 형태로 저장됩니다.

다국어 텍스트를 웹앱에서 불러오는 것 까지는 성공했는데, 이쯤에서 고민해봐야 할 것이 있습니다.

바로 다국어 텍스트를 불러오는 시점, 즉 이 파일을 언제 실행하느냐에 대한 문제입니다.

이 문제는 제가 이 글을 쓰게 된 가장 큰 이유이기도 합니다.


다국어 텍스트를 불러오는 시점

웹앱의 라이프사이클을 아주 간단히 요약하면 다음과 같습니다.

  1. 웹앱 빌드
  2. 빌드된 코드가 유저의 요청에 의해 브라우저에서 실행

결론부터 이야기하면, 다국어 텍스트를 불러오는 시점은 웹앱 빌드 시점이어야 합니다.

아니면 적어도, 유저의 요청에 의해 브라우저에서 웹앱 코드가 실행되기 전에는 소스 코드에 이미 다국어 텍스트가 포함되어 있어야 합니다.

왜냐하면 다국어 텍스트를 Google Sheets 같은 외부 공간에서 불러오는 데에는 일정 시간이 소요되기 때문인데요.

약 4000줄의 다국어 텍스트를 가지고 여러번 테스트해본 결과, 평균 2초에서 많게는 10초까지 걸렸습니다.

만약 다국어 텍스트를 웹앱 빌드 시점이 아닌 브라우저에서 코드가 실행되는 런타임에 불러오려고 한다면,

유저는 제대로 된 텍스트를 보기 위해 몇 초를 기다려야 한다는 의미이며, 이는 사용자 경험에 심각한 악영향을 끼칠 수 있습니다.


반면 웹앱이 빌드되는 시점에 다국어 텍스트를 불러와 저장하면, 앞에서 얘기한 2초 정도의 시간이 브라우저 런타임이 아닌 빌드타임으로 옮겨오게 됩니다.

사용자 입장에서의 2초는 참아주기 어려운 딜레이지만, 웹앱 빌드 관점에서의 2초는 그 무게가 비교적 가벼운 편입니다.

따라서, 다국어 텍스트를 Google Sheets 등의 외부로부터 불러오는 시점은 웹앱이 브라우저 등의 런타임에서 실행되기 이전이어야 합니다.


이렇게 빌드타임에 불러온 다국어 텍스트는 소스 코드에 포함되도록 하여, 런타임에는 딜레이 없이 사용자에게 다국어 텍스트가 보여지도록 합니다.

const koMap = {};
const enMap = {};
const jaMap = {};

cells.forEach(([key, ko, en, ja]) => {
  koMap[key] = ko;
  enMap[key] = en;
  jaMap[key] = ja;
});

fs.writeFileSync(`yourFolderName/yourFileName.ts`, `
  export const koI18n = ${JSON.stringify(koMap)};
  export const enI18n = ${JSON.stringify(enMap)};
  export const jaI18n = ${JSON.stringify(jaMap)};
`);

package.json 의 scripts 에 pre 키워드를 사용하여 해당 스크립트 파일이 빌드 타임 직전에 실행되도록 하면, 런타임에는 다국어 텍스트 파일이 존재하리라는 것을 확신할 수 있습니다.

// package.json
{
  ...,
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "format": "prettier --write \"**/*.{ts,tsx,md}\"",
        "predev": "turbo translate",
      "prebuild": "turbo translate",
      "translate": "node 다국어텍스트불러오는스크립트파일.js"
  }
}

다국어 텍스트 파일 서비스에 적용하기

생성된 다국어 텍스트 파일의 내용을 보면 아래와 같습니다.

// youtFolderName/yourFileName.ts

export const koI18n = {
 "hello": "안녕하세요",
 "contact": "연락처"
};

export const enI18n = {
 "hello": "Hello",
 "contact": "contact"
};

export const jaI18n = {
    "hello": "こんにちは",
     "contact": "連絡先情報",
};

이렇게 생성된 다국어 텍스트 파일은 i18next 와 같은 국제화 라이브러리에 주입하여 실제 서비스 화면에 렌더링되게 됩니다.

i18next 에 저희가 저장한 다국어 텍스트 파일을 주입해 보겠습니다.

import i18next from 'i18next';
import {koI18n, enI18n, jaI18n} from '.youtFolderName/yourFileName.ts'

i18next.init({
  lng: 'ko',
  debug: true,
  resources: {
    ko: {
      translation: koI18n
    },
    en: {
      translation: enI18n
    },
    ja: {
      translation: jaI18n
    }
  }
}, function(err, t) {
  // i18next 초기화 완료.
  // i18next.t('키 이름') 으로 다국어 텍스트를 렌더링할 수 있습니다.
  document.getElementById('output').innerHTML = i18next.t('hello'); // 안녕하세요 or Hello or こんにちは
});

i18next 에서는 언어 초기화 이외에도 언어 변경 등 다양한 기능을 제공하지만, 이 글에서는 해당 라이브러리의 사용 방법 등에 대해 자세히 다루지는 않겠습니다. i18next 에 대해 더 알고 싶은 분은 i18next API 문서에서 확인하실 수 있습니다.


요약

지금까지의 내용을 요약하면,

  1. 다국어 텍스트는 웹앱에서 사용하는 소스 코드 외부에 두고 관리합니다.
  2. 적어도 웹앱 런타임 이전에는 외부에 존재하는 다국어 텍스트가 소스 코드에 존재할 수 있도록 합니다.
  3. 다국어 텍스트를 소스 코드 외부에 두고 관리하는 이유는 개발자 경험을 위해서입니다.
  4. 소스 코드 외부에서 관리하는 다국어 텍스트를 다시 빌드 타임에 소스 코드로 불러오는 이유는, 유저 경험을 위해서입니다.

전체 스크립트

글에서 예시로 든 다국어 텍스트 불러오기 & 저장 과정의 전체 스크립트 입니다.

const dotenv = require('dotenv');
const fs = require('fs');
const { JWT } = require('google-auth-library');
const { GoogleSpreadsheet } = require('google-spreadsheet');

const GOOGLE_SPREAD_SHEET_ID = 'your_google_sheets_id';
const TRANSLATION_NAMESPACE_SHEET_GID = 'yout_spread_sheet_tab_gid'

const fetchTranslationSheet = async () => {
  const authorize = new JWT({
    email: process.env.YOUR_GOOGLE_SHEETS_API_EMAIL,
    key: process.env.YOUR_GOOGLE_SHEETS_API_PRIAVTE_KEY,
    scopes: [
      'https://www.googleapis.com/auth/spreadsheets'
    ]
  });

  const doc = new GoogleSpreadsheet(GOOGLE_SPREAD_SHEET_ID, authorize);
  await doc.loadInfo();
  const sheet = doc.sheetsById[`${TRANSLATION_NAMESPACE_SHEET_GID}`];
  // 불러올 셀의 범위는 각 행의 라벨을 제외하고, 다국어 텍스트 키와 값만 해당되는 범위로 설정합니다.
  const cells = await sheet.getCellsInRange('A2:D');

  const koMap = {};
  const enMap = {};
  const jaMap = {};

  cells.forEach(([key, ko, en, ja]) => {
    koMap[key] = ko;
    enMap[key] = en;
    jaMap[key] = ja;
  });

  fs.writeFileSync(`yourFolderName/yourFileName.ts`, `
    export const koI18n = ${JSON.stringify(koMap)};
    export const enI18n = ${JSON.stringify(enMap)};
    export const jaI18n = ${JSON.stringify(jaMap)};
  `);
}

(async() => {
  dotenv.config();
  await fetchTranslationSheet();
})();

마치며

다국어 텍스트를 서비스에 적용하면서 겪었던 비효율을 직접 해결해 본 경험이라서, 시간가는 줄 모르고 즐겁게 작업했습니다.

특히 서비스가 커져감에 따라 Git 에 포함되는 다국어 텍스트의 라인 수가 4,000 줄이 넘어가고 있던 상황이었는데,

이 작업이 머지되면서 해당 텍스트 파일을 모두 지우는 PR을 올릴 때 조금의 희열(?)을 느꼈습니다.

물론 글 작성 시점에서는 아직 해당 코드를 작성한 지 얼마 되지 않았기 때문에, 시간이 흘러서 위 방법의 허점을 발견한다거나, 좀 더 효율적인 방법을 발견할 지도 모르겠습니다.

그때가 오면 그 일을 계기로 조금 더 성장할 수 있었으면 좋겠습니다.

i18n_commit_lines_001.png


레퍼런스

https://developers.google.com/sheets/api/guides/concepts?hl=ko

https://simplelocalize.io/

https://tolgee.io/

https://www.i18next.com/