# component-systems — 컴포넌트 라이브러리 위에서 일하기, 폼·데이터 테이블
기존 컴포넌트 라이브러리 위에서 일할 때 4단계 전에 읽는다. `components.json`, `radix-ui`·`@radix-ui/*`, Base UI 계열 패키지, `react-aria-components`, `cva`·`tailwind-merge` 같은 컴포넌트 기반이 감지되면 여는 조건부 문서다. 감지되지 않으면 §0~§9는 읽지 않는다 — designpaca의 기본 파이프라인(SVG 필터·three.js·순수 CSS)은 그 절들 없이도 완결된다. **단, §10 폼 레이아웃과 §11 데이터 테이블은 컴포넌트 기반 유무와 상관없이 폼·표를 만들 때 읽는다.**
## 이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 지금 컴포넌트 기반이 있는지부터 확인해야 한다 | §0 |
| 화면을 만들기 전에 뭘 먼저 검색해야 하는지 | §1 |
| "이건 브랜드 디자인 시스템 작업인가?"가 헷갈린다 | §2 |
| 토큰 이름을 라이브러리 변수로 옮겨야 한다 | §3 |
| Dialog·Avatar·Group 같은 합성 실수를 피해야 한다 | §4 |
| Radix·Base UI·React Aria 중 뭘 쓰는 프로젝트인지 API가 다르다 | §5 |
| CLI로 컴포넌트를 추가·갱신해야 한다 | §6 |
| Tailwind 프로젝트다 | §7(조건부). 토큰을 테마에 넣는 법은 §7-1 |
| 설정·대시보드 같은 화면 유형의 조합을 참고하고 싶다 | §8(관찰 후보) |
| 채팅·스트리밍 UI를 만든다 | §9(조건부) |
| **폼**을 만든다(배치·필수 표시·오류 위치·한국 폼 관례·본인인증·제출 뒤) — 컴포넌트 기반이 없어도 | §10 |
| **데이터 테이블**을 만든다(마크업·정렬·숫자·밀도·고정 헤더·모바일) — 컴포넌트 기반이 없어도 | §11 |
| 커머스·SaaS·에디토리얼 같은 장르의 필수 블록 | [genre-conventions.md](genre-conventions.md) |
| 작업 직전 마지막 확인 | §12 |
---
## 0. 언제·감지 방법
**하드 게이트는 없다.** 이 절은 "이 문서를 열어야 하는가"만 판정한다.
다음 중 하나라도 있으면 컴포넌트 기반이 있는 프로젝트다.
- 저장소 루트 또는 앱 루트에 `components.json`이 있다(shadcn/ui 계열의 표준 설정 파일)
- `package.json` 의존성에 `radix-ui`(통합 패키지) 또는 `@radix-ui/*`, Base UI 계열(`@base-ui/react`, 구 `@base-ui-components/react`), `react-aria-components`, `cva`, `tailwind-merge`/`clsx` 중 하나 이상이 있다
- `src/components/ui/`(또는 동등한 디렉터리)에 `button.tsx`, `dialog.tsx` 같은 생성된 프리미티브 파일이 이미 있다
`components.json`이 있으면 그 안의 `aliases`, `style`, `iconLibrary`, `tailwind` 필드를 먼저 읽는다. CLI가 있으면(`npx shadcn@latest info` 등) 실행해 프로젝트가 실제로 어떤 프리미티브 계열(`style` 접두어 `radix-`/`base-`/`aria-`, §5)·경로 별칭·아이콘 라이브러리를 쓰는지 확정하고, 이후 모든 결정에서 그 값을 그대로 따른다. 설정을 읽지 못했는데 라이브러리 종류·경로를 추측해서 설치·수정을 진행하지 않는다 — 불확실하면 사용자에게 묻는다([brief-interview.md](brief-interview.md)) [SKILL-SHADCN].
---
## 1. 기존 컴포넌트·반복 패턴 우선
**프로젝트 계약.** 커스텀 마크업(스타일링한 `div`)을 쓰기 전에 같은 역할의 컴포넌트가 이미 있는지 먼저 검색한다. 콜아웃·빈 상태·로딩 placeholder·상태 배지처럼 화면마다 반복되는 UI는 손으로 다시 짜지 않고 기존 컴포넌트를 확장하거나 그대로 쓴다.
| 대신 손으로 짜지 않는다 | 이미 있는지 검색한다 |
|---|---|
| 색칠한 `div` + 아이콘 + 텍스트 | Alert/Callout류 컴포넌트 |
| `animate-pulse` 커스텀 div | Skeleton류 컴포넌트 |
| `rounded-full bg-*` span | Badge류 컴포넌트 |
| `
` 또는 `border-t` div | Separator류 컴포넌트 |
| 빈 목록을 위한 임의 레이아웃 | Empty state류 컴포넌트 |
검색 순서는 (1) 프로젝트에 이미 설치된 컴포넌트 디렉터리, (2) 그 라이브러리의 공식 카탈로그, (3) 그래도 없을 때만 새로 합성이다. 새 컴포넌트를 만들기로 했다면 §2의 역할 분담을 먼저 확인한다 [SKILL-SHADCN].
---
## 2. 역할 분담 — 컴포넌트 기반은 브랜드 디자인 시스템이 아니다
**프로젝트 계약.** shadcn/ui, Base UI, Radix 같은 컴포넌트 기반은 "이 프로젝트가 어떻게 보여야 하는가"에 답하지 않는다. 이들은 조립 규칙(합성·상태·접근성 배선)을 제공할 뿐, 브랜드·타입 위계·간격 리듬·모션 언어는 여전히 designpaca가 3~4단계에서 정한 값이다.
역할을 나누면 다음과 같다.
- **designpaca가 맡는 것**: 방향 결정(1~2단계), 토큰 값(3단계: 타입 스케일·색·간격·radius·모션), 브랜드 표면(히어로·섹션 구성·카피 목소리), 검증(5단계: 접근성·성능·시각 감사)
- **컴포넌트 라이브러리가 맡는 것**: 인터랙션 프리미티브의 조립(Dialog의 포커스 트랩, Select의 키보드 탐색, Tabs의 ARIA 배선), 상태 관리, 합성 규칙
두 역할이 섞이면 실패한다. 라이브러리의 기본 variant·기본 그림자·기본 radius를 그대로 두고 "이게 이 프로젝트의 디자인"이라고 부르면, `tokens.md`가 정의한 역할 토큰이 무의미해진다. 역으로 라이브러리가 이미 처리하는 포커스 트랩·키보드 탐색을 손으로 재구현하면 §4의 합성 규칙을 깨고 접근성 배선이 두 벌로 충돌한다.
**컴포넌트 라이브러리를 새로 설치할지 여부는 사용자 결정이다.** 브리프에 없는데 컴포넌트 기반을 새로 도입하지 않는다. 이미 있는 프로젝트에서는 그 기반을 존중하고, 그 위에 designpaca의 토큰·브랜드 표면·검증을 얹는다 [SKILL-SHADCN].
---
## 3. 토큰 매핑표 — designpaca 역할 토큰 → 라이브러리 CSS 변수
**프로젝트 계약.** 라이브러리가 이미 CSS 변수 기반 테마 시스템을 갖추고 있으면(shadcn/ui 계열이 대표적이다) `tokens.md` §2에서 정한 역할 토큰 값을 새 변수 체계로 중복 정의하지 말고, 라이브러리가 읽는 변수에 그대로 대입한다. 변수 이름은 라이브러리마다 다를 수 있으므로 실제 설치된 테마 파일(보통 `globals.css`)에서 확인하고, 아래는 shadcn/ui 계열에서 관찰되는 이름의 매핑 시작값이다.
| designpaca 역할 토큰(`tokens.md` §2) | 라이브러리 CSS 변수(관찰값) | 비고 |
|---|---|---|
| `--surface` | `--background` | 페이지 바탕 |
| `--surface-raised` | `--card` (또는 `--popover`) | 카드·패널·오버레이 표면 |
| `--ink` | `--foreground` | 본문 텍스트 |
| `--ink-muted` | `--muted-foreground` | 보조 텍스트. 대비 4.5:1은 여전히 검증한다 |
| `--line` | `--border` | 경계선. 폼 입력 테두리는 `--input`으로 분리되어 있을 수 있다 |
| `--accent` | `--primary` | 강조 역할의 기본 토큰 |
| `--accent-ink` | `--primary-foreground` | 강조 위 글자색 |
| (역할 확장) | `--destructive`(구 테마에는 `--destructive-foreground`도 있다) | 위험·삭제 상태. designpaca 쪽 상태색 확장 자리(`tokens.md` §2)에 대응 |
| (역할 확장) | `--ring` | 포커스 링. `accessibility.md`의 포커스 하드 게이트와 연결 |
| `--radius-*` | `--radius` | 라이브러리는 단일 `--radius`에서 `calc()`로 `--radius-sm`~`--radius-4xl`을 파생시킨다. designpaca의 "역할 토큰으로만, 역할당 하나" radius 어휘와 정신이 같다(`tokens.md` §3-b가 정본) |
이 표는 라이브러리 변수명을 designpaca가 강제하는 것이 아니라, 이미 있는 값을 designpaca 쪽 역할 토큰과 대응시켜 이중 정의를 막는 목적이다. 표에 없는 변수(`--sidebar-*`·`--chart-*` 등)는 설치된 CSS에서 확인해 같은 방식으로 대응시킨다. 색상 값 자체(라이트/다크 쌍)를 어떻게 정하는지는 `tokens.md` §2와 [color.md](color.md)를 따른다. `--dur-*`·`--ease-*` 모션 토큰은 라이브러리가 별도로 정의하지 않는 한 `tokens.md` §4와 [motion.md](motion.md)의 값을 그대로 쓴다 — 라이브러리 컴포넌트의 트랜지션 duration이 프로젝트 모션 토큰과 다르면 그 불일치를 기록하고 맞출지 결정한다 [SKILL-SHADCN].
---
## 4. 합성 규칙
**하드 게이트 하나, 나머지는 프로젝트 계약.** 컴포넌트 기반을 쓰기로 한 이상 아래는 그 라이브러리의 계약이며, 어기면 스타일이 아니라 동작이 깨진다.
### 4-1. Dialog·Sheet·Drawer는 접근 가능한 제목이 필수다 — 하드 게이트
시각적으로 숨기더라도 `sr-only` 텍스트로는 존재해야 한다. 완전 생략은 WCAG 4.1.2(Name, Role, Value) 위반이다 [WCAG-NRV, SKILL-SHADCN]. 상세 판정 기준과 검사 방법은 [accessibility.md](accessibility.md)를 따른다.
```tsx
프로필 수정정보를 업데이트합니다.
...
```
시각적으로 제목을 숨겨야 한다면:
```tsx
프로필 수정
```
### 4-2. Avatar는 Fallback이 필수다 — 프로젝트 계약
이미지 로드가 실패했을 때의 대체 표시가 없으면 빈 원이 남는다.
```tsx
홍
```
### 4-3. 라벨이 있는 묶음은 Group으로 — 프로젝트 계약
리스트형 컴포넌트에서 **라벨(`SelectLabel` 등)로 이름 붙인 묶음**은 Group으로 감싼다. Group이 있어야 라벨이 그 묶음의 접근 가능한 이름으로 연결되고, 라벨 컴포넌트는 Group 안에서 쓰도록 설계돼 있다. 묶음 없이 항목만 나열하는 단일 목록에서 Group은 선택 사항이다(Radix 기준 — 라이브러리 문서로 확인한다). 아래는 묶음이 있는 경우다.
| Item | Group |
|---|---|
| `SelectItem`, `SelectLabel` | `SelectGroup` |
| `DropdownMenuItem`, `DropdownMenuLabel` | `DropdownMenuGroup` |
| `CommandItem` | `CommandGroup` |
| `MenubarItem` | `MenubarGroup` |
```tsx
// 틀림 — 라벨이 묶음 밖에 떠 있어 어느 항목들의 이름인지 연결되지 않는다
과일사과
// 맞음
과일사과
```
[SKILL-SHADCN]
### 4-4. 폼 필드 묶음 — 프로젝트 계약
라이브러리가 필드 묶음 컴포넌트(FieldGroup/Field류)를 제공하면 raw `div` + `space-y-*`/`grid gap-*`로 폼 레이아웃을 다시 짜지 않는다. 검증 상태는 컨테이너와 컨트롤에 이중으로 표시한다 — 컨테이너에는 `data-invalid`(또는 동등 속성), 실제 입력 요소에는 `aria-invalid`. 비활성 상태도 같은 패턴(`data-disabled`는 컨테이너, `disabled`는 컨트롤)이다. 스타일링과 스크린리더 판정이 같은 진실을 가리키게 하는 목적이며, 한쪽만 표시하면 시각과 보조기술 판정이 어긋난다 [SKILL-SHADCN].
### 4-5. 오버레이의 z-index를 손으로 조정하지 않는다 — 프로젝트 계약
Dialog, Sheet, Popover, DropdownMenu, Tooltip, HoverCard 같은 오버레이 컴포넌트는 자체 스태킹 컨텍스트를 관리한다. `z-50`이나 `z-[999]` 같은 임의값을 얹으면 라이브러리가 관리하는 레이어 순서와 충돌해 다른 오버레이 뒤에 가려지거나 앞으로 튀어나온다. 스태킹 순서를 바꿔야 할 실제 필요가 있으면 라이브러리가 제공하는 portal·z-index prop을 먼저 찾고, 없으면 그 사실을 design.md에 남긴다 [SKILL-SHADCN].
---
## 5. Radix · Base UI · React Aria — API 차이
**프로젝트 계약.** 같은 컴포넌트 이름이라도 밑바탕 프리미티브가 Radix인지 Base UI인지 React Aria인지에 따라 prop 시그니처가 다르다. `components.json`에 `base` 필드는 없다. **`style` 값의 접두어**가 계열이다 — `radix-*`·`new-york`·`default`는 Radix, `base-*`는 Base UI, `aria-*`는 React Aria(공식 스키마, 확인 2026-10-06). `style`과 `info` 출력으로 확인하고 그 문법만 쓴다. 섞어 쓰면 조용히 동작하지 않는 코드가 나온다 [SKILL-SHADCN].
`aria-*`는 아래 표의 어느 열도 아니다. 표를 추측으로 확장하지 말고 설치된 컴포넌트 파일과 React Aria 문서에서 문법을 확인한다.
| 상황 | Radix | Base UI |
|---|---|---|
| 커스텀 트리거 엘리먼트 | `asChild` | `render` |
| non-button 엘리먼트로 트리거를 렌더링할 때 | 해당 없음 | `nativeButton={false}` 추가 필요 |
| Select 아이템 정의 | JSX로 인라인 | root에 `items` prop 필요 |
| Select placeholder | `` | `{ value: null }` 아이템으로 표현 |
| ToggleGroup 단일 선택 | `type="single"`, `defaultValue`는 문자열 | prop 불필요(기본 단일), `defaultValue`는 항상 배열 |
| ToggleGroup 다중 선택 | `type="multiple"` | `multiple` boolean prop |
| Slider 단일 thumb | 항상 배열 | 단일 숫자 허용(range는 둘 다 배열) |
| Accordion | `type="single"\|"multiple"` 필요, `collapsible` 지원, `defaultValue`는 문자열 | `type` prop 없음, `multiple` boolean, `defaultValue`는 항상 배열 |
트리거를 감싸는 예시:
```tsx
// 틀림 — 트리거를 추가 엘리먼트로 감쌈
// 맞음 (Radix)
// 맞음 (Base UI)
}>열기
```
non-button 렌더 대상 예시(Base UI):
```tsx
// 틀림 — nativeButton={false} 누락
}>문서 보기
// 맞음
} nativeButton={false}>
문서 보기
```
---
## 6. CLI 안전
**프로젝트 계약.** 컴포넌트 추가·갱신에 CLI(예: `shadcn` CLI)를 쓸 수 있으면 다음 순서를 지킨다.
1. **먼저 `info`로 프로젝트 설정을 확인한다.** 별칭 경로(`aliases`), Tailwind 버전, 프리미티브 계열(`style` 접두어), `iconLibrary`를 읽고 그 값을 그대로 따른다. 값을 추측하거나 프로젝트 대신 기본값을 고르지 않는다.
2. **`--dry-run`·`--diff`로 먼저 미리 본다.** 실제 적용 전에 영향받는 파일과 upstream 대비 diff를 확인한다. 로컬 변경이 없는 파일은 덮어써도 안전하지만, 로컬 변경이 있는 파일은 diff를 읽고 upstream 갱신을 로컬 수정과 함께 병합한다.
3. **`--overwrite`는 사용자의 명시적 승인 없이 쓰지 않는다.** "그냥 다 업데이트해" 같은 포괄 지시가 와도 실행 전에 한 번 더 확인한다.
4. **레지스트리를 추측하지 말고 묻는다.** 사용자가 "로그인 블록 추가해줘"처럼 레지스트리를 명시하지 않고 요청하면, 기본 레지스트리를 임의로 고르지 않고 어느 레지스트리를 쓸지 먼저 묻는다.
5. **원격 파일을 손으로 fetch하지 않는다.** GitHub raw 등에서 파일을 직접 받아오지 않고 CLI의 레지스트리 해석·경로 처리·CSS diff 기능을 쓴다.
6. **추가한 컴포넌트는 반드시 읽고 검증한다.** 빠진 하위 컴포넌트(예: `SelectGroup` 없는 `SelectItem`), 빠진 import, §4 합성 규칙 위반을 확인한 뒤 다음 단계로 넘어간다. 아이콘 import는 프로젝트의 `iconLibrary`로 맞춘다.
[SKILL-SHADCN]
---
## 7. Tailwind 프로젝트 요약
> 언제: 프로젝트가 Tailwind CSS를 쓸 때만. designpaca는 특정 CSS 프레임워크를 전제하지 않으므로, Tailwind가 아닌 프로젝트에는 이 절을 적용하지 않는다.
**프로젝트 계약.** 컴포넌트 라이브러리가 Tailwind 위에서 동작하도록 설계돼 있으면, 아래는 그 라이브러리와 충돌하지 않기 위한 클래스 위생 요약이다. 취향 규범이 아니라 라이브러리가 이미 semantic 토큰·유틸리티로 값을 관리하고 있어서 수동으로 덮어쓰면 어긋나는 항목들이다.
- 컴포넌트 색상·타이포그래피를 `className`으로 직접 오버라이드하지 않는다. 레이아웃(간격·정렬·폭)에만 쓴다
- 수직 스택은 `space-y-*` 대신 `flex flex-col gap-*`(또는 `grid gap-*`)를 쓴다
- 너비와 높이가 같으면 `w-10 h-10` 대신 `size-10`을 쓴다
- 잘린 텍스트는 `overflow-hidden text-ellipsis whitespace-nowrap` 대신 `truncate` 축약을 쓴다
- 수동 `dark:` 색상 오버라이드 대신 라이브러리의 semantic 토큰(`bg-background`, `text-muted-foreground` 등)을 쓴다 — §3 매핑표 참고
- 조건부 클래스는 수동 템플릿 리터럴 삼항 대신 `cn()`류 유틸리티를 쓴다
- 오버레이 컴포넌트에는 §4-5의 이유로 수동 `z-index`를 얹지 않는다
- built-in variant(`variant="outline"` 등)를 먼저 시도하고, 그걸로 안 될 때만 `className`으로 보강한다
레지스트리·CLI 플래그·`registry.json` 스키마 자체는 designpaca 범위 밖이므로 다루지 않는다 [SKILL-SHADCN].
### 7-1. designpaca 토큰을 Tailwind 테마에 넣는다
**프로젝트 계약.** 토큰은 유틸리티 클래스가 읽는 자리에 둔다. 따로 만든 `:root` 변수를 임의값(`text-[…]`, `bg-[#…]`)으로 꺼내 쓰면 죽은 토큰이 된다. `package.json`의 `tailwindcss` 버전과 전역 CSS를 읽어 방식을 확정하고 두 방식을 섞지 않는다.
| 프로젝트 | 토큰을 두는 곳과 방법 |
|---|---|
| v4 (CSS에 `@import "tailwindcss"`) | 전역 CSS의 `@theme`. `tailwind.config.js`를 새로 만들지 않는다. `--color-*`·`--font-*`·`--text-*`·`--spacing-*`·`--radius-*`·`--ease-*` 네임스페이스로 넣으면 같은 이름의 유틸리티가 생긴다. 다른 변수를 참조하면 `@theme inline { --color-surface: var(--surface); }` |
| v4 + shadcn/ui | §3 대응표대로 `:root`·`.dark`의 **값만** 바꾸고 `@theme inline`의 연결은 그대로 둔다. shadcn에 없는 토큰(타입 스케일·모션)은 `@theme`에 더한다 |
| v3 (`tailwind.config.*`) | `theme.extend`에서 CSS 변수를 `var(--…)`로 참조한다 |
네임스페이스 이름은 v4.3 문서 기준이다(확인 2026-10-06) — 다른 버전이면 그 문서로 확인하고, 넣은 뒤 빌드 결과 CSS에 유틸리티가 생겼는지 본다. v4의 브라우저 하한(Chrome 111·Safari 16.4·Firefox 128)과 브리프의 지원 범위도 대조한다.
---
## 8. 화면 유형별 조합 참고
> 언제: 컴포넌트 기반 위에서 익숙한 화면 유형(설정, 대시보드 등)을 조립할 때. 아래는 **관찰 후보**이며 정답이 아니다 — 브리프·업종·목표 행동에 따라 완전히 다른 조합이 맞을 수 있다.
| 화면 유형 | 자주 보이는 조합(예시일 뿐) |
|---|---|
| 설정 페이지 | Tabs 또는 사이드 내비 + Card + Form(FieldGroup/Field) |
| 대시보드 | 고정 사이드바 + Card + 차트 컴포넌트 + 데이터 테이블 |
| 목록 + 상세 | 목록 패널 + 인스펙터 패널(넓은 화면에서만, `layout.md` §4-f(데스크톱 어포던스) 참고) |
**주의**:
1. `reference-method.md`의 레퍼런스 우선 원칙이 이 표보다 앞선다 — 실제 레퍼런스 조사 없이 이 표만으로 화면을 확정하지 않는다
2. 같은 조합을 모든 설정·대시보드 화면에 기계적으로 반복하면 그 자체가 제네릭 신호다(`antipatterns.md` 참고)
[SKILL-SHADCN]
---
## 9. 스트리밍·채팅 UI는 라이브러리 기본 동작을 재발명하지 않는다
> 언제: 프로젝트의 컴포넌트 라이브러리가 채팅·스트리밍 UI 프리미티브를 이미 제공할 때.
**프로젝트 계약.** 스트리밍 채팅 화면에서 자동 스크롤 추적·앵커링·"최신으로 이동" 로직은 흔히 재발명되는 영역이다. 라이브러리가 이 동작을 소유하는 컴포넌트(메시지 스크롤 컨테이너류)를 이미 제공하면, `useStickToBottom`이나 수동 `ResizeObserver` 훅으로 직접 재구현하지 않는다. 합성으로 표현할 수 없는 동작만 라이브러리가 제공하는 훅으로 보강하고, 그것도 없을 때만 직접 구현한다. 메시지 행·표면(bubble)·첨부·시스템 구분선도 마찬가지로 라이브러리 프리미티브가 있으면 그것을 합성하고, flex `div`로 직접 재구성하지 않는다 [SKILL-SHADCN].
---
## 10. 폼 레이아웃 — 컴포넌트 기반이 없어도 이 절을 따른다
> 언제: 가입·주문·문의·설정처럼 입력 필드가 셋 이상인 폼을 만들 때. 라벨·`autocomplete`·오류 배선·인증 같은 접근성 계약은 [accessibility.md](accessibility.md) §6이 정본이고, 이 절은 **배치와 한국어 폼 관례**만 다룬다. 문구는 [product-copy.md](product-copy.md)를 따른다.
### 10-1. 배치
- 기본은 **단일 열, 라벨은 입력 위**다. 두 열은 의미상 한 쌍(시·분, 카드 유효기간)일 때만 쓴다. 라벨을 입력 왼쪽에 두는 배치는 넓은 설정 화면에서만, 라벨 폭을 고정해 쓴다.
- **입력 폭은 예상 값 길이에 맞춘다.** 우편번호·인증번호·수량은 짧게, 주소·이메일은 길게. 전부 같은 폭이면 무엇을 얼마나 써야 하는지 단서가 사라진다.
- 플레이스홀더를 라벨 대신 쓰지 않는다. 형식 예시(`예: 010-1234-5678`)는 라벨 아래 도움말로 둔다.
- 관련 필드는 `fieldset` + `legend`로 묶는다(배송지, 결제 수단, 동의 항목).
- 선택지가 5개 이하면 라디오나 세그먼트로 전부 보여 준다. 6개 이상이거나 값이 익숙한 목록(시·도)이면 셀렉트, 아주 길면 검색 가능한 콤보박스다.
- 자라는 `textarea`는 스크립트 대신 `field-sizing: content` + `max-height`를 쓴다(Baseline Newly 2026-06) [WEB-BASELINE].
- 긴 폼은 단계로 나누고 진행 표시(`1/3 배송 정보`)를 둔다. 단계 이동 때 입력값을 잃지 않고, 이전 단계로 돌아갈 수 있다.
- 주 행동 버튼은 폼 끝, 마지막 필드 바로 아래에 둔다. 모바일에서 긴 폼이면 하단 고정 바도 후보다. 제출 버튼을 조기에 비활성화하지 않는다([accessibility.md](accessibility.md) §6.6).
### 10-2. 필수·선택 표시
- **소수 쪽만 표시한다.** 대부분 필수면 선택 항목에 `(선택)`을, 대부분 선택이면 필수 항목에 `*` 또는 `(필수)`를 단다. `*`를 쓰면 폼 상단에 "* 필수 입력"을 한 번 적는다.
- 색만으로 필수를 나타내지 않는다. 필수 입력은 `required`(또는 `aria-required="true"`)로도 표시한다.
### 10-3. 오류 메시지 위치와 시점
- 오류 문구는 **해당 필드 바로 아래**(라벨 → 입력 → 오류 순서)에 아이콘과 텍스트로 둔다. 토스트·알림창만으로 오류를 알리지 않는다.
- 검증 시점은 "늦게 꾸짖고 일찍 풀어 준다": 형식 오류는 입력을 마친 뒤(blur) 또는 제출 시 보이고, 이미 오류인 필드는 고치는 즉시 해제한다. 타이핑 도중 오류를 띄우지 않는다.
- 제출 시 오류가 여러 개거나 필드가 5개 이상이면 폼 상단에 **오류 요약**(각 항목이 해당 필드로 가는 링크)을 두고 요약 또는 첫 오류 필드로 포커스를 옮긴다. 배선은 [accessibility.md](accessibility.md) §6.8을 따른다.
- 오류 문구는 무엇이 틀렸고 어떻게 고치는지 말한다(`휴대폰 번호 11자리를 입력해 주세요`). "잘못된 입력입니다"는 쓰지 않는다.
### 10-4. 한국어 폼 관례
| 항목 | 기본 | 이유·주의 |
|---|---|---|
| 이름 | **한 칸**(`autocomplete="name"`). 성/이름 분리는 해외 결제·여권 정보처럼 분리가 요구될 때만 | 한국 이름은 성·이름을 붙여 쓰고 복성·외국인 이름도 있다. 두 칸은 입력 순서를 헷갈리게 한다 |
| 휴대폰 번호 | **한 칸**, `type="tel"` `inputmode="numeric"` `autocomplete="tel"`. 입력 중 `010-1234-5678` 형태로 하이픈을 자동 표시하되 저장은 숫자만 | 세 칸 분리 입력은 붙여넣기·자동완성을 깨뜨린다. 하이픈을 직접 넣은 붙여넣기도 받아들인다 |
| 주소 | **우편번호 찾기 버튼** → 검색 결과 선택 → 우편번호·기본주소 자동 입력(읽기 전용 표시) → **상세주소 필드로 포커스 이동** | 도로명·지번 검색 서비스를 쓴다. 검색 창이 막히거나 실패하면 직접 입력 경로를 둔다. 상세주소를 빈칸으로 둔 채 넘어가는 실수가 잦으니 자동 포커스가 중요하다 |
| 생년월일 | 8자리 숫자 한 칸(`19900101`, `inputmode="numeric"` `autocomplete="bday"`) 또는 연·월·일 세 칸 숫자 입력 | 연도 셀렉트로 100개 항목을 스크롤하게 하지 않는다 |
| 이메일 | 한 칸 + `type="email"` `autocomplete="email"` | 아이디·도메인 셀렉트 분리(`@` + 도메인 선택)는 흔한 관례지만 자동완성·붙여넣기를 깨뜨린다. 쓰더라도 "직접 입력"을 기본에 둔다 |
| 본인인증 | 아래 10-5 | |
| 약관 동의 | "전체 동의" + 개별 항목. 항목마다 `(필수)`/`(선택)` 표기와 "보기" 링크. **선택 항목은 미리 체크하지 않는다** | 필수와 선택(마케팅 수신 등)을 한 체크박스로 묶으면 선택 동의를 강제하는 셈이 된다. 정확한 고지 문구는 법무 확인 대상이다 |
### 10-5. 본인인증 흐름
- 인증을 요구하기 **전에** 왜 필요한지 한 줄로 말한다(`만 14세 이상 확인을 위해 휴대폰 본인인증이 필요합니다`).
- 인증 수단(휴대폰·간편인증 앱 등)은 실제 연동된 것만 보여 준다. 인증 창은 새 창·팝업·앱 전환으로 열리므로 팝업 차단과 모바일 앱 전환 뒤 복귀를 시험한다.
- 인증을 마치면 **원래 단계로 돌아오고, 그때까지 입력한 값이 남아 있어야 한다.** 인증으로 받은 이름·휴대폰 번호는 자동으로 채우고 "인증됨" 상태와 함께 수정 불가로 표시한다(바꾸려면 재인증 버튼).
- 실패·취소·시간 초과 각각에 다음 행동을 안내한다. 인증 없이 진행할 수 있는 경로가 있으면 함께 보여 준다.
- 실제 인증 연동이 없는 시안·쇼케이스에서는 인증이 된 것처럼 보이게 만들지 않는다 — 버튼과 흐름만 두고 "연동 필요"를 `design.md`에 적는다.
### 10-6. 제출 뒤
- 성공하면 같은 자리에서 무엇이 접수됐고 다음에 무슨 일이 언제 일어나는지 말한다(`예약 요청을 받았습니다. 영업일 하루 안에 문자로 확정해 드립니다`). 상태 메시지로 알린다([accessibility.md](accessibility.md) §7).
- 전송 중 중복 제출을 막고, 실패하면 입력을 지우지 않는다.
- **받을 곳이 없으면 성공을 흉내 내지 않는다.** 전송처(이메일·폼 서비스·API)는 사용자가 정한다. 임의의 폼 서비스를 붙이지 말고 제출을 막은 채 '전송처 미정'을 `design.md`와 납품 보고에 적는다.
---
## 11. 데이터 테이블
> 언제: 관리자 목록·주문 내역·요금 비교·사양표처럼 행과 열로 비교하는 화면. 차트·지표 설계는 데이터 시각화 스킬로 넘기고, 이 절은 표 자체만 다룬다. 정렬 기준선의 기본(숫자는 끝, 텍스트는 시작)은 [layout.md](layout.md)를 따른다.
### 11-1. 마크업
- 진짜 `