Files
2nd/10_Wiki/Topics/Coding/Frontend_Progressive_Enhancement.md
T
2026-05-09 21:08:02 +09:00

307 lines
7.8 KiB
Markdown

---
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
<form method="post" action="/api/login">
<input name="email" type="email" required>
<input name="password" type="password" required minlength="8" required>
<button>Sign in</button>
</form>
```
→ 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 (
<form action={login}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button>Sign in</button>
</form>
);
}
```
→ JS 없어도 form submit. JS 가 활성화되면 SPA-like UX.
### useFormStatus (loading state)
```tsx
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Signing in…' : 'Sign in'}</button>;
}
```
→ 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 (
<Form method="post">
<input name="email" type="email" />
<input name="password" type="password" />
<button>Sign in</button>
</Form>
);
}
```
→ Remix `<Form>` = JS 없어도 native form, JS 있으면 SPA submit.
### HTMX (server-rendered + AJAX)
```html
<button hx-post="/api/like" hx-target="#likes">
Like
</button>
<span id="likes">42</span>
```
→ Server 가 새 HTML fragment 반환. JS 없으면 일반 button (또는 fallback).
```html
<!-- Fallback -->
<form action="/like" method="post">
<button hx-post="/api/like" hx-target="#likes" hx-swap="innerHTML">
Like
</button>
</form>
<span id="likes">42</span>
```
### Native dialog (JS 안)
```html
<dialog id="modal">
<form method="dialog">
<p>Are you sure?</p>
<button value="confirm">Yes</button>
<button value="cancel">No</button>
</form>
</dialog>
<button onclick="document.getElementById('modal').showModal()">Open</button>
```
→ JS 안 쓰고도 modal. (showModal 만 JS — fallback 가능).
### View Transitions (declarative animation)
```html
<a href="/about">About</a>
<!-- 새 page navigation 자동 transition (Chrome) -->
```
```css
@view-transition { navigation: auto; }
```
### Anchor link / form / radio = SPA-like 가능
```html
<!-- Tabs = radio + label -->
<input type="radio" id="tab1" name="tabs" checked>
<label for="tab1">Tab 1</label>
<input type="radio" id="tab2" name="tabs">
<label for="tab2">Tab 2</label>
<div class="content">
<section data-tab="tab1">Tab 1 content</section>
<section data-tab="tab2">Tab 2 content</section>
</div>
<style>
section[data-tab] { display: none; }
#tab1:checked ~ .content [data-tab="tab1"] { display: block; }
#tab2:checked ~ .content [data-tab="tab2"] { display: block; }
</style>
```
→ JS 0 — 작동.
### Search — native form + URL
```tsx
<form method="get" action="/search">
<input name="q" type="search" placeholder="Search…">
<button>Search</button>
</form>
```
→ 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
<img src="..." loading="lazy" decoding="async" alt="...">
<iframe src="..." loading="lazy"></iframe>
```
### Tailwind + native HTML
```html
<details class="border rounded p-4">
<summary class="cursor-pointer font-bold">FAQ</summary>
<p class="mt-2">Answer</p>
</details>
```
→ JS 없는 accordion.
### Form validation (browser native)
```html
<input type="email" required>
<input type="number" min="0" max="100" step="0.1">
<input pattern="[A-Z]{3}-\d{4}" title="ABC-1234">
<input type="text" required minlength="3" maxlength="20">
```
```css
input:invalid { border-color: red; }
input:invalid:not(:placeholder-shown) { ... }
```
### Web standards 활용
```html
<input type="date"> <!-- date picker native -->
<input type="color"> <!-- color picker -->
<input type="range"> <!-- slider -->
<input type="search"> <!-- clear button native -->
<input type="file" accept="image/*" multiple>
```
→ 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),
});
}
<form onSubmit={handleSubmit} action="/api/..." method="post">
...
</form>
```
→ 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
<button>Click</button> <!-- focus, role 자동 -->
<a href="...">Link</a> <!-- 키보드 navigation 자동 -->
<form> <!-- enter submit, label association -->
```
→ Custom div 보다 우월.
### React Server Components (RSC)
```tsx
// 서버 컴포넌트 — JS 0 client
async function Page() {
const data = await db.query(...);
return <ul>{data.map(d => <li>{d}</li>)}</ul>;
}
```
→ 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.
- **`<div onClick>` 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]]