--- id: wiki-2026-0508-spa-single-page-application title: SPA (Single Page Application) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [single-page-app, client-side-routing-app] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [frontend, spa, react, vue, routing, architecture] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: react-vue-svelte --- # SPA (Single Page Application) ## 매 한 줄 > **"매 single HTML shell 의 의 client-side routing + dynamic rendering 의 driven 의 web app 의 architecture."**. 매 2010s 의 의 dominant pattern (AngularJS, Backbone, Ember → React, Vue, Angular), 매 2026 의 RSC / SSR / Islands 의 ascendency 의 의 SPA 의 niche 화 — 매 highly-interactive dashboards, internal tools, complex stateful UIs 의 의 the right tool 이며 marketing/content 의 의 의 SSG/SSR 의 권장. ## 매 핵심 ### 매 정의 - 1 HTML document 의 load — subsequent navigation 의 JS 의 driven (no full page reload). - Client-side router 의 URL ↔ component tree 의 mapping. - Data 의 fetch via XHR/Fetch — JSON API or RPC. - State 의 client 의 in-memory (Redux, Zustand, Jotai, TanStack Query). ### 매 trade-offs - **Pros**: snappy in-app navigation, rich interactivity, app-like UX, shared state across views. - **Cons**: SEO 의 weak (without SSR), initial bundle 의 large, white screen 의 risk, accessibility 의 careful 의 wiring. ### 매 vs. modern alternatives - **SSR / SSG (Next.js, Remix)**: server 의 HTML 의 generate, hydration 의 client interactivity 의 add. - **RSC (React Server Components, 2026 mainstream)**: server-only components + client islands. - **Islands (Astro, Fresh, 11ty)**: static HTML + targeted hydration 의 islands. - **MPA (Multi-Page App)**: traditional server-rendered pages — Hotwire / Inertia 의 의 modern revival. ### 매 응용 1. Internal admin dashboards (high interactivity, no SEO need). 2. Authenticated SaaS apps (Linear, Figma, Notion). 3. Real-time collaboration (CRDT-driven, websocket-heavy). 4. Browser-based tools (Excalidraw, Tldraw). ## 💻 패턴 ### React Router (data router, 2026) ```tsx import { createBrowserRouter, RouterProvider } from 'react-router'; const router = createBrowserRouter([ { path: '/', Component: Layout, children: [ { index: true, Component: Home }, { path: 'projects/:id', Component: Project, loader: projectLoader }, { path: '*', Component: NotFound }, ], }, ]); export const App = () => ; ``` ### Data fetching (TanStack Query) ```tsx import { useQuery } from '@tanstack/react-query'; export function ProjectView({ id }: { id: string }) { const { data, isPending, error } = useQuery({ queryKey: ['project', id], queryFn: () => fetch(`/api/projects/${id}`).then((r) => r.json()), staleTime: 60_000, }); if (isPending) return ; if (error) return ; return ; } ``` ### Client state (Zustand) ```ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; export const useUIStore = create( persist<{ sidebar: boolean; toggle: () => void }>( (set) => ({ sidebar: true, toggle: () => set((s) => ({ sidebar: !s.sidebar })), }), { name: 'ui-store' }, ), ); ``` ### Code splitting (lazy routes) ```tsx import { lazy, Suspense } from 'react'; const Settings = lazy(() => import('./routes/Settings')); const router = createBrowserRouter([ { path: '/settings', element: ( }> ), }, ]); ``` ### History API (without router) ```ts window.history.pushState({}, '', '/projects/42'); window.dispatchEvent(new PopStateEvent('popstate')); window.addEventListener('popstate', () => { render(parseRoute(location.pathname)); }); ``` ### Auth guard (route loader) ```tsx async function projectLoader({ params }: LoaderFunctionArgs) { const session = await getSession(); if (!session) throw redirect('/login'); return fetch(`/api/projects/${params.id}`).then((r) => r.json()); } ``` ### Vite SPA의 setup ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { sourcemap: true, target: 'es2022' }, server: { historyApiFallback: true }, }); ``` ### SEO 의 fallback (prerender + hydrate) ```ts // vite-plugin-prerender import prerender from 'vite-plugin-prerender'; export default { plugins: [ react(), prerender({ routes: ['/', '/about', '/pricing'] }), ], }; ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Marketing site, blog | **SSG** (Astro, Next.js static, 11ty) | | Content site with personalization | **SSR** (Next.js, Remix) | | Dashboards, admin tools | **SPA** (React Router + Vite) | | Highly interactive editor | **SPA** with code-splitting | | Public app needing SEO + interactivity | **Next.js (RSC + client islands)** | | MPA-style with sprinkles | **Hotwire / Inertia.js** | **기본값**: 2026 의 default 는 **Next.js / Remix (SSR + RSC)** — pure SPA 는 internal tools / authenticated apps 의 으로 의 reserve. ## 🔗 Graph - 부모: [[프론트엔드 및 UIUX 표준|Frontend-Architecture]] - 변형: [[SSR]] · [[SSG]] · [[Islands-Architecture]] · [[React Server Components — 경계 의식]] - Adjacent: [[Vite]] · [[Code Splitting]] ## 🤖 LLM 활용 **언제**: highly-interactive authenticated app, no SEO requirement, complex client state, real-time collaboration. **언제 X**: marketing/content site (SSG), public SEO-critical content (SSR/RSC), simple form-driven app (MPA). ## 안티패턴 - **SPA 의 marketing site**: SEO 의 weak, LCP 의 poor — SSG 의 사용. - **Bundle 의 single chunk**: route-based code splitting 의 default. - **Auth state 의 localStorage 의 raw token**: HttpOnly cookie + refresh flow 의 사용. - **Client routing 의 server fallback 없음**: 404 의 deep link 의 — `historyApiFallback` / catch-all rewrite. - **No skeleton / suspense**: white screen on slow data — Suspense + skeletons. - **Global state 의 overuse**: server state 의 TanStack Query, UI state 의 Zustand/Jotai 의 separate. ## 🧪 검증 / 중복 - Verified (React Router 7 docs, Vue Router 4, MDN SPA architecture, web.dev rendering patterns 2026). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — full SPA architecture with 2026 trade-offs vs RSC/Islands |