올해 초 OKR을 작성하면서 빠르게 이루고 싶었던 목표 중 하나가 사내 서비스 국제화(i18n) 자동화 시스템을 구축하는 것이었다.
지난 번 프로덕트 런칭을 준비하면서 i18n 관련 노가다 작업(복붙이나 json 컨플릭트 해결 등...)이 너무 비효율적이라는 생각이 들었기 때문이다. 자동화 시스템을 구축하면서는 Toast UI 유동식님의 자동화 가이드 포스트를 많이 참고했다. 한 번 시스템을 설정해두니 번역할 키를 하나씩 추가하지 않아도 되어 만족도가 높은 작업이었다. 👍
본 포스팅에서는 국제화 자동화 가이드를 따르면서 헷갈렸던 부분이나 보완한 부분 등을 위주로 설명을 덧붙여보려고 한다.
package.json & Dockerfile 업데이트
scan:i18n - 번역할 키 값을 스캔해서 스캔한 키 값을 바탕으로 json 파일 생성
uplaod:i18n - 생성된 json 파일을 토대로 새롭게 추가된 키 값들을 스프레드시트에 업데이트
download:i18n - 번역된 키 값들을 읽어 언어별 json 파일을 생성
"scripts": {
...,
"scan:i18n": "i18next-scanner --config i18next-scanner.config.js",
"upload:i18n": "npm run scan:i18n && node src/utils/translate/upload.js",
"download:i18n": "node src/utils/translate/download.js"
}
이후 Dockerfile 등에서 build 실행 전에 download:i18n 명령어를 실행해주면 스프레드시트를 토대로 번역본 json 파일이 생성되기 때문에 항상 최신화 된 번역본을 프로젝트에 반영할 수 있다.
...
RUN npm run download:i18n
RUN npm run build:dev
...
로컬에서 테스트할 때는 npm run download:i18n 만 실행해서 테스트해보면 된다.
download.js 파일 수정사항
기존 가이드에 작성된 대로 download 파일을 작성하면 스프레드시트에 번역값이 누락되어 있을 경우 에러가 발생할 수 있다. updateJsonFromSheet 함수를 보면 스프레드시트의 모든 행을 읽어 json 파일을 생성하는데 번역값이 누락되어 있을 경우 키에 빈 스트링이 매핑되어 들어가기 때문이다. 따라서 최종적으로 서비스에 사용할 json 파일을 업데이트하기 전에, 스프레드시트를 읽어 생성한 json 파일에서 값이 비어있는 키를 제외시키는 과정을 추가해주었다. 키에 빈 스트링이 매핑되어 있는 경우 아예 해당 문자열이 화면에 나오지 않게 되지만 키 자체가 존재하지 않는 경우에는 키값 그대로 화면에 표시되게 된다.
async function updateJsonFromSheet() {
await checkAndMakeLocaleDir(localesPath, lngs);
const doc = await loadSpreadsheet();
const lngsMap = await fetchTranslationsFromSheetToJson(doc);
fs.readdir(localesPath, (error, lngs) => {
if (error) {
throw error;
}
lngs.forEach((lng) => {
const localeJsonFilePath = `${localesPath}/${lng}/${ns}.json`;
// 아래 부분 추가
const filteredMap = Object.fromEntries(
Object.entries(lngsMap[lng]).filter(([_, v]) => v)
);
const jsonString = JSON.stringify(filteredMap, null, 2);
fs.writeFile(localeJsonFilePath, jsonString, 'utf8', (err) => {
if (err) {
throw err;
}
});
});
});
구글 스프레드시트 관련 주의사항
구글 스프레드시트 서비스 어카운트를 발급 후 키를 등록했음에도 에러가 발생하는 경우에는 다음 사항을 체크해보자.
1. 파일이 마이크로소프트 엑셀 파일(.xlsx)이 아니라 구글 스프레드시트로 등록되어 있는지 확인한다.
엑셀 파일의 경우 파일 > Google Sheets로 저장을 클릭해 구글 스프레드시트 형식으로 변경할 수 있다.

2. 서비스 어카운트 계정을 공유 권한에 추가한다.
권한 관련 에러가 발생할 경우 발급 받은 서비스 어카운트를 편집자 권한으로 스프레드시트 공유 권한에 추가하면 에러가 발생하지 않는다.

덧) 고민해야 했던 부분들 🤔
1. 이메일 템플릿
유저에게 발송하는 이메일 템플릿을 컴포넌트로 구현한 뒤 ReactDOM.renderToStaticMarkup 메서드를 이용해 이메일용 html을 구성하고 있었다. 이 경우 다른 페이지들에서 이용하는 next-i18next의 useTranslation을 사용할 수 없기 때문에 이메일 템플릿에 들어가는 메세지들의 키 값이 i18next-scanner에 인식되도록 하면서, 스프레드시트에 업데이트 된 번역된 값들을 이메일 템플릿에서도 사용할 수 있도록 직접 구현하는 과정이 필요했다.
말이 거창하다뿐이지 next-i18next의 t 함수와 똑같이 동작하는 함수를 만들어주는 것으로 이 문제는 쉽게 해결할 수 있었다.
import enTranslation from 'public/locales/en/common.json';
import koTranslation from 'public/locales/ko/common.json';
import zhTranslation from 'public/locales/zh/common.json';
export const translationMap = {
en: enTranslation,
ko: koTranslation,
zh: zhTranslation,
};
const t = (value, obj) => {
let translation = translationMap[locale][value] || value;
if (obj) {
Object.keys(obj).forEach((key) => {
translation = translation.replaceAll(`{{${key}}}`, obj[key]);
});
}
return translation;
};
2. 텍스트 중간에 볼드체를 사용하거나 색상을 변경해야 하는 경우

위와 같은 컴포넌트를 구현할 경우 보통 아래처럼 코드를 작성할 것이다.
<p><b>The cat</b> jumped over <u>the fence</u>.<p>
이럴 경우 텍스트 전체를 키로 등록할 수 없기 때문에 문제가 발생한다. The cat, jumped over, the fence 처럼 문장을 쪼개서 키로 등록하려고 해도 언어에 따라 단어의 순서나 조사 등을 고려해야 하기 때문에 쪼갠 단어를 그대로 번역해서 사용할 수도 없다.
이 부분은 사실 아직까지 이거다 싶은 해결책을 찾지 못해서 필요한 키와 번역값을 직접 복붙해서 사용하는 방식을 고수하고 있다. 아래와 같은 식...
const sampleSentence = {
en: (
<p>
<b>The cat</b> jumped over <u>the fence</u>.
</p>
),
ko: (
<p>
<b>고양이</b>가 <u>울타리</u>를 뛰어넘었습니다.
</p>
),
};
...
// 사용할 때는 이런 느낌 ...
<div>
{sampleSentence[locale]}
</div>
이럴 경우 스프레드시트에 값이 업데이트 될 경우 직접 찾아서 변경해주어야 하고 대응하는 언어가 많아질수록 더 번거로워질 가능성이 크다. 사실 이렇게 들어가는 텍스트가 그렇게 많지는 않아서 지금은 괜찮지만... 더 좋은 방법이 있으면 공유 바람 ㅎ 👍
글이 좀 뒤죽박죽인 것 같지만 결론은 반나절 정도만 투입하면 i18n 관련 작업에서 많은 편의를 얻을 수 있으니 필요한 분들은 꼭 시도해보시길 바란다.
참고한 글들
https://ui.toast.com/weekly-pick/ko_20210303
https://hjleee93.github.io/frontend/javascript/autoLocalization/
'웹 개발 > React · Next.js' 카테고리의 다른 글
| [React] Web API를 활용한 영상 녹화 구현하기 (0) | 2023.03.31 |
|---|---|
| [번역] useMemo와 useCallback 제대로 알고 사용하기 (0) | 2023.02.28 |
| [React] 전역상태관리 라이브러리 Recoil (0) | 2023.01.30 |
| multer로 AWS S3에 파일 업로드하기 (0) | 2022.10.30 |
| [Next.js] "window is not defined" 에러 해결 (0) | 2022.05.14 |