designpaca/packages/skill/references/component-systems.md
Yun Chan 6805fb2be7 feat(skill): absorb external design skills, restore interview gate, add review route
- Restore the step-0 interview as a mechanical gate the skill explicitly
  depends on; add harness.md (per-harness question tools, limits,
  fallbacks) and brief-interview.md (slots, question cards, rounds).
- Add 10 reference docs absorbed from external design skills
  (accessibility, interaction-feel, elevation, color, icons, product-copy,
  component-systems, critique, change-review, print-email) and extend
  existing references.
- Add a review-only route and two hard-gate clauses (truncated content
  reachability, three-flashes limit).
- design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers,
  run axe-core when available, and fix false positives found on a real
  site (decorative alt="", stacked wordmark line count, url-only pages).
- lint-skill: fail if the interview gate section or its links disappear.
- Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
2026-09-24 13:26:03 +09:00

19 KiB

component-systems — 기존 컴포넌트 라이브러리 위에서 일하기

기존 컴포넌트 라이브러리 위에서 일할 때 4단계 전에 읽는다. components.json, @radix-ui/*, Base UI 계열 패키지, cva·tailwind-merge 같은 컴포넌트 기반이 감지되면 여는 조건부 문서다. 감지되지 않으면 이 문서는 읽지 않는다 — designpaca의 기본 파이프라인(SVG 필터·three.js·순수 CSS)은 이 문서 없이도 완결된다.

이 문서를 읽는 법

상황 읽을 곳
지금 컴포넌트 기반이 있는지부터 확인해야 한다 §0
화면을 만들기 전에 뭘 먼저 검색해야 하는지 §1
"이건 브랜드 디자인 시스템 작업인가?"가 헷갈린다 §2
토큰 이름을 라이브러리 변수로 옮겨야 한다 §3
Dialog·Avatar·Group 같은 합성 실수를 피해야 한다 §4
Base UI와 Radix 중 뭘 쓰는 프로젝트인지 API가 다르다 §5
CLI로 컴포넌트를 추가·갱신해야 한다 §6
Tailwind 프로젝트다 §7(조건부)
설정·대시보드 같은 화면 유형의 조합을 참고하고 싶다 §8(관찰 후보)
채팅·스트리밍 UI를 만든다 §9(조건부)
작업 직전 마지막 확인 §10

0. 언제·감지 방법

하드 게이트는 없다. 이 절은 "이 문서를 열어야 하는가"만 판정한다.

다음 중 하나라도 있으면 컴포넌트 기반이 있는 프로젝트다.

  • 저장소 루트 또는 앱 루트에 components.json이 있다(shadcn/ui 계열의 표준 설정 파일)
  • package.json 의존성에 @radix-ui/*, Base UI 계열 패키지(@base-ui/react, 구 @base-ui-components/react 등), cva(class-variance-authority), tailwind-merge/clsx 중 하나 이상이 있다
  • src/components/ui/(또는 동등한 디렉터리)에 button.tsx, dialog.tsx 같은 생성된 프리미티브 파일이 이미 있다

components.json이 있으면 그 안의 aliases, style, iconLibrary, tailwind 필드를 먼저 읽는다. CLI가 있으면(npx shadcn@latest info 등) 실행해 프로젝트가 실제로 어떤 base(radix 또는 base)·경로 별칭·아이콘 라이브러리를 쓰는지 확정하고, 이후 모든 결정에서 그 값을 그대로 따른다. 설정을 읽지 못했는데 라이브러리 종류·경로를 추측해서 설치·수정을 진행하지 않는다 — 불확실하면 사용자에게 묻는다(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()로 나머지 반경을 파생시킨다. designpaca가 3종 미만으로 제한한 radius 어휘와 정신이 같다(tokens.md §3-b)

이 표는 라이브러리 변수명을 designpaca가 강제하는 것이 아니라, 이미 있는 값을 designpaca 쪽 역할 토큰과 대응시켜 이중 정의를 막는 목적이다. 색상 값 자체(라이트/다크 쌍)를 어떻게 정하는지는 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 안에 Item — 프로젝트 계약

리스트형 컴포넌트는 콘텐츠 컨테이너에 Item을 직접 렌더링하지 않고 Group으로 감싼다. 감싸지 않으면 스크린리더가 그룹 경계를 읽지 못하거나 레이아웃이 깨진다.

Item Group
SelectItem, SelectLabel SelectGroup
DropdownMenuItem, DropdownMenuLabel DropdownMenuGroup
CommandItem CommandGroup
MenubarItem MenubarGroup
// 틀림 — Item이 콘텐츠 컨테이너에 직접
<SelectContent>
  <SelectItem value="apple">사과</SelectItem>
</SelectContent>

// 맞음
<SelectContent>
  <SelectGroup>
    <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. Base UI vs Radix — API 차이

프로젝트 계약. 같은 컴포넌트 이름이라도 밑바탕 프리미티브가 Base UI인지 Radix인지에 따라 prop 시그니처가 다르다. components.json의 base 필드나 info 출력에서 어느 쪽인지 먼저 확인하고, 프로젝트가 쓰는 쪽의 문법만 쓴다. 섞어 쓰면 조용히 동작하지 않는 코드가 나온다 [SKILL-SHADCN].

상황 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 버전, base(radix/base), 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].


8. 화면 유형별 조합 참고

언제: 컴포넌트 기반 위에서 익숙한 화면 유형(설정, 대시보드 등)을 조립할 때. 아래는 관찰 후보이며 정답이 아니다.

관찰 후보. 특정 화면 유형에서 자주 관찰되는 컴포넌트 조합 예시다. 그대로 베끼면 템플릿화된 결과가 나온다는 경고와 함께 둔다.

화면 유형 자주 보이는 조합(예시일 뿐)
설정 페이지 Tabs 또는 사이드 내비 + Card + Form(FieldGroup/Field)
대시보드 고정 사이드바 + Card + 차트 컴포넌트 + 데이터 테이블
목록 + 상세 목록 패널 + 인스펙터 패널(넓은 화면에서만, layout.md §4-f(데스크톱 어포던스) 참고)

주의:

  1. 이 표를 정답으로 채택하지 않는다. 브리프·업종·목표 행동에 따라 완전히 다른 조합이 맞을 수 있다
  2. reference-method.md의 레퍼런스 우선 원칙이 이 표보다 앞선다 — 실제 레퍼런스 조사 없이 이 표만으로 화면을 확정하지 않는다
  3. 같은 조합을 모든 설정·대시보드 화면에 기계적으로 반복하면 그 자체가 제네릭 신호다(antipatterns.md 참고)

[SKILL-SHADCN]


9. 스트리밍·채팅 UI는 라이브러리 기본 동작을 재발명하지 않는다

언제: 프로젝트의 컴포넌트 라이브러리가 채팅·스트리밍 UI 프리미티브를 이미 제공할 때.

프로젝트 계약. 스트리밍 채팅 화면에서 자동 스크롤 추적·앵커링·"최신으로 이동" 로직은 흔히 재발명되는 영역이다. 라이브러리가 이 동작을 소유하는 컴포넌트(메시지 스크롤 컨테이너류)를 이미 제공하면, useStickToBottom이나 수동 ResizeObserver 훅으로 직접 재구현하지 않는다. 합성으로 표현할 수 없는 동작만 라이브러리가 제공하는 훅으로 보강하고, 그것도 없을 때만 직접 구현한다. 메시지 행·표면(bubble)·첨부·시스템 구분선도 마찬가지로 라이브러리 프리미티브가 있으면 그것을 합성하고, flex div로 직접 재구성하지 않는다 [SKILL-SHADCN].


10. 체크리스트

  • components.json(또는 동등 설정)과 info 명령 출력을 프로젝트에서 실제로 확인했다. 값을 추측하지 않았다
  • 새 UI를 손으로 짜기 전에 기존 컴포넌트·반복 패턴을 먼저 검색했다(§1)
  • 이 작업이 "라이브러리 조립"과 "designpaca 토큰·브랜드 표면·검증" 중 어디에 속하는지 구분했다(§2)
  • 새 CSS 변수를 중복 정의하지 않고 §3 매핑표로 기존 라이브러리 변수에 연결했다
  • Dialog·Sheet·Drawer에 접근 가능한 제목이 있다(§4-1, 하드 게이트)
  • Avatar·Group/Item·폼 필드 묶음·오버레이 z-index를 §4의 합성 규칙대로 처리했다
  • 프로젝트가 Base UI인지 Radix인지 확인하고 그 문법만 썼다(§5)
  • CLI로 추가·갱신했다면 info → --dry-run/--diff → 검증 순서를 지켰고, --overwrite는 승인 없이 쓰지 않았다(§6)
  • Tailwind 프로젝트라면 §7 클래스 위생을 지켰다. 아니라면 이 절은 적용하지 않았다
  • §8의 화면 유형 조합을 정답이 아니라 참고로만 썼고, 실제 레퍼런스 조사로 확인했다