465 lines
13 KiB
TypeScript
465 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { ComponentDefinition } from "@/types/component";
|
|
import { ComponentStandard } from "@/hooks/admin/useComponents";
|
|
|
|
/**
|
|
* 컴포넌트 시스템 성능 최적화 도구
|
|
*/
|
|
|
|
export interface PerformanceMetrics {
|
|
totalComponents: number;
|
|
registrationTime: number;
|
|
searchTime: number;
|
|
renderTime: number;
|
|
memoryUsage: number;
|
|
cacheHitRate: number;
|
|
}
|
|
|
|
export interface OptimizationOptions {
|
|
enableBatchRegistration: boolean;
|
|
enableLazyLoading: boolean;
|
|
enableMemoryOptimization: boolean;
|
|
enableIndexing: boolean;
|
|
batchSize: number;
|
|
cacheSize: number;
|
|
debounceTime: number;
|
|
}
|
|
|
|
export const DEFAULT_OPTIMIZATION_OPTIONS: OptimizationOptions = {
|
|
enableBatchRegistration: true,
|
|
enableLazyLoading: true,
|
|
enableMemoryOptimization: true,
|
|
enableIndexing: true,
|
|
batchSize: 50,
|
|
cacheSize: 1000,
|
|
debounceTime: 300,
|
|
};
|
|
|
|
/**
|
|
* 성능 최적화 클래스
|
|
*/
|
|
export class PerformanceOptimizer {
|
|
private static options: OptimizationOptions = DEFAULT_OPTIMIZATION_OPTIONS;
|
|
private static metrics: PerformanceMetrics = {
|
|
totalComponents: 0,
|
|
registrationTime: 0,
|
|
searchTime: 0,
|
|
renderTime: 0,
|
|
memoryUsage: 0,
|
|
cacheHitRate: 0,
|
|
};
|
|
|
|
// 캐시와 인덱스
|
|
private static searchCache = new Map<string, ComponentDefinition[]>();
|
|
private static categoryIndex = new Map<string, Set<string>>();
|
|
private static webTypeIndex = new Map<string, Set<string>>();
|
|
private static nameIndex = new Map<string, Set<string>>();
|
|
|
|
// 성능 측정
|
|
private static performanceMarks = new Map<string, number>();
|
|
private static cacheHits = 0;
|
|
private static cacheRequests = 0;
|
|
|
|
/**
|
|
* 성능 최적화 초기화
|
|
*/
|
|
static initialize(options: Partial<OptimizationOptions> = {}): void {
|
|
this.options = { ...DEFAULT_OPTIMIZATION_OPTIONS, ...options };
|
|
|
|
// 메모리 사용량 모니터링 (개발 모드에서만)
|
|
if (process.env.NODE_ENV === "development") {
|
|
this.startMemoryMonitoring();
|
|
}
|
|
|
|
// 브라우저 개발자 도구 등록
|
|
if (typeof window !== "undefined") {
|
|
(window as any).__PERFORMANCE_OPTIMIZER__ = this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 컴포넌트 등록 (성능 최적화)
|
|
*/
|
|
static async batchRegisterComponents(
|
|
components: ComponentDefinition[],
|
|
onProgress?: (completed: number, total: number) => void,
|
|
): Promise<void> {
|
|
if (!this.options.enableBatchRegistration) {
|
|
// 배치 비활성화된 경우 순차 등록
|
|
for (let i = 0; i < components.length; i++) {
|
|
await this.registerComponent(components[i]);
|
|
onProgress?.(i + 1, components.length);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const startTime = performance.now();
|
|
const { batchSize } = this.options;
|
|
|
|
console.log(`📦 배치 등록 시작: ${components.length}개 컴포넌트 (배치 크기: ${batchSize})`);
|
|
|
|
for (let i = 0; i < components.length; i += batchSize) {
|
|
const batch = components.slice(i, i + batchSize);
|
|
|
|
// 배치 처리 (비동기)
|
|
await Promise.all(batch.map((component) => this.registerComponent(component)));
|
|
|
|
// 진행률 콜백
|
|
onProgress?.(Math.min(i + batchSize, components.length), components.length);
|
|
|
|
// UI 블로킹 방지를 위한 마이크로태스크 양보
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
|
|
const endTime = performance.now();
|
|
this.metrics.registrationTime = endTime - startTime;
|
|
this.metrics.totalComponents += components.length;
|
|
|
|
console.log(`✅ 배치 등록 완료: ${components.length}개 (${Math.round(this.metrics.registrationTime)}ms)`);
|
|
}
|
|
|
|
/**
|
|
* 개별 컴포넌트 등록 (인덱싱 포함)
|
|
*/
|
|
private static async registerComponent(component: ComponentDefinition): Promise<void> {
|
|
// 인덱싱 활성화된 경우
|
|
if (this.options.enableIndexing) {
|
|
this.updateIndices(component);
|
|
}
|
|
|
|
// 실제 등록 로직은 ComponentRegistry에서 처리
|
|
// 여기서는 성능 측정만 수행
|
|
}
|
|
|
|
/**
|
|
* 인덱스 업데이트
|
|
*/
|
|
private static updateIndices(component: ComponentDefinition): void {
|
|
// 카테고리 인덱스
|
|
if (!this.categoryIndex.has(component.category)) {
|
|
this.categoryIndex.set(component.category, new Set());
|
|
}
|
|
this.categoryIndex.get(component.category)!.add(component.id);
|
|
|
|
// 웹타입 인덱스
|
|
if (!this.webTypeIndex.has(component.webType)) {
|
|
this.webTypeIndex.set(component.webType, new Set());
|
|
}
|
|
this.webTypeIndex.get(component.webType)!.add(component.id);
|
|
|
|
// 이름 인덱스 (검색 최적화)
|
|
const nameWords = component.name.toLowerCase().split(/\s+/);
|
|
nameWords.forEach((word) => {
|
|
if (word.length > 1) {
|
|
if (!this.nameIndex.has(word)) {
|
|
this.nameIndex.set(word, new Set());
|
|
}
|
|
this.nameIndex.get(word)!.add(component.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 최적화된 컴포넌트 검색
|
|
*/
|
|
static optimizedSearch(
|
|
query: string,
|
|
components: ComponentDefinition[],
|
|
options: { useCache?: boolean; useIndex?: boolean } = {},
|
|
): ComponentDefinition[] {
|
|
const { useCache = true, useIndex = true } = options;
|
|
const normalizedQuery = query.toLowerCase().trim();
|
|
|
|
const startTime = performance.now();
|
|
|
|
// 캐시 확인
|
|
if (useCache && this.searchCache.has(normalizedQuery)) {
|
|
this.cacheHits++;
|
|
this.cacheRequests++;
|
|
const endTime = performance.now();
|
|
this.metrics.searchTime = endTime - startTime;
|
|
|
|
console.log(`🎯 캐시 히트: "${query}" (${Math.round(this.metrics.searchTime)}ms)`);
|
|
return this.searchCache.get(normalizedQuery)!;
|
|
}
|
|
|
|
this.cacheRequests++;
|
|
let results: ComponentDefinition[] = [];
|
|
|
|
// 인덱스 기반 검색 (활성화된 경우)
|
|
if (useIndex && this.options.enableIndexing) {
|
|
results = this.indexBasedSearch(normalizedQuery, components);
|
|
} else {
|
|
// 전체 검색
|
|
results = this.fullSearch(normalizedQuery, components);
|
|
}
|
|
|
|
const endTime = performance.now();
|
|
this.metrics.searchTime = endTime - startTime;
|
|
|
|
// 캐시에 저장 (크기 제한)
|
|
if (useCache && this.searchCache.size < this.options.cacheSize) {
|
|
this.searchCache.set(normalizedQuery, results);
|
|
}
|
|
|
|
// 캐시 히트율 업데이트
|
|
this.metrics.cacheHitRate = this.cacheHits / this.cacheRequests;
|
|
|
|
console.log(`🔍 검색 완료: "${query}" → ${results.length}개 (${Math.round(this.metrics.searchTime)}ms)`);
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* 인덱스 기반 빠른 검색
|
|
*/
|
|
private static indexBasedSearch(query: string, components: ComponentDefinition[]): ComponentDefinition[] {
|
|
const componentMap = new Map(components.map((c) => [c.id, c]));
|
|
const matchingIds = new Set<string>();
|
|
|
|
// 이름 인덱스에서 검색
|
|
const queryWords = query.split(/\s+/);
|
|
queryWords.forEach((word) => {
|
|
const ids = this.nameIndex.get(word);
|
|
if (ids) {
|
|
ids.forEach((id) => matchingIds.add(id));
|
|
}
|
|
});
|
|
|
|
// 부분 문자열 검색 (이름 인덱스에서 부족한 부분)
|
|
this.nameIndex.forEach((ids, indexedWord) => {
|
|
if (indexedWord.includes(query) || query.includes(indexedWord)) {
|
|
ids.forEach((id) => matchingIds.add(id));
|
|
}
|
|
});
|
|
|
|
// 결과 변환
|
|
return Array.from(matchingIds)
|
|
.map((id) => componentMap.get(id))
|
|
.filter((component): component is ComponentDefinition => component !== undefined);
|
|
}
|
|
|
|
/**
|
|
* 전체 검색 (폴백)
|
|
*/
|
|
private static fullSearch(query: string, components: ComponentDefinition[]): ComponentDefinition[] {
|
|
return components.filter(
|
|
(component) =>
|
|
component.name.toLowerCase().includes(query) ||
|
|
component.description.toLowerCase().includes(query) ||
|
|
component.tags?.some((tag) => tag.toLowerCase().includes(query)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 메모리 최적화
|
|
*/
|
|
static optimizeMemory(): void {
|
|
if (!this.options.enableMemoryOptimization) return;
|
|
|
|
const before = this.getMemoryUsage();
|
|
|
|
// 검색 캐시 정리 (오래된 항목 제거)
|
|
if (this.searchCache.size > this.options.cacheSize * 0.8) {
|
|
const entries = Array.from(this.searchCache.entries());
|
|
entries.slice(0, Math.floor(entries.length / 2)).forEach(([key]) => {
|
|
this.searchCache.delete(key);
|
|
});
|
|
}
|
|
|
|
// 가비지 컬렉션 제안 (브라우저에서 가능한 경우)
|
|
if ((window as any).gc) {
|
|
(window as any).gc();
|
|
}
|
|
|
|
const after = this.getMemoryUsage();
|
|
console.log(`🧹 메모리 최적화: ${before}MB → ${after}MB (${before - after}MB 절약)`);
|
|
}
|
|
|
|
/**
|
|
* 지연 로딩 컴포넌트 관리
|
|
*/
|
|
static async lazyLoadComponent(componentId: string): Promise<ComponentDefinition | null> {
|
|
if (!this.options.enableLazyLoading) {
|
|
return null;
|
|
}
|
|
|
|
const startTime = performance.now();
|
|
|
|
try {
|
|
// 동적 import를 통한 지연 로딩 시뮬레이션
|
|
// 실제 구현에서는 컴포넌트 파일을 동적으로 로드
|
|
await new Promise((resolve) => setTimeout(resolve, 50)); // 네트워크 지연 시뮬레이션
|
|
|
|
const endTime = performance.now();
|
|
console.log(`🔄 지연 로딩 완료: ${componentId} (${Math.round(endTime - startTime)}ms)`);
|
|
|
|
return null; // 실제 구현에서는 로드된 컴포넌트 반환
|
|
} catch (error) {
|
|
console.error(`❌ 지연 로딩 실패: ${componentId}`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 성능 메트릭 조회
|
|
*/
|
|
static getMetrics(): PerformanceMetrics {
|
|
this.metrics.memoryUsage = this.getMemoryUsage();
|
|
return { ...this.metrics };
|
|
}
|
|
|
|
/**
|
|
* 메모리 사용량 측정
|
|
*/
|
|
private static getMemoryUsage(): number {
|
|
if (typeof (performance as any).memory !== "undefined") {
|
|
return Math.round(((performance as any).memory.usedJSHeapSize / 1024 / 1024) * 100) / 100;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 메모리 모니터링 시작
|
|
*/
|
|
private static startMemoryMonitoring(): void {
|
|
setInterval(() => {
|
|
const usage = this.getMemoryUsage();
|
|
if (usage > 100) {
|
|
// 100MB 초과 시 경고
|
|
console.warn(`⚠️ 높은 메모리 사용량: ${usage}MB`);
|
|
this.optimizeMemory();
|
|
}
|
|
}, 30000); // 30초마다 체크
|
|
}
|
|
|
|
/**
|
|
* 성능 마크 시작
|
|
*/
|
|
static markStart(name: string): void {
|
|
this.performanceMarks.set(name, performance.now());
|
|
}
|
|
|
|
/**
|
|
* 성능 마크 종료 및 측정
|
|
*/
|
|
static markEnd(name: string): number {
|
|
const startTime = this.performanceMarks.get(name);
|
|
if (!startTime) {
|
|
console.warn(`⚠️ 성능 마크를 찾을 수 없음: ${name}`);
|
|
return 0;
|
|
}
|
|
|
|
const duration = performance.now() - startTime;
|
|
this.performanceMarks.delete(name);
|
|
|
|
console.log(`⏱️ ${name}: ${Math.round(duration)}ms`);
|
|
return duration;
|
|
}
|
|
|
|
/**
|
|
* 인덱스 상태 조회
|
|
*/
|
|
static getIndexStats(): {
|
|
categories: number;
|
|
webTypes: number;
|
|
nameWords: number;
|
|
totalIndexEntries: number;
|
|
} {
|
|
return {
|
|
categories: this.categoryIndex.size,
|
|
webTypes: this.webTypeIndex.size,
|
|
nameWords: this.nameIndex.size,
|
|
totalIndexEntries:
|
|
Array.from(this.categoryIndex.values()).reduce((sum, set) => sum + set.size, 0) +
|
|
Array.from(this.webTypeIndex.values()).reduce((sum, set) => sum + set.size, 0) +
|
|
Array.from(this.nameIndex.values()).reduce((sum, set) => sum + set.size, 0),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 캐시 상태 조회
|
|
*/
|
|
static getCacheStats(): {
|
|
searchCacheSize: number;
|
|
hitRate: number;
|
|
requests: number;
|
|
hits: number;
|
|
} {
|
|
return {
|
|
searchCacheSize: this.searchCache.size,
|
|
hitRate: this.metrics.cacheHitRate,
|
|
requests: this.cacheRequests,
|
|
hits: this.cacheHits,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 모든 캐시와 인덱스 클리어
|
|
*/
|
|
static clearAll(): void {
|
|
this.searchCache.clear();
|
|
this.categoryIndex.clear();
|
|
this.webTypeIndex.clear();
|
|
this.nameIndex.clear();
|
|
this.performanceMarks.clear();
|
|
this.cacheHits = 0;
|
|
this.cacheRequests = 0;
|
|
|
|
console.log("🧹 모든 캐시와 인덱스 클리어 완료");
|
|
}
|
|
|
|
/**
|
|
* 설정 업데이트
|
|
*/
|
|
static updateOptions(newOptions: Partial<OptimizationOptions>): void {
|
|
this.options = { ...this.options, ...newOptions };
|
|
console.log("⚙️ 성능 최적화 설정 업데이트:", this.options);
|
|
}
|
|
|
|
/**
|
|
* 벤치마크 실행
|
|
*/
|
|
static async runBenchmark(components: ComponentDefinition[]): Promise<{
|
|
batchRegistration: number;
|
|
indexedSearch: number;
|
|
fullSearch: number;
|
|
memoryUsage: number;
|
|
}> {
|
|
console.log("🏃 성능 벤치마크 실행 중...");
|
|
|
|
// 배치 등록 벤치마크
|
|
this.markStart("batchRegistration");
|
|
await this.batchRegisterComponents(components);
|
|
const batchTime = this.markEnd("batchRegistration");
|
|
|
|
// 인덱스 검색 벤치마크
|
|
this.markStart("indexedSearch");
|
|
this.optimizedSearch("button", components, { useIndex: true });
|
|
const indexedSearchTime = this.markEnd("indexedSearch");
|
|
|
|
// 전체 검색 벤치마크
|
|
this.markStart("fullSearch");
|
|
this.optimizedSearch("button", components, { useIndex: false });
|
|
const fullSearchTime = this.markEnd("fullSearch");
|
|
|
|
const memoryUsage = this.getMemoryUsage();
|
|
|
|
const results = {
|
|
batchRegistration: batchTime,
|
|
indexedSearch: indexedSearchTime,
|
|
fullSearch: fullSearchTime,
|
|
memoryUsage,
|
|
};
|
|
|
|
console.log("📊 벤치마크 결과:", results);
|
|
return results;
|
|
}
|
|
}
|
|
|
|
// 자동 초기화
|
|
if (typeof window !== "undefined") {
|
|
PerformanceOptimizer.initialize();
|
|
}
|