- @clerk/clerk-react 패키지 설치 추가 - Vite external 설정에서 Clerk 번들링 허용으로 변경 - ChunkLoadError 복구 시스템 Playwright 테스트 추가 - Clerk CDN 실패 시나리오 검증 및 Mock 인증 폴백 시스템 확인 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
250 lines
7.8 KiB
TypeScript
250 lines
7.8 KiB
TypeScript
import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react-swc";
|
|
import path from "path";
|
|
import { visualizer } from "rollup-plugin-visualizer";
|
|
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
|
|
|
// https://vitejs.dev/config/
|
|
export default defineConfig(({ mode }) => ({
|
|
server: {
|
|
host: "0.0.0.0",
|
|
port: 3000,
|
|
cors: true,
|
|
headers: {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
},
|
|
},
|
|
plugins: [
|
|
react(),
|
|
visualizer({
|
|
filename: "dist/stats.html",
|
|
open: false,
|
|
gzipSize: true,
|
|
brotliSize: true,
|
|
}),
|
|
// Sentry 플러그인 (프로덕션 빌드 + Auth Token 있을 때만 활성화)
|
|
mode === "production" &&
|
|
process.env.SENTRY_AUTH_TOKEN &&
|
|
process.env.SENTRY_DISABLE_SOURCEMAPS !== "true" &&
|
|
sentryVitePlugin({
|
|
org: process.env.SENTRY_ORG,
|
|
project: process.env.SENTRY_PROJECT,
|
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
|
|
// 소스맵 업로드 설정
|
|
sourcemaps: {
|
|
// 빌드된 모든 JS, CSS 파일의 소스맵 업로드
|
|
assets: ["./dist/**/*.js", "./dist/**/*.css"],
|
|
// 업로드 후 로컬 소스맵 파일 삭제 (보안)
|
|
filesToDeleteAfterUpload: ["./dist/**/*.map"],
|
|
// 소스맵 업로드 재시도 설정
|
|
rewrite: true,
|
|
// 업로드 진행상황 표시
|
|
verbose: true,
|
|
},
|
|
|
|
// 릴리즈 설정
|
|
release: {
|
|
name:
|
|
process.env.SENTRY_RELEASE ||
|
|
`zellyy-finance@${new Date().toISOString().slice(0, 19)}`,
|
|
// 릴리즈에 Git 커밋 정보 자동 첨부
|
|
setCommits: {
|
|
auto: true,
|
|
ignoreMissing: true,
|
|
},
|
|
// 배포 환경 정보
|
|
deploy: {
|
|
env: mode,
|
|
name: `${mode}-deployment`,
|
|
url: process.env.VERCEL_URL || process.env.DEPLOY_URL,
|
|
},
|
|
// 릴리즈 완료 처리
|
|
finalize: true,
|
|
},
|
|
|
|
// 에러 리포팅 설정
|
|
errorHandler: (err) => {
|
|
console.warn("Sentry 소스맵 업로드 실패:", err.message);
|
|
// 빌드는 계속 진행 (소스맵 업로드 실패로 빌드가 중단되지 않도록)
|
|
return false;
|
|
},
|
|
|
|
// 텔레메트리 비활성화
|
|
telemetry: false,
|
|
|
|
// 디버그 모드 (개발자가 문제를 진단할 수 있도록)
|
|
debug: process.env.SENTRY_DEBUG === "true",
|
|
|
|
// 업로드 타임아웃 설정 (네트워크 상황에 따라 조정)
|
|
uploadTimeout: 120000, // 2분
|
|
}),
|
|
].filter(Boolean),
|
|
resolve: {
|
|
alias: {
|
|
"@": path.resolve(__dirname, "./src"),
|
|
},
|
|
},
|
|
build: {
|
|
// 소스맵 생성 설정 (프로덕션에서도 에러 디버깅을 위해 생성)
|
|
sourcemap: mode === "production" ? "hidden" : true,
|
|
|
|
// 고급 Tree shaking 최적화
|
|
treeshake: {
|
|
// 모듈 사이드 이펙트 제거 (CSS imports 제외)
|
|
moduleSideEffects: (id) => {
|
|
return (
|
|
id.includes(".css") || id.includes(".scss") || id.includes(".less")
|
|
);
|
|
},
|
|
// 프로퍼티 읽기 사이드 이펙트 제거
|
|
propertyReadSideEffects: false,
|
|
// 사용하지 않는 클래스 및 함수 제거
|
|
tryCatchDeoptimization: false,
|
|
// 알려진 글로벌 사이드 이펙트 제거
|
|
unknownGlobalSideEffects: false,
|
|
},
|
|
|
|
// 청크 로딩 실패에 대한 재시도 설정
|
|
target: "esnext",
|
|
rollupOptions: {
|
|
// 외부 종속성 명시적 처리 (CDN 오류 방지)
|
|
external: (id) => {
|
|
// 빌드 시 @clerk 모듈을 정상적으로 번들에 포함시키고,
|
|
// 런타임에서만 조건부로 비활성화
|
|
return false;
|
|
},
|
|
output: {
|
|
// 청크 파일명 일관성 보장 (ChunkLoadError 방지)
|
|
chunkFileNames: "assets/[name]-[hash].js",
|
|
entryFileNames: "assets/[name]-[hash].js",
|
|
assetFileNames: "assets/[name]-[hash].[ext]",
|
|
|
|
// 청크 로딩 실패 시 재시도 로직 추가
|
|
intro: `
|
|
window.__vitePreloadOriginal = window.__vitePreload;
|
|
window.__vitePreload = function(baseModule, deps) {
|
|
return window.__vitePreloadOriginal(baseModule, deps).catch(err => {
|
|
console.warn('Chunk loading failed, retrying...', err);
|
|
// 청크 오류 처리 시스템이 이미 활성화되어 있으므로 에러를 다시 던짐
|
|
throw err;
|
|
});
|
|
};
|
|
`,
|
|
|
|
manualChunks: (id) => {
|
|
// 노드 모듈들을 카테고리별로 분할
|
|
if (id.includes("node_modules")) {
|
|
// 핵심 React 라이브러리
|
|
if (id.includes("react") || id.includes("react-dom")) {
|
|
return "vendor-react";
|
|
}
|
|
|
|
// 라우팅
|
|
if (id.includes("react-router")) {
|
|
return "vendor-router";
|
|
}
|
|
|
|
// UI 컴포넌트 라이브러리 (Radix UI)
|
|
if (id.includes("@radix-ui")) {
|
|
return "vendor-ui";
|
|
}
|
|
|
|
// 차트 라이브러리 (Recharts - 가장 큰 청크)
|
|
if (id.includes("recharts")) {
|
|
return "vendor-charts";
|
|
}
|
|
|
|
// 상태 관리 및 쿼리
|
|
if (
|
|
id.includes("@tanstack/react-query") ||
|
|
id.includes("zustand")
|
|
) {
|
|
return "vendor-state";
|
|
}
|
|
|
|
// 인증 및 백엔드
|
|
if (id.includes("@clerk") || id.includes("@supabase")) {
|
|
return "vendor-auth";
|
|
}
|
|
|
|
// 모니터링 및 에러 추적
|
|
if (id.includes("@sentry")) {
|
|
return "vendor-sentry";
|
|
}
|
|
|
|
// 유틸리티 라이브러리
|
|
if (
|
|
id.includes("date-fns") ||
|
|
id.includes("clsx") ||
|
|
id.includes("tailwind-merge")
|
|
) {
|
|
return "vendor-utils";
|
|
}
|
|
|
|
// 폼 관련
|
|
if (
|
|
id.includes("react-hook-form") ||
|
|
id.includes("@hookform") ||
|
|
id.includes("zod")
|
|
) {
|
|
return "vendor-forms";
|
|
}
|
|
|
|
// 나머지 모든 노드 모듈들
|
|
return "vendor-misc";
|
|
}
|
|
|
|
// 소스 코드를 기능별로 분할
|
|
if (id.includes("/src/")) {
|
|
// 페이지 컴포넌트들
|
|
if (id.includes("/pages/")) {
|
|
return "pages";
|
|
}
|
|
|
|
// 차트 및 분석 관련 컴포넌트
|
|
if (id.includes("/analytics/") || id.includes("Chart")) {
|
|
return "analytics";
|
|
}
|
|
|
|
// 거래 관련 컴포넌트
|
|
if (id.includes("/transaction/") || id.includes("Transaction")) {
|
|
return "transactions";
|
|
}
|
|
|
|
// 예산 관련 컴포넌트
|
|
if (id.includes("/budget/") || id.includes("Budget")) {
|
|
return "budget";
|
|
}
|
|
|
|
// 인증 관련 컴포넌트
|
|
if (id.includes("/auth/") || id.includes("Auth")) {
|
|
return "auth";
|
|
}
|
|
|
|
// UI 컴포넌트
|
|
if (id.includes("/ui/")) {
|
|
return "ui-components";
|
|
}
|
|
|
|
// 훅 및 유틸리티
|
|
if (
|
|
id.includes("/hooks/") ||
|
|
id.includes("/utils/") ||
|
|
id.includes("/lib/")
|
|
) {
|
|
return "core-utils";
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
// 청크 크기 경고 임계값 조정
|
|
chunkSizeWarningLimit: 1000,
|
|
// 압축 최적화 (esbuild 사용)
|
|
minify: "esbuild",
|
|
},
|
|
}));
|