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

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
frontend
progressive-enhancement
html
vibe-coding
language applicable_to
HTML / TS
Frontend
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

<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; }
<!-- 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 친화.

🔗 관련 문서