From 8d55885897919494d43c49940804c4982212c4cc Mon Sep 17 00:00:00 2001 From: light Date: Sat, 31 Jan 2026 11:25:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=B1=EB=8A=A5=20=EB=A9=94=ED=8A=B8?= =?UTF-8?q?=EB=A6=AD=20=EC=88=98=EC=A7=91=20=EC=A0=84=EC=97=AD=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지 로드 성능: domContentLoaded, load, TTFB, FCP, LCP - API 응답 성능: responseTime, slowCalls 추적 - 리소스 사용량: transferSize, requestCount - 메모리 모니터링: usedJSHeapSize, usagePercent - DOM 메트릭: nodeCount - 성능 등급 기준 및 권장사항 포함 Co-Authored-By: Claude Opus 4.5 --- _global-performance-config.json | 252 ++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 _global-performance-config.json diff --git a/_global-performance-config.json b/_global-performance-config.json new file mode 100644 index 0000000..abe93bf --- /dev/null +++ b/_global-performance-config.json @@ -0,0 +1,252 @@ +{ + "$schema": "E2E Performance Metrics Configuration", + "version": "1.0.0", + "description": "성능 메트릭 수집 및 분석을 위한 전역 설정", + "lastUpdated": "2026-01-31", + + "metrics": { + "pageLoad": { + "enabled": true, + "description": "페이지 로드 성능 측정", + "thresholds": { + "good": 1000, + "acceptable": 2000, + "slow": 3000, + "critical": 5000 + }, + "measurements": [ + "domContentLoaded", + "load", + "firstContentfulPaint", + "largestContentfulPaint", + "timeToInteractive" + ] + }, + + "apiResponse": { + "enabled": true, + "description": "API 응답 시간 측정", + "thresholds": { + "fast": 200, + "good": 500, + "acceptable": 1000, + "slow": 2000, + "critical": 5000 + } + }, + + "resourceUsage": { + "enabled": true, + "description": "리소스 사용량 모니터링", + "track": [ + "jsHeapSize", + "domNodes", + "networkRequests", + "transferSize" + ] + }, + + "userInteraction": { + "enabled": true, + "description": "사용자 인터랙션 응답 시간", + "thresholds": { + "instant": 100, + "fast": 300, + "acceptable": 1000, + "slow": 3000 + } + } + }, + + "scripts": { + "initPerformanceMonitoring": "(function() { window.__PERF_METRICS__ = { startTime: Date.now(), pageLoads: [], apiCalls: [], interactions: [], resources: [], errors: [] }; window.__PERF_OBSERVER__ = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { if (entry.entryType === 'navigation') { window.__PERF_METRICS__.pageLoads.push({ url: entry.name, domContentLoaded: entry.domContentLoadedEventEnd - entry.startTime, load: entry.loadEventEnd - entry.startTime, dns: entry.domainLookupEnd - entry.domainLookupStart, tcp: entry.connectEnd - entry.connectStart, ttfb: entry.responseStart - entry.requestStart, timestamp: new Date().toISOString() }); } else if (entry.entryType === 'paint') { window.__PERF_METRICS__[entry.name] = entry.startTime; } else if (entry.entryType === 'largest-contentful-paint') { window.__PERF_METRICS__.lcp = entry.startTime; } }); }); try { window.__PERF_OBSERVER__.observe({ entryTypes: ['navigation', 'paint', 'largest-contentful-paint'] }); } catch(e) { console.warn('Performance observer not fully supported'); } return 'Performance monitoring initialized'; })()", + + "getNavigationTiming": "(function() { const perf = performance.getEntriesByType('navigation')[0]; if (!perf) return null; return { url: window.location.href, domContentLoaded: Math.round(perf.domContentLoadedEventEnd - perf.startTime), load: Math.round(perf.loadEventEnd - perf.startTime), domInteractive: Math.round(perf.domInteractive - perf.startTime), dns: Math.round(perf.domainLookupEnd - perf.domainLookupStart), tcp: Math.round(perf.connectEnd - perf.connectStart), ttfb: Math.round(perf.responseStart - perf.requestStart), responseTime: Math.round(perf.responseEnd - perf.responseStart), domProcessing: Math.round(perf.domComplete - perf.domInteractive), transferSize: perf.transferSize, encodedBodySize: perf.encodedBodySize }; })()", + + "getPaintTiming": "(function() { const paints = performance.getEntriesByType('paint'); const result = {}; paints.forEach(p => { result[p.name] = Math.round(p.startTime); }); return result; })()", + + "getLCP": "(function() { return new Promise((resolve) => { let lcp = 0; const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach(entry => { lcp = entry.startTime; }); }); try { observer.observe({ type: 'largest-contentful-paint', buffered: true }); setTimeout(() => { observer.disconnect(); resolve(Math.round(lcp)); }, 1000); } catch(e) { resolve(null); } }); })()", + + "getResourceMetrics": "(function() { const resources = performance.getEntriesByType('resource'); const summary = { total: resources.length, totalSize: 0, byType: {}, slowest: [] }; resources.forEach(r => { const type = r.initiatorType || 'other'; summary.byType[type] = summary.byType[type] || { count: 0, size: 0, totalTime: 0 }; summary.byType[type].count++; summary.byType[type].size += r.transferSize || 0; summary.byType[type].totalTime += r.duration || 0; summary.totalSize += r.transferSize || 0; }); summary.slowest = resources.sort((a, b) => b.duration - a.duration).slice(0, 5).map(r => ({ name: r.name.split('/').pop().substring(0, 30), duration: Math.round(r.duration), size: r.transferSize })); summary.totalSizeKB = Math.round(summary.totalSize / 1024); return summary; })()", + + "getMemoryUsage": "(function() { if (!performance.memory) return { supported: false }; return { supported: true, usedJSHeapSize: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024), totalJSHeapSize: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024), jsHeapSizeLimit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024), usagePercent: Math.round((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100) }; })()", + + "getDOMMetrics": "(function() { return { nodeCount: document.getElementsByTagName('*').length, bodySize: document.body.innerHTML.length, maxDepth: (function getDepth(el) { if (!el.children.length) return 0; return 1 + Math.max(...Array.from(el.children).map(getDepth)); })(document.body), forms: document.forms.length, inputs: document.querySelectorAll('input, textarea, select').length, images: document.images.length, scripts: document.scripts.length, stylesheets: document.styleSheets.length }; })()", + + "measureInteraction": "(async function(action, selector) { const start = performance.now(); try { const el = document.querySelector(selector); if (el) { el.click(); await new Promise(r => setTimeout(r, 100)); } const duration = performance.now() - start; return { action, selector, duration: Math.round(duration), timestamp: new Date().toISOString() }; } catch(e) { return { action, selector, error: e.message }; } })", + + "getFullPerformanceReport": "(function() { const nav = performance.getEntriesByType('navigation')[0]; const paints = {}; performance.getEntriesByType('paint').forEach(p => paints[p.name] = Math.round(p.startTime)); const resources = performance.getEntriesByType('resource'); const apiLogs = window.__API_LOGS__ || []; return { timestamp: new Date().toISOString(), url: window.location.href, navigation: nav ? { domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime), load: Math.round(nav.loadEventEnd - nav.startTime), ttfb: Math.round(nav.responseStart - nav.requestStart), domInteractive: Math.round(nav.domInteractive - nav.startTime) } : null, paint: paints, resources: { count: resources.length, totalSizeKB: Math.round(resources.reduce((sum, r) => sum + (r.transferSize || 0), 0) / 1024) }, api: { totalCalls: apiLogs.length, avgResponseTime: apiLogs.length > 0 ? Math.round(apiLogs.reduce((sum, l) => sum + (l.duration || 0), 0) / apiLogs.length) : 0, slowCalls: apiLogs.filter(l => l.duration > 2000).length }, memory: performance.memory ? { usedMB: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024), usagePercent: Math.round((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100) } : null, dom: { nodeCount: document.getElementsByTagName('*').length } }; })()", + + "getPerformanceSummary": "(function() { const nav = performance.getEntriesByType('navigation')[0]; const apiLogs = window.__API_LOGS__ || []; const thresholds = { pageLoad: { good: 1000, acceptable: 2000, slow: 3000 }, api: { good: 500, acceptable: 1000, slow: 2000 } }; const pageLoadTime = nav ? Math.round(nav.loadEventEnd - nav.startTime) : 0; const avgApiTime = apiLogs.length > 0 ? Math.round(apiLogs.reduce((sum, l) => sum + (l.duration || 0), 0) / apiLogs.length) : 0; const getGrade = (value, th) => { if (value <= th.good) return '🟢 Good'; if (value <= th.acceptable) return '🟡 Acceptable'; if (value <= th.slow) return '🟠 Slow'; return '🔴 Critical'; }; return { pageLoad: { time: pageLoadTime, grade: getGrade(pageLoadTime, thresholds.pageLoad) }, api: { avgTime: avgApiTime, grade: getGrade(avgApiTime, thresholds.api), totalCalls: apiLogs.length, slowCalls: apiLogs.filter(l => l.duration > 2000).length }, overall: pageLoadTime <= 2000 && avgApiTime <= 1000 ? '✅ PASS' : '⚠️ NEEDS ATTENTION' }; })()" + }, + + "thresholds": { + "pageLoad": { + "domContentLoaded": { + "good": 800, + "acceptable": 1500, + "slow": 2500, + "critical": 4000 + }, + "load": { + "good": 1000, + "acceptable": 2000, + "slow": 3000, + "critical": 5000 + }, + "ttfb": { + "good": 200, + "acceptable": 500, + "slow": 1000, + "critical": 2000 + }, + "fcp": { + "good": 1000, + "acceptable": 2000, + "slow": 3000, + "critical": 4000 + }, + "lcp": { + "good": 2500, + "acceptable": 4000, + "slow": 6000, + "critical": 8000 + } + }, + "api": { + "responseTime": { + "fast": 200, + "good": 500, + "acceptable": 1000, + "slow": 2000, + "critical": 5000 + } + }, + "memory": { + "usagePercent": { + "good": 50, + "acceptable": 70, + "warning": 85, + "critical": 95 + } + }, + "dom": { + "nodeCount": { + "good": 1000, + "acceptable": 2000, + "warning": 3000, + "critical": 5000 + } + } + }, + + "grading": { + "levels": { + "excellent": { "symbol": "🟢", "label": "Excellent", "range": "< good" }, + "good": { "symbol": "🟢", "label": "Good", "range": "good ~ acceptable" }, + "acceptable": { "symbol": "🟡", "label": "Acceptable", "range": "acceptable ~ slow" }, + "slow": { "symbol": "🟠", "label": "Slow", "range": "slow ~ critical" }, + "critical": { "symbol": "🔴", "label": "Critical", "range": "> critical" } + }, + "overall": { + "pass": "모든 핵심 지표가 acceptable 이상", + "warning": "일부 지표가 slow", + "fail": "하나 이상의 지표가 critical" + } + }, + + "reporting": { + "includeInTestReport": true, + "separatePerformanceReport": true, + "format": { + "summary": { + "sections": [ + "pageLoadMetrics", + "apiMetrics", + "resourceMetrics", + "memoryMetrics" + ] + }, + "detail": { + "includeRawData": false, + "includeCharts": false, + "includeRecommendations": true + } + }, + "template": { + "header": "## ⚡ 성능 메트릭", + "sections": { + "pageLoad": "### 페이지 로드 성능", + "api": "### API 응답 성능", + "resources": "### 리소스 사용량", + "memory": "### 메모리 사용량", + "recommendations": "### 개선 권장사항" + } + } + }, + + "recommendations": { + "slowPageLoad": [ + "이미지 최적화 (WebP 형식, lazy loading)", + "JavaScript 번들 크기 최적화", + "CSS 최적화 및 Critical CSS 적용", + "서버 응답 시간 개선" + ], + "slowApi": [ + "API 응답 캐싱 적용", + "데이터베이스 쿼리 최적화", + "불필요한 API 호출 제거", + "페이지네이션 적용" + ], + "highMemory": [ + "메모리 누수 확인", + "불필요한 이벤트 리스너 제거", + "대용량 데이터 가상화 적용", + "컴포넌트 언마운트 시 정리" + ], + "tooManyDomNodes": [ + "가상 스크롤 적용", + "조건부 렌더링 최적화", + "불필요한 DOM 요소 제거", + "컴포넌트 분할" + ] + }, + + "hooks": { + "beforeNavigation": { + "description": "페이지 이동 전 성능 데이터 수집 시작", + "actions": ["initPerformanceMonitoring"] + }, + "afterNavigation": { + "description": "페이지 로드 후 성능 데이터 수집", + "actions": ["getNavigationTiming", "getPaintTiming", "getResourceMetrics"], + "delay": 2000 + }, + "afterTest": { + "description": "테스트 종료 후 성능 리포트 생성", + "actions": ["getFullPerformanceReport", "generatePerformanceSection"] + } + }, + + "baseline": { + "description": "성능 기준선 (비교용)", + "values": { + "dashboard": { + "pageLoad": 1500, + "apiCalls": 5, + "avgApiTime": 200 + }, + "listPage": { + "pageLoad": 2000, + "apiCalls": 3, + "avgApiTime": 300 + }, + "formPage": { + "pageLoad": 1200, + "apiCalls": 2, + "avgApiTime": 150 + } + }, + "tolerance": 20 + } +}