hongsoohyuk
HomeResumeGuestbookProjectInstagramBlogAI Chat

© 2026 hongsoohyuk. All rights reserved.

GitHubLinkedIn
Blog/Google Spreadsheet 기반 다국어(i18n) 자동화 파이프라인 구축기

Google Spreadsheet 기반 다국어(i18n) 자동화 파이프라인 구축기

Frontend

Last edited: March 7, 2026

배경

멀티 엔티티 빌링 플랫폼(GCP, Datadog 등)을 개발하면서 한국어, 영어, 일본어 3개 언어를 지원해야 했다. 번역 데이터의 관리 주체는 PO(Product Owner)이고, 개발자는 코드에서 키만 참조하는 구조가 필요했다.

핵심 요구사항:

  • PO가 비개발 도구(Google Spreadsheet)에서 직접 번역을 관리
  • 개발자는 pnpm i18n:pull 명령어 실행으로 최신 번역을 동기화
  • 키 규칙 위반, 중복 키, 번역 누락을 스크립트 레벨에서 검증
  • i18next t() 함수에서 타입 안전한 키 참조 (as any 제로)

전체 아키텍처

plain text
┌─────────────────────┐    pnpm i18n:pull     ┌─────────────────────┐
│  Google Spreadsheet │ ──────────────────▶   │   pull-i18n.mjs     │
│  PO 번역 관리         │ ◀──────────────────   │   (변환 스크립트)      │
└─────────────────────┘  Service Account      └──────────┬──────────┘
                          + Drive API                   │
                                              XLSX 파싱 + 검증
                                                        │
                                                        ▼
                                            ┌─────────────────────┐
                                            │    JSON 리소스        │
                                            │  src/config/locale/ │
                                            │   {ko,en,ja}/*.json │
                                            └──────────┬──────────┘
                                                       │
                                                 static import
                                                       │
                                                       ▼
┌─────────────────────┐                     ┌─────────────────────┐
│   i18next.d.ts      │  CustomTypeOptions  │      i18n.ts        │
│    (타입 선언)        │ ──────────────────▶ │  (i18next 초기화)     │
└─────────────────────┘                     └──────────┬──────────┘
                                                       │
                                                  typed t()함수
                                                       │
                                                       ▼
                                            ┌─────────────────────┐
                                            │   React 컴포넌트      │
                                            │   useTranslation()  │
                                            └─────────────────────┘

1단계: Google Spreadsheet 구조 설계

시트 하나가 네임스페이스 하나에 대응된다. 각 시트의 컬럼 구조는 고정이다.

keykoenja
dashboard대시보드Dashboardダッシュボード
costValidation매입 관리Cost Validation仕入管理
message.saveSuccess저장되었습니다.Saved successfully.保存しました。

시트 목록 = 네임스페이스: common, account, cost_validation, customer_contract, setting 등


2단계: 변환 스크립트 (pull-i18n.mjs)

인증: Google Service Account

초기에는 API Key 방식을 시도했지만 비공개 시트에서 401 오류가 발생했다. Google Service Account + google-auth-library로 전환하여 해결했다.

[heading_4]
  1. Google Cloud Console 접속 → 프로젝트 선택 (없으면 새로 생성)
  2. API 및 서비스 → 라이브러리 → Google Sheets API + Google Drive API 둘 다 사용 설정
  3. IAM 및 관리자 → 서비스 계정 → "서비스 계정 만들기"
    • 이름: i18n-sheet-reader (용도가 명확한 이름 권장)
    • 역할: 별도 지정 불필요 (시트 접근은 시트 공유로 해결)
  4. 생성된 서비스 계정 클릭 → 키 탭 → "키 추가" → JSON 선택 → 다운로드
  5. PO에게 서비스 계정 이메일 전달 → 시트에 뷰어로 초대 (슬랙에 팀원 추가하듯 이메일 입력)
서비스 계정은 메일함이 없어서 초대 수락이 필요 없다. 시트에서 공유 추가하면 즉시 접근 가능하다.
[heading_4]

키 JSON 파일은 credentials/ 디렉토리에 두고 .gitignore에 추가한다.

javascript
# .env
I18N_SHEET_ID=your_google_sheet_id_here
GOOGLE_SERVICE_ACCOUNT_PATH=credentials/google-service-account.json
javascript
# .gitignore
credentials/
.env

Node 20.6+의 --env-file 플래그를 사용하므로 dotenv 의존성이 불필요하다.

json
// package.json
{
  "scripts": {
    "i18n:pull": "node --env-file=.env scripts/pull-i18n.mjs"
  }
}

설치 의존성:

javascript
pnpm add -D xlsx google-auth-library
[heading_4]
javascript
const auth = new GoogleAuth({
  keyFile: path.resolve(ROOT, process.env.GOOGLE_SERVICE_ACCOUNT_PATH),
  scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
})
const client = await auth.getClient()
const token = await client.getAccessToken()

다운로드 & 파싱

Google Sheets export URL로 XLSX를 한 번에 내보내고, xlsx 라이브러리로 파싱한다. XLSX 1회 다운로드로 전체 시트 이름 + 데이터를 한번에 가져오므로 네임스페이스를 하드코딩할 필요가 없다.

javascript
// XLSX 한 번에 다운로드
const url = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/export?format=xlsx`
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${token.token}` },
})
const buffer = await res.arrayBuffer()
const workbook = XLSX.read(buffer, { type: 'array' })

// 시트 이름 자동 감지 → 네임스페이스 하드코딩 불필요
const namespaces = workbook.SheetNames

// 시트별 파싱
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], { defval: '' })

키 규칙 검증

PO가 입력하는 키의 품질을 자동으로 검증한다.

패턴규칙예시
flat keycamelCasedashboard, costValidation
message 접두사message.{camelCase}message.saveSuccess
error 접두사error.{코드}error.NOT_FOUND
plural suffixi18next 문법 허용item_one, item_other

검증 항목:

  • 한글 포함 여부 (경고)
  • 띄어쓰기 포함 (에러)
  • 3단계 이상 중첩 금지 (최대 2단계: message.saveSuccess)
  • 중복 키 검사 — 같은 시트 내 동일 키 감지
  • 번역 누락 검사 — 특정 언어에 빈 값이 있으면 경고

JSON 생성

각 시트를 언어별 JSON 파일로 출력한다.

javascript
src/config/locale/
├── ko/
│   ├── common.json      # { "dashboard": "대시보드", ... }
│   ├── account.json
│   └── setting.json
├── en/
│   ├── common.json      # { "dashboard": "Dashboard", ... }
│   └── ...
└── ja/
    ├── common.json      # { "dashboard": "ダッシュボード", ... }
    └── ...

3단계: i18next 초기화 및 타입 안전성

리소스 등록

생성된 JSON을 static import로 가져와 i18next에 등록한다.

typescript
// src/config/i18n.ts
import koCommon from './locale/ko/common.json'
import enCommon from './locale/en/common.json'
import jaCommon from './locale/ja/common.json'

export const resources = {
  ko: { common: koCommon, account: koAccounts, ... },
  en: { common: enCommon, account: enAccounts, ... },
  ja: { common: jaCommon, account: jaAccounts, ... },
} as const  // ← as const로 리터럴 타입 추론

타입 선언

i18next.d.ts에서 CustomTypeOptions를 확장하면 t() 함수가 JSON 키만 허용한다.

typescript
// src/types/i18next.d.ts
import type { defaultNamespace, resources } from '../config/i18n'

declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: typeof defaultNamespace
    resources: (typeof resources)\['en'\]
  }
}

이 선언만으로 t('dashboard')는 자동완성되고, t('typoKey')는 컴파일 타임에 에러가 발생한다.

동적 키 타입 안전성 — as any 완전 제거

네비게이션 메뉴처럼 런타임에 키를 조합하는 경우가 문제였다. i18next의 typed t()는 string 타입 키를 거부하기 때문에 처음에는 (t as any)(key)로 우회했다.

이를 해결하기 위해 JSON 리소스 타입에서 유효한 키를 자동 추출하는 유틸리티 타입을 도입했다.

typescript
// NavChildKeys — 리소스 JSON에서 sub-nav 키 자동 추출
type NavChildKeys<NS extends Namespace> =
  NS extends NS  // ← distributive conditional type 트릭
    ? keyof (typeof resources)\['en'\]\[NS\] & string
    : never
NS extends NS 트릭은 TypeScript의 distributive conditional type을 강제로 활성화하여, 유니온 타입의 각 멤버를 독립적으로 평가하게 만든다.

실행 결과

pnpm i18n:pull 실행 시 아래와 같은 리포트가 출력된다.

javascript
🌐 Google Spreadsheet → i18n JSON 변환 시작
📥 스프레드시트 다운로드 중...
📋 감지된 시트: common, account, cost_validation, setting

📄 common ... ✅ 45개 키
📄 account ... ✅ 32개 키
📄 cost_validation ... ✅ 28개 키
📄 setting ... ✅ 18개 키

──────────────────────────────────────────────────
✅ 완료: 4개 네임스페이스, 123개 키, 3개 언어
📁 출력: src/config/locale/{ko,en,ja}/*.json

📝 번역 누락 (2건):
   ⚠️  [setting] 번역 누락: "timezone" → ja (행 15)
   ⚠️  [account] 번역 누락: "billingType" → en (행 8)

핵심 의사결정 & 트레이드오프

결정선택지선택 이유
번역 관리 도구Crowdin / Phrase vs Google SpreadsheetPO가 이미 익숙한 도구, 추가 비용 없음, 빠른 도입
인증 방식API Key vs Service Account비공개 시트 접근 필요, API Key는 401 발생
리소스 로딩dynamic import (lazy) vs static importVite 빌드 타임에 JSON이 번들에 포함되어 as const 타입 추론 가능
타입 안전성as any 캐스팅 vs 유틸리티 타입 추출런타임 키 조합에서도 컴파일 타임 검증, 오타 방지
JSON 편집 정책개발자 직접 수정 vs 스크립트만 허용SSOT(Single Source of Truth)를 Spreadsheet로 고정, 충돌 방지

회고

잘된 점:

  • PO ↔ 개발자 간 번역 워크플로우가 명확하게 분리됨
  • 키 규칙 검증으로 잘못된 키가 코드에 유입되는 것을 사전 차단
  • as const + CustomTypeOptions로 별도 코드젠 없이 타입 안전성 확보

아쉬운 점:

  • static import 방식이라 네임스페이스 추가 시 i18n.ts에 수동 등록 필요
  • 번역 키가 많아지면 JSON 파일이 커지므로 네임스페이스 분리 전략이 중요

다음 개선:

  • CI 파이프라인에 i18n:pull + diff 체크 통합 검토
  • 네임스페이스 자동 감지 및 i18n.ts 코드젠 자동화

부록: 팀 온보딩 체크리스트

새 팀원이 pnpm i18n:pull을 실행하기 위한 최소 설정:

  1. 서비스 계정 JSON 키 파일을 credentials/google-service-account.json에 배치
  2. .env 파일에 I18N_SHEET_ID, GOOGLE_SERVICE_ACCOUNT_PATH 설정
  3. pnpm install (xlsx, google-auth-library 자동 설치)
  4. pnpm i18n:pull 실행
서비스 계정 키 파일은 팀 비밀 관리 도구(1Password, Vault 등)를 통해 공유하고, 절대 git에 커밋하지 않는다.