무결성 보장(Deterministic i18n): 자동화 스크립트로 번역 누락 차단하기
분산된 다국어 리소스의 관리 부채
글로벌 SaaS 빌더에게 다국어 리소스 관리는 단순 작업이 아닌 ‘품질 보증(QA)’의 영역입니다. 서비스가 고도화됨에 따라 언어 파일은 비대해지고, 수동 검수는 불가능에 가까워집니다. 저는 이 문제를 해결하기 위해 시스템적 접근 방식을 택했습니다.
스코프 최적화와 네비게이션 생산성
가장 먼저 수행한 리팩토링은 리소스의 모듈화입니다. dictionaries 디렉토리 하위에 로케일(Locale)별로 폴더를 구성하고, 기능을 기준으로 JSON 파일을 분할했습니다.
- 효과: 단일 파일의 복잡도를 낮추어 스크롤링 비용을 획기적으로 절감했습니다.
- 팁: JSON 파일을 모듈로 임포트하여 정적 타입을 지원받으면, IDE에서
Ctrl + Click을 통한 심볼 네비게이션이 활성화되어 유지보수 속도가 200% 이상 향상됩니다.
검증 자동화: Recursive Key Comparison Script
시스템의 무결성을 보장하기 위해 Node.js 기반의 검증 스크립트를 구축했습니다. 이 스크립트는 기준 언어(Base Locale)를 상정한 뒤, 타겟 언어셋과의 구조적 차이를 검출합니다.
작성된 코드는 fs와 path 모듈을 활용하여 파일 시스템을 순회하며, 특히 compareKeys 함수는 객체의 깊이에 상관없이 재귀적으로 모든 경로를 추적합니다.
- Error Handling: 단순 로깅에 그치지 않고
hasError플래그를 통해 프로세스 종료 코드를 제어합니다. 이는 CI/CD 파이프라인과의 통합을 염두에 둔 설계입니다. - Validation Logic: 배열을 제외한 순수 객체만을 타겟으로 하여 깊은 비교(Deep Comparison)를 수행함으로써, 복잡한 다국어 계층 구조에서도 누락된 엔드포인트를 정확히 식별합니다.
const fs = require('fs');
const path = require('path');
const BASE_PATH = path.join(process.cwd(), 'dictionaries');
/**
* 💡 유틸리티: 폴더 및 파일 목록 가져오기
*/
function getDirectories(source) {
if (!fs.existsSync(source)) return [];
return fs
.readdirSync(source, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
}
function getJsonFiles(source) {
if (!fs.existsSync(source)) return [];
return fs
.readdirSync(source)
.filter(file => file.endsWith('.json'))
.map(file => file.replace('.json', ''));
}
/**
* 💡 핵심: 재귀적으로 JSON 키 구조 비교
*/
function compareKeys(baseObj, targetObj, locale, fileName, path = '') {
let missingKeys = [];
for (const key in baseObj) {
const currentPath = path ? `${path}.${key}` : key;
// 1. 키 존재 여부 확인
if (!(key in targetObj)) {
missingKeys.push(
`❌ [Key 누락] ${locale}/${fileName}.json -> "${currentPath}"`,
);
continue;
}
// 2. 값이 객체인 경우 재귀적으로 깊은 비교 수행 (배열 제외)
if (
typeof baseObj[key] === 'object' &&
baseObj[key] !== null &&
!Array.isArray(baseObj[key])
) {
const nestedMissing = compareKeys(
baseObj[key],
targetObj[key],
locale,
fileName,
currentPath,
);
missingKeys = missingKeys.concat(nestedMissing);
}
}
return missingKeys;
}
function checkI18n() {
const locales = getDirectories(BASE_PATH);
if (locales.length === 0) {
console.error('❌ [Error] 딕셔너리 폴더가 비어 있습니다.');
process.exit(1);
}
const baseLocale = locales.includes('en') ? 'en' : locales[0];
const baseDirPath = path.join(BASE_PATH, baseLocale);
const baseDomains = getJsonFiles(baseDirPath);
console.log(
`🔍 기준 로케일 [${baseLocale}]을 바탕으로 정밀 검사를 시작합니다...\n`,
);
let hasError = false;
locales.forEach(locale => {
if (locale === baseLocale) return;
const targetDirPath = path.join(BASE_PATH, locale);
const targetDomains = getJsonFiles(targetDirPath);
baseDomains.forEach(domain => {
// 파일 존재 여부 먼저 확인
if (!targetDomains.includes(domain)) {
console.error(
`❌ [File 누락] ${locale}/${domain}.json 파일이 없습니다.`,
);
hasError = true;
return;
}
// 💡 파일 내부 키값 정밀 비교
try {
const baseContent = JSON.parse(
fs.readFileSync(path.join(baseDirPath, `${domain}.json`), 'utf-8'),
);
const targetContent = JSON.parse(
fs.readFileSync(path.join(targetDirPath, `${domain}.json`), 'utf-8'),
);
const errors = compareKeys(baseContent, targetContent, locale, domain);
if (errors.length > 0) {
errors.forEach(err => console.error(err));
hasError = true;
}
} catch (e) {
console.error(
`❌ [Parsing 에러] ${locale}/${domain}.json 파일을 읽는 중 오류 발생.`,
);
hasError = true;
}
});
});
if (hasError) {
console.log(
'\n❌ 정합성 체크 실패. 위 오류들을 수정한 뒤 다시 시도해 주세요.',
);
process.exit(1);
} else {
console.log(
`✅ 완벽합니다! 모든 언어의 파일과 키 구조가 '${baseLocale}'과 100% 일치합니다.`,
);
}
}
checkI18n();
런타임 안정성 확보
이러한 사전 검증 시스템은 실서비스 환경에서 발생할 수 있는 ‘Undefined’ 노출 사고를 원천 봉쇄합니다. 개발자가 실수하더라도 스크립트가 빌드 단계에서 에러를 던지기 때문에, 안정적인 사용자 경험(UX)을 유지할 수 있습니다.
비효율을 자동화로 치환하는 빌더의 습관
처음에는 키 하나를 직접 확인하는 게 더 빠를 것 같지만, 프로젝트가 커질수록 이 스크립트의 가치는 빛을 발합니다. 빌더는 단순히 코드를 짜는 사람이 아니라, 시스템이 스스로를 증명하게 만드는 사람입니다.
💡 Tip:
process.exit(1)을 활용하면 Git Hook 설정 시 오류가 있는 코드는 커밋 자체가 되지 않도록 강제할 수 있습니다.