react-spa
React SPA (Vite/CRA) 프로젝트 컨벤션. react-router-dom v6 라우팅, 빌드/개발 설정.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
React SPA (Vite/CRA) 프로젝트 컨벤션. react-router-dom v6 라우팅, 빌드/개발 설정.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
세션 핸드오프. 컨텍스트가 차기 전에 중요 맥락을 progress.md에 문서화하고 새 세션 킥오프 프롬프트를 클립보드에 복사한다. "핸드오프", "세션 정리하고 넘기자", "컨텍스트 정리", "이어갈 준비" 등의 요청에 사용.
MD 파일 또는 텍스트로 작업을 정의하면, 분석 → 디자인 확인 → 구현 → 검증 → 커밋 → PR까지 전체 플로우를 수행한다. "이거 만들어줘", "기능 구현해줘", "작업 시작하자", "이 티켓 진행해줘" 등 코드 작업 착수 발화에 사용.
Claude + OpenAI Codex 협업 스킬. MCP 서버 설정 시에만 사용 가능.
code-forge 상태 대시보드. REFLECT flag, quality 이벤트, notepad/decisions, usage 집계를 한 번에. forge-glow 같은 외부 도구는 --json으로 파싱.
교차 모델 토론. Agent Teams / Codex CLI / self-debate 모드 선택. 설계 결정, 아키텍처 선택 시 활용. "어느 쪽이 나을까", "설계 비교해줘", "토론시켜줘", "교차 검증해줘" 등의 요청에 사용.
화면 단위 E2E 테스트 자동화. Figma/코드 기반 테스트 케이스 도출 → Playwright 코드 생성 → Forge Loop(에스컬레이션 기반 자율 실행). "E2E 돌려줘", "화면 테스트 만들어줘", "이 페이지 자동 테스트" 등의 요청에 사용.
| name | react-spa |
| description | React SPA (Vite/CRA) 프로젝트 컨벤션. react-router-dom v6 라우팅, 빌드/개발 설정. |
이 프로젝트는 Vite 또는 CRA(Create React App) 기반의 React SPA다.
src/
├── main.tsx # 진입점
├── App.tsx # 루트 컴포넌트
├── routes/ # 라우트 정의
│ └── index.tsx
├── pages/ # 페이지 컴포넌트
│ ├── HomePage.tsx
│ └── OrderPage.tsx
├── components/ # 공유 컴포넌트
├── hooks/ # 커스텀 훅
├── services/ # API 서비스
├── store/ # 상태 관리
├── types/ # TypeScript 타입
└── utils/ # 유틸리티 함수
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: process.env.VITE_API_URL,
changeOrigin: true,
},
},
},
});
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import './index.css';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>
</React.StrictMode>,
);
// src/routes/index.tsx
import { Routes, Route, Navigate } from 'react-router-dom';
import { Suspense, lazy } from 'react';
// 코드 스플리팅
const HomePage = lazy(() => import('@/pages/HomePage'));
const OrderPage = lazy(() => import('@/pages/OrderPage'));
const OrderDetailPage = lazy(() => import('@/pages/OrderDetailPage'));
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'));
export function AppRoutes() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/orders" element={<OrderPage />} />
<Route path="/orders/:orderId" element={<OrderDetailPage />} />
<Route path="/404" element={<NotFoundPage />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
</Suspense>
);
}
// src/routes/index.tsx
import { Outlet } from 'react-router-dom';
function DashboardLayout() {
return (
<div>
<nav>대시보드 메뉴</nav>
<Outlet /> {/* 자식 라우트가 렌더링되는 위치 */}
</div>
);
}
export function AppRoutes() {
return (
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} /> {/* /dashboard */}
<Route path="orders" element={<DashboardOrders />} /> {/* /dashboard/orders */}
<Route path="settings" element={<DashboardSettings />} /> {/* /dashboard/settings */}
</Route>
</Routes>
);
}
// src/routes/ProtectedRoute.tsx
import { Navigate, Outlet } from 'react-router-dom';
import { useAuthStore } from '@/store/auth';
export function ProtectedRoute() {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
function OrderDetailPage() {
const { orderId } = useParams<{ orderId: string }>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const handleBack = () => navigate(-1);
const handleGoToOrders = () => navigate('/orders');
// 쿼리 파라미터
const page = searchParams.get('page') ?? '1';
return <div>주문 #{orderId}</div>;
}
// .env.local
VITE_API_URL=http://localhost:8080
VITE_ENV=development
// 코드에서 사용
const apiUrl = import.meta.env.VITE_API_URL;
const isDev = import.meta.env.DEV; // Vite 내장 변수
// ❌ process.env는 Vite에서 동작하지 않음
const url = process.env.REACT_APP_API_URL; // CRA 방식
// ✅ Vite 방식
const url = import.meta.env.VITE_API_URL;
// package.json
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint src --ext ts,tsx",
"test": "vitest"
}
}
// src/App.tsx
import { AppRoutes } from '@/routes';
function App() {
return <AppRoutes />;
}
export default App;
lazy() + Suspense 사용?VITE_ 접두사 사용 (Vite)?Outlet 사용?ProtectedRoute 래퍼 사용?