d8a80f6272
이름만 다른(표기 변형) [[위키링크]]를 대상 문서의 canonical 제목으로 치환해 끊겼던 1,200개 링크를 연결. 제목/파일명 정규화 일치만 적용하고 별칭 매칭은 과병합 위험으로 제외(애매성 가드). 원본은 _link_reconcile_backup/ 에 백업. 도구: Datacollect/scripts/link_reconcile_apply.mjs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
234 lines
7.3 KiB
Markdown
234 lines
7.3 KiB
Markdown
---
|
|
id: wiki-2026-0508-웹-접근성-및-성능-최적화
|
|
title: 웹 접근성 및 성능 최적화
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [Web Accessibility, A11y, Performance Optimization, WCAG, Core Web Vitals]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.9
|
|
verification_status: applied
|
|
tags: [accessibility, a11y, performance, wcag, core-web-vitals, frontend]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: TypeScript
|
|
framework: React/Web
|
|
---
|
|
|
|
# 웹 접근성 및 성능 최적화
|
|
|
|
## 매 한 줄
|
|
> **"매 a11y 의 의 minority feature 의 의 — 매 keyboard / screen reader / high contrast user 의 의 majority 의 의 better UX."**. WCAG 2.2 (2023) + WCAG 3 (draft 2026) 의 의 standard, ARIA 1.3, Core Web Vitals (LCP/INP/CLS) 의 modern user-centric performance metric. 매 2026 의 의 INP (Interaction to Next Paint, 2024 replaced FID) 의 의 main interaction metric 의 의.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 WCAG 2.2 의 4 principle (POUR)
|
|
- **Perceivable**: alt text, captions, contrast 4.5:1 (AA) / 7:1 (AAA).
|
|
- **Operable**: keyboard navigable, focus visible, no seizure-inducing flash.
|
|
- **Understandable**: clear language, error messages, predictable.
|
|
- **Robust**: valid HTML, ARIA properly used, works across AT.
|
|
|
|
### 매 Core Web Vitals (2026)
|
|
- **LCP (Largest Contentful Paint)**: < 2.5s — main content visible.
|
|
- **INP (Interaction to Next Paint)**: < 200ms — responsiveness (replaces FID).
|
|
- **CLS (Cumulative Layout Shift)**: < 0.1 — visual stability.
|
|
|
|
### 매 응용
|
|
1. Public sector — WCAG AA 의 의 (ADA, EU EN 301 549).
|
|
2. E-commerce — checkout 의 keyboard accessibility (2024 EAA mandate).
|
|
3. SEO — Core Web Vitals 의 Google ranking factor.
|
|
4. AI tooling — voice / dictation / screen reader user 의 의 의.
|
|
|
|
## 💻 패턴
|
|
|
|
### Semantic HTML + ARIA
|
|
```html
|
|
<!-- 매 BAD: div soup -->
|
|
<div class="button" onclick="submit()">Submit</div>
|
|
|
|
<!-- 매 GOOD: semantic + accessible -->
|
|
<button type="submit" aria-describedby="submit-help">
|
|
Submit
|
|
</button>
|
|
<p id="submit-help" class="text-sm">매 reset 의 의 의.</p>
|
|
```
|
|
|
|
### Focus management (modal trap)
|
|
```tsx
|
|
import { useEffect, useRef } from 'react';
|
|
import FocusTrap from 'focus-trap-react';
|
|
|
|
export function Modal({ open, onClose, children }: Props) {
|
|
const closeRef = useRef<HTMLButtonElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) closeRef.current?.focus();
|
|
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [open, onClose]);
|
|
|
|
if (!open) return null;
|
|
return (
|
|
<FocusTrap>
|
|
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
<h2 id="modal-title">Settings</h2>
|
|
{children}
|
|
<button ref={closeRef} onClick={onClose}>Close</button>
|
|
</div>
|
|
</FocusTrap>
|
|
);
|
|
}
|
|
```
|
|
|
|
### LCP optimization (image priority)
|
|
```tsx
|
|
// Next 15 의 의
|
|
import Image from 'next/image';
|
|
|
|
<Image
|
|
src="/hero.webp"
|
|
alt="Product showcase"
|
|
width={1200}
|
|
height={630}
|
|
priority // 매 LCP image 의 의 preload
|
|
fetchPriority="high"
|
|
sizes="(max-width: 768px) 100vw, 1200px"
|
|
/>
|
|
```
|
|
|
|
### INP optimization (yield to main thread)
|
|
```ts
|
|
// 매 long task 의 의 의 break up
|
|
async function processLargeList(items: Item[]) {
|
|
const CHUNK = 100;
|
|
for (let i = 0; i < items.length; i += CHUNK) {
|
|
items.slice(i, i + CHUNK).forEach(process);
|
|
// 매 yield to browser — keep INP < 200ms
|
|
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
|
|
}
|
|
}
|
|
|
|
// React 19 useTransition
|
|
function Search() {
|
|
const [isPending, startTransition] = useTransition();
|
|
const [results, setResults] = useState([]);
|
|
|
|
function onChange(q: string) {
|
|
startTransition(() => {
|
|
setResults(filter(largeList, q)); // 매 deprioritize
|
|
});
|
|
}
|
|
return <input onChange={e => onChange(e.target.value)} />;
|
|
}
|
|
```
|
|
|
|
### CLS prevention (reserve space)
|
|
```css
|
|
/* 매 explicit aspect ratio — image / video 의 layout shift 의 */
|
|
.media {
|
|
aspect-ratio: 16 / 9;
|
|
width: 100%;
|
|
background: #eee;
|
|
}
|
|
|
|
/* 매 font 의 layout shift — size-adjust + fallback metric */
|
|
@font-face {
|
|
font-family: 'Inter';
|
|
src: url('/inter.woff2') format('woff2');
|
|
size-adjust: 107%;
|
|
ascent-override: 90%;
|
|
font-display: swap;
|
|
}
|
|
```
|
|
|
|
### Color contrast check
|
|
```ts
|
|
// 매 WCAG 의 의 contrast ratio
|
|
function contrastRatio(fg: string, bg: string): number {
|
|
const lum = (hex: string) => {
|
|
const [r, g, b] = hex.match(/\w\w/g)!.map(h => parseInt(h, 16) / 255);
|
|
const lin = (c: number) =>
|
|
c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
};
|
|
const [l1, l2] = [lum(fg), lum(bg)].sort((a, b) => b - a);
|
|
return (l1 + 0.05) / (l2 + 0.05); // ≥ 4.5 for AA, ≥ 7 for AAA
|
|
}
|
|
```
|
|
|
|
### Automated testing (axe-core + Playwright)
|
|
```ts
|
|
import { test, expect } from '@playwright/test';
|
|
import { injectAxe, checkA11y } from 'axe-playwright';
|
|
|
|
test('homepage 의 a11y', async ({ page }) => {
|
|
await page.goto('/');
|
|
await injectAxe(page);
|
|
await checkA11y(page, undefined, {
|
|
detailedReport: true,
|
|
axeOptions: { runOnly: ['wcag2a', 'wcag2aa', 'wcag22aa'] },
|
|
});
|
|
});
|
|
```
|
|
|
|
### Skip link + landmarks
|
|
```html
|
|
<a href="#main" class="skip-link">매 본문으로 건너뛰기</a>
|
|
<header role="banner">...</header>
|
|
<nav role="navigation" aria-label="Primary">...</nav>
|
|
<main id="main" tabindex="-1">...</main>
|
|
<footer role="contentinfo">...</footer>
|
|
|
|
<style>
|
|
.skip-link {
|
|
position: absolute; left: -9999px;
|
|
}
|
|
.skip-link:focus {
|
|
left: 0; top: 0; z-index: 100; padding: 1rem;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| Public sector / regulated | WCAG 2.2 AA mandatory + 3rd party audit |
|
|
| E-commerce | INP optimization (form/checkout) + keyboard nav |
|
|
| Marketing site | LCP < 2s + image priority + Next Image |
|
|
| SPA / dashboard | Focus management + ARIA live regions |
|
|
| Form-heavy | Error association (aria-describedby) + autocomplete |
|
|
| Animation-heavy | prefers-reduced-motion + INP budget |
|
|
|
|
**기본값**: Semantic HTML + axe-core CI + Lighthouse 의 LCP/INP/CLS budget + manual screen reader test (NVDA/VoiceOver) per release.
|
|
|
|
## 🔗 Graph
|
|
- 부모: [[Frontend Architecture]] · [[Web_Standards]]
|
|
- 변형: [[WCAG_2_2]] · [[ARIA]]
|
|
- 응용: [[Core Web Vitals Optimization (INP, LCP, CLS)|Core-Web-Vitals]] · [[Lighthouse]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: ARIA pattern lookup, semantic HTML refactor, WCAG criteria explanation, axe rule remediation.
|
|
**언제 X**: 의 actual screen reader UX evaluation (의 user testing 의 의), legal compliance ruling.
|
|
|
|
## ❌ 안티패턴
|
|
- **`<div onclick>`**: keyboard / screen reader 의 의.
|
|
- **Color-only signal**: red error 의 — icon / text label 의 의.
|
|
- **Auto-focus on load**: 매 disorienting — modal 의 의 의 의.
|
|
- **`outline: none` 의 focus state 의 X**: keyboard user 의 의 의 의.
|
|
- **Lazy-load LCP image**: 매 LCP 의 의 — `loading="eager"` + `fetchpriority="high"`.
|
|
- **`tabindex` 의 의 (>0)**: tab order 의 의 의.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (W3C WCAG 2.2 spec, web.dev Core Web Vitals, axe-core rules, MDN ARIA Authoring Practices).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — a11y + Core Web Vitals (INP) 의 의 |
|