@lunariajs/core 심층 분석: LunariaJS 핵심 아키텍처 이해하기
LunariaJS 핵심 라이브러리의 아키텍처 설계와 구현 원리를 깊이 있게 분석하고, 파일 파싱, 상태 추적, 설정 처리 등 핵심 모듈을 소스 코드 수준에서 이해하여 사용자 정의 확장과 고급 활용의 기반을 마련합니다.
@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 Client | Git 작업 캡슐화 (커밋 히스토리, 파일 상태) | git.ts |
| Dashboard Generator | 정적 HTML 대시보드 생성 | dashboard/ |
| Reporter | 상태 보고서 및 로그 출력 생성 | reporter.ts |
기술 스택 선택
LunariaJS는 다음 기술 스택을 선택했습니다:
| 기술 | 용도 | 선택 이유 |
|---|---|---|
| TypeScript | 주요 언어 | 타입 안전, 더 나은 IDE 지원 |
| Zod | 스키마 검증 | 설정 파일 검증 |
| Vite | 빌드 도구 | 빠른 ES 모듈 지원 |
| simple-git | Git 작업 | 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의 고급 설정과 사용자 정의 전략을 살펴보고, 다음 방법을 배우겠습니다:
- 기업급 시나리오의 복잡한 설정 처리
- 다중 저장소 현지화 관리 구현
- 사용자 정의 현지화 전략 설계
- 대규모 프로젝트 성능 최적화
기대해 주세요!
💡 추천 읽기: