블로그

@lunariajs/core 심층 분석: LunariaJS 핵심 아키텍처 이해하기

LunariaJS 핵심 라이브러리의 아키텍처 설계와 구현 원리를 깊이 있게 분석하고, 파일 파싱, 상태 추적, 설정 처리 등 핵심 모듈을 소스 코드 수준에서 이해하여 사용자 정의 확장과 고급 활용의 기반을 마련합니다.

LibDoc Team 2026년 3월 6일 LunariaJS 칼럼 82 분 읽기
#LunariaJS #소스 코드 분석 #핵심 아키텍처 #@lunariajs/core #TypeScript

@lunariajs/core 심층 분석: LunariaJS 핵심 아키텍처 이해하기

앞선 일곱 편의 입문 글에서는 LunariaJS의 설치, 설정, CLI 명령어, 대시보드, Git 워크플로우, Starlight 통합, CI/CD 통합을 포괄적으로 배웠습니다. 이제 소스 코드 수준에서 LunariaJS의 핵심 아키텍처와 구현 원리를 깊이 있게 이해해 보겠습니다.

💡 공식 문서: LunariaJS 한국어 문서 - API 참조

핵심 아키텍처 개요

전체 아키텍처 다이어그램

LunariaJS의 핵심 아키텍처는 다음 계층으로 요약할 수 있습니다:

┌─────────────────────────────────────────────────────────────────┐
│                        CLI Layer                                 │
│                   (lunaria init/build/preview)                   │
├─────────────────────────────────────────────────────────────────┤
│                       API Layer                                  │
│              (Public API for external use)                       │
├─────────────────────────────────────────────────────────────────┤
│                     Core Modules                                 │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐             │
│  │   Config     │ │   File       │ │   Status     │             │
│  │   System     │ │   Parsers    │ │   Tracker    │             │
│  └──────────────┘ └──────────────┘ └──────────────┘             │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐             │
│  │   Git        │ │   Dashboard  │ │   Reporter   │             │
│  │   Client     │ │   Generator  │ │              │             │
│  └──────────────┘ └──────────────┘ └──────────────┘             │
├─────────────────────────────────────────────────────────────────┤
│                     Infrastructure                               │
│            (File System, Git, Logger, Utils)                     │
└─────────────────────────────────────────────────────────────────┘

핵심 모듈 책임

모듈책임핵심 파일
Config System설정 로드, 검증, 기본값 처리config.ts
File Parsers다양한 포맷의 현지화 파일 파싱parsers/
Status Tracker번역 상태 추적 (done/outdated/missing)status.ts
Git ClientGit 작업 캡슐화 (커밋 히스토리, 파일 상태)git.ts
Dashboard Generator정적 HTML 대시보드 생성dashboard/
Reporter상태 보고서 및 로그 출력 생성reporter.ts

기술 스택 선택

LunariaJS는 다음 기술 스택을 선택했습니다:

기술용도선택 이유
TypeScript주요 언어타입 안전, 더 나은 IDE 지원
Zod스키마 검증설정 파일 검증
Vite빌드 도구빠른 ES 모듈 지원
simple-gitGit 작업Node.js Git 클라이언트
handlebars템플릿 엔진대시보드 HTML 생성

디렉토리 구조

@lunariajs/core/
├── src/
│   ├── index.ts              # 공개 API 진입점
│   ├── config.ts             # 설정 시스템
│   ├── status.ts             # 상태 추적
│   ├── git.ts                # Git 작업
│   ├── reporter.ts           # 보고서 생성
│   ├── parsers/              # 파일 파서
│   │   ├── index.ts
│   │   ├── json.ts
│   │   ├── yaml.ts
│   │   └── markdown.ts
│   ├── dashboard/            # 대시보드 생성
│   │   ├── index.ts
│   │   ├── templates/
│   │   └── assets/
│   ├── types/                # 타입 정의
│   │   ├── config.ts
│   │   └── status.ts
│   └── utils/                # 유틸리티 함수
│       ├── file.ts
│       └── logger.ts
├── package.json
├── tsconfig.json
└── README.md

설정 시스템

설정 타입 정의

LunariaJS는 엄격한 설정 타입을 TypeScript로 정의합니다:

// src/types/config.ts

/**
 * 지원하는 파일 포맷
 */
export type FileFormat = 'json' | 'yaml' | 'md' | 'csv';

/**
 * 언어 설정
 */
export interface LocaleConfig {
  /** 언어 레이블 (예: 'en', 'ko') */
  label: string;
  /** 언어 방향 */
  dir?: 'ltr' | 'rtl';
  /** 사용자 정의 데이터 */
  [key: string]: unknown;
}

/**
 * 파일 패턴 설정
 */
export interface FilePattern {
  /** 원본 언어 파일 경로 패턴 */
  sourcePath: string;
  /** 번역 파일 경로 패턴 */
  localizationPath: string;
  /** 포함할 파일 glob 패턴 */
  include?: string[];
  /** 제외할 파일 glob 패턴 */
  exclude?: string[];
}

/**
 * 대시보드 설정
 */
export interface DashboardConfig {
  /** 출력 디렉토리 */
  outputDir: string;
  /** 대시보드 제목 */
  title?: string;
  /** 대시보드 설명 */
  description?: string;
  /** 인터페이스 언어 */
  uiLanguage?: string;
  /** 사용자 정의 CSS 경로 */
  customCss?: string;
  /** 사이트 URL (SEO용) */
  site?: string;
}

/**
 * LunariaJS 전체 설정
 */
export interface LunariaConfig {
  /** 원본 언어 */
  sourceLanguage: string;
  /** 지원하는 모든 언어 */
  languages: string[];
  /** 언어 설정 매핑 */
  locales?: Record<string, LocaleConfig>;
  /** 파일 패턴 목록 */
  files: FilePattern[];
  /** 대시보드 설정 */
  dashboard?: DashboardConfig;
  /** 무시할 키워드 (플레이스홀더 번역 감지용) */
  ignoreKeywords?: string[];
}

설정 로드 메커니즘

설정 로드 과정은 세 단계로 나뉩니다:

// src/config.ts

import { promises as fs } from 'fs';
import { pathExists } from './utils/file';
import { LunariaConfig } from './types/config';
import { defaultConfig } from './config/defaults';

/**
 * 설정 로더
 */
export async function loadConfig(
  configPath: string = 'lunaria.config.json'
): Promise<LunariaConfig> {
  // 단계 1: 사용자 설정 로드 시도
  const userConfig = await loadUserConfig(configPath);

  // 단계 2: 기본 설정과 병합
  const mergedConfig = mergeConfig(defaultConfig, userConfig);

  // 단계 3: 설정 검증
  const validatedConfig = validateConfig(mergedConfig);

  return validatedConfig;
}

/**
 * 사용자 설정 파일 로드
 */
async function loadUserConfig(
  configPath: string
): Promise<Partial<LunariaConfig>> {
  // JSON과 JS/TS 포맷 지원
  if (configPath.endsWith('.json')) {
    return loadJsonConfig(configPath);
  } else if (configPath.endsWith('.js') || configPath.endsWith('.ts')) {
    return loadJsConfig(configPath);
  }

  // 기본적으로 JSON 시도
  return loadJsonConfig(configPath);
}

설정 검증

Zod를 사용한 엄격한 설정 검증:

// src/config/validation.ts

import { z } from 'zod';

/**
 * 설정 스키마 정의
 */
const LunariaConfigSchema = z.object({
  sourceLanguage: z.string().min(1, 'sourceLanguage is required'),
  languages: z.array(z.string()).min(1, 'At least one language is required'),
  files: z.array(
    z.object({
      sourcePath: z.string(),
      localizationPath: z.string(),
      include: z.array(z.string()).optional(),
      exclude: z.array(z.string()).optional(),
    })
  ).min(1, 'At least one file pattern is required'),
  dashboard: z.object({
    outputDir: z.string().default('lunaria-dashboard'),
    title: z.string().optional(),
    description: z.string().optional(),
  }).optional(),
});

/**
 * 설정 검증
 */
export function validateConfig(config: unknown): LunariaConfig {
  const result = LunariaConfigSchema.safeParse(config);

  if (!result.success) {
    const errors = result.error.errors.map(
      (e) => `${e.path.join('.')}: ${e.message}`
    );
    throw new Error(`Invalid configuration:\n${errors.join('\n')}`);
  }

  // 추가 의미론적 검증
  validateSemantics(result.data);

  return result.data;
}

파일 파서

파서 인터페이스 설계

LunariaJS는 통합된 파서 인터페이스를 정의합니다:

// src/parsers/types.ts

/**
 * 현지화 파일 콘텐츠
 */
export interface LocalizedContent {
  /** 파일 경로 */
  path: string;
  /** 언어 코드 */
  lang: string;
  /** 키-값 쌍 콘텐츠 */
  entries: Record<string, string>;
  /** 원본 콘텐츠 */
  raw: string;
  /** 최종 수정 시간 */
  lastModified?: Date;
}

/**
 * 파일 파서 인터페이스
 */
export interface FileParser {
  /** 지원하는 파일 확장자 */
  extensions: string[];

  /** 파일 콘텐츠 파싱 */
  parse(content: string, path: string): Promise<LocalizedContent>;

  /** 콘텐츠 직렬화 (파일 생성용) */
  stringify(data: Record<string, string>): string;
}

JSON 파서

// src/parsers/json.ts

import { FileParser, LocalizedContent } from './types';

export const JsonParser: FileParser = {
  extensions: ['.json'],

  async parse(content: string, path: string): Promise<LocalizedContent> {
    try {
      const data = JSON.parse(content);

      // 중첩 객체 평탄화
      const entries = flattenObject(data);

      return {
        path,
        lang: extractLangFromPath(path),
        entries,
        raw: content,
      };
    } catch (error) {
      throw new Error(`Failed to parse JSON file ${path}: ${error}`);
    }
  },

  stringify(data: Record<string, string>): string {
    // 평탄한 구조를 중첩 객체로 복원
    const nested = unflattenObject(data);
    return JSON.stringify(nested, null, 2);
  },
};

Markdown 파서

Markdown 파일의 처리는 단순한 키-값 쌍이 아니므로 조금 다릅니다:

// src/parsers/markdown.ts

import { FileParser, LocalizedContent } from './types';
import { matter } from 'gray-matter';

export const MarkdownParser: FileParser = {
  extensions: ['.md', '.mdx'],

  async parse(content: string, path: string): Promise<LocalizedContent> {
    try {
      // frontmatter 파싱
      const { data: frontmatter, content: body } = matter(content);

      // frontmatter와 body를 별도의 항목으로 처리
      const entries: Record<string, string> = {
        // frontmatter 항목
        ...flattenObject(frontmatter || {}),
        // body를 하나의 항목으로 처리
        '_body': body.trim(),
      };

      return {
        path,
        lang: extractLangFromPath(path),
        entries,
        raw: content,
      };
    } catch (error) {
      throw new Error(`Failed to parse Markdown file ${path}: ${error}`);
    }
  },

  stringify(data: Record<string, string>): string {
    const { _body, ...frontmatter } = data;
    const nested = unflattenObject(frontmatter);

    const frontmatterStr = Object.keys(nested).length > 0
      ? matter.stringify(_body || '', nested)
      : _body || '';

    return frontmatterStr;
  },
};

상태 추적 엔진

상태 계산 핵심 로직

상태 추적은 LunariaJS의 핵심 기능입니다:

// src/status.ts

import { LunariaConfig } from './types/config';
import { FileStatus, TranslationStatus } from './types/status';
import { getFileLastModified } from './git';

/**
 * 단일 파일의 번역 상태 계산
 */
export async function calculateFileStatus(
  sourcePath: string,
  translationPath: string,
  lang: string,
  config: LunariaConfig
): Promise<FileStatus> {
  // 번역 파일 존재 여부 확인
  const translationExists = await fileExists(translationPath);

  if (!translationExists) {
    return {
      path: translationPath,
      lang,
      status: 'missing',
      sourcePath,
    };
  }

  // 최종 수정 시간 획득
  const sourceLastModified = await getFileLastModified(sourcePath);
  const translationLastModified = await getFileLastModified(translationPath);

  // 타임스탬프 비교
  if (translationLastModified >= sourceLastModified) {
    return {
      path: translationPath,
      lang,
      status: 'done',
      sourcePath,
      lastModified: translationLastModified,
    };
  }

  // 번역이 오래됨
  const daysOutdated = Math.floor(
    (sourceLastModified.getTime() - translationLastModified.getTime()) /
      (1000 * 60 * 60 * 24)
  );

  return {
    path: translationPath,
    lang,
    status: 'outdated',
    sourcePath,
    lastModified: translationLastModified,
    sourceLastModified,
    daysOutdated,
  };
}

Git 작업 캡슐화

// src/git.ts

import simpleGit, { SimpleGit } from 'simple-git';

let gitClient: SimpleGit | null = null;

/**
 * Git 클라이언트 인스턴스 획득
 */
function getGitClient(): SimpleGit {
  if (!gitClient) {
    gitClient = simpleGit();
  }
  return gitClient;
}

/**
 * 파일의 최종 수정 시간 획득
 */
export async function getFileLastModified(filePath: string): Promise<Date> {
  const git = getGitClient();

  try {
    // 파일 최종 수정 커밋 획득
    const log = await git.log(['-1', '--format=%ct', '--', filePath]);

    if (log.latest) {
      const timestamp = parseInt(log.latest.hash, 10);
      return new Date(timestamp * 1000);
    }

    // Git 히스토리가 없으면 파일 시스템 시간 사용
    const stats = await fs.stat(filePath);
    return stats.mtime;
  } catch (error) {
    // Git 작업 실패 시 파일 시스템 시간 사용
    const stats = await fs.stat(filePath);
    return stats.mtime;
  }
}

대시보드 생성기

템플릿 시스템

LunariaJS는 Handlebars 템플릿 엔진으로 대시보드를 생성합니다:

// src/dashboard/generator.ts

import Handlebars from 'handlebars';
import { TranslationStatus } from '../types/status';
import { readTemplate, copyAssets } from './utils';

// Handlebars 헬퍼 등록
Handlebars.registerHelper('progressBar', (progress: number) => {
  const color = progress >= 90 ? 'green' : progress >= 70 ? 'yellow' : 'red';
  return new Handlebars.SafeString(
    `<div class="progress-bar">
      <div class="progress-fill ${color}" style="width: ${progress}%"></div>
    </div>`
  );
});

Handlebars.registerHelper('statusIcon', (status: string) => {
  const icons: Record<string, string> = {
    done: '✅',
    outdated: '⚠️',
    missing: '❌',
  };
  return icons[status] || '❓';
});

/**
 * 대시보드 HTML 생성
 */
export async function generateDashboard(
  status: TranslationStatus,
  outputDir: string
): Promise<void> {
  // 템플릿 읽기
  const indexTemplate = await readTemplate('index.html');
  const compiledTemplate = Handlebars.compile(indexTemplate);

  // 템플릿 데이터 준비
  const templateData = {
    title: 'Localization Status',
    generatedAt: new Date().toISOString(),
    sourceLanguage: status.sourceLanguage,
    languages: Object.entries(status.languages).map(([lang, data]) => ({
      code: lang,
      total: data.total,
      done: data.done,
      outdated: data.outdated,
      missing: data.missing,
      progress: Math.round((data.done / data.total) * 100),
    })),
  };

  // HTML 렌더링
  const html = compiledTemplate(templateData);

  // 파일 쓰기
  await fs.writeFile(path.join(outputDir, 'index.html'), html);

  // 정적 자산 복사
  await copyAssets(outputDir);
}

API 설계 분석

공개 API

LunariaJS는 다음 공개 API를 내보냅니다:

// src/index.ts

export { loadConfig } from './config';
export { calculateAllStatus, calculateProgress } from './status';
export { generateDashboard } from './dashboard/generator';
export { getFileLastModified, getFileHistory } from './git';
export { parseFile, getParser } from './parsers';

// 타입 내보내기
export type { LunariaConfig, FilePattern, DashboardConfig } from './types/config';
export type { TranslationStatus, FileStatus } from './types/status';
export type { LocalizedContent, FileParser } from './parsers/types';

사용 예시

import {
  loadConfig,
  calculateAllStatus,
  generateDashboard,
  calculateProgress,
} from '@lunariajs/core';

async function main() {
  // 1. 설정 로드
  const config = await loadConfig('lunaria.config.json');

  // 2. 번역 상태 계산
  const status = await calculateAllStatus(config);

  // 3. 대시보드 생성
  await generateDashboard(status, config.dashboard?.outputDir || 'lunaria-dashboard');

  // 4. 진행률 보고서 출력
  for (const lang of Object.keys(status.languages)) {
    const progress = calculateProgress(status, lang);
    console.log(`${lang}: ${progress}% complete`);
  }
}

main().catch(console.error);

확장 개발 기초

사용자 정의 파서

// 사용자 정의 CSV 파서
import { FileParser, LocalizedContent } from '@lunariajs/core/parsers/types';

export const CsvParser: FileParser = {
  extensions: ['.csv'],

  async parse(content: string, path: string): Promise<LocalizedContent> {
    const lines = content.split('\n');
    const entries: Record<string, string> = {};

    for (const line of lines.slice(1)) { // 헤더 건너뛰기
      const [key, value] = line.split(',');
      if (key && value) {
        entries[key.trim()] = value.trim();
      }
    }

    return {
      path,
      lang: extractLangFromPath(path),
      entries,
      raw: content,
    };
  },

  stringify(data: Record<string, string>): string {
    const lines = ['key,value'];
    for (const [key, value] of Object.entries(data)) {
      lines.push(`${key},${value}`);
    }
    return lines.join('\n');
  },
};

요약

이 글에서는 @lunariajs/core의 핵심 아키텍처를 깊이 있게 분석했습니다:

모듈핵심 기능핵심 기술
설정 시스템로드, 검증, 병합 설정Zod 스키마 검증
파일 파서JSON/YAML/Markdown 파싱통합 인터페이스 설계
상태 추적번역 상태 계산Git 타임스탬프 비교
Git 클라이언트Git 작업 캡슐화simple-git
대시보드 생성정적 HTML 생성Handlebars 템플릿

핵심 포인트:

  • TypeScript로 타입 안전 제공
  • 모듈화 설계, 책임 명확
  • Zod로 설정 검증
  • Git 히스토리로 번역 상태 추적
  • 템플릿 엔진으로 대시보드 생성

다음 단계

다음 글에서는 LunariaJS의 고급 설정과 사용자 정의 전략을 살펴보고, 다음 방법을 배우겠습니다:

  • 기업급 시나리오의 복잡한 설정 처리
  • 다중 저장소 현지화 관리 구현
  • 사용자 정의 현지화 전략 설계
  • 대규모 프로젝트 성능 최적화

기대해 주세요!


💡 추천 읽기: