docs: audit frontend platform capabilities
This commit is contained in:
@@ -0,0 +1,868 @@
|
||||
# 디자인 시스템 플랫폼 계약
|
||||
|
||||
이 문서는 도메인 기능을 추가하기 전에 프론트엔드 스켈레톤이 제공해야 하는
|
||||
디자인 시스템의 소유권, 계층, 기본 구성요소, 외부 라이브러리 경계, 검증 방법을
|
||||
정의한다. 목표는 특정 제품의 시각 언어를 미리 결정하는 것이 아니라, 제품 팀이
|
||||
접근성·반응형·국제화·테스트 규칙을 다시 발명하지 않고 기능 화면을 만들 수 있게
|
||||
하는 것이다.
|
||||
|
||||
이 문서에서 `필수`는 모든 제품이 기반으로 사용할 계약을 뜻한다. `선택`은
|
||||
컴포넌트의 경계와 도입 기준은 제공하지만 실제 의존성이나 구현은 제품 요구가
|
||||
생긴 뒤 추가해도 되는 항목을 뜻한다.
|
||||
|
||||
## 1. 현재 기준선과 확인된 공백
|
||||
|
||||
현재 저장소에는 다음 기반이 이미 있다.
|
||||
|
||||
- `src/presentation/styles/theme.css`
|
||||
- 의미 기반 light/dark 색상 토큰
|
||||
- focus indicator
|
||||
- 반응형 앱 셸
|
||||
- reduced-motion 처리
|
||||
- `src/presentation/providers/theme-provider.jsx`
|
||||
- `system`, `light`, `dark` 선호도
|
||||
- 저장소 포트를 통한 선호도 영속화
|
||||
- 운영체제 색상 변경 구독
|
||||
- `src/presentation/components/ui/`
|
||||
- `Button`
|
||||
- `TextField`
|
||||
- `Card`
|
||||
- `Alert`
|
||||
- `Badge`
|
||||
- `Dialog`
|
||||
- `src/presentation/components/async-surface.jsx`
|
||||
- 초기 로딩, 빈 화면, terminal error, background 상태
|
||||
- `src/presentation/components/state-surfaces.jsx`
|
||||
- 인증 필요, 권한 없음, 찾을 수 없음
|
||||
- `/examples/ui`, `/examples/states`
|
||||
- 실행 가능한 primitive와 상태 예제
|
||||
- component/E2E/axe 테스트
|
||||
- 필드 설명과 오류 연결
|
||||
- native dialog 닫기와 trigger focus 복원
|
||||
- 320px reflow
|
||||
- Chromium, Firefox, WebKit
|
||||
- 등록 라우트의 자동 접근성 검사
|
||||
|
||||
이 기반은 유효하지만 아직 디자인 시스템 플랫폼 전체는 아니다.
|
||||
|
||||
1. 토큰이 색상 중심이며 typography, elevation, motion, z-layer, control size,
|
||||
breakpoint가 계약으로 닫혀 있지 않다.
|
||||
2. 공통 입력은 `TextField` 하나뿐이어서 일반적인 폼을 공통 규칙으로 만들 수
|
||||
없다.
|
||||
3. 앱 셸과 예제 화면에서 공용 primitive 대신 raw `button`, `select`, 링크
|
||||
class를 다시 작성하는 곳이 있다.
|
||||
4. `☰`, `×` 같은 문자 glyph를 직접 사용하며 아이콘 공급자 경계가 없다.
|
||||
5. `theme.css` 하나가 token, layout, primitive, pattern 스타일을 모두 소유한다.
|
||||
6. runtime gallery는 있지만 격리된 story, interaction story, 시각 회귀 기준선이
|
||||
없다.
|
||||
7. 사용자 문구가 한국어 literal로 고정되어 locale과 RTL 계약이 없다.
|
||||
8. `StandardPage`, `CollectionPage`, `DetailPage`, `FormPage` 같은 공통 페이지
|
||||
템플릿이 없다.
|
||||
9. 모바일 sidebar는 표시 전환은 되지만 modal drawer 수준의 focus 이동, focus
|
||||
복원, 배경 비활성화 계약은 없다.
|
||||
|
||||
따라서 기존 구성요소는 폐기하지 않고 아래 목표 계층으로 이동·확장한다.
|
||||
|
||||
## 2. 소유권과 의존성 원칙
|
||||
|
||||
디자인 시스템은 `presentation` 계층이 소유한다. 색상, 아이콘, 키보드 상호작용,
|
||||
포커스, 화면 배치와 같은 문제는 도메인 규칙이 아니다.
|
||||
|
||||
```text
|
||||
product page
|
||||
-> template
|
||||
-> pattern
|
||||
-> primitive
|
||||
-> token
|
||||
```
|
||||
|
||||
외부 UI 라이브러리를 사용하는 경우 흐름은 다음과 같다.
|
||||
|
||||
```text
|
||||
product page
|
||||
-> local design-system API
|
||||
-> local vendor facade
|
||||
-> Lucide / React Aria / Radix
|
||||
```
|
||||
|
||||
다음 규칙은 필수다.
|
||||
|
||||
- 제품 페이지는 `lucide-react`, `react-aria-components`, `@radix-ui/*`를 직접
|
||||
import하지 않는다.
|
||||
- 외부 UI 라이브러리 type을 공용 컴포넌트 API로 그대로 노출하지 않는다.
|
||||
- 도메인과 application 계층은 React component, CSS class, icon name을 알지
|
||||
않는다.
|
||||
- primitive는 API 요청, query cache, 인증 상태와 같은 외부 상태를 직접 읽지
|
||||
않는다.
|
||||
- pattern과 template도 application use case를 직접 선택하지 않는다. 필요한
|
||||
상태와 command callback을 props/slot으로 받는다.
|
||||
- 제품별 색상이나 명칭을 primitive 내부에 하드코딩하지 않는다.
|
||||
- 같은 의미의 접근성·키보드 동작을 페이지마다 다시 구현하지 않는다.
|
||||
- 세 번째 사용 사례가 확인되기 전에는 제품 전용 조합을 무리하게 primitive로
|
||||
승격하지 않는다.
|
||||
|
||||
아이콘과 headless UI는 React inbound adapter의 vendor facade로 충분하다.
|
||||
이들을 위한 application port를 만들지 않는다. HTTP, storage, telemetry처럼
|
||||
런타임 외부 자원을 교체하는 capability와 UI 구현 라이브러리를 구분한다.
|
||||
|
||||
## 3. 목표 디렉터리
|
||||
|
||||
TypeScript 전환 이후의 목표 구조는 다음과 같다. 마이그레이션 중에는 기존
|
||||
경로에서 같은 소유권 규칙을 지키고, 한 번에 전체 경로를 이동하지 않아도 된다.
|
||||
|
||||
```text
|
||||
src/adapters/inbound/react/design-system/
|
||||
index.ts
|
||||
tokens/
|
||||
primitive.css
|
||||
semantic.css
|
||||
component.css
|
||||
token-contract.ts
|
||||
primitives/
|
||||
button/
|
||||
field/
|
||||
checkbox/
|
||||
dialog/
|
||||
...
|
||||
patterns/
|
||||
async-surface/
|
||||
form/
|
||||
data-table/
|
||||
confirmation/
|
||||
...
|
||||
templates/
|
||||
standard-page/
|
||||
collection-page/
|
||||
detail-page/
|
||||
form-page/
|
||||
status-page/
|
||||
icons/
|
||||
icon.tsx
|
||||
icon-button.tsx
|
||||
semantic-icons.tsx
|
||||
vendors/
|
||||
lucide.tsx
|
||||
vendors/
|
||||
react-aria/
|
||||
testing/
|
||||
story-decorators.tsx
|
||||
render-design-system.tsx
|
||||
```
|
||||
|
||||
`index.ts`는 제품의 React inbound 코드가 사용할 public API다. 내부 파일 deep
|
||||
import는 디자인 시스템 자체와 테스트에만 허용한다. 이 경계는 ESLint의
|
||||
`no-restricted-imports`로 검사한다.
|
||||
|
||||
현재 `src/presentation`은 React inbound adapter 역할을 한다. TypeScript
|
||||
마이그레이션 동안 기존 경로를 유지할 수 있지만, 최종 canonical target은 위
|
||||
경로다. 같은 컴포넌트를 `presentation`과 `adapters/inbound/react` 양쪽에
|
||||
복제하지 않고 feature 단위로 이동한다.
|
||||
|
||||
## 4. 계층 계약
|
||||
|
||||
### 4.1 Tokens
|
||||
|
||||
토큰은 시각적 결정을 이름으로 표현한다. 토큰은 세 계층으로 관리한다.
|
||||
|
||||
```text
|
||||
primitive token -> semantic token -> component token
|
||||
```
|
||||
|
||||
예:
|
||||
|
||||
```css
|
||||
--palette-blue-600: ...;
|
||||
--color-action: var(--palette-blue-600);
|
||||
--button-primary-background: var(--color-action);
|
||||
```
|
||||
|
||||
#### Primitive tokens
|
||||
|
||||
원시 palette와 scale이다. 제품 코드에서 직접 소비하지 않는다.
|
||||
|
||||
- palette
|
||||
- spacing scale
|
||||
- font size와 line height scale
|
||||
- radius scale
|
||||
- shadow scale
|
||||
- duration과 easing scale
|
||||
- fixed size scale
|
||||
|
||||
#### Semantic tokens
|
||||
|
||||
제품 코드와 대부분의 primitive가 소비하는 이름이다.
|
||||
|
||||
- `surface`, `surface-muted`, `surface-elevated`
|
||||
- `content`, `content-muted`, `content-inverse`
|
||||
- `border`, `border-strong`
|
||||
- `action`, `action-hover`, `action-pressed`
|
||||
- `danger`, `warning`, `success`, `info`
|
||||
- `focus-ring`
|
||||
- `disabled-content`, `disabled-surface`
|
||||
|
||||
#### Component tokens
|
||||
|
||||
특정 primitive가 여러 semantic token을 조합할 때만 사용한다.
|
||||
|
||||
- `button-primary-background`
|
||||
- `field-border-invalid`
|
||||
- `dialog-elevation`
|
||||
- `navigation-active-background`
|
||||
|
||||
컴포넌트 토큰은 제품별 variant를 만들기 위한 우회 경로가 아니다. 두 개 이상의
|
||||
컴포넌트가 같은 의미를 공유한다면 semantic token으로 승격한다.
|
||||
|
||||
#### 필수 토큰 범주
|
||||
|
||||
| 범주 | 필수 내용 |
|
||||
| --- | --- |
|
||||
| Color | surface/content/border/action/status/focus, light/dark |
|
||||
| Typography | family, size, line-height, weight, letter-spacing |
|
||||
| Spacing | inset, inline, stack, section, page spacing scale |
|
||||
| Size | control height, icon size, touch target, container width |
|
||||
| Border | width, style, semantic border |
|
||||
| Radius | control, surface, modal, full |
|
||||
| Elevation | panel, popover, dialog, sticky shell |
|
||||
| Z-layer | base, sticky, navigation, popover, modal, toast |
|
||||
| Motion | fast/normal/slow duration, standard/emphasized easing |
|
||||
| Breakpoint | compact, medium, wide와 container 계약 |
|
||||
| Opacity | disabled, scrim, skeleton |
|
||||
|
||||
토큰의 완료 조건은 다음과 같다.
|
||||
|
||||
- light와 dark에서 모든 semantic token이 정의된다.
|
||||
- `forced-colors`에서도 focus와 control 경계가 사라지지 않는다.
|
||||
- 상태는 색상만으로 구분하지 않는다.
|
||||
- 같은 raw value가 반복되면 named token으로 승격한다.
|
||||
- 사용자 입력으로 CSS class나 CSS variable 이름을 조립하지 않는다.
|
||||
- token contract test가 필수 token의 누락을 차단한다.
|
||||
- chart나 canvas처럼 JavaScript 값이 필요한 경우에만 typed token accessor를
|
||||
제공한다.
|
||||
|
||||
## 5. 기본 컴포넌트 카탈로그
|
||||
|
||||
### 5.1 Primitives
|
||||
|
||||
Primitive는 하나의 접근 가능한 상호작용 또는 작은 시각 단위를 제공한다.
|
||||
|
||||
| 그룹 | 기본 제공 | 우선순위 | 핵심 계약 |
|
||||
| --- | --- | --- | --- |
|
||||
| Action | `Button` | 필수 | intent, size, disabled, pending, native semantics |
|
||||
| Action | `LinkButton` | 필수 | navigation은 anchor/router link semantics 유지 |
|
||||
| Action | `IconButton` | 필수 | accessible name 필수, 44px 권장 target |
|
||||
| Form | `Field` | 필수 | label, description, error ID 조립 |
|
||||
| Form | `TextField` | 필수 | text/email/password/search/autocomplete |
|
||||
| Form | `TextArea` | 필수 | resize와 글자 수 안내 |
|
||||
| Form | `Select` | 필수 | native 우선, 복합 선택은 headless 구현 |
|
||||
| Form | `Checkbox` | 필수 | checked/indeterminate |
|
||||
| Form | `RadioGroup` | 필수 | arrow-key와 group label |
|
||||
| Form | `Switch` | 필수 | boolean setting 전용 |
|
||||
| Form | `SearchField` | 필수 | clear action, submit semantics |
|
||||
| Feedback | `Alert` | 필수 | inline feedback와 live-region 정책 분리 |
|
||||
| Feedback | `Badge` | 필수 | color-only 금지 |
|
||||
| Feedback | `Spinner` | 필수 | accessible label 또는 decorative |
|
||||
| Feedback | `ProgressBar` | 필수 | determinate/indeterminate |
|
||||
| Feedback | `Skeleton` | 필수 | 실제 layout과 유사한 크기, reduced motion |
|
||||
| Feedback | `Toast` | 필수 | queue, 중복 방지, timeout pause |
|
||||
| Surface | `Card` | 필수 | heading level 강제 금지, label 선택 가능 |
|
||||
| Surface | `Separator` | 필수 | decorative/semantic 구분 |
|
||||
| Overlay | `Dialog` | 필수 | modal semantics, focus trap/restore, Escape |
|
||||
| Overlay | `Drawer` | 필수 | compact navigation과 side sheet |
|
||||
| Overlay | `Popover` | 필수 | anchor, dismiss, collision |
|
||||
| Overlay | `Tooltip` | 필수 | hover와 keyboard, 중요한 정보 단독 보유 금지 |
|
||||
| Overlay | `Menu` | 필수 | roving focus, typeahead, Escape |
|
||||
| Navigation | `Breadcrumbs` | 필수 | 현재 위치와 overflow |
|
||||
| Navigation | `Tabs` | 필수 | manual/automatic activation 정책 |
|
||||
| Navigation | `Pagination` | 필수 | current page, previous/next label |
|
||||
| Utility | `VisuallyHidden` | 필수 | screen-reader-only content |
|
||||
| Utility | `Portal` | 필수 | overlay root와 SSR-safe fallback |
|
||||
| Utility | `FocusRing` | 필수 | input modality 인식 |
|
||||
| Advanced | date/time picker | 선택 | locale/time zone 요구가 있을 때 |
|
||||
| Advanced | file upload/dropzone | 선택 | upload adapter 요구가 있을 때 |
|
||||
| Advanced | virtualizer/tree | 선택 | 실제 데이터 규모가 입증될 때 |
|
||||
|
||||
Primitive API는 다음 규칙을 지킨다.
|
||||
|
||||
- 기본 HTML semantics를 보존한다.
|
||||
- `div onClick`로 button이나 link를 흉내 내지 않는다.
|
||||
- `variant`, `size`, `tone`은 closed union과 정적 class map을 사용한다.
|
||||
- pending은 focus를 잃게 하는 무조건적인 `disabled`와 구분한다.
|
||||
- controlled와 uncontrolled 지원 여부를 문서화한다.
|
||||
- ref 전달과 focus contract를 테스트한다.
|
||||
- `className`은 escape hatch이지 public variant를 대체하지 않는다.
|
||||
- 임의의 polymorphic `as`보다 `Button`과 `LinkButton`처럼 semantics가 명확한
|
||||
API를 우선한다.
|
||||
- visual-only prop이 도메인 의미를 표현하지 않도록 한다.
|
||||
|
||||
### 5.2 Patterns
|
||||
|
||||
Pattern은 여러 primitive를 조합해 반복되는 사용자 문제를 해결한다.
|
||||
|
||||
| Pattern | 필수 내용 |
|
||||
| --- | --- |
|
||||
| `AsyncSurface` | loading, success, empty, terminal error, stale, refresh |
|
||||
| `AccessSurface` | auth required, forbidden, not found |
|
||||
| `Form` | submit, error summary, first-invalid focus, pending |
|
||||
| `ConfirmationDialog` | destructive action 설명과 명시적 확인 |
|
||||
| `ToastRegion` | queue, announcement, dismiss, focus policy |
|
||||
| `SearchFilterToolbar` | search, filter, reset, result count |
|
||||
| `DataTable` | caption, sorting, selection, responsive fallback |
|
||||
| `PaginationBar` | result range와 page navigation |
|
||||
| `NavigationDrawer` | mobile focus scope와 background inert |
|
||||
| `DisclosureGroup` | help/settings section |
|
||||
|
||||
Pattern은 application failure object 전체를 렌더링하지 않는다. 안전한 message key,
|
||||
사용자 action, 표시 가능한 metadata만 받는다.
|
||||
|
||||
### 5.3 Templates
|
||||
|
||||
Template은 페이지 레이아웃과 상태 slot을 제공한다. API 호출과 도메인 use case는
|
||||
소유하지 않는다.
|
||||
|
||||
#### `StandardPageTemplate`
|
||||
|
||||
- breadcrumb 또는 back navigation
|
||||
- eyebrow
|
||||
- `h1`
|
||||
- description
|
||||
- status/metadata
|
||||
- primary/secondary actions
|
||||
- main content와 aside slot
|
||||
|
||||
#### `CollectionPageTemplate`
|
||||
|
||||
- page header
|
||||
- search/filter/sort toolbar
|
||||
- result count
|
||||
- loading/empty/error slot
|
||||
- table/list/card-grid slot
|
||||
- pagination slot
|
||||
- compact viewport에서의 대체 배치
|
||||
|
||||
#### `DetailPageTemplate`
|
||||
|
||||
- breadcrumb/back
|
||||
- title과 entity status
|
||||
- page actions
|
||||
- description list/section slot
|
||||
- loading/not-found/forbidden/error 상태
|
||||
|
||||
#### `FormPageTemplate`
|
||||
|
||||
- heading과 설명
|
||||
- error summary
|
||||
- field section
|
||||
- sticky 또는 inline action bar
|
||||
- submit/cancel
|
||||
- submitting, success, conflict
|
||||
- unsaved-change navigation blocker 연결 지점
|
||||
|
||||
#### `StatusPageTemplate`
|
||||
|
||||
- 401, 403, 404, 500, offline, maintenance
|
||||
- safe description
|
||||
- primary recovery action
|
||||
- optional support reference
|
||||
- raw endpoint, stack, token 노출 금지
|
||||
|
||||
Template의 heading 순서는 slot 소비자가 임의로 깨뜨리지 못하게 예제와 테스트로
|
||||
고정한다. 다만 `Card` 같은 하위 primitive가 무조건 `h3`를 생성해서도 안 된다.
|
||||
|
||||
## 6. Lucide 아이콘 facade
|
||||
|
||||
[Lucide for React 공식 문서](https://lucide.dev/guide/react)는 각 아이콘을 독립
|
||||
React component로 제공하며 명시적으로 import한 아이콘만 번들에 포함될 수 있는
|
||||
tree-shaking 구조를 제공한다. 기본 아이콘 공급자로 사용하기에 적합하지만 제품
|
||||
코드가 공급자 API에 결합되면 안 된다.
|
||||
|
||||
### 6.1 필수 규칙
|
||||
|
||||
1. `lucide-react` import는 `icons/vendors/lucide.tsx`에서만 허용한다.
|
||||
2. 제품 코드에는 Lucide component type, icon name, stroke prop을 노출하지 않는다.
|
||||
3. 디자인 시스템이 승인한 작은 아이콘 집합만 명시적으로 import한다.
|
||||
4. 모든 아이콘을 이름으로 동적 import하는 범용 `DynamicIcon`은 기본 제공하지
|
||||
않는다.
|
||||
5. 아이콘 색은 기본적으로 `currentColor`를 사용한다.
|
||||
6. 크기와 stroke는 `--icon-size-*`, `--icon-stroke-*` 토큰을 사용한다.
|
||||
7. 장식 아이콘은 기본적으로 `aria-hidden="true"`와 `focusable="false"`다.
|
||||
8. 정보를 단독으로 전달하는 아이콘은 local `Icon` API의 `label`을 통해
|
||||
`role="img"`와 accessible name을 가진다.
|
||||
9. 버튼 안의 아이콘은 장식으로 처리하고 버튼 자체에 visible label 또는
|
||||
`aria-label`을 둔다.
|
||||
10. 상태를 아이콘이나 색상 하나만으로 전달하지 않는다.
|
||||
|
||||
### 6.2 목표 API 예시
|
||||
|
||||
아래 예시는 방향을 설명한다. 실제 type과 경로는 TypeScript 전환 작업에서
|
||||
확정한다.
|
||||
|
||||
```tsx
|
||||
import { MenuIcon } from "@/presentation/design-system/icons";
|
||||
import { IconButton } from "@/presentation/design-system";
|
||||
|
||||
<IconButton accessibleName="메뉴 열기" onPress={openNavigation}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
vendor 파일은 의미 이름과 공급자 이름을 분리한다.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Menu as LucideMenu,
|
||||
X as LucideClose,
|
||||
AlertTriangle as LucideWarning,
|
||||
} from "lucide-react";
|
||||
|
||||
export const MenuGlyph = LucideMenu;
|
||||
export const CloseGlyph = LucideClose;
|
||||
export const WarningGlyph = LucideWarning;
|
||||
```
|
||||
|
||||
`MenuGlyph` 같은 vendor export는 `semantic-icons.tsx` 밖으로 다시 노출하지
|
||||
않는다. 제품 코드는 `MenuIcon`, `CloseIcon`, `WarningIcon`만 사용한다.
|
||||
|
||||
### 6.3 아이콘 추가 완료 조건
|
||||
|
||||
- 기존 semantic icon으로 표현할 수 없는지 먼저 확인했다.
|
||||
- 서로 다른 제품 기능이 같은 의미 아이콘을 공유한다.
|
||||
- accessible name과 decorative 여부를 결정했다.
|
||||
- light/dark/high-contrast에서 보인다.
|
||||
- 200% zoom과 compact viewport에서 잘리지 않는다.
|
||||
- 신규 import 후 bundle budget을 통과한다.
|
||||
- 공급자 교체 시 제품 코드 수정이 필요하지 않다.
|
||||
|
||||
## 7. Headless UI 선택 기준
|
||||
|
||||
복잡한 overlay, collection, focus management를 직접 구현하기 전에 native
|
||||
platform과 검증된 headless UI를 평가한다.
|
||||
|
||||
- [React Aria 공식 시작 문서](https://react-aria.adobe.com/getting-started)
|
||||
- [Radix Primitives 공식 소개](https://www.radix-ui.com/primitives/docs/overview/introduction)
|
||||
|
||||
### 7.1 선택 순서
|
||||
|
||||
1. native HTML만으로 요구 semantics와 모든 지원 브라우저 동작을 충족하는지
|
||||
확인한다.
|
||||
2. native 구현이 불충분하면 React Aria와 Radix를 평가한다.
|
||||
3. 한 프로젝트에서 같은 문제를 해결하는 headless 공급자를 둘 이상 기본값으로
|
||||
사용하지 않는다.
|
||||
4. 선택 결과와 버전·라이선스·bundle 영향을 ADR에 기록한다.
|
||||
5. 공급자 component를 local primitive로 감싼 뒤에만 제품 코드에 노출한다.
|
||||
|
||||
### 7.2 평가표
|
||||
|
||||
| 기준 | 확인 질문 |
|
||||
| --- | --- |
|
||||
| Semantics | WAI-ARIA pattern과 native semantics를 올바르게 사용하는가 |
|
||||
| Keyboard | roving focus, typeahead, Escape, arrow key가 완전한가 |
|
||||
| Focus | trap, restore, initial focus, nested overlay가 안전한가 |
|
||||
| Input modality | mouse, touch, keyboard, screen reader가 동일하게 동작하는가 |
|
||||
| i18n | RTL, locale number/date, IME를 지원하는가 |
|
||||
| Styling | semantic token과 Tailwind/CSS layer에 결합 가능한가 |
|
||||
| Bundle | component 단위 import와 tree shaking이 가능한가 |
|
||||
| React/Vite | 현재 React와 Vite 조합을 공식 지원하는가 |
|
||||
| SSR | future SSR profile에서 hydration ID가 안정적인가 |
|
||||
| Testing | 접근성·상호작용 테스트 자료와 utility가 있는가 |
|
||||
| Maintenance | release cadence, security 대응, license가 허용 가능한가 |
|
||||
|
||||
현재 스켈레톤의 접근성·국제화 목표에는 React Aria Components가 우선 후보다.
|
||||
Radix는 overlay와 primitive composition을 중심으로 평가할 수 있다. 최종 선택은
|
||||
실제 prototype으로 `Select`, `Menu`, `Drawer` 세 가지를 구현해 다음을 비교한 뒤
|
||||
확정한다.
|
||||
|
||||
- keyboard matrix
|
||||
- screen reader announcement
|
||||
- RTL
|
||||
- mobile touch
|
||||
- dark/high-contrast
|
||||
- gzip 증가량
|
||||
- local API로 감쌀 때 필요한 코드량
|
||||
|
||||
공급자를 선택하더라도 `SelectProps = AriaSelectProps`처럼 vendor type alias를
|
||||
그대로 공용 API로 만들지 않는다. 제품이 실제로 지원하는 의미만 local prop으로
|
||||
닫는다.
|
||||
|
||||
## 8. 접근성 계약
|
||||
|
||||
목표는 WCAG 2.2 AA에 맞춘 기본 동작이다. 자동 도구 통과는 적합성 선언이 아니며,
|
||||
키보드와 보조기술 검토를 함께 수행한다.
|
||||
|
||||
### 8.1 모든 컴포넌트의 공통 기준
|
||||
|
||||
- 올바른 native element와 role을 사용한다.
|
||||
- visible label 또는 accessible name이 있다.
|
||||
- keyboard만으로 모든 action을 실행할 수 있다.
|
||||
- focus 순서가 DOM과 시각 순서에 맞는다.
|
||||
- focus indicator를 제거하지 않는다.
|
||||
- disabled와 readonly를 구분한다.
|
||||
- 상태는 색상만으로 전달하지 않는다.
|
||||
- 오류는 해당 control과 programmatically 연결한다.
|
||||
- 필요한 변경만 live region으로 한 번 알린다.
|
||||
- `prefers-reduced-motion`에서 불필요한 motion을 제거한다.
|
||||
- 200% text zoom과 400% page zoom에서 정보가 손실되지 않는다.
|
||||
- 320 CSS px reflow에서 양방향 scroll을 강요하지 않는다.
|
||||
- touch target은 기본 44x44 CSS px를 목표로 한다.
|
||||
- forced-colors에서 border, focus, selected state가 사라지지 않는다.
|
||||
- RTL에서 logical direction과 key behavior를 검증한다.
|
||||
- 한글·일본어·중국어 IME composition 중 입력을 조기에 검증하거나 제출하지
|
||||
않는다.
|
||||
|
||||
### 8.2 Overlay 기준
|
||||
|
||||
- 열기 trigger를 기록한다.
|
||||
- 열릴 때 의미 있는 initial focus를 둔다.
|
||||
- modal일 때 focus가 내부를 벗어나지 않는다.
|
||||
- 배경 content를 `inert` 또는 동등한 방식으로 비활성화한다.
|
||||
- Escape와 명시적 닫기 action을 제공한다.
|
||||
- 닫은 뒤 trigger 또는 안전한 대체 위치로 focus를 복원한다.
|
||||
- nested overlay와 route transition 중 orphan portal을 남기지 않는다.
|
||||
|
||||
### 8.3 Form 기준
|
||||
|
||||
- label, description, error가 동일한 field ID 체계로 연결된다.
|
||||
- required는 시각적 기호와 programmatic 상태를 함께 가진다.
|
||||
- submit 실패 시 error summary로 focus를 이동한다.
|
||||
- error summary 항목은 해당 field로 이동한다.
|
||||
- async validation은 현재 입력보다 오래된 결과를 폐기한다.
|
||||
- pending 상태를 screen reader에 알리되 같은 문구를 반복하지 않는다.
|
||||
- 서버 오류의 raw body, stack, 내부 field path를 출력하지 않는다.
|
||||
|
||||
## 9. 국제화 계약
|
||||
|
||||
디자인 시스템 primitive는 한국어 문구를 내부 기본값으로 숨기지 않는다.
|
||||
접근성 label이나 오류 문구가 필요하면 명시적인 prop 또는 message key를 받는다.
|
||||
|
||||
필수 국제화 기반:
|
||||
|
||||
- `LocaleProvider`
|
||||
- typed message key
|
||||
- fallback locale
|
||||
- `<html lang>`과 `dir` 갱신
|
||||
- `Intl.DateTimeFormat`
|
||||
- `Intl.NumberFormat`
|
||||
- `Intl.ListFormat`
|
||||
- plural/select message
|
||||
- locale별 validation message
|
||||
- pseudo-locale
|
||||
- RTL story와 E2E smoke
|
||||
|
||||
금지 패턴:
|
||||
|
||||
- 번역 문장 중간에 JSX 문자열을 연결한다.
|
||||
- 날짜를 `substring`이나 고정 구분자로 조립한다.
|
||||
- icon direction을 locale 확인 없이 좌/우 이름으로 고정한다.
|
||||
- route title, navigation label, toast message를 JSX literal로 분산한다.
|
||||
- 번역 누락 시 빈 문자열을 렌더링한다.
|
||||
|
||||
Storybook toolbar에서 최소한 다음 조합을 전환할 수 있어야 한다.
|
||||
|
||||
- `ko-KR`, light
|
||||
- `en-US`, light
|
||||
- `en-US`, dark
|
||||
- pseudo-locale
|
||||
- 대표 RTL locale
|
||||
|
||||
## 10. Storybook과 시각 검증
|
||||
|
||||
현재 `/examples/ui`는 실행 중인 앱 셸과 primitive 통합을 확인하는 데 유지한다.
|
||||
그러나 runtime gallery는 isolated component workshop을 대체하지 않는다.
|
||||
|
||||
[Storybook 공식 UI 테스트 문서](https://storybook.js.org/docs/writing-tests)는
|
||||
story를 기반으로 interaction, accessibility, visual test를 구성하는 방법을
|
||||
제공한다.
|
||||
|
||||
### 10.1 Storybook 필수 구성
|
||||
|
||||
- Vite 기반 Storybook
|
||||
- TypeScript CSF story
|
||||
- global theme toolbar
|
||||
- locale/RTL toolbar
|
||||
- compact/wide viewport
|
||||
- Router, Theme, Locale, Session, Query decorator
|
||||
- `@storybook/addon-a11y`
|
||||
- a11y 결과를 CI에서 error로 처리
|
||||
- interaction `play` test
|
||||
- story build gate
|
||||
|
||||
### 10.2 각 컴포넌트의 필수 story
|
||||
|
||||
- default
|
||||
- 모든 semantic variant
|
||||
- disabled와 readonly
|
||||
- pending/loading
|
||||
- 오류
|
||||
- 긴 문구
|
||||
- 빈 값
|
||||
- light/dark
|
||||
- compact viewport
|
||||
- keyboard interaction
|
||||
- 해당되는 경우 open/closed, controlled/uncontrolled
|
||||
|
||||
Form과 overlay는 다음 story를 추가한다.
|
||||
|
||||
- submit 실패와 error summary
|
||||
- async pending
|
||||
- server field error
|
||||
- Escape close
|
||||
- outside interaction
|
||||
- initial focus
|
||||
- focus restoration
|
||||
- nested content와 long content
|
||||
|
||||
### 10.3 시각 회귀
|
||||
|
||||
기본 시각 회귀는 Playwright `toHaveScreenshot()`을 사용해 저장소 안에서 실행할
|
||||
수 있게 한다. Chromatic 같은 외부 서비스는 선택 사항이다.
|
||||
|
||||
필수 안정화:
|
||||
|
||||
- 동일한 Linux image, browser version, font
|
||||
- animation과 caret 비활성화
|
||||
- 시간·랜덤·network 응답 고정
|
||||
- 동적 ID와 민감 데이터 mask
|
||||
- light/dark baseline
|
||||
- compact/wide baseline
|
||||
- 변경된 baseline은 코드 리뷰 대상
|
||||
|
||||
failure screenshot은 시각 회귀가 아니다. 의도된 baseline과 실제 렌더링을
|
||||
비교하는 assertion이 있어야 한다.
|
||||
|
||||
### 10.4 테스트 계층
|
||||
|
||||
| 계층 | 검증 대상 |
|
||||
| --- | --- |
|
||||
| Type check | prop union, ref, event와 slot type |
|
||||
| Unit | token/variant map, pure formatter와 state reducer |
|
||||
| Component | native semantics, keyboard, focus, callback |
|
||||
| Story interaction | 실제 browser의 isolated behavior |
|
||||
| Story a11y | 모든 주요 state의 axe |
|
||||
| Visual | pixel/layout/theme regression |
|
||||
| E2E | 앱 셸, route, form/query integration |
|
||||
| Manual | screen reader, zoom, touch, forced-colors |
|
||||
|
||||
DOM snapshot은 보이는 UI를 보장하지 않으므로 public markup contract가 꼭 필요한
|
||||
경우에만 사용한다.
|
||||
|
||||
## 11. 컴포넌트 추가 Recipe
|
||||
|
||||
새 component를 추가할 때 다음 순서를 생략하지 않는다.
|
||||
|
||||
### Step 1. 문제와 계층을 결정한다
|
||||
|
||||
다음 질문을 issue 또는 설계 메모에 기록한다.
|
||||
|
||||
- 단일 control인가, 반복되는 조합인가, 페이지 구조인가?
|
||||
- primitive, pattern, template 중 어디에 속하는가?
|
||||
- 기존 component의 variant로 해결할 수 있는가?
|
||||
- 제품 전용 의미를 공통 디자인 시스템으로 잘못 올리는 것은 아닌가?
|
||||
- native HTML로 충분한가?
|
||||
|
||||
### Step 2. 상태 행렬을 작성한다
|
||||
|
||||
최소 상태:
|
||||
|
||||
```text
|
||||
default
|
||||
hover
|
||||
focus-visible
|
||||
active/pressed
|
||||
disabled
|
||||
pending
|
||||
invalid
|
||||
light
|
||||
dark
|
||||
compact
|
||||
long-content
|
||||
```
|
||||
|
||||
선택/overlay component는 open, selected, indeterminate, empty, loading도 포함한다.
|
||||
|
||||
### Step 3. 접근성 명세를 작성한다
|
||||
|
||||
- element/role
|
||||
- accessible name
|
||||
- keyboard table
|
||||
- focus entry/exit/restore
|
||||
- announcement
|
||||
- error association
|
||||
- reduced motion
|
||||
- touch와 screen reader
|
||||
|
||||
명세가 불명확하면 구현보다 먼저 native, React Aria, Radix prototype으로 검증한다.
|
||||
|
||||
### Step 4. API를 닫는다
|
||||
|
||||
- TypeScript public props를 정의한다.
|
||||
- semantic `variant`를 사용하고 raw color prop을 노출하지 않는다.
|
||||
- callback 이름과 payload를 domain-neutral하게 정한다.
|
||||
- controlled/uncontrolled 정책을 정한다.
|
||||
- `className` escape hatch의 범위를 정한다.
|
||||
- vendor prop과 type을 public API에 노출하지 않는다.
|
||||
|
||||
### Step 5. 필요한 토큰을 추가한다
|
||||
|
||||
- 기존 semantic token으로 표현 가능한지 확인한다.
|
||||
- raw 값 반복을 추가하지 않는다.
|
||||
- light/dark/forced-colors 값을 함께 정의한다.
|
||||
- token contract test를 갱신한다.
|
||||
|
||||
### Step 6. 구현한다
|
||||
|
||||
- native semantics 우선
|
||||
- local vendor facade만 import
|
||||
- static variant map
|
||||
- ref/focus 지원
|
||||
- DOM 구조 최소화
|
||||
- render 중 전역 상태 변경 금지
|
||||
- 제품 API나 application service import 금지
|
||||
|
||||
### Step 7. Story를 작성한다
|
||||
|
||||
- 상태 행렬을 모두 story로 표현한다.
|
||||
- interaction test를 작성한다.
|
||||
- dark, compact, long-content를 포함한다.
|
||||
- a11y test를 error mode로 실행한다.
|
||||
|
||||
### Step 8. 자동 테스트를 작성한다
|
||||
|
||||
- role/name 중심 query
|
||||
- keyboard와 focus
|
||||
- disabled/pending
|
||||
- callback
|
||||
- controlled state
|
||||
- 오류와 live region
|
||||
- 필요한 visual baseline
|
||||
|
||||
구현 class나 내부 DOM 순서만 확인하는 brittle test는 피한다.
|
||||
|
||||
### Step 9. 통합 예제를 추가한다
|
||||
|
||||
- `/examples/ui`에는 primitive를 추가한다.
|
||||
- `/examples/states`에는 새로운 공통 상태를 추가한다.
|
||||
- pattern/template은 domain-neutral fixture로 실제 조합을 보여 준다.
|
||||
- production profile에서 예제 route를 제외할 수 있는 계약을 유지한다.
|
||||
|
||||
### Step 10. 문서와 public export를 갱신한다
|
||||
|
||||
- 사용 목적
|
||||
- 사용하지 말아야 할 경우
|
||||
- props와 variant
|
||||
- keyboard
|
||||
- 접근성 책임
|
||||
- i18n
|
||||
- 예제
|
||||
- public `index.ts` export
|
||||
- 변경 로그
|
||||
|
||||
### Step 11. 게이트를 실행한다
|
||||
|
||||
최소 실행 범위:
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types
|
||||
corepack pnpm lint
|
||||
corepack pnpm check:architecture
|
||||
corepack pnpm test:component
|
||||
corepack pnpm test:e2e
|
||||
corepack pnpm test:a11y
|
||||
corepack pnpm build
|
||||
corepack pnpm check:bundle
|
||||
```
|
||||
|
||||
Storybook과 visual gate가 도입된 뒤에는 story build, interaction, story a11y,
|
||||
visual comparison도 필수로 추가한다.
|
||||
|
||||
## 12. Definition of Done
|
||||
|
||||
컴포넌트는 아래 항목 중 자신의 state/capability matrix에 해당하는 항목을
|
||||
충족해야 `완료`다. 적용되지 않는 항목은 체크를 생략하지 말고 `N/A`와 이유를
|
||||
component contract에 기록한다. 예를 들어 `Separator`에는 pending/error/IME가,
|
||||
읽기 전용 `Card`에는 controlled state와 field error가 적용되지 않는다.
|
||||
|
||||
### API와 아키텍처
|
||||
|
||||
- [ ] primitive/pattern/template 소유권이 명확하다.
|
||||
- [ ] public TypeScript API가 closed union으로 정의되었다.
|
||||
- [ ] 제품 코드가 vendor package를 직접 import하지 않는다.
|
||||
- [ ] domain/application dependency가 없다.
|
||||
- [ ] public barrel을 통해 소비할 수 있다.
|
||||
- [ ] 상태를 소유하는 경우 controlled/uncontrolled 계약이 문서화되었고, focus
|
||||
target이 있는 경우 ref 계약이 문서화되었다.
|
||||
|
||||
### Styling
|
||||
|
||||
- [ ] raw 색상과 반복되는 임의 값이 없다.
|
||||
- [ ] semantic token을 사용한다.
|
||||
- [ ] light/dark가 완성되었다.
|
||||
- [ ] compact/wide reflow가 된다.
|
||||
- [ ] long text와 번역 확장을 견딘다.
|
||||
- [ ] forced-colors에서도 의미가 유지된다.
|
||||
- [ ] bundle budget을 통과한다.
|
||||
|
||||
### 접근성
|
||||
|
||||
- [ ] native semantics와 accessible name이 있다.
|
||||
- [ ] keyboard 동작이 명세와 일치한다.
|
||||
- [ ] focus indicator, 이동, 복원이 올바르다.
|
||||
- [ ] 상태가 색상에만 의존하지 않는다.
|
||||
- [ ] 오류나 설명을 제공하는 component는 이들을 programmatically 연결한다.
|
||||
- [ ] reduced-motion을 존중한다.
|
||||
- [ ] 200%/400% zoom과 320px reflow를 확인했다.
|
||||
- [ ] screen reader 수동 검토 항목이 정의되었다.
|
||||
|
||||
### 국제화
|
||||
|
||||
- [ ] 사용자 문구를 소유하는 component는 이를 내부 literal로 고정하지 않는다.
|
||||
- [ ] 날짜·숫자·목록을 표시하는 component는 locale formatter를 사용한다.
|
||||
- [ ] pseudo-locale에서 잘리지 않는다.
|
||||
- [ ] RTL에서 배치와 키보드가 올바르다.
|
||||
- [ ] 문자 입력을 받는 component는 IME 입력을 깨뜨리지 않는다.
|
||||
|
||||
### 테스트와 문서
|
||||
|
||||
- [ ] component behavior test가 있다.
|
||||
- [ ] 모든 주요 상태 story가 있다.
|
||||
- [ ] story interaction test가 있다.
|
||||
- [ ] automated a11y가 통과한다.
|
||||
- [ ] 필요한 visual baseline이 있다.
|
||||
- [ ] runtime gallery 또는 pattern fixture가 있다.
|
||||
- [ ] 사용법, 금지 사례, 접근성 책임이 문서화되었다.
|
||||
- [ ] 전체 품질·아키텍처·bundle gate가 통과한다.
|
||||
|
||||
## 13. 금지 패턴
|
||||
|
||||
- 제품 페이지에서 `lucide-react` 직접 import
|
||||
- 제품 페이지에서 React Aria/Radix 직접 import
|
||||
- `div`와 keyboard handler로 native button을 재구현
|
||||
- icon-only button에 accessible name 누락
|
||||
- 문자열 icon name으로 전체 icon package를 동적 로딩
|
||||
- 사용자 값으로 Tailwind/CSS class 조립
|
||||
- raw hex/OKLCH 값을 JSX나 component CSS에 반복
|
||||
- `as="div"`처럼 semantics를 무제한 변경하는 public API
|
||||
- primitive에서 API/query/auth/storage를 직접 호출
|
||||
- 모든 상태를 하나의 전역 store에 저장
|
||||
- 오류 raw body, URL, header, token, stack 표시
|
||||
- locale 문장을 문자열 덧셈으로 구성
|
||||
- axe 통과만으로 접근성 완료 선언
|
||||
- failure screenshot만으로 시각 회귀 완료 선언
|
||||
- Storybook story 없이 runtime gallery 하나로 모든 variant를 대표
|
||||
|
||||
## 14. 단계별 도입 순서
|
||||
|
||||
1. TypeScript public API와 디자인 시스템 public barrel을 만든다.
|
||||
2. token을 primitive/semantic/component 계층으로 분리한다.
|
||||
3. 기존 Button, TextField, Card, Alert, Badge, Dialog를 새 계약으로 이동한다.
|
||||
4. Lucide local facade와 `IconButton`을 추가하고 문자 glyph를 제거한다.
|
||||
5. Field foundation과 필수 form primitives를 추가한다.
|
||||
6. React Aria와 Radix prototype을 비교하고 headless ADR을 확정한다.
|
||||
7. Drawer, Select, Menu, Popover, Tooltip을 local facade로 구현한다.
|
||||
8. pattern과 page template을 추가한다.
|
||||
9. locale provider, pseudo-locale, RTL 검증을 추가한다.
|
||||
10. Storybook, story a11y, interaction test를 추가한다.
|
||||
11. pinned browser 환경의 visual regression gate를 추가한다.
|
||||
12. `/examples/ui`와 `/examples/states`를 통합 smoke gallery로 유지한다.
|
||||
|
||||
이 순서는 라이브러리 수를 늘리는 것이 목표가 아니다. 각 단계가 제품 개발자가
|
||||
중복 구현할 문제를 하나씩 제거하고, 외부 공급자를 교체할 수 있는 로컬 계약과
|
||||
검증 증거를 남기는 것이 목표다.
|
||||
@@ -39,3 +39,10 @@ Arbitrary-value policy:
|
||||
|
||||
The removable sample may demonstrate tokens, but production starter modules do
|
||||
not import from `src/sample/contract-fixture`.
|
||||
|
||||
This file documents the currently implemented token and primitive baseline.
|
||||
The [design-system platform contract](./design-system-platform.md) defines the
|
||||
target token layers, component catalog, local Lucide/headless-library
|
||||
facades, page patterns, accessibility and internationalization rules, isolated
|
||||
workshop, and component-authoring recipe. Items in that target document are not
|
||||
treated as implemented until their acceptance tests pass.
|
||||
|
||||
Reference in New Issue
Block a user