한국어
15장 컴포넌트와 registry
컴포넌트의 정본은 프레임워크와 무관한 CSS class입니다. 카탈로그의 모든 컴포넌트는 같은 이름의 Vue wrapper를 가져야 하며, wrapper는 패키지로 설치하지 않고 소스를 복사해 제품이 소유합니다.
왜 필요한가
Leysys 디자인 시스템은 HTML class 조합만으로 동작합니다. e-button e-button-md e-button-primary-bold라고 쓰면 어떤 프레임워크에서도 같은 버튼이 나옵니다. 그래도 Vue로 일하는 팀은 class를 타입 있는 prop으로 감싼 컴포넌트, 곧 wrapper(래퍼)를 원합니다. 값을 잘못 쓰면 편집기가 바로 알려 주기 때문입니다.
규칙 없이 제품마다 wrapper를 만들면 같은 버튼이 제품마다 다른 prop 이름을 갖고, AI 에이전트가 그 차이를 퍼뜨립니다. 기본값이 빠진 prop은 {component}-undefined 같은 깨진 class를 만들고, 화면은 조용히 기본 모양으로 보입니다. 컴포넌트를 npm 패키지로 받으면 제품이 소스를 직접 고쳐 소유하기도 어렵습니다.
원칙 / 모델
정본은 class, wrapper는 복사본
CSS와 토큰은 @leysys/{eds,mds,print-ds,markdown-ds} 패키지로 설치합니다. Vue wrapper는 src/registry/vue/에 두고 registry로 배포합니다. registry(레지스트리)는 필요한 항목만 골라 복사해 가는 소스 목록이며, 복사한 소스는 제품 저장소가 소유합니다. shadcn/ui의 copy-paste registry를 벤치마크로 삼았습니다.
처음에는 「세 번 반복되면 wrapper로 승격한다」는 규칙이 있었지만 폐기했습니다. 한 번만 쓰여도 타입 있는 prop과 자동 완성의 이점은 같기 때문입니다. 지금은 카탈로그에 등록한 컴포넌트라면 사용 횟수와 관계없이 CSS anatomy(부품 구성과 class 목록), wrapper, 전용 문서 페이지, API 표, registry item을 모두 가져야 합니다. 이 규칙을 정한 2026-08-02 KST에는 wrapper가 없는 항목이 40건이었고, 2026-10 KST에도 MDS 26건(마케팅 계열 25건과 Menu)이 HTML만 가진 채 남아 있습니다. 동작·상태가 없는 단일 CSS utility는 컴포넌트가 아니라 컴포넌트 아래 계층인 foundation으로 둡니다.
wrapper의 세 가지 구조
wrapper는 headless primitive를 감쌉니다. headless primitive는 동작·키보드·ARIA만 제공하고 모양은 없는 부품이며, Leysys는 주로 Reka UI를 씁니다. Compound와 Atomic은 primitive의 하위 부품 수로 가릅니다.
| 구조 | 기준 | 형태 | 예 |
|---|---|---|---|
| Pattern Compound | 하위 부품 3개 이상 | 여러 SFC를 index.ts가 이름으로 내보냄 | Accordion, Tabs |
| Pattern Atomic | primitive가 하나이거나 없음 | 단일 SFC, root는 primitive 또는 native 요소 | Button, Checkbox, Badge |
| Pattern Encapsulated | 내부 합성을 숨기고 slot만 엶 | 단일 SFC + slot | DatePicker 계열 |
SFC(Single File Component)는 Vue 단일 파일 컴포넌트입니다. Compound의 부품은 asChild(자식 요소를 렌더 대상으로 삼아 동작만 합성하는 방식)를 보존하고, Atomic은 asChild 대신 as로 렌더 요소를 바꿉니다. 외형 변형을 조합해 주는 variant 라이브러리는 쓰지 않고, wrapper 안에 정본 class 이름을 글자 그대로 조합합니다. SFC 하나만 읽어도 어떤 class가 붙는지 보입니다.
ts
// src/registry/vue/e-button/EButton.vue (발췌)
const classNames = computed(() =>
[
"e-button",
`e-button-${effectiveColor.value}-${effectiveVariant.value}`,
`e-button-${effectiveSize.value}`,
effectiveSquare.value && "e-button-square",
]
.filter(Boolean)
.join(" "),
);시작 전 점검과 완료 정의
시작 전에 두 가지를 확인합니다.
- primitive 본질 매핑 — 이 컴포넌트가 어떤 primitive의 무엇인지 「왜」를 7~8단계까지 묻고, shadcn/ui와 Nuxt UI의 공식 구성을 직접 확인합니다. 대체 경로(fallback)·별칭(alias)·직접 만든 검색·필터·상태 합성이 필요해 보이면 근본 원인을 피하고 있다는 신호입니다.
- form input의 native label-wrap — 자기 label을 가진 입력 wrapper는
<label>안에 입력을 넣어 연결합니다. id를 만들어for·id로 따로 잇는 구조가 필요해 보이면 백지에서 다시 검토합니다. Field가 설명 문구를aria-describedby로 잇기 위해 id를 만드는 것은 이 점검과 무관합니다.
완료 정의(DoD, Definition of Done)는 네 가지이며, 하나라도 빠지면 「진행 중」입니다.
| 항목 | 내용 |
|---|---|
| wrapper SFC | src/registry/vue/{component}/의 SFC와 index.ts 내보내기 |
| 문서 페이지 | {Component}Page.vue를 문서 페이지 황금 템플릿(16장)에 맞춰 작성 |
| 카탈로그 표시 | component-catalog-data.js의 artifacts.vue: true. 문서 메뉴의 완료 표시가 여기서 파생 |
| prop 선언 | 추가 prop을 선언하고 기본값을 빠짐없이 줌. audit-wrapper-props.mjs로 확인 |
표준 컴포넌트와 ERP 예외
선도 디자인 시스템 6개(shadcn/ui, Nuxt UI, Material Design 3, Atlassian, Tailwind Catalyst, Toss TDS) 중 4개 이상이 제공하면 표준 컴포넌트이며, 지금 쓰는 곳이 없어도 구현합니다. 기준에 못 미쳐도 ① HTML 요소·W3C 규격 같은 표준이 있고 ② B2B·B2G 업무에 빠질 수 없으며 ③ 100곳 이상 반복해 쓰이면 ERP 도메인 예외로 받습니다. Kbd(단축키 표시)가 그 예입니다. <kbd> 요소가 있고, 회계 업무는 F 키를 쓰며, 결정 당시 제품 코드에 같은 용도의 class가 310곳 있었습니다. 투표·청구서처럼 한 업무에만 쓰는 컴포넌트는 제품이 소유합니다.
크기가 필요한가와 크기 공리
Button·Input은 한 화면에서 다른 크기로 나란히 쓰이므로 크기가 필요하고, Checkbox·Switch·Accordion은 기본 크기로 충분하다고 봅니다. 이것은 설계 판단일 뿐 「스케일이 없다」는 뜻이 아닙니다. 실제 스케일은 크기 공리(10장)가 정하므로 Checkbox·Switch·Accordion도 md를 가운데 둔 xs~xl 5단계를 가집니다.
registry item
registry JSON은 빌드 스크립트가 소스에서 생성합니다. 카탈로그는 https://design.leysys.net/registry/eds.json, item은 https://design.leysys.net/r/eds/e-button-vue.json 형식입니다. item은 설치에 필요한 것을 모두 적습니다.
json
{
"name": "e-checkbox-vue",
"dependencies": ["@leysys/eds@3.1.3", "reka-ui@2.10.5"],
"registryDependencies": ["@leysys-eds/e-field-vue"],
"files": [{ "path": "src/registry/vue/e-checkbox/ECheckbox.vue", "target": "@ui/e-checkbox/ECheckbox.vue" }]
}Checkbox는 Field의 접근성 정보를 읽으므로 e-field-vue가 함께 따라옵니다. 제품은 registryDependencies를 재귀로 따라가 의존 묶음(dependency closure)을 모두 복사합니다. Leysys는 공개 설치 CLI를 배포하지 않으며, 이 호스트는 인증된 접근을 요구할 수 있습니다. 채택 절차는 24장에서 다룹니다.
실제 예 — Checkbox의 root를 primitive로 고친 일
Checkbox wrapper의 root를 <label>로 두었더니 v-model·disabled가 primitive에 닿지 않았습니다. Vue는 바깥에서 받은 속성을 root 요소에만 자동으로 넘기기 때문입니다. 잘 동작하던 Switch와 비교해 원인을 찾았고, 두 규칙을 정했습니다. Atomic wrapper의 root는 Reka primitive이고, label과 설명은 사용하는 쪽 HTML이 감쌉니다.
vue
<label class="e-checkbox e-checkbox-md">
<ECheckbox v-model="checked" />
<span class="e-checkbox-content">
<span class="e-checkbox-label">Receive newsletter</span>
<span class="e-checkbox-description">월 1회 신제품 소식을 받습니다.</span>
</span>
</label>같은 종류의 패치가 두 번 쌓이면 멈추고 백지에서 다시 진단합니다.
경계와 예외
asChild는 wrapper를 만들지 않는 핑계가 아닙니다. trigger가 자식 wrapper를 합성하는 기능입니다.- EDS와 MDS의 wrapper는 서로 import하지 않습니다. 공유하고 싶은 headless 코드도 진영마다 따로 둡니다.
- Print DS의 registry 카탈로그는 비어 있고, Markdown DS는 Vue registry가 없습니다. React·Svelte registry도 아직 없습니다.
- 복사한 소스는 제품이 의도적으로 고칠 수 있습니다. 정본과의 차이는 알림(alert)이며 그것만으로 배포를 막지 않습니다. 다른 제품도 겪을 결함은 정본에서 먼저 고칩니다.
정리
- 정본은 CSS class이고, 카탈로그의 모든 컴포넌트는 같은 이름의 타입 있는 Vue wrapper를 가져야 합니다.
- wrapper는 세 구조 중 하나로 만들고, 시작 전 점검 두 가지와 완료 정의 네 가지를 지킵니다.
- 표준 컴포넌트는 선도 디자인 시스템 6개 중 4개 이상이 기준이며, ERP 예외는 세 조건을 모두 요구합니다.
- wrapper는 registry에서 의존 묶음과 함께 복사하고, 복사한 소스는 제품이 소유합니다.
정본
leysys-designdocs/process/REGISTRY-GUIDE.md— wrapper 구조·registry item·복사 절차leysys-designdocs/decisions/COMPONENT-LAYER-VUE-HYBRID-2026-04-27.md— CSS 정본과 Vue wrapperleysys-designdocs/decisions/PATTERN-B-ATOMIC-PRIMITIVE-2026-04-28.md— Atomic wrapper의 rootleysys-designdocs/decisions/WRAPPER-PROP-DECLARATION-AUDIT-2026-05-02.md— 추가 prop 선언과 감사leysys-designdocs/decisions/COMPONENT-CATALOG-WRAPPER-CARDINALITY-2026-08-02.md— 카탈로그와 wrapper 1:1leysys-designdocs/decisions/PUBLIC-REGISTRY-SOURCE-CONTRACT-2026-07-17.md— 패키지와 registry의 배포 경계leysys-designdocs/specs/COMPONENT-CONVENTION.md— class 명명과 wrapper 분류leysys-designdocs/process/COMPONENT-PRINCIPLES.md— 생성·편집·감사 체크리스트