frontend — 모든 사용자, 모든 상태에서 작동하며 변경하기 쉬운 화면
저장소의 CLAUDE.md, docs/conventions.md, docs/design-system.md를 먼저 따르세요. 이 스킬은 그 사이의 빈틈을 채우고 절대 덮어쓰지 않습니다. 시각적 작업(토큰, 레이아웃, 색상, CSS)은 css-design에 속합니다. 앱을 사용자처럼 실행하고 최종 판정을 내리는 것은 verify-app에 속합니다.
스택을 선택하세요. Vue 3와 React는 모두 일급(first-class)이며 동일한 규칙을 따릅니다. 저장소에 이미 있는 프레임워크를 사용하세요. 새 앱의 경우 전달 프로필(프로필 README 참조)의 프론트엔드 기본값을 사용하고, 그렇지 않으면 트리아지(triage)에서 한 번 물어보세요. Vue: TypeScript + Vite (npm create vue@latest -- --ts --router --pinia --vitest --playwright --eslint --prettier) 및 references/vue-patterns.md. React: TypeScript + Vite (또는 저장소의 메타 프레임워크) 및 references/react-patterns.md. 한 앱에서 프레임워크를 절대 혼용하지 마세요. SEO를 위해 SSR 또는 SSG가 필요합니까? Nuxt 또는 Next.js를 ADR(architect)에 기록하세요. 이 스킬은 어느 쪽이든 클라이언트 측 부분을 다룹니다.
워크플로우
- 저장소의 컨벤션과 인벤토리를 로드합니다.
CLAUDE.md,docs/conventions.md,docs/design-system.md를 읽고, 재사용 가능한 부분을 나열하세요(ls src/components src/features src/composables src/hooks src/stores). 데이터 계층, 라우터, 폼 및 i18n 라이브러리를 감지하세요 (Vue:grep -E '"(@tanstack/vue-query|pinia|vue-router|vee-validate|vue-i18n)"' package.json; React:grep -E '"(@tanstack/react-query|@tanstack/react-router|react-router|react-hook-form|i18next|react-intl)"' package.json). 두 번째 데이터 계층, 라우터, 스토어, 폼 또는 i18n 라이브러리를 절대 추가하지 마세요. 증거: 스토리 노트에 재사용된 컴포넌트와 composable/hook 및 새로 만든 것을 이유와 함께 나열합니다. - 코드 작성 전에 상태 테이블을 작성합니다. 각 뷰에 대해: 모든 데이터 조각과 그 위치 — 서버(쿼리), URL(라우트 파라미터/검색), 공유 클라이언트(Pinia 스토어, React 컨텍스트 또는 저장소의 스토어), 로컬(
ref/useState), 파생(computed/렌더 내 const), 외부(composable 또는 브라우저 API를 감싸는useSyncExternalStore). 그런 다음 네 가지 상태(로딩, 빈, 에러, 성공)와 각각의 카피 키를 나열합니다. 증거: 스토리의 테이블. - 먼저 실패하는 테스트를 작성합니다. Vitest +
@testing-library/vue또는@testing-library/react,userEvent.setup(), role 및 접근 가능한 이름으로 쿼리, 테스트당 하나의 동작, 서비스 경계에서 네트워크를 가짜로 처리(MSW 또는src/test-utils의 저장소 가짜 클라이언트). 각 상태, 키보드 경로 및 하나의 부정적 사례를 다룹니다. 증거: 실행 결과가 올바른 이유로 빨간색을 표시합니다. - 기능 폴더 내부에서 구축합니다.
src/features/<name>/services(타입이 있는 API 호출, 프레임워크 없음) →composables/hooks(useQuery/useMutation과 키 팩토리를 감싸는useX) →components(<script setup lang="ts">가 있는Name.vue또는Name.tsx) → 이들을 조합하는 라우트 뷰. Props와 events는 타입이 지정되고,any금지, non-null!금지,@/임포트 사용. 증거:git diff --stat이 기능 내부와 의도된 공유 변경 사항 외에는 벗어나지 않습니다. - 카피 및 포매팅. 모든 표시 문자열은 로케일 파일(vue-i18n
useI18n(), 또는 React 저장소의 i18n 라이브러리 또는 카피 모듈)에서 가져오며 모든 로케일에 동일한 키가 있습니다. 테스트가 일관성을 강제합니다. 숫자와 날짜는 i18n 포매터 또는Intl을 통해 처리합니다. 증거: 일관성 테스트 통과; 하드코딩된 카피 없음, 린트가 강제(@intlify/vue-i18n/no-raw-text; React:react/jsx-no-literals또는 i18n 플러그인 규칙) — 린트 종료 코드를 인용합니다. css-design으로 스타일링합니다. 토큰만 사용, BEM 블록 = 컴포넌트 이름, 모든 상태에 스타일 적용;.vue스타일은postcss-html을 통해 린트 검사. 증거: Stylelint 통과.- 스토리 (컴포넌트 라이브러리). 상태별로 하나의 스토리, 두 테마 모두; Storybook axe 통과. 증거: 스토리 테스트 및 axe 실행.
- 게이트, 종료 코드로 연결.
npm run lint && npm run type-check && npm run test:unit -- --run(Vue:vue-tsc --build; React:tsc -b), 또는make ci;| tail로 판단하지 마세요. 증거: 종료 코드 0 및 커버리지가 저장소 임계값 이상. - 프로덕션 빌드에 대해 실제 브라우저에서 여정 테스트.
npm run build후 출력을 서빙합니다(vite preview또는 저장소가 사용하는 정적 서버).@axe-core/playwright로 주요 경로에 대한 Playwright 스펙을 추가하거나 확장합니다(태그wcag2a wcag2aa wcag21aa wcag22aa, 심각 및 치명 = 0).docs/delivery/<slug>/evidence/<view>-390.png및-1280.png를 캡처하고, 앱에 다크 테마가 있으면 함께 캡처합니다. 모든 스크린샷을 인용하기 전에 엽니다. 증거: 스펙 통과, PNG 파일. - 키보드 및 스크린리더 통과. 새 뷰를 Tab으로 이동: 순서, 보이는 포커스, Escape가 오버레이를 닫고 포커스가 트리거로 돌아옴; 내비게이션 후 포커스가 새 제목에 도달하고 제목이 변경됨. 스크린 리더(VoiceOver
Cmd+F5또는 NVDA)로 이름과 알림을 점검합니다. 증거: 스토리의 QA 섹션에 세 줄 노트. - 인계. 엔드투엔드 클레임을 위해
verify-app을 실행합니다.deliver-loop내에서 독립 리뷰가 아이템을 평가합니다. 여기에 그들의 단계를 다시 설명하지 마세요.
핵심 체크리스트
- 파생 가능한 것은 저장하지 마세요:
computed사용 (React: 렌더 내 const); 다른 상태를 설정하는 것이 유일한 목적인 watcher나 effect를 사용하지 마세요. - Watcher와 effect는 외부(DOM API, 구독, 타이머, 서드파티 위젯)와만 동기화하고 정리(
onWatcherCleanup/onUnmounted; React: effect의 return)합니다. 사용자 동작으로 인한 작업은 이벤트 핸들러에서 실행합니다. - 서버 데이터는 로컬 상태나 스토어에 복사하지 마세요. 쿼리 키는 페치가 사용하는 모든 입력을 포함합니다(Vue에서 ref나 getter로, 변경 시 재페치).
- 사용자가 공유하거나 리로드할 선택, 필터, 탭, 페이지네이션은 URL 쿼리에 위치합니다.
- 모든 데이터 뷰는 로딩, 빈, 에러(재시도 포함) 및 성공을 렌더링하며 테스트는 네 가지를 모두 다룹니다.
- 모든 입력에는 보이는
<label for>가 있습니다(두 프레임워크 모두useId()의 id 사용); 오류는aria-describedby로 연결된 텍스트, 입력은 유지, 제출 시 포커스는 첫 번째 유효하지 않은 필드로 이동합니다. - 네이티브 요소 우선 (
button,a href/라우터 링크,dialog,details,input type);div @click/div onClick금지. - 커스텀 위젯은 해당 WAI-ARIA APG 패턴을 따르며 키보드 포함.
- 포커스를 이동하지 않는 비동기 결과는 한 번 렌더링되고 비어 있는
role="status"/role="alert"영역을 통해 알립니다(Vue: body로 teleport; React: portal, 뷰보다 오래 살아야 하는 경우). - 라우트 변경:
document.title업데이트 및 포커스를 페이지h1또는main으로 이동(Vue:router.afterEach+ 템플릿 ref; React: 라우터의 동등한 기능). - 오류는 라우트별, 위험한 위젯별로 잡고(Vue:
onErrorCaptured경계 컴포넌트; React: error boundary) 재시도를 보여주며, 빈 페이지를 보여주지 않습니다. - 하드코딩된 표시 문자열 금지; 모든 로케일이 동일한 키를 가집니다.
- 테스트는 role/레이블/텍스트로 쿼리하고,
user-event로 상호작용하며, 클래스 이름, 컴포넌트 내부(wrapper.vm, state 값) 또는 composable/hook 호출을 단언하지 않습니다. v-html/dangerouslySetInnerHTML은 사용 직전에 DOMPurify로 살균하지 않으면 금지; 데이터에서 온 URL은https:/mailto:/상대 경로로 허용 목록에 추가합니다.shallowRef/markRaw/v-memo/KeepAlive(React:memo/useMemo/useCallback)는 프로파일러 측정 또는 문서화된 필요가 있을 때만 사용합니다.- 차트 및 데이터 시각화는 접근 가능한 이름과 요약, 데이터 테이블(또는 텍스트) 대안, 색맹 친화적인 팔레트(시리즈는 레이블, 모양 또는 패턴으로도 구분 가능), 상호작용에 대한 키보드 접근을 제공합니다 — 저장소가 사용하는 차트 라이브러리에 관계없이.
안티패턴
Vue:
watch(items, v => { filtered.value = v.filter(...) })—computed를 사용하세요.const { count } = reactive(state)또는const { name } = store— 반응성 손실됨;toRefs/storeToRefs를 사용하거나ref를 선호하세요.watch(props.id, ...)또는 구조 분해된 prop을 composable에 전달 — getter() => id를 전달하세요.- Prop 또는
useQuery데이터를 변경; 쿼리 데이터를v-model에 직접 바인딩 — 폼에 복사하세요. - 같은 요소에
v-if와v-for; 재정렬 가능한 리스트에:key="index". - composable에서
onWatcherCleanup전이나 생명주기 훅 등록 전에await. - 라우트 컴포넌트에
defineAsyncComponent(라우트는() => import(...)를 직접 받습니다). - 내비게이션 가드에서
next()—false, 위치 또는 아무것도 반환하지 않음. wrapper.vm에 접근하거나setData를 호출하는 테스트.
React:
useEffect(() => setFiltered(items.filter(...)), [items])— 렌더에서 계산하세요.- id가 변경될 때 effect에서 폼 초기화 —
key={id}를 전달하세요. - 조건이나 루프 내에서 훅 호출; 상태를 대체하지 않고 직접 변이.
- 재정렬 가능한 리스트에
key={index}; 측정된 이유로 메모이제이션된 자식에 매 렌더마다 새로운 객체나 함수 prop 전달.
둘 다:
- 취소, 캐시, 재시도 또는 에러 분기 없이 마운트 시 페치 — 데이터 레이어를 사용하세요.
- 빈 또는 에러 분기가 없는 스피너;
console.log로 오류 삼킴. - 매일 변경되는 데이터에 대해
staleTime을 0으로 남겨두어 매 포커스마다 재페치. - 스냅샷, 롤백 및 settle 시 무효화 없는 낙관적 업데이트.
- 레이블 대신 Placeholder 사용; 오류 표시에 색상만 사용; 키보드 없는
role="button". - 모든 곳에서
getByTestId사용; 전체 페이지 스냅샷; 구현 세부 사항 테스트. - 모든 것을 첫 번째 라우트 청크로 가져오는 기능 배럴 파일.
:href="user.url"/href={user.url}스킴 확인 없이 사용.
"완료" 전 증거
- 각 상태와 키보드 경로에 대해 실패 후 통과하는 단위 테스트.
- Lint,
vue-tsc또는tsc, 테스트가 연결되어 종료 코드 0; 커버리지가 임계값 이상. - 프로덕션 빌드에 대한 Playwright 여정, axe 심각/치명 = 0.
docs/delivery/<slug>/evidence/에 390px 및 1280px 스크린샷(다크 테마 포함), 각각 열어봄.- 스토리의 QA 섹션에 키보드 및 스크린리더 노트.
- 그런 다음
verify-app.
참고 자료
references/vue-patterns.md— Vue 체크리스트: SFC 및 컴포넌트 API, 반응성 함정, computed vs watch, composable, Pinia, Vue용 TanStack Query, Vue Router(URL 상태, 가드, 포커스), 폼, i18n, 접근성 세부 사항, 성능, 보안, 도구; 각 항목에 출처 포함.references/react-patterns.md— React 체크리스트: 동일 주제에 정적 내보내기(Next.js/Nuxt generate) 변형 포함.references/a11y-and-testing.md— UI 코드를 위한 WCAG 2.2 AA 항목, ARIA/APG, 키보드 및 라이브 리전, 차트, Vue 및 React용 Testing Library, MSW, Playwright + axe, 테스트하지 말아야 할 것.