들어가며
이전 글에서 구글 시트를 이용하여 i18n에 대응하는 예시를 보여드렸었습니다.
그리고 이번에 갑작스레 일어를 추가하자는 요건에 대응하여 기존에 있는 기능을 확장한 방법에 대해 공유하겠습니다.
대상 독자
- 여러 가지 언어를 대응해야 하는 프론트엔드 개발자
- 개발과 번역을 분리하고 싶은 한글을 사랑하는 개발자
이 글을 읽기 위해선 선행글이 존재합니다. ( 필수는 아니지만 보고 오면 좋아요 )
문제 인식 및 설계
행복한 주말을 기다리는 금요일 오전 10시,
갑작스레 다음주에 일본 출장을 가는데, 현재 사이트에 일어를 추가해줄 수 있냐는 동료의 요구사항이 들어왔습니다.
고민을 잠깐 하다가 이번에 기술 부채를 해소할 기회라고 생각되어 기능 설계에 들어갔습니다.
프로세스 설계
설계는 이전에 구상해놨던 아래 프로세스를 따라가도록 했습니다.
- 구글 스프레드 시트에 locale에 맞춰 데이터 업데이트
- 스크립트를 통해 locale json 파일 생성
- 생성된 json 파일 기반으로 프로젝트 내부에서 사용
- 빌드될 때 생성된 파일을 가지고 i18n 적용
따라서 이번 글에서 다룰 내용은 2번과정까지에 대해 다룹니다. 3,4번은 이전글에서 다룬 내용과 동일합니다.
필요사항 도출
- i18n 데이터를 저정할 구글 계정과 접근 방법
- 구글 sheet api 권한 및 로직
- 구글 sheet -> locale json으로 변경하는 로직
- json을 파일로 write하는 로직
이로써 준비는 끝났습니다. 이제 하나씩 구현을 시작하도록 하겠습니다.
구현
구글 스프레드 시트 생성 및 연결 준비
공용 계정에 구글 스프레드 시트를 생성해줍니다. 이 부분은 다들 아실 테니 건너뛰겠습니다.
그리고 이 파일을 수정할 수 있도록 권한을 주는 작업을 마무리합니다.
그런 다음 i18n 데이터를 저장합니다.
i18n 데이터 저장
데이터를 저장하는 방법론은 크게 2가지가 있습니다.
언어별로 할 것인가? 모듈별로 할 것인가?
- 언어별로 할 경우 하나의 sheet가 하나의 언어가 됩니다.
- 모듈별로 할 경우 하나의 sheet가 하나의 모듈이 됩니다.
하지만 저는 기존의 locale들이 모듈별로 되어 있어 이번에도 모듈별로 작성하도록 하겠습니다.
그리고 이 파일 내부에는 다음과 같은 json 형식이 있습니다.
다만 react-i18next를 사용하기 위한 구조를 잡다가 이렇게 된 것이지 구체적인 제약이 있는 건 아닙니다.
핵심은 구글시트에서 데이터를 가져와 json으로 변경하고 json으로 react-i18 next를 사용하는 것이니깐요.
이제 기존의 데이터를 옮겨주겠습니다.
변환에 필요한 부분만 copy한 다음에 console.table안에 넣고 찍으면 다음과 같이 테이블이 만들어집니다.
그리고 이것을 다시 복사해서 구글 시트에 붙여 넣기 하면 아래와 같은 형태가 됩니다.
그냥 쓰면 되지 뭘 이렇게 해?라고 할 수 있지만 기존에 번역이 많이 된 모듈은 수십 개의 번역문이 있기 때문에 일일이 하기가 번거롭습니다.
따라서 table로 만들어하는 편이 훨씬 간편합니다.
구글 시트 key 가져오기
그리고 이 시트에 대한 key를 얻어봅시다.
이제 데이터는 준비되었습니다.
구글 시트 API key 가져오기
구글 클라우드 콘솔에 들어가 Google Sheets API를 검색합니다.
사용을 누르고 들어가 사용자 인증 정보 -> API 키를 만듭니다.
이게 데이터는 준비가 완료되었습니다.
스크립트 및 변환, 호출 로직 구현
이제 프로젝트로 가봅시다.
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 호출 시 로직 순서는 다음과 같습니다.
- getI18nMetaFromGoogleSheet() : 구글 시트 i18n의 meta 정보를 가져오는 API
- makeLocaleModuleJson()
- getI18nDataModule(): meta에서 읽어온 모듈정보를 가지고 해당 모듈 이름으로 sheet를 조회하는 API
- writeLocaleModuleJson(): node의 fs를 이용해서 json 파일로 변환 및 저장하는 메소드
일단 1번째 getI18nMetaFromGoogleSheet는 구글 시트의 진입점입니다.
여기에 i18n에 필요한 기본 정보가 들어가 있습니다.
기본 정보라 함은 아래 2가지 정보를 포함하고 있습니다.
- 사용 가능한 언어 코드 리스트
- 준비되어 있는 모듈 시트명
그런 이후에 3번 getI18nDataModule(moduleName)에 가져온 시트 정보를 넣고 4번 writeLocaleModuleJson으로 모듈별 json 파일을 생성합니다.
이렇게 만들어진 json 파일을 이제 사용하면 되겠습니다.
사용하는 방법에 대한 로직은 이전에 구현해놨던 로직을 변경한 것이 없어서 링크로 대체하겠습니다.
유의사항 및 Tip
배포할 때마다 자동으로 빌드하면 안돼요?
혹자는 이 과정을 빌드에서 완전 자동화하면 어떠냐라고 생각할 수도 있습니다. (지금은 개발자가 명시적으로 스크립트를 입력해야함)
하지만 저는 로컬에서 i18n 파일을 만들고 push 하여 적용하도록 하였는데요. 그 이유는 다음과 같습니다.
- 개발자과 번역가가 분리됨에 따라 업데이트되는 시점을 누군가는 컨트롤해야 한다.
- 빌드 시에 적용되면 로컬에서 UI를 테스트해볼 수가 없다
번역을 자동화해보자 GOOGLETRANSLATE
구글 시트에는 셀을 번역하여 적용하는 GOOGLETRANSLATE라는 함수가 있습니다.
이렇게 기본적인 기계번역을 해줍니다.
이걸 통해서 기본적인 번역값을 확인할 수 있습니다. 다만 기계번역 자체가 부정확하여 해당 기능으로 바로 서비스하는 것을 권장하지 않습니다.
이제 번역가에게 다가가 "해당 셀에서 함수를 지우고 알맞은 언어로 넣어주세요"라고 요청하면 됩니다.
조금 더 나아가면 기계번역이 된 부분에 대한 시트스타일을 적용하여 번역가가 조금더 편하게 확인할 수 있도록 제공할 수도 있겠네요.
i18next의 fallbackLng 옵션을 활용해라
이건 i18next의 기능인데 설정된 언어에 매칭 되는 번역어가 없으면 기본 locale언어를 매칭 하여 표시하는 기능입니다.
따라서 기본 값이 되는 en언어로 세팅해놓으면 시트가 잘못되어도 최소한 영어로 표현되어 에러를 최소화를 할 수 있습니다.
참고자료
nhn의 자동화 가이드는 프론트에서 key를 파싱 해서 업로드하는 과정도 있지만 여기에는 간소화되어있습니다. (나중에 필요하면 호옥시 넣어볼 생각도..?)
마무리
이번 글에서는 개발자의 레포에 갇혀있었던 i18n을 구글 시트에 연결하여 마음껏 날뛰도록 변경해보았습니다.
이렇게 변경된 이후로 번역을 고쳐달라는 요청 대신에 반영해달라는 요청으로 바뀌어서 나름 편하게 작업할 수 있게 되었습니다.
또한 node.js의 fs를 이용해서 실제 프로덕션 서비스에서 파일을 생성하고 관리하는 경험을 해보았네요.
제가 알기로는 이러한 기능을 가진 서비스가 몇 개 존재하는 것으로 알고 있습니다.
자주 사용하는 라이브러리인 MUI 또한 외부 플랫폼에서 i18n를 관리한다고 알고 있습니다.
따라서 다음 글은 해당 서비스를 도입해보는 과정이 될 수 있겠네요.
끝.
'개발 > 개발지식' 카테고리의 다른 글
[유튜브] url로 영상의 정보를 가져오는 기능 만들기 (0) | 2023.07.25 |
---|---|
[포트폴리오] AI로 만드는 연차별 포트폴리오 (ChatGPT) (1) | 2023.01.27 |
[개발지식] 다른 회사는 어떤 걸 써요? ( Wappalyzer ) (0) | 2022.08.15 |
[클린코드] 카멜, 파스칼은 가라. 세종대왕 표기법이 온다 (네이밍 컨벤션) (1) | 2022.07.25 |
[이직] 4년차 프론트엔드 개발자의 이직 후기 (8) | 2022.06.12 |
댓글