무결성 보장(Deterministic i18n): 자동화 스크립트로 번역 누락 차단하기

Author Kini
·

분산된 다국어 리소스의 관리 부채

글로벌 SaaS 빌더에게 다국어 리소스 관리는 단순 작업이 아닌 ‘품질 보증(QA)’의 영역입니다. 서비스가 고도화됨에 따라 언어 파일은 비대해지고, 수동 검수는 불가능에 가까워집니다. 저는 이 문제를 해결하기 위해 시스템적 접근 방식을 택했습니다.

스코프 최적화와 네비게이션 생산성

가장 먼저 수행한 리팩토링은 리소스의 모듈화입니다. dictionaries 디렉토리 하위에 로케일(Locale)별로 폴더를 구성하고, 기능을 기준으로 JSON 파일을 분할했습니다.

  • 효과: 단일 파일의 복잡도를 낮추어 스크롤링 비용을 획기적으로 절감했습니다.
  • 팁: JSON 파일을 모듈로 임포트하여 정적 타입을 지원받으면, IDE에서 Ctrl + Click을 통한 심볼 네비게이션이 활성화되어 유지보수 속도가 200% 이상 향상됩니다.

검증 자동화: Recursive Key Comparison Script

시스템의 무결성을 보장하기 위해 Node.js 기반의 검증 스크립트를 구축했습니다. 이 스크립트는 기준 언어(Base Locale)를 상정한 뒤, 타겟 언어셋과의 구조적 차이를 검출합니다.

작성된 코드는 fspath 모듈을 활용하여 파일 시스템을 순회하며, 특히 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 설정 시 오류가 있는 코드는 커밋 자체가 되지 않도록 강제할 수 있습니다.

Share this post