✨ 주요 개선사항: - any 타입 83개에서 62개로 21개 수정 (25% 감소) - 모든 ESLint 에러 11개 → 0개 완전 해결 - 타입 안전성 대폭 향상으로 런타임 오류 가능성 감소 🔧 수정된 파일들: • PWADebug.tsx - 사용하지 않는 import들에 _ prefix 추가 • categoryUtils.ts - 불필요한 any 캐스트 제거 • TransactionsHeader.tsx - BudgetData 인터페이스 정의 • storageUtils.ts - generic 타입과 unknown 타입 적용 • 각종 error handler들 - Error | {message?: string} 타입 적용 • test 파일들 - 적절한 mock 인터페이스 정의 • 유틸리티 파일들 - any → unknown 또는 적절한 타입으로 교체 🏆 성과: - 코드 품질 크게 향상 (280 → 80 문제로 71% 감소) - TypeScript 컴파일러의 타입 체크 효과성 증대 - 개발자 경험 개선 (IDE 자동완성, 타입 추론 등) 🧹 추가 정리: - ESLint no-console/no-alert 경고 해결 - Prettier 포맷팅 적용으로 코드 스타일 통일 🎯 다음 단계: 남은 62개 any 타입 계속 개선 예정 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
311 lines
9.6 KiB
JavaScript
311 lines
9.6 KiB
JavaScript
/**
|
||
* Playwright를 사용한 ChunkLoadError 상세 분석 스크립트
|
||
*
|
||
* 브라우저를 자동화하여 콘솔 로그, 네트워크 요청, 오류를 상세히 캡처합니다.
|
||
*/
|
||
|
||
const { chromium } = require("playwright");
|
||
|
||
async function analyzeChunkLoadError() {
|
||
console.log("🔍 ChunkLoadError 상세 분석 시작...\n");
|
||
|
||
const browser = await chromium.launch({
|
||
headless: false, // 실제 브라우저 창을 열어서 시각적으로 확인
|
||
devtools: true, // 개발자 도구 자동 열기
|
||
args: [
|
||
"--disable-web-security",
|
||
"--disable-features=VizDisplayCompositor",
|
||
"--no-sandbox",
|
||
"--disable-dev-shm-usage",
|
||
],
|
||
});
|
||
|
||
const context = await browser.newContext({
|
||
viewport: { width: 1280, height: 720 },
|
||
// 캐시 비활성화로 항상 최신 리소스 요청
|
||
ignoreHTTPSErrors: true,
|
||
});
|
||
|
||
const page = await context.newPage();
|
||
|
||
// 이벤트 수집 배열
|
||
const consoleMessages = [];
|
||
const networkRequests = [];
|
||
const networkFailures = [];
|
||
const errors = [];
|
||
const chunkErrors = [];
|
||
|
||
// 콘솔 메시지 캡처
|
||
page.on("console", (msg) => {
|
||
const message = {
|
||
type: msg.type(),
|
||
text: msg.text(),
|
||
location: msg.location(),
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
consoleMessages.push(message);
|
||
|
||
// 실시간 콘솔 출력
|
||
const prefix =
|
||
{
|
||
error: "❌",
|
||
warning: "⚠️ ",
|
||
info: "ℹ️ ",
|
||
log: "📝",
|
||
}[msg.type()] || "📄";
|
||
|
||
console.log(`${prefix} [CONSOLE] ${msg.text()}`);
|
||
});
|
||
|
||
// 네트워크 요청 모니터링
|
||
page.on("request", (request) => {
|
||
const requestInfo = {
|
||
url: request.url(),
|
||
method: request.method(),
|
||
headers: request.headers(),
|
||
timestamp: new Date().toISOString(),
|
||
resourceType: request.resourceType(),
|
||
};
|
||
networkRequests.push(requestInfo);
|
||
|
||
// Clerk 관련 요청만 출력
|
||
if (request.url().includes("clerk")) {
|
||
console.log(`🌐 [REQUEST] ${request.method()} ${request.url()}`);
|
||
}
|
||
});
|
||
|
||
// 네트워크 응답 모니터링
|
||
page.on("response", (response) => {
|
||
const isClerkRelated = response.url().includes("clerk");
|
||
const status = response.status();
|
||
|
||
if (isClerkRelated) {
|
||
const statusIcon = status >= 400 ? "🔴" : status >= 300 ? "🟡" : "🟢";
|
||
console.log(`${statusIcon} [RESPONSE] ${status} ${response.url()}`);
|
||
}
|
||
|
||
// 실패한 응답 기록
|
||
if (status >= 400) {
|
||
networkFailures.push({
|
||
url: response.url(),
|
||
status: status,
|
||
statusText: response.statusText(),
|
||
headers: response.headers(),
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
});
|
||
|
||
// 페이지 오류 캡처
|
||
page.on("pageerror", (error) => {
|
||
const errorInfo = {
|
||
name: error.name,
|
||
message: error.message,
|
||
stack: error.stack,
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
errors.push(errorInfo);
|
||
|
||
// ChunkLoadError 특별 처리
|
||
if (
|
||
error.message.includes("Loading chunk") ||
|
||
error.name === "ChunkLoadError"
|
||
) {
|
||
chunkErrors.push(errorInfo);
|
||
console.log(`💥 [CHUNK ERROR] ${error.message}`);
|
||
console.log(` Stack: ${error.stack?.split("\n")[0]}`);
|
||
} else {
|
||
console.log(`❌ [PAGE ERROR] ${error.name}: ${error.message}`);
|
||
}
|
||
});
|
||
|
||
// 네트워크 실패 처리
|
||
page.on("requestfailed", (request) => {
|
||
const failure = {
|
||
url: request.url(),
|
||
method: request.method(),
|
||
failure: request.failure()?.errorText,
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
networkFailures.push(failure);
|
||
|
||
if (request.url().includes("clerk")) {
|
||
console.log(`💔 [REQUEST FAILED] ${request.url()}`);
|
||
console.log(` Error: ${request.failure()?.errorText}`);
|
||
}
|
||
});
|
||
|
||
try {
|
||
console.log("🚀 페이지 로딩 시작...");
|
||
|
||
// 무한 새로고침 감지를 위한 네비게이션 카운터
|
||
let navigationCount = 0;
|
||
page.on("framenavigated", () => {
|
||
navigationCount++;
|
||
console.log(`🔄 [NAVIGATION] 페이지 네비게이션 ${navigationCount}회`);
|
||
|
||
if (navigationCount > 3) {
|
||
console.log("⚠️ 무한 새로고침 감지됨!");
|
||
}
|
||
});
|
||
|
||
// 페이지 로드 (타임아웃 30초로 단축)
|
||
await page.goto("http://localhost:3002", {
|
||
waitUntil: "domcontentloaded", // networkidle 대신 domcontentloaded 사용
|
||
timeout: 30000,
|
||
});
|
||
|
||
console.log("✅ 페이지 로드 완료. 10초 대기 중...");
|
||
|
||
// 무한 새로고침 체크를 위해 10초 대기
|
||
await page.waitForTimeout(10000);
|
||
|
||
console.log(`📊 총 네비게이션 횟수: ${navigationCount}회`);
|
||
|
||
if (navigationCount > 3) {
|
||
console.log("❌ 무한 새로고침 문제 발생");
|
||
chunkErrors.push({
|
||
name: "InfiniteRefresh",
|
||
message: `무한 새로고침 감지: ${navigationCount}회 네비게이션`,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
// JavaScript 실행 상태 확인
|
||
const isJSWorking = await page.evaluate(() => {
|
||
return (
|
||
typeof window.React !== "undefined" ||
|
||
document.querySelector("[data-reactroot]") !== null ||
|
||
document.querySelector("#root > *") !== null
|
||
);
|
||
});
|
||
|
||
console.log(`🔧 JavaScript 실행 상태: ${isJSWorking ? "정상" : "실패"}`);
|
||
|
||
// Clerk 로딩 상태 확인
|
||
const clerkStatus = await page.evaluate(() => {
|
||
return {
|
||
clerkLoaded: typeof window.Clerk !== "undefined",
|
||
clerkProviderExists:
|
||
document.querySelector("[data-clerk-provider]") !== null,
|
||
clerkErrors: window.sessionStorage?.getItem("chunkLoadErrorMaxRetries"),
|
||
skipClerk: window.sessionStorage?.getItem("skipClerk"),
|
||
};
|
||
});
|
||
|
||
console.log("🔐 Clerk 상태:", JSON.stringify(clerkStatus, null, 2));
|
||
|
||
// 페이지 내용 확인
|
||
const pageContent = await page.evaluate(() => ({
|
||
title: document.title,
|
||
hasContent: document.body.children.length > 1,
|
||
rootContent: document.getElementById("root")?.children.length || 0,
|
||
errorMessages: Array.from(document.querySelectorAll("*"))
|
||
.filter(
|
||
(el) =>
|
||
el.textContent?.includes("ChunkLoadError") ||
|
||
el.textContent?.includes("Loading chunk") ||
|
||
el.textContent?.includes("오류")
|
||
)
|
||
.map((el) => el.textContent?.substring(0, 100)),
|
||
}));
|
||
|
||
console.log("📄 페이지 내용 분석:", JSON.stringify(pageContent, null, 2));
|
||
|
||
// 추가로 10초 더 대기하여 지연된 오류 캡처
|
||
console.log("⏳ 추가 10초 대기 중 (지연된 오류 캡처)...");
|
||
await page.waitForTimeout(10000);
|
||
} catch (error) {
|
||
console.log(`💥 페이지 로드 실패: ${error.message}`);
|
||
}
|
||
|
||
// 결과 분석 및 출력
|
||
console.log("\n" + "=".repeat(80));
|
||
console.log("📊 분석 결과 요약");
|
||
console.log("=".repeat(80));
|
||
|
||
console.log(`\n🔢 통계:`);
|
||
console.log(` 콘솔 메시지: ${consoleMessages.length}개`);
|
||
console.log(` 네트워크 요청: ${networkRequests.length}개`);
|
||
console.log(` 네트워크 실패: ${networkFailures.length}개`);
|
||
console.log(` 페이지 오류: ${errors.length}개`);
|
||
console.log(` 청크 오류: ${chunkErrors.length}개`);
|
||
|
||
if (chunkErrors.length > 0) {
|
||
console.log(`\n💥 ChunkLoadError 상세 정보:`);
|
||
chunkErrors.forEach((error, index) => {
|
||
console.log(` ${index + 1}. ${error.message}`);
|
||
console.log(` 시간: ${error.timestamp}`);
|
||
if (error.stack) {
|
||
console.log(
|
||
` 스택: ${error.stack.split("\n").slice(0, 3).join("\n ")}`
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (networkFailures.filter((f) => f.url?.includes("clerk")).length > 0) {
|
||
console.log(`\n💔 Clerk 관련 네트워크 실패:`);
|
||
networkFailures
|
||
.filter((f) => f.url?.includes("clerk"))
|
||
.forEach((failure, index) => {
|
||
console.log(` ${index + 1}. ${failure.url}`);
|
||
console.log(` 오류: ${failure.failure || failure.status}`);
|
||
console.log(` 시간: ${failure.timestamp}`);
|
||
});
|
||
}
|
||
|
||
// Clerk 관련 요청 분석
|
||
const clerkRequests = networkRequests.filter((req) =>
|
||
req.url.includes("clerk")
|
||
);
|
||
if (clerkRequests.length > 0) {
|
||
console.log(`\n🔐 Clerk 관련 요청 (${clerkRequests.length}개):`);
|
||
clerkRequests.forEach((req, index) => {
|
||
console.log(` ${index + 1}. ${req.method} ${req.url}`);
|
||
});
|
||
}
|
||
|
||
// 오류가 있는 콘솔 메시지
|
||
const errorMessages = consoleMessages.filter((msg) => msg.type === "error");
|
||
if (errorMessages.length > 0) {
|
||
console.log(`\n❌ 콘솔 오류 메시지 (${errorMessages.length}개):`);
|
||
errorMessages.forEach((msg, index) => {
|
||
console.log(` ${index + 1}. ${msg.text}`);
|
||
if (msg.location?.url) {
|
||
console.log(
|
||
` 위치: ${msg.location.url}:${msg.location.lineNumber}`
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 브라우저를 5초 더 열어둔 후 종료
|
||
console.log(
|
||
"\n🔍 5초 후 브라우저를 닫습니다. 직접 확인하고 싶다면 Ctrl+C로 중단하세요."
|
||
);
|
||
await page.waitForTimeout(5000);
|
||
|
||
await browser.close();
|
||
|
||
console.log("\n✅ 분석 완료!");
|
||
|
||
// ChunkLoadError 해결 제안
|
||
if (chunkErrors.length > 0) {
|
||
console.log("\n💡 ChunkLoadError 해결 제안:");
|
||
console.log(" 1. 개발 서버 재시작: npm run dev");
|
||
console.log(" 2. node_modules/.vite 캐시 삭제");
|
||
console.log(" 3. 브라우저 하드 새로고침: Ctrl+Shift+R");
|
||
console.log(
|
||
" 4. Clerk 설정 확인: .env 파일의 VITE_CLERK_PUBLISHABLE_KEY"
|
||
);
|
||
}
|
||
}
|
||
|
||
// 스크립트 실행
|
||
if (require.main === module) {
|
||
analyzeChunkLoadError().catch(console.error);
|
||
}
|
||
|
||
module.exports = { analyzeChunkLoadError };
|