쌈@뽕하게 다국어화 소통 비용 줄이기
GoogleSheet로 Next환경의 i18n과 동기화 해보자
2024-12-15 · 18분 · 조회 0외부 링크 안내
다국어화
다국어화(i18n) 는 글로벌 서비스를 제공하기 위해 필수적인 기능이다.
하지만 프로젝트가 커질수록 다국어화 작업은 단순한 번역 작업이 아니라, 팀 내 소통과 관리의 복잡성이라는 문제를 야기한다.
- 디자이너: 다국어 UX를 반영해 디자인을 수정해야 한다.
- 번역가: 문맥과 키(key) 값을 모두 이해해야 하며, 번역 과정에서 혼동이 생길 수 있다.
- 개발자: JSON 파일을 수동으로 관리하며 각 작업자와 데이터를 동기화해야 한다.
이러한 소통 비용은 회사가 성장하고 지원 언어가 늘어날수록 점점 더 커진다.
필자가 속한 회사에서는 국제화(i18n)를 구현할 시간이 3개월 남짓이었고, 기존 번역 작업 방식이 비효율적이라는 것을 깨닫게 되었다.
이 글은 이러한 비효율성을 해결하기 위해 Google Sheets를 중심으로 번역 데이터를 중앙화하고, 자동화된 동기화 프로세스를 구축한 과정을 공유한다.
본 글은 모든 코드를 포함하지 않고 pseudo-code 형태로 요약된 설명을 제공합니다. 이해와 참고의 목적으로 읽어주시면 감사하겠습니다.
i18n이란?
i18n은 “internationalization”이라는 단어에서 첫 번째 글자 “i”와 마지막 글자 “n” 사이의 18자를 줄여 만든 용어다. 즉, 국제화(internationalization) 는 소프트웨어를 개발 단에서 변경 없이 다양한 언어와 지역에 맞게 조정할 수 있는 프로세스를 의미한다.
더 자세한 개념을 공부하고 싶다면 위키피디아-국제화와_지역화 를 보면 좋을 것 같다.
기존 방식의 문제점
본격적으로 문제를 살펴보기 전에, 프론트엔드에서 국제화를 어떻게 구현하고 사용하는지 간단히 설명하겠다.
프론트엔드에서 i18n을 사용하는 법
필자가 속한 팀에서는 Next.js 12와 next-i18next를 사용해 서버사이드에서 필요한 JSON 파일을 불러와 다국어화를 구현하고 있었다.
📂 public
┣ 📂 locales
┃ ┗ 📂 ko
┃ ┗ 📜 common.json // 한국어 common 다국어화 파일
┃ ┗ 📜 campaign.json // 한국어 campaign 다국어화 파일
┃ ┗ 📜 ...
┃ ┗ 📂 ja
┃ ┗ 📜 common.json // 일본어 common 다국어화 파일
┃ ┗ 📜 campaign.json // 일본어 campaign 다국어화 파일
┃ ┗ 📜 ...JSON 예시
{
"common": {
"title": "쌈@뽕하게 다국어화 소통 비용 줄이기",
"description": "GoogleSheet로 Next환경의 i18n과 동기화 해보자"
}
}{
"common": {
"title": "多言語化するための標準的な方法",
"description": "GoogleスプレッドシートでNext.js環境のi18nと同期する"
}
}번역 데이터 사용 예시
const Page = () => {
const { t } = useTranslation('common');
return <h1>{t('title')}</h1>; // 쌈@뽕하게 다국어화 소통 비용 줄이기
};
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
...(await serverSideTranslations(ctx.locale ?? 'ko', ['common'])),
},
};
};
export default Page;이 방식에서는 번역 데이터를 관리하는 주체가 자연스럽게 프론트엔드 개발자로 한정되었다.
문제점
- JSON 파일 관리의 비효율성
수천 개의 키 값을 IDE에서 관리하다 보면 실수나 누락이 발생하기 쉬웠다. - 범위와 책임의 혼란
개발자마다 작업 범위가 다르고, Git으로 코드를 관리하는 것만으로도 버거운데 번역 데이터까지 충돌 문제를 해결해야 했다. - 소통 방식의 비효율성
번역가나 디자이너가 번역 데이터를 수정하려면 개발자를 통해야만 했다.
QA 중간에 생긴 수정 사항도 바로 반영하기 어려워 생산성이 저하되었다.
목표
- 번역 데이터의 중앙화
번역 데이터를 Google Sheets에 저장하고, 이를 프로젝트의 단일 소스로 활용한다. - 자동 동기화
Google Sheets와 JSON 파일 간의 동기화를 스크립트를 통해 자동화하여 최신 상태를 유지한다. - 확장 가능성
일본어뿐 아니라 영어, 중국어 등 추가 언어를 쉽게 지원할 수 있는 구조를 설계한다.
Google Spread Sheet으로 관리하기
Why Google Spread Sheet?
- 즉시 사용할 수 있는 도구였다.
국제화 작업에 할당된 시간이 3개월로 제한적이였기에, 별도 시스템을 구축할 시간이 없었다. - 협업이 가능했다.
번역가와 디자이너가 Google Sheets를 통해 직접 데이터를 동시에 수정이 가능했다. - JSON 데이터와 유사했다
Sheets 데이터를 JSON형식으로 변환하기 쉽다고 판단했다.
Google Sheets를 활용해 JSON 파일 구조와 유사한 데이터를 관리했다.
각 언어의 번역 데이터는 **열(column)**로, JSON의 계층 구조는 **키(key)**로 표현했다.
| ko | ja | en | key | key1 | key2 | key3 | ... |
|---|---|---|---|---|---|---|---|
| =CONCATENATE(key1, key2, key3, ...) | ... |
예시로,
{
"key": "value",
"key1": "value1",
"key2": {
"key3": "value3",
"key4": "value4",
"key5": {
"key6": "value6"
}
}
}같은 JSON파일은
| ko | ja | en | key | key1 | key2 | key3 | ... |
|---|---|---|---|---|---|---|---|
| value | value-ja | value-en | key | key | |||
| value1 | value1-ja | value1-en | key1 | key1 | |||
| value3 | value3-ja | value3-en | key2.key3 | key2 | key3 | ||
| value4 | value4-ja | value4-en | key2.key4 | key2 | key4 | ||
| value6 | value6-ja | value6-en | key2.key5.key6 | key2 | key5 | key6 |
위와 같은 Google Sheets Table로 표현될 수 있다.
이 방식으로 디자이너와 번역가도 Google Sheets에서 직접 작업할 수 있는 환경을 만들었다.
JSON 파일로 자동 변환하기
Google Sheets 데이터를 JSON 파일로 변환하기 위해 google-spreadsheet 라이브러리를 사용했다.
다음은 주요 로직의 pseudo-code다:
1. IMPORT 필수 모듈
- GoogleSpreadsheet, JWT, fs, path
2. SET 기본 설정
- Google API 인증 정보 (i18n-key.json)
- Google Spreadsheet ID (SHEET_ID)
- LOG: OUTPUT_DIR
- 명령행 인자로 가져온 sheet 목록 (args)
3. CONFIGURE JWT 인증
- JWT 객체 생성 (Google API 인증용)
4. INITIALIZE Google Spreadsheet 객체
- GoogleSpreadsheet(SHEET_ID, JWT)
5. DEFINE createJsonBySheet 함수
- INPUT: Sheet 객체, Sheet 인덱스(optional)
- LOG: Sheet 제목 출력
- FETCH rows (Sheet 데이터 행 가져오기)
- GET headerValues (첫 번째 행의 헤더 정보)
- DETERMINE 언어 코드(langCodes)
- 'key' 이전의 컬럼들
- LOOP through langCodes (언어별 처리)
- CHECK: 해당 언어 폴더가 없으면 생성
- CREATE JSON 파일 경로 및 이름
- INITIALIZE JSON 데이터 객체
- LOOP through rows (행 데이터를 JSON으로 변환)
- Split key를 기준으로 계층적 JSON 구조 생성
- WRITE JSON 데이터를 파일에 저장
- LOG: JSON 파일 생성 완료 메시지 출력
6. DEFINE run 함수
- LOG: Google Sheet API 호출 중
- LOAD Google Sheet 정보
- LOG: 성공 메시지 및 시트 개수 출력
- IF: 특정 시트 이름(args) 제공
- LOOP through args (Sheet 이름으로 특정 시트 처리)
- ELSE: 모든 시트 처리
- LOOP through 모든 시트 (Sheet 인덱스로 반복)
- CALL createJsonBySheet 함수
7. EXECUTE run 함수
- Google Sheet 데이터를 JSON으로 변환 후 저장아래와 같이 명령어 한 줄로 json파일을 생성할 수 있게 되었다.
node scripts/i18n.js -> 모든 sheet 업데이트
node scripts/i18n.js common -> common sheet만 업데이트
확장 가능하게 만들기
언어를 추가하고 싶다면, 단순히 Google Sheets에 새로운 언어 열(column)을 추가하고, 각 키에 해당하는 번역 값을 입력하기만 하면 된다. 이후 스크립트를 실행하면, 모든 번역 데이터가 JSON 파일로 변환되어 프로젝트 폴더에 저장된다.
필요 시 기존 언어 데이터를 복사하거나, 미번역 데이터를 기본 값으로 두어 점진적으로 번역 작업을 진행할 수 있었다.
더 생각해봐야 할 점
버전관리
Google Sheets의 기본 버전 관리 기능은 제한적이다.
가끔씩 누락되는 경우도 있고, 너무 자유분방한 수정 기능때문에 누가 언제 어떤 데이터를 수정했고 어떤 버전이 배포되었는지 명확히 관리할 수가 없다.
이를 해결하기 위해서는 시트의 버전관리를 추가, 또는 오픈소스 라이브러리나 회사만의 다국어화 서비스와 db를 만들어서 안정화해야 할 것 같다.
키 값에 대한 소통 및 자동화(?)
디자인 초기 단계에서 디자이너와 번역가가 Google Sheets를 통해 키 값을 바로 생성하고 작업할 수 있다면 더 효율적일 것이라고 생각한다.
이를 위해 Google Sheets와 Figma를 연동하거나, 자동으로 키 값을 생성한다면 더 좋을 것 같다.
하지만 그렇게 된다면 디자인 속도에 제한이 걸리거나 소통 비용이 더 커질 것 같아, 조금 더 검토해볼 필요가 있을 것 같다.
다국어 테스트 QA
언어별 UI 테스트와 QA를 더욱 체계적으로 진행할 방법도 고민해야 한다.
특히 번역 길이가 달라질 경우 UI가 깨지지 않도록 디자인 시스템에서 이를 사전에 시뮬레이션하거나, 자동화된 테스트를 추가하면 도움이 될 것이다.
Trouble Shooting
SEO & meta
Next/Head를 사용해 페이지의 <title>과 <meta> 태그에 동적으로 번역된 값을 넣으려고 했다.
이를 위해 pages/index.tsx에서 {t('...')} 함수를 사용해 번역된 값을 전달하려 했으나, 번역 문구가 제대로 들어가지 않는 문제가 발생했다.
이는 Next.js의 서버사이드 렌더링 과정에서 <Head>가 이미 설정되었기 때문으로 보인다.
결과적으로, 직접 <Head>를 사용하는 컴포넌트 안에서 {t('...')}를 호출하면 정상적으로 번역 문구가 적용된다는 것을 알게 되었다.
다만, SaaS 서비스 특성상 SEO와 메타 태그가 크게 중요하지 않아서 이 문제를 별도로 해결하지는 않았다.
Constant
프론트엔드 개발에서는 상수(constant) 로 다양한 변수, 카테고리, 설정 등을 관리하는 경우가 많다.
이를 위해 constant.ts 파일을 만들어 중앙에서 관리하는 패턴이 일반적이다.
문제는, next-i18next에서 제공하는 useTranslation 훅은 TypeScript 상수 파일에서 직접 사용할 수 없다는 점이다. 그래서 i18n.t를 직접 import해 사용하려 했으나, 이 방법은 auto-type-checking을 제공하지 않아 불편했다.
tF 함수
이를 해결하기 위해 tF 함수를 따로 정의하여 사용했다.
tF는 i18n.t를 감싸고, 런타임에 호출될 수 있도록 하는 함수다.
import { type i18n as i18nn } from 'i18next';
import { i18n } from 'next-i18next';
type Param = Exclude<Parameters<i18nn['t']>[0], string | string[] | TemplateStringsArray>[number];
export const tF = (param: Param, options?: Record<string, string>) => (() => i18n?.t(param, options)) as () => string;tF 함수는 상수를 정의할 때 유용하다.
예를 들어, 설정 목록 데이터를 상수로 관리하려면 아래와 같이 작성한다:
import { tF } from '@utils/i18n';
export const settingList = [
{ name: tF('common:settings.common'), path: '.../workspace_setting' },
{ name: tF('common:settings.team'), path: '.../team_members_management' },
{ name: tF('common:settings.membership'), path: '.../membership_management' },
{ name: tF('common:settings.payment'), path: '.../payment_management' },
{ name: tF('common:settings.coupon'), path: '.../coupon' },
] as const;tF 함수는 함수 형태로 반환되기 때문에, 호출을 통해 번역 문구를 얻어야 한다.
<ul>
{settingList.map((item, idx) => (
<li key={idx}>{item.name()}</li> // item.name()을 호출해야 함
))}
</ul>tF 함수는 상수로 보이지만 사실은 함수이기 때문에, 컴포넌트 렌더링 시마다 호출이 필요하다.
tF 함수를 먼저 호출하여 상수처럼 저장하려는 시도는 실패했다.
그 이유는 useTranslation 훅과 i18n.t 함수가 번역 파일(JSON)을 로드해야만 동작하기 때문이다.
- 빌드 타임에는 번역 파일이 로드되지 않았으므로, 상수 형태로 번역 데이터를 저장할 수 없다.
- 런타임에 JSON 파일이 로드되고, 키(key)와 매칭되는 값을 가져오기 때문에 이러한 문제가 발생한다.
결론적으로, tF 함수는 런타임에 호출하는 방식으로 사용할 수밖에 없었다.
결론
Sheet Row 400개에 2초, 전체 Row는 약 15초 정도로 JSON파일을 생성 할 수 있게 되었다.


전체 다국어화 작업은 약 2개월 만에 성공적으로 진행되었다.
특히 비개발자도 이해할 수 있는 문서를 작성하고 공유한 덕분에, 프론트엔드 개발자뿐 아니라 다른 개발자들도 작업에 참여할 수 있었던 것이 성공 요인 중 하나였다고 생각한다.
개발을 진행하다 보면 엣지 케이스가 점점 더 많아지고, 이를 모두 해결하려고 하면 스코프가 과도하게 확장되는 경우가 있다. 현재 시점에서 가장 효율적이고 실현 가능한 방법을 선택하고, 이를 잘 활용하는 것이 더 중요한 것 같다.