7.8 KiB
7.8 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| frontend-progressive-enhancement | Progressive Enhancement — Server-first / 점진 JS | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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
<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)
// 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)
'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
// 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)
<button hx-post="/api/like" hx-target="#likes">
Like
</button>
<span id="likes">42</span>
→ Server 가 새 HTML fragment 반환. JS 없으면 일반 button (또는 fallback).
<!-- 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 안)
<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)
<a href="/about">About</a>
<!-- 새 page navigation 자동 transition (Chrome) -->
@view-transition { navigation: auto; }
Anchor link / form / radio = SPA-like 가능
<!-- 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
<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
'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"
<img src="..." loading="lazy" decoding="async" alt="...">
<iframe src="..." loading="lazy"></iframe>
Tailwind + native 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)
<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">
input:invalid { border-color: red; }
input:invalid:not(:placeholder-shown) { ... }
Web standards 활용
<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 전환
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 자연
<button>Click</button> <!-- focus, role 자동 -->
<a href="...">Link</a> <!-- 키보드 navigation 자동 -->
<form> <!-- enter submit, label association -->
→ Custom div 보다 우월.
React Server Components (RSC)
// 서버 컴포넌트 — 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 친화.