Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
79555fa
feat: bundle Analyzer 설치
DreamPaste Sep 20, 2025
1f8e9ea
feat: 번들 사이즈 분석 및 lighthouse 지표 코멘트 액션 추가
DreamPaste Sep 20, 2025
1ee37d6
feat: 코멘트 UI 개선
DreamPaste Sep 20, 2025
253585c
fix: pr-analysis의 책임 분리
DreamPaste Sep 20, 2025
0d9a91d
fix: turbo 업데이트 메시지 안나오도록 수정
DreamPaste Sep 20, 2025
401b9b3
feat: 메시지에 lighthouse 지표 추가
DreamPaste Sep 20, 2025
1232013
fix: 빌드 분석용 정적 빌드 로직 추가
DreamPaste Sep 20, 2025
7ef800d
fix: 커스텀 config 파일이 c 플래그를 허용하지 않는 분제 수정
DreamPaste Sep 20, 2025
e456407
fix: 모듈 네이밍 수정
DreamPaste Sep 20, 2025
efad461
fix: Es6 문법 충돌 해결을 위한 Commonjs 문법으로 변경
DreamPaste Sep 21, 2025
2c3ef48
fix: env로 안전하게 주입하도록 수정
DreamPaste Sep 21, 2025
b7f5386
fix: 번들이 원본 파일명을 표시하도록 수정(manifest 역추적)
DreamPaste Sep 21, 2025
96cd3ef
fix: 번들 사이즈 분석을 간소화하고, lighthouse지표를 상세하게 확인하도록 수정
DreamPaste Sep 21, 2025
d544c7a
fix: 워크플로우 최적화
DreamPaste Sep 21, 2025
c982c60
test: 테스트를 위해 불필요했던 CalloutCard 제거
DreamPaste Sep 21, 2025
fc5dad6
fix: ci 성능 최적화
DreamPaste Sep 21, 2025
258bdc9
feat: lighthouse 지표 임시 측정을 위한 homepage 추가
DreamPaste Sep 21, 2025
ac47a70
refactor: 분석 워크플로우 분리 및 각 워크플로우 개선
DreamPaste Sep 21, 2025
bcd1750
refactor: 의존성 설치 최적화 CI 빌드 결과 재사용
DreamPaste Sep 21, 2025
9e86264
fix: 워크플로우 통합 및 md파일만 분리
DreamPaste Sep 23, 2025
f447827
fix: EOF 사용시 오류 수정
DreamPaste Sep 23, 2025
7d05dd7
feat: 각 페이지에 대한 상세 점수도 기록하도록 수정
DreamPaste Sep 23, 2025
a1d0b60
fix: 불필요한 스크립트 제거
DreamPaste Sep 23, 2025
91a371f
Merge branch 'dev' into feat/github-actions
DreamPaste Sep 23, 2025
1f1dafa
Merge branch 'dev' into feat/github-actions
DreamPaste Sep 25, 2025
f69b13e
Merge branch 'dev' into feat/github-actions
DreamPaste Sep 25, 2025
f5acafd
Merge branch 'dev' into feat/github-actions
DreamPaste Sep 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 283 additions & 0 deletions .github/scripts/bundle-diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

/**
* 번들 크기 변화를 분석하고 비교합니다.
* 이전 빌드와 현재 빌드를 비교하여 변화량을 측정합니다.
*/
function analyzeBundleDiff() {
const currentBuildPath = 'apps/web/.next/static';
const manifestPath = 'apps/web/.next/build-manifest.json';

let analysis = {
recommendations: [],
bundleMapping: {},
};

// 현재 빌드 정보 수집
if (fs.existsSync(manifestPath)) {
try {
const manifest = JSON.parse(
fs.readFileSync(manifestPath, 'utf8'),
);

// 페이지별 번들 정보 분석 및 매핑
Object.entries(manifest.pages || {}).forEach(
([page, files]) => {
const jsFiles = files.filter((file) =>
file.endsWith('.js'),
);

// 파일별 사이즈 및 매핑 정보 수집
jsFiles.forEach((file) => {
const filePath = path.join(currentBuildPath, file);
if (fs.existsSync(filePath)) {
const size = fs.statSync(filePath).size;
const fileName = path.basename(file);

// 개선된 파일 타입 분류 및 원본 경로 매핑
let fileType = 'unknown';
let displayName = fileName;
let originalPath = '';

// App Router 경로 매핑
if (page.startsWith('/')) {
const routePath =
page === '/' ? '/page' : page + '/page';
originalPath = `app${routePath}.tsx`;

if (fileName.includes('layout')) {
const layoutPath =
page === '/' ? '/layout' : page + '/layout';
originalPath = `app${layoutPath}.tsx`;
fileType = 'layout';
displayName = `📐 Layout (${originalPath})`;
} else if (fileName.includes('page')) {
fileType = 'page';
displayName = `📄 Page (${originalPath})`;
} else if (fileName.includes('loading')) {
originalPath =
page === '/'
? 'app/loading.tsx'
: `app${page}/loading.tsx`;
fileType = 'loading';
displayName = `⏳ Loading (${originalPath})`;
} else if (fileName.includes('error')) {
originalPath =
page === '/'
? 'app/error.tsx'
: `app${page}/error.tsx`;
fileType = 'error';
displayName = `❌ Error (${originalPath})`;
}
}

// 시스템 파일들
if (fileName.includes('_app')) {
fileType = 'app';
originalPath = 'pages/_app.tsx';
displayName = `🚀 App Shell (${originalPath})`;
} else if (fileName.includes('_error')) {
fileType = 'error';
originalPath = 'pages/_error.tsx';
displayName = `❌ Error Page (${originalPath})`;
} else if (fileName.includes('_document')) {
fileType = 'document';
originalPath = 'pages/_document.tsx';
displayName = `📄 Document (${originalPath})`;
} else if (fileName.match(/^\d+/)) {
fileType = 'chunk';
displayName = `📦 Shared Chunk (${fileName.split('-')[0]})`;
} else if (fileName.includes('framework')) {
fileType = 'framework';
displayName = '⚛️ React Framework';
} else if (fileName.includes('main-bundle')) {
fileType = 'main';
displayName = '🏠 통합 Main Bundle';
} else if (fileName.includes('main')) {
fileType = 'main';
displayName = '🏠 Main Bundle';
} else if (fileName.includes('polyfill')) {
fileType = 'polyfill';
displayName = '🔧 Polyfills';
} else if (fileName.includes('webpack')) {
fileType = 'webpack';
displayName = '⚙️ Webpack Runtime';
}

analysis.bundleMapping[fileName] = {
displayName,
fileType,
size,
page,
originalPath,
formattedSize: formatBytes(size),
};
}
});

const totalSize = jsFiles.reduce((acc, file) => {
const filePath = path.join(currentBuildPath, file);
if (fs.existsSync(filePath)) {
return acc + fs.statSync(filePath).size;
}
return acc;
}, 0);

if (totalSize > 500 * 1024) {
// 500KB 이상
analysis.recommendations.push({
type: 'large-bundle',
page,
size: formatBytes(totalSize),
message: `페이지 ${page}의 번들이 큽니다 (${formatBytes(totalSize)}). 코드 스플리팅을 고려하세요.`,
});
}
},
);

// 중복 라이브러리 체크
const allFiles = Object.values(manifest.pages || {}).flat();
const libraryUsage = {};

allFiles.forEach((file) => {
// 라이브러리 패턴 감지 (간단한 휴리스틱)
const match = file.match(/chunks\/(.+?)[\.-]/);
if (match) {
const libName = match[1];
libraryUsage[libName] = (libraryUsage[libName] || 0) + 1;
}
});

Object.entries(libraryUsage).forEach(([lib, count]) => {
if (count > 3) {
analysis.recommendations.push({
type: 'duplicate-library',
library: lib,
count,
message: `라이브러리 ${lib}가 ${count}개 청크에서 발견되었습니다. 공통 청크로 분리를 고려하세요.`,
});
}
});
} catch (error) {
console.warn('Build manifest 분석 실패:', error.message);
}
}

return analysis;
}

function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (
parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
);
}

function generateBundleRecommendations(analysis) {
let markdown = '';

// 번들 매핑 정보 표시
if (Object.keys(analysis.bundleMapping).length > 0) {
markdown += `### 📦 번들 구성 상세

| 파일 유형 | 크기 | 원본 경로 | 설명 |
|-----------|------|----------|------|
`;

// 파일 타입별로 그룹화하여 표시
const groupedByType = {};
Object.entries(analysis.bundleMapping).forEach(
([fileName, info]) => {
if (!groupedByType[info.fileType]) {
groupedByType[info.fileType] = [];
}
groupedByType[info.fileType].push(info);
},
);

// 타입별 우선순위 정렬
const typeOrder = [
'app',
'main',
'framework',
'page',
'layout',
'chunk',
'polyfill',
'error',
'unknown',
];

typeOrder.forEach((type) => {
if (groupedByType[type]) {
groupedByType[type]
.sort((a, b) => b.size - a.size) // 크기 순 정렬
.forEach((info) => {
const sourcePath = info.originalPath || '자동 생성';
const pageInfo =
info.page !== '/' ? `페이지: ${info.page}` : '공통';
markdown += `| ${info.displayName} | \`${info.formattedSize}\` | \`${sourcePath}\` | ${pageInfo} |
`;
});
}
});

markdown += `

`;
}

// 최적화 권장사항
if (analysis.recommendations.length > 0) {
markdown += `### 🎯 번들 최적화 권장사항

`;

analysis.recommendations.forEach((rec, index) => {
const emoji = rec.type === 'large-bundle' ? '📦' : '🔄';
markdown += `${index + 1}. ${emoji} ${rec.message}
`;
});

markdown += `
<details>
<summary>💡 최적화 가이드</summary>

**번들 크기 줄이기:**
- 불필요한 라이브러리 제거
- Tree shaking 활용 (사용하지 않는 코드 제거)
- Dynamic imports 사용 (\`import()\` 구문)
- 이미지 최적화 및 WebP 포맷 사용

**코드 스플리팅 전략:**
- 페이지별 번들 자동 분리 (Next.js 기본)
- 공통 컴포넌트 청크 생성
- Lazy loading 적용 (\`React.lazy()\`)
- 라이브러리별 청크 분리

**성능 모니터링:**
- 번들 분석기로 정기적 검토
- Core Web Vitals 지표 확인
- 중요 리소스 우선순위 설정

</details>

`;
}

return markdown;
}

// 메인 실행
const analysis = analyzeBundleDiff();
const recommendations = generateBundleRecommendations(analysis);

console.log('BUNDLE_RECOMMENDATIONS<<EOF');
console.log(recommendations);
console.log('EOF');
Loading