react-spa
React SPA (Vite/CRA) 프로젝트 컨벤션. react-router-dom v6 라우팅, 빌드/개발 설정.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
React SPA (Vite/CRA) 프로젝트 컨벤션. react-router-dom v6 라우팅, 빌드/개발 설정.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
세션 핸드오프. 컨텍스트가 차기 전에 중요 맥락을 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 래퍼 사용?