본문 바로가기
개발/개발지식

[리액트] 글로벌한 웹을 향하여 #2(google sheets, 자동화)

by 핸디(Handy) 2022. 10. 7.

들어가며

이전 글에서 구글 시트를 이용하여 i18n에 대응하는 예시를 보여드렸었습니다.

그리고 이번에 갑작스레 일어를 추가하자는 요건에 대응하여 기존에 있는 기능을 확장한 방법에 대해 공유하겠습니다.

대상 독자

  • 여러 가지 언어를 대응해야 하는 프론트엔드 개발자
  • 개발과 번역을 분리하고 싶은 한글을 사랑하는 개발자

이 글을 읽기 위해선 선행글이 존재합니다. ( 필수는 아니지만 보고 오면 좋아요 )

 

[리액트] 글로벌한 웹을 향하여 (react-i18next, 다국어지원)

들어가며 이번 글에서는 글로벌한 웹을 위해서 react i18n를 적용하는 방법과 나름의 best practice에 대해 적어보겠습니다. 이 글은 아직 이제 막 프로젝트에 i18n을 던지려는 용기 있는 개발자를 위한

all-dev-kang.tistory.com

문제 인식 및 설계

행복한 주말을 기다리는 금요일 오전 10시,

갑작스레 다음주에 일본 출장을 가는데, 현재 사이트에 일어를 추가해줄 수 있냐는 동료의 요구사항이 들어왔습니다.

고민을 잠깐 하다가 이번에 기술 부채를 해소할 기회라고 생각되어 기능 설계에 들어갔습니다.

프로세스 설계

설계는 이전에 구상해놨던 아래 프로세스를 따라가도록 했습니다.

  1. 구글 스프레드 시트에 locale에 맞춰 데이터 업데이트
  2. 스크립트를 통해 locale json 파일 생성
  3. 생성된 json 파일 기반으로 프로젝트 내부에서 사용
  4. 빌드될 때 생성된 파일을 가지고 i18n 적용

따라서 이번 글에서 다룰 내용은 2번과정까지에 대해 다룹니다. 3,4번은 이전글에서 다룬 내용과 동일합니다.

필요사항 도출

  • i18n 데이터를 저정할 구글 계정과 접근 방법
  • 구글 sheet api 권한 및 로직
  • 구글 sheet -> locale json으로 변경하는 로직
  • json을 파일로 write하는 로직

이로써 준비는 끝났습니다. 이제 하나씩 구현을 시작하도록 하겠습니다.

구현

구글 스프레드 시트 생성 및 연결 준비

공용 계정에 구글 스프레드 시트를 생성해줍니다. 이 부분은 다들 아실 테니 건너뛰겠습니다.

그리고 이 파일을 수정할 수 있도록 권한을 주는 작업을 마무리합니다.

그런 다음 i18n 데이터를 저장합니다.

i18n 데이터 저장

데이터를 저장하는 방법론은 크게 2가지가 있습니다.

언어별로 할 것인가? 모듈별로 할 것인가?

  • 언어별로 할 경우 하나의 sheet가 하나의 언어가 됩니다.
  • 모듈별로 할 경우 하나의 sheet가 하나의 모듈이 됩니다.

분류별 sheet 예시

하지만 저는 기존의 locale들이 모듈별로 되어 있어 이번에도 모듈별로 작성하도록 하겠습니다.

모듈별로 json 구조 및 예시

그리고 이 파일 내부에는 다음과 같은 json 형식이 있습니다.

다만 react-i18next를 사용하기 위한 구조를 잡다가 이렇게 된 것이지 구체적인 제약이 있는 건 아닙니다.

핵심은 구글시트에서 데이터를 가져와 json으로 변경하고 json으로 react-i18 next를 사용하는 것이니깐요.

이제 기존의 데이터를 옮겨주겠습니다.

변환에 필요한 부분만 copy한 다음에 console.table안에 넣고 찍으면 다음과 같이 테이블이 만들어집니다.

그리고 이것을 다시 복사해서 구글 시트에 붙여 넣기 하면 아래와 같은 형태가 됩니다.

그냥 쓰면 되지 뭘 이렇게 해?라고 할 수 있지만 기존에 번역이 많이 된 모듈은 수십 개의 번역문이 있기 때문에 일일이 하기가 번거롭습니다.

따라서 table로 만들어하는 편이 훨씬 간편합니다. 

구글 시트 key 가져오기

그리고 이 시트에 대한 key를 얻어봅시다.

해당 부분이 sheet key입니다.

이제 데이터는 준비되었습니다.

구글 시트 API key 가져오기

구글 클라우드 콘솔에 들어가 Google Sheets API를 검색합니다.

사용을 누르고 들어가 사용자 인증 정보 -> API 키를 만듭니다.

테스트용 key라 공개하도 상관없....

이게 데이터는 준비가 완료되었습니다.

스크립트 및 변환, 호출 로직 구현

이제 프로젝트로 가봅시다.

package.json에 명령어 추가하기

package.json에 명령어를 추가해보겠습니다.

그리고 해당하는 함수를 구현하러 가볼까요

yarn i18n 로직 구현하기

이건 코드가 좀 길긴 하지만 아래에서 전체적인 로직을 설명하겠습니다.

const axios = require("axios"); // Google Sheet API를 위해
const fs = require("fs"); // locale json 생성을 위해

const GOOGLE_SHEET_BASE_URL = "https://sheets.googleapis.com/v4/spreadsheets";
const GOOGLE_API_KEY = "시트 API key";
const GOOGLE_SHEET_ID = "시트 key";

/**
 * 구글 시트 i18n의 meta 정보를 가져오는 API
 */
const getI18nMetaFromGoogleSheet = async () => {
  const response = await axios.get(
    `${GOOGLE_SHEET_BASE_URL}/${GOOGLE_SHEET_ID}/values/meta?key=${GOOGLE_API_KEY}`
  );

  if (response.status !== 200) {
    throw new Error();
  }

  const rowDataList = response.data.values;

  let moduleList = [];

  rowDataList.forEach((row) => {
    const rowJson = parseRowdata(row);
    if (rowJson.key === "module") {
      moduleList = rowJson.value;
    }
  });

  // 가져온 모듈 정보 기반으로 json 파일 생성 로직 수행
  moduleList.forEach((module) => {
    makeLocaleModuleJson(module);
  });
};

/*
  2차원 배열인 값을 파싱하여 object 형태로 반환하는 로직
  Input :
  [
    [ 'available_languages', 'ko', 'en', 'ja' ],
  ]

  Output : 
  { key: 'available_languages', value: [ 'ko', 'en', 'ja' ] }
 */
const parseRowdata = (row) => {
  const [key, ...value] = row;
  return {
    key: key,
    value: value,
  };
};

const makeLocaleModuleJson = async (moduleName) => {
  const rowDataJsonList = await getI18nDataModule(moduleName);
  writeLocaleModuleJson(moduleName, rowDataJsonList);
};

/**
 * meta에서 읽어온 모듈정보를 가지고 해당 모듈 이름으로 sheet를 조회하는 API
 */
const getI18nDataModule = async (moduleName) => {
  const response = await axios.get(
    `${GOOGLE_SHEET_BASE_URL}/${GOOGLE_SHEET_ID}/values/${moduleName}?key=${GOOGLE_API_KEY}`
  );

  if (response.status !== 200) {
    return;
  }

  const rowDataArrayList = response.data.values;
  const rowDataJsonList = [];
  rowDataArrayList.forEach((row) => {
    rowDataJsonList.push(parseRowdata(row));
  });
  return rowDataJsonList;
};

/**
 * node의 fs를 이용해서 json 파일로 변환 및 저장하는 메소드
 */
const writeLocaleModuleJson = async (moduleName, rowDataJsonList) => {
  const moduleJson = { namespace: moduleName, locale: {} };

  const localeRow = rowDataJsonList.shift();

  while (rowDataJsonList.length > 0) {
    const row = rowDataJsonList.shift();
    moduleJson.locale[row.key] = {};
    row.value.forEach((value, index) => {
      moduleJson.locale[row.key][localeRow.value[index]] = value;
    });
  }

  fs.writeFileSync(
    `./src/locale/module/${moduleName}.json`,
    JSON.stringify(moduleJson)
  );
};

getI18nMetaFromGoogleSheet();

yarn i18n 호출 시 로직 순서는 다음과 같습니다.

  1. getI18nMetaFromGoogleSheet() : 구글 시트 i18n의 meta 정보를 가져오는 API
  2. makeLocaleModuleJson()
  3. getI18nDataModule(): meta에서 읽어온 모듈정보를 가지고 해당 모듈 이름으로 sheet를 조회하는 API
  4. writeLocaleModuleJson(): node의 fs를 이용해서 json 파일로 변환 및 저장하는 메소드

일단 1번째 getI18nMetaFromGoogleSheet는 구글 시트의 진입점입니다.

여기에 i18n에 필요한 기본 정보가 들어가 있습니다.

기본 정보라 함은 아래 2가지 정보를 포함하고 있습니다.

  • 사용  가능한 언어 코드 리스트
  • 준비되어 있는 모듈 시트명

그런 이후에 3번 getI18nDataModule(moduleName)에 가져온 시트 정보를 넣고 4번 writeLocaleModuleJson으로 모듈별 json 파일을 생성합니다.

 

1.6초라는 짧은 시간만에 만들어짐

이렇게 만들어진 json 파일을 이제 사용하면 되겠습니다.

사용하는 방법에 대한 로직은 이전에 구현해놨던 로직을 변경한 것이 없어서 링크로 대체하겠습니다.

 

[리액트] 글로벌한 웹을 향하여 (react-i18next, 다국어지원)

들어가며 이번 글에서는 글로벌한 웹을 위해서 react i18n를 적용하는 방법과 나름의 best practice에 대해 적어보겠습니다. 이 글은 아직 이제 막 프로젝트에 i18n을 던지려는 용기 있는 개발자를 위한

all-dev-kang.tistory.com

 

유의사항 및 Tip

배포할 때마다 자동으로 빌드하면 안돼요?

혹자는 이 과정을 빌드에서 완전 자동화하면 어떠냐라고 생각할 수도 있습니다. (지금은 개발자가 명시적으로 스크립트를 입력해야함)

하지만 저는 로컬에서 i18n 파일을 만들고 push 하여 적용하도록 하였는데요. 그 이유는 다음과 같습니다.

  1. 개발자과 번역가가 분리됨에 따라 업데이트되는 시점을 누군가는 컨트롤해야 한다.
  2. 빌드 시에 적용되면 로컬에서 UI를 테스트해볼 수가 없다

번역을 자동화해보자 GOOGLETRANSLATE

구글 시트에는 셀을 번역하여 적용하는 GOOGLETRANSLATE라는 함수가 있습니다.

이렇게 기본적인 기계번역을 해줍니다.

이걸 통해서 기본적인 번역값을 확인할 수 있습니다. 다만 기계번역 자체가 부정확하여 해당 기능으로 바로 서비스하는 것을 권장하지 않습니다.

이제 번역가에게 다가가 "해당 셀에서 함수를 지우고 알맞은 언어로 넣어주세요"라고 요청하면 됩니다.

조금 더 나아가면 기계번역이 된 부분에 대한 시트스타일을 적용하여 번역가가 조금더 편하게 확인할 수 있도록 제공할 수도 있겠네요.

i18next의 fallbackLng 옵션을 활용해라

이건 i18next의 기능인데 설정된 언어에 매칭 되는 번역어가 없으면 기본 locale언어를 매칭 하여 표시하는 기능입니다.

따라서 기본 값이 되는 en언어로 세팅해놓으면 시트가 잘못되어도 최소한 영어로 표현되어 에러를 최소화를 할 수 있습니다.

 

Fallback - i18next documentation

By default, if a variant (containing region, script, etc) is not found, i18next will look for the same key in the broader version of that language. With this in mind, a common strategy if you're supporting language variants is to write common text inside t

www.i18next.com

참고자료

nhn의 자동화 가이드는 프론트에서 key를 파싱 해서 업로드하는 과정도 있지만 여기에는 간소화되어있습니다. (나중에 필요하면 호옥시 넣어볼 생각도..?)

 

국제화(i18n) 자동화 가이드 : NHN Cloud Meetup

프런트엔드 개발을 하다 보면 국제화와 번역을 수작업과 막일로 하는 경우가 있습니다."복붙"이나 반복적인 수작업으로 인해 고통받는 모든 프런트엔드 개발자를 자동화 가이드를 작성하였습

meetup.toast.com

마무리

이번 글에서는 개발자의 레포에 갇혀있었던 i18n을 구글 시트에 연결하여 마음껏 날뛰도록 변경해보았습니다.

이렇게 변경된 이후로 번역을 고쳐달라는 요청 대신에 반영해달라는 요청으로 바뀌어서 나름 편하게 작업할 수 있게 되었습니다.

또한 node.js의 fs를 이용해서 실제 프로덕션 서비스에서 파일을 생성하고 관리하는 경험을 해보았네요.

제가 알기로는 이러한 기능을 가진 서비스가 몇 개 존재하는 것으로 알고 있습니다.

자주 사용하는 라이브러리인 MUI 또한 외부 플랫폼에서 i18n를 관리한다고 알고 있습니다.

따라서 다음 글은 해당 서비스를 도입해보는 과정이 될 수 있겠네요.

끝.

댓글