핵심 아키텍처 설계
마이크로 프론트엔드 구성 요소
interface MicroAppConfig {
appId: string;
entryPoint: string;
mountSelector: string;
activationPath: string;
customProps?: Record<string, unknown>;
}
interface ComponentOptions {
elementName: string;
useShadow?: boolean;
externalStyles?: string[];
}
class MicroFrontend {
private config: MicroAppConfig;
private componentOpts: ComponentOptions;
private mountedState: boolean = false;
constructor(appConfig: MicroAppConfig, compOptions: ComponentOptions) {
this.config = appConfig;
this.componentOpts = compOptions;
}
async loadResources(): Promise<void> {
// 외부 자원 동적 로드
}
attachToDOM(container: HTMLElement): void {
// DOM에 애플리케이션 마운트
}
detachFromDOM(): void {
// DOM에서 애플리케이션 언마운트
}
}
웹 컴포넌트 기본 구현
커스텀 요소 베이스 클래스
abstract class BaseMicroApp extends HTMLElement {
protected shadowRootRef: ShadowRoot;
protected appRoot: HTMLElement;
protected componentCSS: string;
constructor() {
super();
this.shadowRootRef = this.attachShadow({ mode: 'open' });
this.componentCSS = this.generateStyles();
this.appRoot = document.createElement('div');
this.appRoot.className = 'app-root-element';
}
connectedCallback() {
this.initializeComponent();
this.mountApplication();
}
disconnectedCallback() {
this.unmountApplication();
}
attributeChangedCallback(attrName: string, prevVal: string, newVal: string) {
if (prevVal !== newVal) {
this.handleAttrUpdate(attrName, newVal);
}
}
protected abstract generateStyles(): string;
protected abstract mountApplication(): Promise<void>;
protected abstract unmountApplication(): void;
protected abstract handleAttrUpdate(attrName: string, value: string): void;
protected initializeComponent() {
this.shadowRootRef.innerHTML = `
<style>${this.componentCSS}</style>
<div class="micro-app-wrapper">
<div class="loader" id="loader">로드 중...</div>
${this.appRoot.outerHTML}
</div>
`;
this.appRoot = this.shadowRootRef.querySelector('.app-root-element') as HTMLElement;
}
protected displayLoader() {
const loader = this.shadowRootRef.getElementById('loader');
if (loader) loader.style.display = 'flex';
}
protected hideLoader() {
const loader = this.shadowRootRef.getElementById('loader');
if (loader) loader.style.display = 'none';
}
}
구현 예시: 사용자 관리 앱
class UserAdminComponent extends BaseMicroApp {
private appScript?: HTMLScriptElement;
private appStyles?: HTMLLinkElement;
private application: any;
static get observedAttributes() {
return ['api-endpoint', 'color-scheme', 'language'];
}
protected generateStyles(): string {
return `
:host { display: block; width: 100%; height: 100%; }
.micro-app-wrapper { width: 100%; height: 100%; }
.loader { display: flex; justify-content: center; align-items: center; height: 200px; }
`;
}
protected async mountApplication(): Promise<void> {
try {
this.displayLoader();
await this.fetchDependencies();
await this.initAppInstance();
this.hideLoader();
} catch (err) {
console.error('사용자 관리 앱 마운트 실패:', err);
this.showFailureMessage('앱 로드 실패');
}
}
protected unmountApplication(): void {
if (this.application?.destroy) this.application.destroy();
if (this.appScript) document.head.removeChild(this.appScript);
if (this.appStyles) document.head.removeChild(this.appStyles);
this.appRoot.innerHTML = '';
}
private async fetchDependencies(): Promise<void> {
const resourcePath = this.getAttribute('resource-path') || 'https://cdn.example.com/user-app';
this.appStyles = document.createElement('link');
this.appStyles.rel = 'stylesheet';
this.appStyles.href = `${resourcePath}/styles.css`;
document.head.appendChild(this.appStyles);
this.appScript = document.createElement('script');
this.appScript.src = `${resourcePath}/app.js`;
this.appScript.type = 'module';
await new Promise((resolve, reject) => {
this.appScript!.onload = resolve;
this.appScript!.onerror = reject;
document.head.appendChild(this.appScript!);
});
}
private async initAppInstance(): Promise<void> {
const appFactory = (window as any).UserAdminFactory;
if (!appFactory) throw new Error('앱 생성 함수 없음');
const appConfig = {
apiUrl: this.getAttribute('api-endpoint') || '/api',
theme: this.getAttribute('color-scheme') || 'light',
locale: this.getAttribute('language') || 'ko-KR',
rootElement: this.appRoot,
eventHandler: this.handleAppEvents.bind(this)
};
this.application = appFactory(appConfig);
if (this.application?.init) await this.application.init();
}
private handleAppEvents(eventData: any) {
this.dispatchEvent(new CustomEvent('app-event', {
detail: eventData,
bubbles: true,
composed: true
}));
}
}
customElements.define('user-admin-app', UserAdminComponent);
라우팅 및 상태 관리
경로 기반 애플리케이션 로더
class AppRouter {
private registeredApps: Map<string, MicroFrontend> = new Map();
private activeApp: string | null = null;
private routeMap: RouteDefinition[] = [];
constructor(private hostContainer: HTMLElement) {
this.setupRouting();
}
registerApplication(config: MicroAppConfig): void {
const application = new MicroFrontend(config);
this.registeredApps.set(config.appId, application);
this.routeMap.push({
appId: config.appId,
pathRule: config.activationPath
});
}
initialize(): void {
this.routeTo(window.location.pathname);
window.addEventListener('popstate', () => {
this.routeTo(window.location.pathname);
});
}
private routeTo(path: string): void {
const targetApp = this.findTargetApplication(path);
if (!targetApp) return this.showMissingPage();
if (this.activeApp === targetApp.appId) return;
if (this.activeApp) {
const current = this.registeredApps.get(this.activeApp);
current?.detachFromDOM();
}
const nextApp = this.registeredApps.get(targetApp.appId);
if (nextApp) {
this.activeApp = targetApp.appId;
nextApp.attachToDOM(this.hostContainer);
if (window.location.pathname !== path) {
window.history.pushState(null, '', path);
}
}
}
private findTargetApplication(path: string): RouteDefinition | null {
return this.routeMap.find(route =>
typeof route.pathRule === 'function'
? route.pathRule(path)
: path.startsWith(route.pathRule as string)
) || null;
}
}
interface RouteDefinition {
appId: string;
pathRule: string | ((path: string) => boolean);
}
애플리케이션 간 통신
class AppEventSystem {
private eventHandlers: Map<string, Function[]> = new Map();
publish(eventType: string, payload?: any): void {
const handlers = this.eventHandlers.get(eventType) || [];
handlers.forEach(handler => handler(payload));
window.dispatchEvent(new CustomEvent(`app:${eventType}`, { detail: payload }));
}
subscribe(eventType: string, handler: Function): () => void {
if (!this.eventHandlers.has(eventType)) {
this.eventHandlers.set(eventType, []);
}
this.eventHandlers.get(eventType)!.push(handler);
return () => this.unsubscribe(eventType, handler);
}
unsubscribe(eventType: string, handler: Function): void {
const handlers = this.eventHandlers.get(eventType);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) handlers.splice(index, 1);
}
}
}
class SharedStateManager {
private appState: Record<string, any> = {};
private stateListeners: Map<string, Function[]> = new Map();
updateState(key: string, value: any): void {
const previous = this.appState[key];
this.appState[key] = value;
const listeners = this.stateListeners.get(key) || [];
listeners.forEach(listener => listener(value, previous));
}
getState<T = any>(key: string): T | undefined {
return this.appState[key];
}
addStateListener(key: string, callback: Function): () => void {
if (!this.stateListeners.has(key)) this.stateListeners.set(key, []);
this.stateListeners.get(key)!.push(callback);
return () => this.removeStateListener(key, callback);
}
removeStateListener(key: string, callback: Function): void {
const listeners = this.stateListeners.get(key);
if (listeners) {
const index = listeners.indexOf(callback);
if (index > -1) listeners.splice(index, 1);
}
}
}
export const eventSystem = new AppEventSystem();
export const sharedState = new SharedStateManager();
성능 최적화 기법
애플리케이션 사전 로드
class ResourcePreloader {
private preloaded: Set<string> = new Set();
async preloadApplication(appName: string, resourceUrl: string): Promise<void> {
if (this.preloaded.has(appName)) return;
try {
const preloadScript = document.createElement('script');
preloadScript.src = resourceUrl;
preloadScript.async = true;
await new Promise((resolve, reject) => {
preloadScript.onload = resolve;
preloadScript.onerror = reject;
document.head.appendChild(preloadScript);
});
this.preloaded.add(appName);
} catch (err) {
console.error(`${appName} 사전 로드 실패:`, err);
}
}
predictAndPreload(currentRoute: string): void {
const predicted = this.predictNextApplications(currentRoute);
predicted.forEach(app => this.preloadApplication(app.name, app.url));
}
private predictNextApplications(currentRoute: string): {name: string, url: string}[] {
const predictions = [];
if (currentRoute === '/') {
predictions.push({ name: 'user-admin', url: '/apps/user-admin/app.js' });
}
return predictions;
}
}
성능 모니터링
class AppPerformanceTracker {
private metricTimers: Map<string, number> = new Map();
startMeasurement(appId: string, metricName: string): void {
this.metricTimers.set(`${appId}_${metricName}`, performance.now());
}
completeMeasurement(appId: string, metricName: string): number {
const timerKey = `${appId}_${metricName}`;
const start = this.metricTimers.get(timerKey);
if (start) {
const duration = performance.now() - start;
this.metricTimers.delete(timerKey);
this.logMetric(appId, metricName, duration);
return duration;
}
return 0;
}
private logMetric(appId: string, metric: string, value: number): void {
const metricData = {
application: appId,
metricType: metric,
value: value,
timestamp: Date.now()
};
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/perf-metrics', JSON.stringify(metricData));
}
}
}