designpaca/packages/skill/references/component-systems.md
Yun Chan 88c11682b1 feat(skill): route fixes, delivery report, composition diversity, platform refresh
- route table: extension route for products with an existing visual
  language, feedback row, derived state tokens stay local
- completion: blocking 0 plus reported unverified; hard-gate surface is
  the touched scope; delivery report starts with what and how to open
- design.md created at step 0 with a skeleton, fingerprint line and
  change log; product-surface rubric reading; direction delegation
  states the chosen card and reason
- layout: hero families, nav forms, footer forms; art-direction page
  convergence 6 axes; genre sections; image treatments; placeholder
  conventions; multi-screen order; Tailwind theme mapping
- platform: scroll-driven examples, anchor positioning, canvas OT,
  three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid
- design-gate: four result states, exit codes, cwd module resolution,
  --url/--out-dir/--strict, oklch contrast, dead selector word match
- Korean font budget split from the Latin baseline
2026-10-06 16:55:38 +09:00

33 KiB
Raw Permalink Blame History

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
작업 직전 마지막 확인 §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) [SKILL-SHADCN].


1. 기존 컴포넌트·반복 패턴 우선

프로젝트 계약. 커스텀 마크업(스타일링한 div)을 쓰기 전에 같은 역할의 컴포넌트가 이미 있는지 먼저 검색한다. 콜아웃·빈 상태·로딩 placeholder·상태 배지처럼 화면마다 반복되는 UI는 손으로 다시 짜지 않고 기존 컴포넌트를 확장하거나 그대로 쓴다.

대신 손으로 짜지 않는다 이미 있는지 검색한다
색칠한 div + 아이콘 + 텍스트 Alert/Callout류 컴포넌트
animate-pulse 커스텀 div Skeleton류 컴포넌트
rounded-full bg-* span Badge류 컴포넌트
<hr> 또는 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를 따른다. --dur-*·--ease-* 모션 토큰은 라이브러리가 별도로 정의하지 않는 한 tokens.md §4와 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를 따른다.

<DialogContent>
  <DialogHeader>
    <DialogTitle>프로필 수정</DialogTitle>
    <DialogDescription>정보를 업데이트합니다.</DialogDescription>
  </DialogHeader>
  ...
</DialogContent>

시각적으로 제목을 숨겨야 한다면:

<DialogTitle className="sr-only">프로필 수정</DialogTitle>

4-2. Avatar는 Fallback이 필수다 — 프로젝트 계약

이미지 로드가 실패했을 때의 대체 표시가 없으면 빈 원이 남는다.

<Avatar>
  <AvatarImage src="/avatar.png" alt="사용자" />
  <AvatarFallback>홍</AvatarFallback>
</Avatar>

4-3. 라벨이 있는 묶음은 Group으로 — 프로젝트 계약

리스트형 컴포넌트에서 라벨(SelectLabel 등)로 이름 붙인 묶음은 Group으로 감싼다. Group이 있어야 라벨이 그 묶음의 접근 가능한 이름으로 연결되고, 라벨 컴포넌트는 Group 안에서 쓰도록 설계돼 있다. 묶음 없이 항목만 나열하는 단일 목록에서 Group은 선택 사항이다(Radix 기준 — 라이브러리 문서로 확인한다). 아래는 묶음이 있는 경우다.

Item Group
SelectItem, SelectLabel SelectGroup
DropdownMenuItem, DropdownMenuLabel DropdownMenuGroup
CommandItem CommandGroup
MenubarItem MenubarGroup
// 틀림 — 라벨이 묶음 밖에 떠 있어 어느 항목들의 이름인지 연결되지 않는다
<SelectContent>
  <SelectLabel>과일</SelectLabel>
  <SelectItem value="apple">사과</SelectItem>
</SelectContent>

// 맞음
<SelectContent>
  <SelectGroup>
    <SelectLabel>과일</SelectLabel>
    <SelectItem value="apple">사과</SelectItem>
  </SelectGroup>
</SelectContent>

[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 <SelectValue 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는 항상 배열

트리거를 감싸는 예시:

// 틀림 — 트리거를 추가 엘리먼트로 감쌈
<DialogTrigger>
  <div><Button>열기</Button></div>
</DialogTrigger>

// 맞음 (Radix)
<DialogTrigger asChild>
  <Button>열기</Button>
</DialogTrigger>

// 맞음 (Base UI)
<DialogTrigger render={<Button />}>열기</DialogTrigger>

non-button 렌더 대상 예시(Base UI):

// 틀림 — nativeButton={false} 누락
<Button render={<a href="/docs" />}>문서 보기</Button>

// 맞음
<Button render={<a href="/docs" />} nativeButton={false}>
  문서 보기
</Button>

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 §6이 정본이고, 이 절은 배치와 한국어 폼 관례만 다룬다. 문구는 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 §6.6).

10-2. 필수·선택 표시

  • 소수 쪽만 표시한다. 대부분 필수면 선택 항목에 (선택)을, 대부분 선택이면 필수 항목에 * 또는 (필수)를 단다. *를 쓰면 폼 상단에 "* 필수 입력"을 한 번 적는다.
  • 색만으로 필수를 나타내지 않는다. 필수 입력은 required(또는 aria-required="true")로도 표시한다.

10-3. 오류 메시지 위치와 시점

  • 오류 문구는 해당 필드 바로 아래(라벨 → 입력 → 오류 순서)에 아이콘과 텍스트로 둔다. 토스트·알림창만으로 오류를 알리지 않는다.
  • 검증 시점은 "늦게 꾸짖고 일찍 풀어 준다": 형식 오류는 입력을 마친 뒤(blur) 또는 제출 시 보이고, 이미 오류인 필드는 고치는 즉시 해제한다. 타이핑 도중 오류를 띄우지 않는다.
  • 제출 시 오류가 여러 개거나 필드가 5개 이상이면 폼 상단에 오류 요약(각 항목이 해당 필드로 가는 링크)을 두고 요약 또는 첫 오류 필드로 포커스를 옮긴다. 배선은 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 §7).
  • 전송 중 중복 제출을 막고, 실패하면 입력을 지우지 않는다.
  • 받을 곳이 없으면 성공을 흉내 내지 않는다. 전송처(이메일·폼 서비스·API)는 사용자가 정한다. 임의의 폼 서비스를 붙이지 말고 제출을 막은 채 '전송처 미정'을 design.md와 납품 보고에 적는다.

11. 데이터 테이블

언제: 관리자 목록·주문 내역·요금 비교·사양표처럼 행과 열로 비교하는 화면. 차트·지표 설계는 데이터 시각화 스킬로 넘기고, 이 절은 표 자체만 다룬다. 정렬 기준선의 기본(숫자는 끝, 텍스트는 시작)은 layout.md를 따른다.

11-1. 마크업

  • 진짜 <table>을 쓴다. 표 제목은 <caption>(시각적으로 숨겨도 된다), 열 머리글은 <th scope="col">, 행 머리글이 있으면 <th scope="row">.
  • 정렬 가능한 열은 머리글 안에 <button>을 두고, 현재 정렬 열의 <th>에만 aria-sort="ascending|descending"을 단다. 정렬 방향은 아이콘과 함께 보이게 한다.
  • 행 전체를 클릭 영역으로 만들면 행 안의 대표 셀(이름)에 진짜 링크를 두고 키보드로도 같은 곳에 간다.

11-2. 정렬·숫자·빈 값

  • 숫자 열은 끝 정렬 + font-variant-numeric: tabular-nums. 같은 열의 소수 자릿수를 통일한다.
  • 단위는 머리글에 한 번(금액(원), 용량(GB)), 셀마다 반복하지 않는다.
  • 빈 값은 빈칸이 아니라 —로 표시하고, 0과 구분한다.
  • 음수·하락은 색만이 아니라 부호(−)나 아이콘으로도 표시한다.
  • 기본 정렬 기준을 정하고 화면에 보인다(최근 주문순).

11-3. 밀도

  • 행 높이 3단을 시작값으로 둔다: 촘촘 32px · 기본 40px · 여유 48px. 데이터가 많고 숙련 사용자가 쓰면 촘촘, 처음 쓰는 사람이 많거나 터치가 주 입력이면 여유. 터치 대상 크기는 accessibility.md §8 정책을 따른다.
  • 행 구분은 옅은 가로 구분선이 기본이다. 열이 많아 시선이 행을 잃는 넓은 표에서만 줄무늬를 쓴다. 세로 구분선은 대부분 필요 없다.
  • 긴 텍스트 셀은 말줄임하되 전체 값에 닿는 수단(펼치기·상세 링크)을 둔다.

11-4. 고정 헤더·고정 열

  • 세로로 긴 표는 머리글을 고정한다(th { position: sticky; top: 0; } + 불투명 배경). 페이지 상단 고정 헤더가 있으면 그 높이만큼 top을 내린다.
  • 가로로 넓은 표는 첫 열(식별자)을 고정한다(position: sticky; inset-inline-start: 0). 고정 열 경계에는 스크롤 중에만 보이는 그림자 단서를 둔다.
  • 스크롤 컨테이너에는 tabindex="0", role="region", aria-label(표 제목)을 줘 키보드로도 스크롤할 수 있게 한다.

11-5. 모바일

전략을 과업으로 고른다.

과업 전략
여러 행을 열끼리 비교한다(요금제, 사양, 성적) 가로 스크롤 + 첫 열 고정. 카드 변환 금지 — 비교가 불가능해진다
행 하나씩 훑고 들어간다(주문 내역, 게시물 목록) 카드 변환. 카드 안에서도 라벨-값 쌍을 유지한다
열 일부만 자주 본다 열 우선순위를 정해 좁은 화면에서 낮은 열을 숨기고, 행 상세에서 나머지를 보여 준다

11-6. 선택·일괄 작업·페이지

  • 행 선택 체크박스가 있으면 선택 개수와 일괄 작업 바를 표 위(또는 하단 고정)에 띄운다. 파괴적 일괄 작업은 되돌리기를 우선한다(interaction-feel.md §7).
  • 페이지 이동 방식: 특정 위치로 돌아와야 하는 업무 목록은 페이지네이션, 훑어보는 피드는 더 보기 버튼. 무한 스크롤은 푸터에 닿을 수 없고 뒤로 가기 위치 복원이 어려우므로 표에는 쓰지 않는 것이 기본이다.
  • 빈 상태(데이터 없음)와 필터 결과 없음은 다른 문구·다른 행동으로 구분한다(product-copy.md).

12. 체크리스트 (구현 중 자기 점검 — 납품 판정은 preflight.md X9)

납품 판정은 preflight.md X9가 한다. Dialog·Sheet·Drawer의 접근 가능한 제목(§4-1)은 X9의 하드 게이트 행으로 판정한다.

  • components.json(또는 동등 설정)과 info 명령 출력을 프로젝트에서 실제로 확인했다. 값을 추측하지 않았다
  • 새 UI를 손으로 짜기 전에 기존 컴포넌트·반복 패턴을 먼저 검색했다(§1)
  • 이 작업이 "라이브러리 조립"과 "designpaca 토큰·브랜드 표면·검증" 중 어디에 속하는지 구분했다(§2)
  • 새 CSS 변수를 중복 정의하지 않고 §3 매핑표로 기존 라이브러리 변수에 연결했다
  • Avatar·Group/Item·폼 필드 묶음·오버레이 z-index를 §4의 합성 규칙대로 처리했다
  • 프로젝트가 Base UI인지 Radix인지 확인하고 그 문법만 썼다(§5)
  • 폼이 단일 열·라벨 위·값 길이에 맞는 폭이고, 필수/선택은 소수 쪽만 표시했고, 오류는 필드 바로 아래에 있다(§10)
  • 한국어 폼이면 이름 한 칸·휴대폰 한 칸·우편번호 검색 후 상세주소 포커스·선택 동의 미체크를 지켰고, 본인인증 뒤 입력값이 남는다(§10-4·§10-5)
  • 표에 caption·th scope·정렬 열 aria-sort가 있고, 숫자 열은 끝 정렬·tabular, 단위는 머리글에 있다(§11)
  • 모바일 표 전략이 과업에 맞다 — 비교 과업을 카드로 바꾸지 않았다(§11-5)
  • CLI로 추가·갱신했다면 info → --dry-run/--diff → 검증 순서를 지켰고, --overwrite는 승인 없이 쓰지 않았다(§6)
  • Tailwind 프로젝트라면 §7 클래스 위생을 지켰다. 아니라면 이 절은 적용하지 않았다
  • §8의 화면 유형 조합을 정답이 아니라 참고로만 썼고, 실제 레퍼런스 조사로 확인했다