프론트엔드 품질
사용자 대면 웹 UI에 대한 품질 기준입니다. 하드 요구사항(접근성, 성능, 모바일 우선)이며 스타일 선호도가 아닙니다. 리뷰는 실제 사용자에게 영향을 미치는 버그(느린 페이지, 깨진 키보드 탐색, 로딩 중 레이아웃 변경)를 잡아냅니다.
모델 라우팅
모델이 UI, UX, 시각적 계층, 접근성 카피 또는 상호작용 세부사항을 비판하거나 재작성해야 할 때 운영자의 프론트엔드 선호도를 사용하세요:
source "$WALTER_OS_HOME/scripts/walter/lib/model-router.sh"
frontend_model=""
walter_model_resolve frontend frontend_model
기본 선호도는 Claude입니다. 운영자는 ~/.config/walter-os/overlay/personal.env에서 별칭을 다른 LiteLLM 경로로 지정할 수 있습니다.
이 스킬은 프론트엔드 변경 diff에 자동 트리거됩니다. 처음부터 랜딩 페이지를 구축하려면 landing-page-fast(이 규칙을 포함하지만 스캐폴딩도 제공)를 사용하세요.
이 운영자에게 특히 중요한 이유
- [Project A] 사용자는 아르헨티나 SMB 공급업체로, 대부분 휴대폰을 사용하며 종종 3G/4G 연결이 불안정합니다. 느린 사이트 = 사용자 이탈. WCAG가 중요한 이유는 일부 사용자가 기술 문맹률이 낮고 시력이 손상되었기 때문입니다.
- [Project B] 사용자는 장애가 있는 환자를 포함합니다. WCAG는 '있으면 좋은 것'이 아니라 사용성 요구사항입니다.
- [회사] 문서는 정교한 개발자가 읽으며 느리거나 키보드가 깨진 사이트에서 즉시 이탈합니다.
접근성 (WCAG 2.2 AA — 필수)
키보드
- 모든 상호작용 요소가 Tab으로 접근 가능해야 합니다. 버튼, 링크, 양식 필드, 사용자 정의 컨트롤. 수동 Tab-through를 실행하세요. 컨트롤이 하나라도 건너뛰어지면 차단(BLOCKER)입니다.
- 포커스가 보여야 합니다.
:focus-visible아웃라인이 명확해야 합니다. 대체 없이outline: none을 설정하지 마세요. - 논리적인 탭 순서. 읽기 순서가 DOM 순서와 일치해야 하며,
tabindex값이 0보다 큰 것은 피하세요(자연스러운 흐름을 깨뜨립니다). - 건너뛰기 링크. 모든 페이지의 첫 번째 포커스 가능 요소는 '주요 콘텐츠로 건너뛰기'여야 합니다.
- Escape 키로 모달 닫기. 모달이 닫힐 때까지 포커스를 모달 내에 가둡니다.
시맨틱 HTML
<button>은 액션용,<a>는 탐색용입니다. 둘 다<div onClick>을 사용하지 마세요.- 제목 계층 구조: 페이지당 하나의
<h1>;<h2>...<h6>는 단계를 건너뛰지 않습니다. - 랜드마크:
<header>,<nav>,<main>,<footer>. ARIA 역할은 어울리는 시맨틱 요소가 없을 때만 사용합니다. - **목록은
<ul>/<ol>**입니다.<div>로 목록을 위조하지 마세요. - **양식 컨트롤에는
<label>**이 있어야 합니다.for속성이id와 일치해야 합니다.
색상 및 대비
- WCAG AA 최소 대비: 본문 텍스트 4.5:1, 큰 텍스트(18pt+ 또는 14pt+ 볼드) 3:1, UI 구성 요소 및 그래픽 3:1.
- 색상이 유일한 지표가 아님: 오류 상태는 빨간색 테두리뿐만 아니라 아이콘 + 텍스트 + 색상을 표시합니다.
- 색맹 시뮬레이션으로 테스트됨. Stark, axe DevTools 또는 브라우저 개발자 도구 색각 시뮬레이터를 사용하세요.
양식
- 모든 입력에는 눈에 보이는 레이블이 있습니다. 플레이스홀더는 레이블이 아닙니다.
- 오류 메시지는
aria-describedby를 통해 프로그래밍 방식으로 연결됩니다. - 필수 필드 표시. 시각적으로(
*또는 '필수') AND 프로그래밍 방식으로(aria-required="true"또는required) 모두 표시합니다. - 유효성 검사 오류는 포커스를 받아도 사라지지 않습니다. 수정되거나 해제될 때까지 유지됩니다.
- 성공 상태 확인. 제출 후 스크린 리더 사용자가 성공했음을 알 수 있어야 합니다.
이미지 및 미디어
- 모든
<img>에alt속성. 장식용은 빈 값(alt=""), 콘텐츠용은 설명적. 생성형 AI 이미지: 프롬프트가 아니라 이미지에 실제로 있는 내용을 설명하세요. - 비디오/오디오용 자막/대본.
- 소리와 함께 자동 재생 금지.
- 애니메이션이
prefers-reduced-motion을 존중합니다:@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
도구
- 개발 모드에서
@axe-core/react— 접근성 문제를 콘솔에 기록합니다. eslint-plugin-jsx-a11y— 빌드 시점에 잡아냅니다.- 출시 전 수동 스크린 리더 테스트: VoiceOver (Mac), NVDA (Windows), TalkBack (Android).
- CI에서 Lighthouse 접근성 점수 ≥ 95.
성능 — Core Web Vitals
Google이 순위를 매기는 3가지 지표입니다. 'Good'으로 평가받으려면 세 가지 모두 통과해야 합니다:
LCP (Largest Contentful Paint) — 로드 속도
목표: 3G에서 < 2.5초. [Project A]의 실제 환경은 아르헨티나 모바일 네트워크가 중간보다 느리므로 더 공격적입니다.
방법:
- LCP 이미지 사전 로드:
<link rel="preload" as="image" href="...">. - 렌더링 차단 리소스 없음: 중요하지 않은 JS는 지연, 중요 CSS는 인라인.
- 폰트 자체 호스팅
font-display: swap과 함께 또는 시스템 폰트 스택 사용. - 정적 자산용 CDN: Vercel, Cloudflare R2, Bunny.net.
- HTTP/2 또는 HTTP/3: Vercel/Cloudflare 기본값.
- 이미지 압축: WebP/AVIF, 뷰포트에 맞게 크기 조정.
- 히어로 이미지 특별 최적화: 거의 항상 LCP입니다.
INP (Interaction to Next Paint) — 응답성
목표: p75 < 200ms. 2024년 FID를 대체했습니다.
방법:
- 상호작용 중 메인 스레드에 50ms 이상의 긴 작업 없음.
- 하이드레이션 지연 (Astro islands, Next.js streaming, qwik).
- 무거운 계산은 Web Worker.
- 입력 핸들러 디바운스/스로틀.
- 큰 문자열로
dangerouslySetInnerHTML사용 금지 — 파싱이 차단됩니다. useMemo/memo는 측정된 경우에만, 선제적으로 사용하지 않음.
CLS (Cumulative Layout Shift) — 시각적 안정성
목표: < 0.1.
방법:
- 이미지 및 비디오 태그에
width+height(브라우저가 공간을 예약합니다). - 광고/임베드용 공간 예약 min-height로.
- 기존 콘텐츠 위에 콘텐츠를 삽입하지 않음: 알림 배너는 접힌 부분 아래에 배치하거나 transform으로 밀어내되 DOM 위에 삽입하지 않음.
- 웹 폰트:
font-display: swap+size-adjust를 사용하여 메트릭 일치, 또는 가능한 경우font-display: optional. - 스켈레톤 스크린 실제 콘텐츠와 일치하는 크기로.
번들 크기
- 초기 JS 예산: 랜딩 페이지 < 150KB gzipped, 앱 셸 < 250KB.
- 경로별 코드 분할 (Next.js, Astro, SvelteKit이 기본적으로 수행).
- 무겁고 중요하지 않은 종속성(차트, 편집기)은 동적 임포트.
- 종속성 감사:
bundle-analyzer또는next-bundle-analyzer. 하나의 컴포넌트를 위해 가져온 10MB UI 라이브러리를 버리세요. - 트리 쉐이킹 작동: 필요한 것만 가져오기(
import { thing } from 'lib'이지import lib from 'lib'가 아님).
로딩 패턴
- 필요한 출처에 미리 연결:
<link rel="preconnect" href="...">. - 호버/포커스 시 다음 페이지를 미리 가져오기 (Next.js가 수행).
- 접힌 부분 아래 이미지는 지연 로딩:
loading="lazy". - iframe 지연 로딩:
loading="lazy". - 오프라인 우선을 위한 서비스 워커 (적합한 경우 — [Project A]에 유리, 마케팅 페이지는 해당 없음).
상태 — 모든 컴포넌트가 다섯 가지를 모두 처리
데이터 기반 UI의 경우, 상태가 하나라도 처리되지 않으면 차단(BLOCKER)입니다:
- 로딩 — 스켈레톤 또는 스피너. 빈 화면이 아니어야 함. 레이아웃 변경이 없어야 함.
- 빈 상태 — 첫 번째, 데이터가 없는 상태. 채우기 위한 CTA 포함.
- 오류 — 명확한 메시지, 재시도 버튼, 사용자에게 스택 트레이스 없음. Sentry에 기록.
- 부분 / 오래된 상태 — 데이터가 캐시 또는 부분 가져오기에서 온 경우.
- 성공 — 정상 경로.
목록 예시:
if (isLoading) return <ListSkeleton />
if (error) return <ErrorState onRetry={refetch} />
if (data.length === 0) return <EmptyState cta={...} />
return <List items={data} />
컴포넌트가 diff에 성공 상태만 표시했다면, 나머지 4개는 어디에 있는지 물어보세요.
모바일 우선
- 360px 너비를 먼저 디자인하고 확장.
- 터치 대상 ≥ 44×44px (Apple HIG) 또는 48×48px (Material).
- hover 전용 상호작용 없음: hover에서 사용 가능한 모든 것은 포커스/터치 경로도 있어야 함.
- 실제 기기 또는 Chrome DevTools throttled에서 테스트: 3G 네트워크, 4배 CPU 저속.
- 확대 없이 양식 작동: input
font-size: 16px+(그렇지 않으면 iOS가 포커스 시 확대).
도구 참조
CI / 병합 전:
- Lighthouse CI 지표별 예산 포함.
@axe-core/cli또는 Lighthouse 접근성 하위 집합.bundlewatch예산을 초과하는 번들로 PR을 실패 처리.- 시각적 회귀: Chromatic (Storybook), Percy 또는 Playwright 스크린샷 검사.
로컬:
- Chrome DevTools Performance + Lighthouse 패널.
unlighthouse전체 사이트 감사.@unjs/check-vitals일회성 Web Vitals.
출력 형식
**[차단|경고|제안] [FE-N] <한 줄 설명>**
위치: `path/to/Component.tsx:42-58`
범주: <a11y | perf | states | mobile | semantic>
영향: <어떤 사용자 / 시나리오 / 지표>
수정: <구체적인 변경, 적합하면 코드>
검증: <확인 방법 — Lighthouse 실행 / 수동 테스트 / 등>
심각도:
- 차단(BLOCKING): WCAG AA 위반, 핵심 흐름 손상, Web Vitals 'Poor' 범위. 반드시 수정.
- 경고(WARN): 모범 사례 격차, 아직 사용자에게 보이지 않는 성능 회귀.
- 제안(NIT): 개선 기회 (더 작은 번들, 더 나은 스켈레톤).
하드 룰
- 버튼에
<div onClick>사용 금지. 항상. alt없는 이미지 금지. 항상.- 실제 콘텐츠로 인한 레이아웃 변경 금지. 항상 공간을 예약.
- 소리와 함께 자동 재생 금지. 항상.
prefers-reduced-motion존중. 항상.
이 스킬이 다루지 않는 것
- 백엔드 (
web-security-baseline사용). - DB 스키마 / 쿼리 (
data-migration-safety사용). - 브랜드 아이덴티티 / 시각적 디자인 (
brand-creation사용). - 새 랜딩 페이지 구축 (
landing-page-fast사용, 스캐폴딩 제공).
참고 자료
- WCAG 2.2 빠른 참조
- web.dev Core Web Vitals 문서
- web.dev Performance 학습 경로
- Heydon Pickering의 Inclusive Components
- A11y Project 체크리스트