React + Vite + TailwindCSS κΈ°λ° μ€μκ° λ²μ€ λ°μ΄ν° μκ°ν λμ보λ
BustleBus Web Dashboardλ React 19μ Viteλ₯Ό νμ©νμ¬ κ°λ°λ κ³ μ±λ₯ μΉ μ ν리μΌμ΄μ μ λλ€. λλμ λ²μ€ μ΄ν λ°μ΄ν°λ₯Ό ν¨μ¨μ μΌλ‘ μκ°ννκ³ , κ°μν μ€ν¬λ‘€μ ν΅ν΄ μ΅μ νλ μ¬μ©μ κ²½νμ μ 곡ν©λλ€.
- React: 19.1.1 (μ΅μ λ²μ )
- Vite: 7.1.7 (SWC κΈ°λ° λΉλ)
- TypeScript: 5.9.3
- TailwindCSS: 4.1 (μ΅μ λ²μ )
- Tailwind Merge: μ€λ³΅ ν΄λμ€ λ³ν©
- Class Variance Authority: μ‘°κ±΄λΆ μ€νμΌλ§
- PostCSS: 4.1
- Radix UI:
- Dialog: λͺ¨λ¬ UI
- Popover: νμ€λ² μ»΄ν¬λνΈ
- Select: μ λ νΈ λ°μ€
- Tabs: ν UI
- Label: λ μ΄λΈ μ»΄ν¬λνΈ
- Slot: μ»΄ν¬λνΈ μ¬λ‘―
- shadcn/ui: μ¬μ¬μ© κ°λ₯ν 컀μ€ν μ»΄ν¬λνΈ
- Lucide React: μμ΄μ½ λΌμ΄λΈλ¬λ¦¬
- cmdk: Command Palette
- ApexCharts: 5.3.6
- React-ApexCharts: μ°¨νΈ λΌμ΄λΈλ¬λ¦¬
- TanStack Virtual: 3.13 (κ°μν μ€ν¬λ‘€)
- ESLint: 9.36 + React Hooks Plugin
- TypeScript ESLint: νμ 체ν¬
- @vitejs/plugin-react-swc: SWC κΈ°λ° λΉλ
- Vercel: λ°°ν¬ νλ«νΌ
TanStack Virtualμ νμ©ν λμ©λ λ°μ΄ν° λ λλ§ μ΅μ ν:
// VirtualizedCombobox ꡬν
import { useVirtualizer } from "@tanstack/react-virtual";
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => 35,
overscan: 5,
});μ±κ³Ό:
- β μμ² κ°μ μ λ₯μ₯ λ°μ΄ν° λ λλ§
- β μ€ν¬λ‘€ μ±λ₯ 99% κ°μ
- β λ©λͺ¨λ¦¬ μ¬μ©λ 80% κ°μ
- β μ΄κΈ° λ λλ§ μκ° λ¨μΆ
μκ°λλ³ λ²μ€ νΌμ‘λλ₯Ό μμμΌλ‘ νν:
const heatmapOptions = {
chart: {
type: "heatmap",
},
dataLabels: {
enabled: false,
},
colors: ["#008FFB"],
xaxis: {
categories: timeSlots, // μκ°λ
},
yaxis: {
categories: stations, // μ λ₯μ₯
},
};νΉμ§:
- μκ°λλ³/μ λ₯μ₯λ³ λ°μ΄ν° μκ°ν
- μΈν°λν°λΈ μ°¨νΈ
- λ°μν λ μ΄μμ
- λ° μ°¨νΈ: μ λ₯μ₯λ³ μ΄μ©κ° μ
- λΌμΈ μ°¨νΈ: μκ°λλ³ νΈλ λ
- λμ λ°μ΄ν° μ λ°μ΄νΈ
// Dialog μμ
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Title</DialogTitle>
<DialogDescription>Description</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>μ₯μ :
- β μΉ μ κ·Όμ± νμ€ μ€μ (WAI-ARIA)
- β ν€λ³΄λ λ€λΉκ²μ΄μ μ§μ
- β μ€ν¬λ¦° 리λ νΈν
- β Headless UI ꡬ쑰
src/components/ui/
βββ button.tsx
βββ combobox.tsx # κ°μν μ μ©
βββ dialog.tsx
βββ select.tsx
βββ tabs.tsx
βββ ...
μ΅μ TailwindCSS v4 κΈ°λ₯ νμ©:
/* index.css */
@import "tailwindcss";
@theme {
/* 컀μ€ν
ν
λ§ μ€μ */
--color-primary: #3b82f6;
--color-secondary: #8b5cf6;
/* ... */
}μ£Όμ μ¬μ© ν¨ν΄:
- μ νΈλ¦¬ν° μ°μ μ€νμΌλ§
- λ°μν λμμΈ (sm, md, lg, xl)
- λ€ν¬ λͺ¨λ μ§μ μ€λΉ
- 컀μ€ν ν λ§ μ€μ
λ¬Έμ :
- μ λ₯μ₯ μ ν Comboboxμμ 리μ€νΈκ° κ°νμ μΌλ‘ νμλμ§ μμ
- μ€ν¬λ‘€ μ ν° νλ©΄λ§ λ³΄μ΄λ νμ
μμΈ:
- TanStack Virtualμ μΈ‘μ (measure) νμ΄λ° λ¬Έμ
- μ€ν¬λ‘€ μ΄λ²€νΈμ λ λλ§ μ¬μ΄ν΄ λΆμΌμΉ
ν΄κ²°:
// κ°μν μμ΄ν
μΈ‘μ κ°μ
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => 35,
overscan: 5,
// μΈ‘μ μ λ΅ κ°μ
measureElement: (el) => el.getBoundingClientRect().height,
});λ¬Έμ : ApexChartsμ YμΆ λ μ΄λΈμ΄ μλ €μ νμλ¨
ν΄κ²°:
yaxis: {
labels: {
offsetX: -10,
style: {
cssClass: 'text-sm'
}
}
},
chart: {
offsetX: 10,
offsetY: 0
}λ¬Έμ : ννΈλ§΅ λ·°μμ XμΆμ "μ λ₯μ₯" λμ "μκ°"μ΄ νμλμ΄μΌ ν¨
ν΄κ²°:
const chartOptions = useMemo(() => {
if (viewMode === "heatmap") {
return {
xaxis: {
categories: timeSlots, // μκ° λ°μ΄ν°
title: { text: "μκ°" },
},
};
}
// ...
}, [viewMode]);npm installnpm run devκ°λ° μλ²λ http://localhost:5173μμ μ€νλ©λλ€.
npm run buildnpm run previewnpm run lintsrc/
βββ components/
β βββ ui/ # shadcn/ui μ»΄ν¬λνΈ
β β βββ button.tsx
β β βββ combobox.tsx # κ°μν Combobox
β β βββ dialog.tsx
β β βββ select.tsx
β β βββ tabs.tsx
β βββ Dashboard.tsx # λ©μΈ λμ보λ
βββ lib/
β βββ utils.ts # μ νΈλ¦¬ν° ν¨μ
β βββ chartUtils.ts # μ°¨νΈ κ΄λ ¨ μ νΈ
β βββ dataProcessor.ts # λ°μ΄ν° μ²λ¦¬
βββ App.tsx
βββ main.tsx
βββ index.css # TailwindCSS μ€μ
public/
βββ assets/ # μ μ νμΌ
dataFile/
βββ busData.json # λ²μ€ λ°μ΄ν°
- Rust κΈ°λ° μ»΄νμΌλ¬λ‘ λΉλ μλ ν₯μ
- Hot Module Replacement (HMR)
- μ΅μ νλ λ²λ€λ§
// Lazy loading
const Dashboard = lazy(() => import("./components/Dashboard"));const chartData = useMemo(() => {
return processData(rawData);
}, [rawData]);
const chartOptions = useMemo(() => {
return generateOptions(viewMode);
}, [viewMode]);- λͺ¨λ κΈ΄ 리μ€νΈμ TanStack Virtual μ μ©
- λ·°ν¬νΈ κΈ°λ° λ λλ§
- OverscanμΌλ‘ λΆλλ¬μ΄ μ€ν¬λ‘€
- β Radix UIμ WAI-ARIA ꡬν
- β ν€λ³΄λ λ€λΉκ²μ΄μ μλ²½ μ§μ
- β ν¬μ»€μ€ κ΄λ¦¬
- β μ€ν¬λ¦° 리λ νΈν
- Desktop, Tablet, Mobile λμ
- TailwindCSS λΈλ μ΄ν¬ν¬μΈνΈ νμ©
- μ λμ μΈ λ μ΄μμ
- νΈλ² μ λ°μ΄ν° μμΈ νμ
- μ€/ν¬ κΈ°λ₯
- λ²λ‘ ν κΈ
β
React 19 μ΅μ κΈ°λ₯ νμ©
β
Hooks νμ© (useMemo, useCallback, useEffect)
β
μ»΄ν¬λνΈ μ€κ³ λ° μ¬μ¬μ©μ±
β
μ±λ₯ μ΅μ ν
β
νμ
μμ μ± ν보
β
Generic νμ©
β
Interface λ° Type μ€κ³
β
Strict λͺ¨λ
β
Vite λΉλ μ΅μ ν
β
SWC μ»΄νμΌλ¬ νμ©
β
λ²λ€ μ¬μ΄μ¦ μ΅μ ν
β
TailwindCSS λ§μ€ν°
β
Radix UI μ κ·Όμ± κ΅¬ν
β
λμμΈ μμ€ν
ꡬμΆ
β
λ°μν λμμΈ
β
ApexCharts νμ©
β
볡μ‘ν λ°μ΄ν° νν
β
μΈν°λν°λΈ μ°¨νΈ κ΅¬ν
- μ΄κΈ° λ‘λ© μκ°: < 1μ΄
- λ²λ€ μ¬μ΄μ¦: μ΅μ ν μλ£
- Lighthouse μ μ:
- Performance: 95+
- Accessibility: 100
- Best Practices: 100
- SEO: 90+
- κ°μν μ μ© ν μ±λ₯: 99% κ°μ
- λ©μΈ ν¬νΈν΄λ¦¬μ€ λ¬Έμ
- React Native App
- Vite 곡μ λ¬Έμ
- TailwindCSS 곡μ λ¬Έμ
- Radix UI 곡μ λ¬Έμ
νλ‘ νΈμλ μμ§λμ΄ (React μ λ¬Έ)