Skip to main content

spa

Single Page Application architecture — client-side routing, state management, data fetching, bundle optimization, and the SPA vs MPA tradeoff. Covers React, Vue, Angular, Svelte, and Solid ecosystems. USE FOR: SPA architecture, client-side routing, state management patterns, data fetching strategies, bundle optimization, code splitting, lazy loading DO NOT USE FOR: server-side rendering (use ssr), progressive web apps (use pwa), micro-frontend composition (use micro-frontends)

소스 정보

저장소
Tyler-R-Kendrick/agent-skills
최근 소스 활동
2026년 2월 11일 05:14
감지된 SKILL.md 언어
영어
스타
12
포크
4

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
12 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
spa
description
Single Page Application architecture — client-side routing, state management, data fetching, bundle optimization, and the SPA vs MPA tradeoff. Covers React, Vue, Angular, Svelte, and Solid ecosystems. USE FOR: SPA architecture, client-side routing, state management patterns, data fetching strategies, bundle optimization, code splitting, lazy loading DO NOT USE FOR: server-side rendering (use ssr), progressive web apps (use pwa), micro-frontend composition (use micro-frontends)
license
MIT
metadata
{"displayName":"Single Page Applications","author":"Tyler-R-Kendrick"}
compatibility
claude, copilot, cursor
references
[{"title":"MDN Web Docs — Single-Page Applications","url":"https://developer.mozilla.org/en-US/docs/Glossary/SPA"},{"title":"Single-Page Application — Wikipedia","url":"https://en.wikipedia.org/wiki/Single-page_application"}]
# Single Page Applications (SPA) ## Overview A Single Page Application loads a single HTML shell, then uses JavaScript to dynamically render content and handle navigation entirely in the browser. The server provides JSON APIs; the client handles routing, rendering, and state. SPAs deliver rich, app-like experiences — think Gmail, Figma, or Notion — but come with tradeoffs in SEO, initial load performance, and complexity. ## SPA Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ Browser (Client) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │ │ Router │ │ State │ │ Component Tree │ │ │ │ (URL ↔ │ │ Store │ │ (Virtual DOM / │ │ │ │ View) │ │ │ │ Reactive Updates) │ │ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │ │ │ │ │ │ └──────────────┼───────────────┘ │ │ │ │ │ ┌───────▼───────┐ │ │ │ Data Fetching │ │ │ │ (API Client) │ │ │ └───────┬───────┘ │ └──────────────────────┼────────────────────────────────────┘ │ HTTP/WebSocket ┌────────▼────────┐ │ Backend API │ │ (REST/GraphQL) │ └─────────────────┘ ``` ### How It Works 1. Browser requests `index.html` — a minimal shell with a `<div id="root">` and a `<script>` tag 2. JavaScript bundle loads, initializes the router, and renders the initial view 3. User clicks a link — the router intercepts it, updates the URL (History API), and renders the new view without a page reload 4. Data is fetched asynchronously from APIs and rendered into the component tree 5. All subsequent navigation happens client-side — the server is never contacted for HTML again ## SPA vs MPA Decision | Factor | SPA | MPA (Multi-Page App) | |--------|-----|---------------------| | **Navigation** | Instant (client-side) | Full page reload | | **Initial Load** | Slower (large JS bundle) | Faster (server HTML) | | **SEO** | Challenging (needs prerendering or SSR) | Native | | **Interactivity** | Rich, app-like | Page-based, simpler | | **Offline** | Possible (with Service Workers) | Difficult | | **State Persistence** | Survives navigation | Lost on page reload | | **Complexity** | Higher (routing, state, hydration) | Lower | | **Best For** | Dashboards, SaaS, tools | Content sites, blogs, e-commerce | **Rule of thumb**: If your app feels like a *document*, use an MPA or SSR. If it feels like an *application*, use a SPA. ## Client-Side Routing ### React Router (v6+) ```tsx import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom'; function App() { return ( <BrowserRouter> <nav> <Link to="/">Home</Link> <Link to="/dashboard">Dashboard</Link> </nav> <Routes> <Route path="/" element={<Home />} /> <Route path="/dashboard" element={<DashboardLayout />}> <Route index element={<Overview />} /> <Route path="settings" element={<Settings />} /> </Route> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); } function DashboardLayout() { return ( <div> <Sidebar /> <Outlet /> {/* Nested route renders here */} </div> ); } ``` ### Vue Router ```typescript import { createRouter, createWebHistory } from 'vue-router'; const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('./views/Home.vue') }, { path: '/dashboard', component: () => import('./views/Dashboard.vue'), children: [ { path: '', component: () => import('./views/Overview.vue') }, { path: 'settings', component: () => import('./views/Settings.vue') }, ], }, ], }); ``` ### Angular Router ```typescript const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'dashboard', component: DashboardComponent, children: [ { path: '', component: OverviewComponent }, { path: 'settings', component: SettingsComponent }, ], canActivate: [AuthGuard], }, { path: '**', component: NotFoundComponent }, ]; ``` ## State Management ### State Management Landscape | Library | Ecosystem | Philosophy | |---------|-----------|-----------| | **Redux Toolkit** | React | Single store, immutable, actions + reducers | | **Zustand** | React | Minimal, hook-based, no boilerplate | | **Jotai** | React | Atomic state, bottom-up, derived atoms | | **Valtio** | React | Proxy-based, mutable API, reactive | | **Pinia** | Vue | Composition API-friendly, modular stores | | **NgRx** | Angular | RxJS-based Redux for Angular, effects | | **Angular Signals** | Angular | Fine-grained reactivity, no RxJS needed | | **Svelte Stores** | Svelte | Built-in writable/readable/derived stores | ### Zustand (Modern React State) ```typescript import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; interface AuthStore { user: User | null; token: string | null; login: (credentials: Credentials) => Promise<void>; logout: () => void; } const useAuthStore = create<AuthStore>()( devtools( persist( (set) => ({ user: null, token: null, login: async (credentials) => { const { user, token } = await api.login(credentials); set({ user, token }); }, logout: () => set({ user: null, token: null }), }), { name: 'auth-storage' } ) ) ); // Usage in component function Profile() { const user = useAuthStore((state) => state.user); const logout = useAuthStore((state) => state.logout); // ... } ``` ### Pinia (Vue State) ```typescript import { defineStore } from 'pinia'; export const useCartStore = defineStore('cart', () => { const items = ref<CartItem[]>([]); const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.qty, 0) ); function addItem(product: Product) { const existing = items.value.find((i) => i.id === product.id); if (existing) existing.qty++; else items.value.push({ ...product, qty: 1 }); } function removeItem(id: string) { items.value = items.value.filter((i) => i.id !== id); } return { items, total, addItem, removeItem }; }); ``` ## Data Fetching ### The Server State Problem Server data is not the same as client state. Server data is: - **Asynchronous** — fetched over the network - **Shared** — multiple components may need the same data - **Stale** — can be outdated the moment it arrives - **Cacheable** — often the same data is requested repeatedly Libraries like TanStack Query and SWR solve these problems with caching, deduplication, background refetching, and optimistic updates. ### TanStack Query (React Query) ```typescript import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; function TodoList() { const queryClient = useQueryClient(); const { data: todos, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: () => api.getTodos(), staleTime: 5 * 60 * 1000, // consider fresh for 5 min gcTime: 30 * 60 * 1000, // garbage collect after 30 min }); const addTodo = useMutation({ mutationFn: (newTodo: NewTodo) => api.createTodo(newTodo), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, // Optimistic update onMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: ['todos'] }); const previous = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], (old: Todo[]) => [ ...old, { ...newTodo, id: 'temp' }, ]); return { previous }; }, onError: (_err, _newTodo, context) => { queryClient.setQueryData(['todos'], context?.previous); }, }); if (isLoading) return <Spinner />; if (error) return <ErrorMessage error={error} />; // render todos... } ``` ### SWR (Vercel) ```typescript import useSWR from 'swr'; const fetcher = (url: string) => fetch(url).then((r) => r.json()); function Profile() { const { data, error, isLoading, mutate } = useSWR('/api/user', fetcher, { revalidateOnFocus: true, revalidateOnReconnect: true, dedupingInterval: 2000, }); // mutate() to trigger revalidation // mutate(newData, false) for optimistic update } ``` ### Apollo Client (GraphQL) ```typescript import { useQuery, gql } from '@apollo/client'; const GET_TODOS = gql` query GetTodos($status: Status) { todos(status: $status) { id title completed } } `; function TodoList({ status }: { status: Status }) { const { loading, error, data } = useQuery(GET_TODOS, { variables: { status }, pollInterval: 30000, // refetch every 30s }); // ... } ``` ## Code Splitting and Lazy Loading ### Route-Based Splitting (React) ```tsx import { lazy, Suspense } from 'react'; const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); const Analytics = lazy(() => import('./pages/Analytics')); function App() { return ( <Suspense fallback={<PageSkeleton />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/analytics" element={<Analytics />} /> </Routes> </Suspense> ); } ``` ### Component-Level Splitting ```tsx const HeavyChart = lazy(() => import('./components/HeavyChart')); function AnalyticsPage() { const [showChart, setShowChart] = useState(false); return ( <div> <button onClick={() => setShowChart(true)}>Show Chart</button> {showChart && ( <Suspense fallback={<ChartSkeleton />}> <HeavyChart /> </Suspense> )} </div> ); } ``` ### Prefetching on Hover ```tsx function NavLink({ to, children }: { to: string; children: React.ReactNode }) { const prefetch = () => { // Trigger dynamic import on hover — browser caches the chunk if (to === '/analytics') import('./pages/Analytics'); if (to === '/settings') import('./pages/Settings'); }; return ( <Link to={to} onMouseEnter={prefetch}> {children} </Link> ); } ``` ## Bundle Optimization ### Vite Configuration ```typescript // vite.config.ts import { defineConfig } from 'vite'; export default defineConfig({ build: { rollupOptions: {
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기