--- id: frontend-progressive-enhancement title: Progressive Enhancement — Server-first / 점진 JS category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, progressive-enhancement, html, vibe-coding] tech_stack: { language: "HTML / TS", applicable_to: ["Frontend"] } applied_in: [] aliases: [progressive enhancement, server-first, no-JS fallback, HTMX, web standards, MPA] --- # Progressive Enhancement > Server-rendered HTML 부터 → JS 로 향상. **JS 없어도 작동, 있으면 더 좋음**. Remix / Next App Router / HTMX / web standards. SPA 의 reaction. ## 📖 핵심 개념 - HTML 가 baseline. - Form / link 가 native (JS 없어도). - JS = enhancement. - 학교 wifi / slow 3G / 옛 browser 도 작동. ## 💻 코드 패턴 ### 기본 — Form 가 native ```html
``` → JS 없이 작동. 검증도 native. ### Next.js Server Action (PE) ```tsx // app/login/page.tsx async function login(formData: FormData) { 'use server'; const email = formData.get('email') as string; const password = formData.get('password') as string; // ... auth redirect('/dashboard'); } export default function LoginPage() { return (
); } ``` → JS 없어도 form submit. JS 가 활성화되면 SPA-like UX. ### useFormStatus (loading state) ```tsx 'use client'; import { useFormStatus } from 'react-dom'; function SubmitButton() { const { pending } = useFormStatus(); return ; } ``` → JS 활성 시 loading 표시. JS 없으면 기본 button. ### Remix loader / action ```tsx // app/routes/login.tsx export async function action({ request }: ActionArgs) { const formData = await request.formData(); const email = formData.get('email'); // ... return redirect('/dashboard'); } export default function Login() { return (
); } ``` → Remix `
` = JS 없어도 native form, JS 있으면 SPA submit. ### HTMX (server-rendered + AJAX) ```html 42 ``` → Server 가 새 HTML fragment 반환. JS 없으면 일반 button (또는 fallback). ```html
42 ``` ### Native dialog (JS 안) ```html

Are you sure?

``` → JS 안 쓰고도 modal. (showModal 만 JS — fallback 가능). ### View Transitions (declarative animation) ```html About ``` ```css @view-transition { navigation: auto; } ``` ### Anchor link / form / radio = SPA-like 가능 ```html
Tab 1 content
Tab 2 content
``` → JS 0 — 작동. ### Search — native form + URL ```tsx
``` → JS 가 query param + result render. 없어도 server 가 SSR. ### Optimistic UI + rollback ```tsx 'use client'; import { useOptimistic } from 'react'; const [optimistic, addOptimistic] = useOptimistic(items); async function add(item) { addOptimistic([...items, item]); // 즉시 UI await fetch('/api/add', { method: 'POST', body: JSON.stringify(item) }); } ``` → JS 활성 시 즉시 반응. 없으면 server 의 form submit. ### loading="lazy", decoding="async" ```html ... ``` ### Tailwind + native HTML ```html
FAQ

Answer

``` → JS 없는 accordion. ### Form validation (browser native) ```html ``` ```css input:invalid { border-color: red; } input:invalid:not(:placeholder-shown) { ... } ``` ### Web standards 활용 ```html ``` → JS 라이브러리 없이. ### Fetch with form + JSON 전환 ```tsx async function handleSubmit(e: FormEvent) { e.preventDefault(); const data = Object.fromEntries(new FormData(e.target as HTMLFormElement)); await fetch('/api/...', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(data), }); }
...
``` → JS 활성 = JSON. 없으면 form encode + server 가 처리. ### MPA vs SPA vs PE ``` MPA (Multi-Page App): 서버 매번 HTML 보냄. 옛. SPA: JS 가 모든 거. PE 무시 자주. Progressive Enhanced: 둘 다 — MPA 처럼 baseline, SPA 처럼 향상. → Next App Router / Remix / Phoenix LiveView / Rails Hotwire. ``` ### a11y 자연 ```html Link
``` → Custom div 보다 우월. ### React Server Components (RSC) ```tsx // 서버 컴포넌트 — JS 0 client async function Page() { const data = await db.query(...); return ; } ``` → HTML 만 보냄. 작은 page = JS 0. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Public site (SEO, accessibility) | PE strict | | 내부 dashboard (관리자만) | SPA OK | | Form-heavy | Server actions / Remix | | 인터랙션 없는 콘텐츠 | RSC + 0 client JS | | 복잡 인터랙션 | SPA + PE 가능 한도 | | 옛 browser / slow 3G | PE 강 | ## ❌ 안티패턴 - **모든 거 client-side JS**: SEO / a11y / slow. - **`
` button**: 키보드 안 됨. - **Form 가 fetch + JSON 만 — server action 없음**: JS X = 작동 X. - **Required JS — fallback X**: 옛 browser fail. - **Client-only routing — server URL 안 됨**: refresh = 404. - **Loading skeleton 만 + 데이터 없음**: PE 무시. ## 🤖 LLM 활용 힌트 - Form action + method = baseline. - Native HTML 우선 (button, dialog, details). - JS 는 향상 — 없어도 작동. - Next App Router / Remix 가 PE 친화. ## 🔗 관련 문서 - [[React_RSC_Server_Actions_Deep]] - [[Frontend_A11y_Testing]] - [[Web_Performance_Core_Vitals]]