학습 목표
- AI 기반 성능 프로파일링과 병목 지점 분석
- 프론트엔드 최적화 (번들 크기, 렌더링, 캐싱)
- 백엔드 최적화 (쿼리, 인덱싱, 캐싱 전략)
- 보안 취약점 자동 스캔과 수정
- OWASP Top 10 대응과 보안 모범 사례
프론트엔드 성능 최적화
Cursor AI는 웹 애플리케이션의 성능을 분석하고, Core Web Vitals를 개선하는 구체적인 최적화 방안을 제시합니다. 번들 크기부터 렌더링 성능까지 종합적으로 개선합니다.
번들 사이즈 최적화
요구사항: React 애플리케이션 번들 최적화
AI에게 요청: "번들 크기를 분석하고 최적화 방안을 제시해줘"
AI가 제안한 Webpack 설정
// webpack.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: {
main: './src/index.tsx',
// 벤더 청크 분리
vendor: ['react', 'react-dom', 'react-router-dom']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? '[name].[contenthash:8].js'
: '[name].js',
chunkFilename: isProduction
? '[name].[contenthash:8].chunk.js'
: '[name].chunk.js',
clean: true,
publicPath: '/'
},
optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
drop_console: isProduction,
drop_debugger: isProduction
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
parallel: true,
}),
new CssMinimizerPlugin(),
],
// 코드 스플리팅 전략
splitChunks: {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
// 벤더 라이브러리
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
return `vendor.${packageName.replace('@', '')}`;
},
priority: 20,
reuseExistingChunk: true,
},
// React 관련
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
name: 'react',
priority: 30,
reuseExistingChunk: true,
},
// UI 라이브러리
ui: {
test: /[\\/]node_modules[\\/](@mui|antd|@ant-design)[\\/]/,
name: 'ui',
priority: 25,
reuseExistingChunk: true,
},
// 공통 모듈
common: {
minChunks: 2,
priority: 10,
name: 'common',
reuseExistingChunk: true,
enforce: true,
},
},
},
// 런타임 청크 분리
runtimeChunk: {
name: 'runtime',
},
// 모듈 ID 최적화
moduleIds: isProduction ? 'deterministic' : 'named',
chunkIds: isProduction ? 'deterministic' : 'named',
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
modules: false,
}],
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
// 동적 import 지원
'@babel/plugin-syntax-dynamic-import',
// 프로덕션에서 PropTypes 제거
isProduction && [
'babel-plugin-transform-react-remove-prop-types',
{ removeImport: true }
],
// lodash 최적화
['babel-plugin-lodash', { id: ['lodash', 'recompose'] }],
].filter(Boolean),
cacheDirectory: true,
},
},
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: isProduction
? '[hash:base64:5]'
: '[path][name]__[local]',
},
sourceMap: !isProduction,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
isProduction && ['cssnano', { preset: 'default' }],
].filter(Boolean),
},
},
},
],
},
],
},
plugins: [
// HTML 생성
new HtmlWebpackPlugin({
template: './public/index.html',
minify: isProduction ? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
} : false,
}),
// CSS 추출
isProduction && new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].chunk.css',
}),
// Gzip 압축
isProduction && new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
// Brotli 압축
isProduction && new CompressionPlugin({
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
filename: '[path][base].br',
}),
// 번들 분석
isProduction && process.env.ANALYZE && new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
}),
// PWA 지원
isProduction && new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true,
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
runtimeCaching: [
{
urlPattern: /^https:\/\/api\./,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 300, // 5분
},
},
},
{
urlPattern: /\.(png|jpg|jpeg|svg|gif|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30일
},
},
},
],
}),
// 환경 변수 정의
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(argv.mode),
}),
].filter(Boolean),
};
};
컴포넌트 지연 로딩
// App.tsx - 라우트 레벨 코드 스플리팅
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import LoadingSpinner from './components/LoadingSpinner';
import ErrorBoundary from './components/ErrorBoundary';
// 지연 로딩 컴포넌트
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')
);
const Profile = lazy(() =>
import(/* webpackChunkName: "profile" */ './pages/Profile')
);
const Settings = lazy(() =>
import(/* webpackChunkName: "settings" */ './pages/Settings')
);
// 프리로드 함수
const preloadComponent = (component: () => Promise) => {
component();
};
function App() {
// 마우스 호버 시 프리로드
const handleMouseEnter = (component: () => Promise) => {
preloadComponent(component);
};
return (
}>
} />
} />
} />
} />
);
}
// 컴포넌트 레벨 지연 로딩
const HeavyComponent = lazy(() =>
import(/* webpackChunkName: "heavy-component" */ './components/HeavyComponent')
);
// 조건부 렌더링과 함께 사용
function ConditionalComponent({ shouldLoad }) {
const [showHeavy, setShowHeavy] = useState(false);
return (
{showHeavy && (
Loading component... }>
)}
성능 모니터링 코드
// utils/performance.ts
class PerformanceMonitor {
private observer: PerformanceObserver | null = null;
private metrics: Map = new Map();
constructor() {
this.initializeObserver();
this.measureWebVitals();
}
private initializeObserver() {
if ('PerformanceObserver' in window) {
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.processEntry(entry);
}
});
// 다양한 성능 메트릭 관찰
try {
this.observer.observe({
entryTypes: ['measure', 'navigation', 'resource', 'paint', 'largest-contentful-paint']
});
} catch (e) {
// 일부 엔트리 타입은 지원되지 않을 수 있음
console.warn('Some performance entry types not supported', e);
}
}
}
private processEntry(entry: PerformanceEntry) {
const metricName = entry.name;
const duration = entry.duration || 0;
if (!this.metrics.has(metricName)) {
this.metrics.set(metricName, []);
}
this.metrics.get(metricName)!.push(duration);
// 실시간 모니터링 데이터 전송
if (this.shouldReport(metricName)) {
this.reportMetric(metricName, duration);
}
}
private measureWebVitals() {
// First Contentful Paint (FCP)
new PerformanceObserver((list) => {
const fcp = list.getEntries()[0];
this.reportWebVital('FCP', fcp.startTime);
}).observe({ entryTypes: ['paint'] });
// Largest Contentful Paint (LCP)
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1];
this.reportWebVital('LCP', lcp.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
// First Input Delay (FID)
new PerformanceObserver((list) => {
const fid = list.getEntries()[0];
this.reportWebVital('FID', fid.processingStart - fid.startTime);
}).observe({ entryTypes: ['first-input'] });
// Cumulative Layout Shift (CLS)
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
this.reportWebVital('CLS', clsValue);
}
}
}).observe({ entryTypes: ['layout-shift'] });
}
private reportWebVital(name: string, value: number) {
// Analytics로 전송
if (window.gtag) {
gtag('event', name, {
value: Math.round(name === 'CLS' ? value * 1000 : value),
metric_value: value,
metric_delta: value,
event_category: 'Web Vitals',
});
}
// 커스텀 모니터링 서비스로 전송
this.sendToMonitoring({
metric: name,
value: value,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
});
}
private sendToMonitoring(data: any) {
// 비동기로 모니터링 엔드포인트에 전송
if ('sendBeacon' in navigator) {
navigator.sendBeacon('/api/metrics', JSON.stringify(data));
} else {
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(data),
keepalive: true,
}).catch(() => {});
}
}
// 커스텀 성능 측정
startMeasure(name: string) {
performance.mark(`${name}-start`);
}
endMeasure(name: string) {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
}
// 리소스 타이밍 분석
analyzeResources() {
const resources = performance.getEntriesByType('resource');
const analysis = {
total: resources.length,
byType: {} as Record,
slowest: [] as any[],
totalSize: 0,
};
resources.forEach(resource => {
const type = this.getResourceType(resource.name);
analysis.byType[type] = (analysis.byType[type] || 0) + 1;
if (resource.duration > 1000) {
analysis.slowest.push({
name: resource.name,
duration: resource.duration,
size: resource.transferSize || 0,
});
}
analysis.totalSize += resource.transferSize || 0;
});
analysis.slowest.sort((a, b) => b.duration - a.duration);
return analysis;
}
private getResourceType(url: string): string {
if (url.includes('.js')) return 'js';
if (url.includes('.css')) return 'css';
if (/\.(png|jpg|jpeg|gif|svg|webp)/.test(url)) return 'image';
if (/\.(woff|woff2|ttf|eot)/.test(url)) return 'font';
return 'other';
}
private shouldReport(metricName: string): boolean {
// 중요한 메트릭만 실시간 리포트
const importantMetrics = ['FCP', 'LCP', 'FID', 'CLS', 'TTFB'];
return importantMetrics.includes(metricName);
}
}
export const performanceMonitor = new PerformanceMonitor();