# 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](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()`로 나머지 반경을 파생시킨다. designpaca가 3종 미만으로 제한한 radius 어휘와 정신이 같다(`tokens.md` §3-b) | 이 표는 라이브러리 변수명을 designpaca가 강제하는 것이 아니라, 이미 있는 값을 designpaca 쪽 역할 토큰과 대응시켜 이중 정의를 막는 목적이다. 색상 값 자체(라이트/다크 쌍)를 어떻게 정하는지는 `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 안에 Item — 프로젝트 계약 리스트형 컴포넌트는 콘텐츠 컨테이너에 Item을 직접 렌더링하지 않고 Group으로 감싼다. 감싸지 않으면 스크린리더가 그룹 경계를 읽지 못하거나 레이아웃이 깨진다. | Item | Group | |---|---| | `SelectItem`, `SelectLabel` | `SelectGroup` | | `DropdownMenuItem`, `DropdownMenuLabel` | `DropdownMenuGroup` | | `CommandItem` | `CommandGroup` | | `MenubarItem` | `MenubarGroup` | ```tsx // 틀림 — Item이 콘텐츠 컨테이너에 직접 사과 // 맞음 사과 ``` [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 | `` | `{ 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} 누락 // 맞음 ``` --- ## 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의 화면 유형 조합을 정답이 아니라 참고로만 썼고, 실제 레퍼런스 조사로 확인했다