디자인 레이어
Wippy 프론트엔드는 독립적으로 배포된 여러 모듈이 하나의 애플리케이션으로 렌더링되는 구조입니다. 두 개의 자리는 명확합니다. 모든 서피스가 소비하는 테마, 그리고 스스로를 소유하는 모듈입니다. 그 사이의 간극은 명확하지 않으며, 바로 그곳에 중복이 쌓입니다 — 여러 모듈이 실제로 공유하지만 테마에는 해당 컴포넌트가 없는 개념입니다.
이 페이지는 세 개의 레이어를 명명하고, 그중에서 고르는 기준을 제시하며, 각 선택이 잘 되었을 때와 잘못되었을 때 어떤 모습인지 보여줍니다.
레이어
| 레이어 | 도달 범위 | 소유 대상 |
|---|---|---|
| 테마 | 여러분이 소유하지 않은 모듈을 포함한 모든 서피스 | PrimeVue 컴포넌트, 공유 시맨틱 토큰, 문서화된 클래스 |
| 공유 디자인 레이어 | 옵트인한 모듈만 | 해당 모듈들이 공유하지만 뒷받침하는 테마 컴포넌트가 없는 어휘 |
| 모듈 | 자기 자신 | 하나의 서피스에만 진정으로 특수한 것 |
테마는 보편적이며, 그것이 곧 제약이다
테마는 여러분이 소유하지 않은 마크업을 스타일링합니다. 어떤 모듈이든 — 여러분의 앱을 본 적 없는 사람이 작성한 서드파티 플러그인을 포함해 — 같은 호스트로 렌더링되고 같은 테마로 칠해집니다. 그래서 테마가 보편 레이어이며, 이는 양방향으로 작용합니다.
앱에 특수한 것은 어떤 것도 테마에 들어갈 수 없습니다. 그것을 요청한 적 없는 모든 모듈에 강요되기 때문입니다.
모듈은 앱에 특수한 무언가가 테마에 있다는 것에 의존해서는 안 됩니다.
계약은 PrimeVue 컴포넌트 + 공유 Wippy 시맨틱 토큰 + 문서화된 클래스이며,
애플리케이션이 그 위에 추가한 것은 포함되지 않습니다. PrimeVue 자체의 프리셋도
계약이 아닙니다. Wippy는 PrimeVue를 theme: 'none'으로 실행하므로, 여러분이
의존하는 것은 Wippy 시맨틱 토큰입니다.
/* 좋음 — 모든 모듈에 존재하는 공유 Wippy 시맨틱 토큰 */
.my-panel {
color: var(--p-text-color);
background: var(--p-content-background);
border: 1px solid var(--p-content-border-color);
}
/* 나쁨 — 애플리케이션에 특수한 토큰. 이제 모듈은 하나의 앱 안에서만
동작하며, 다른 곳에서는 선언이 조용히 사라집니다: 정의되지 않은
커스텀 프로퍼티는 계산값 시점에 선언을 무효로 만들고, 선언이 버려지면서
엘리먼트는 조용히 상속을 받게 됩니다. */
.my-panel { background: var(--kx-surface-2); }
이것은 *"우리의 공유 어휘를 파사드에 넣어도 될까?"*에 대한 답이기도 합니다. 임의의, 소유하지 않은 마크업에 진정으로 도달해야 할 때만 그렇습니다. 여러분의 모듈 집합으로 범위가 한정된다면 그것은 테마에 속하지 않습니다 — 그 아래 레이어에 속합니다.
백본, 그리고 컴포넌트가 옵트아웃할 수 있는 경우
호스트가 제공하는 PrimeVue와 Tailwind는 모든 컴포넌트에 권장되는 백본입니다. 컴포넌트는 옵트아웃할 수 있습니다 — 그러나 관례적인 무언가를 렌더링하는 순간 옵트아웃 범위는 좁아지며, 이 사다리는 한 방향으로만 갑니다.
| 컴포넌트가… | 그러면 로드해야 하는 것 |
|---|---|
| 표현 중립적이다 — 캔버스, SVG, 컨트롤 없는 차트, 토큰 없음, 유틸리티 없음, 스크롤 없음 | 없음: hostCssKeys: [] |
| 시맨틱 토큰이나 다크 모드를 사용한다 | themeConfigUrl |
| 스크롤할 수 있다 | iframeCssUrl |
| 마크다운을 렌더링한다 | markdownCssUrl |
| Tailwind로 표현 가능한 무언가를 렌더링한다 | Tailwind — 직접 작성한 CSS 대신 유틸리티를 사용 |
| PrimeVue가 컴포넌트를 제공하는 무언가를 렌더링한다 — 버튼, 입력, 폼, 테이블, 다이얼로그, 메뉴, 태그, 툴팁, 모든 피드백 컨트롤 | primeVueCssUrl 및 PrimeVuePlugin |
캔버스 위의 차트는 정당한 옵트아웃의 전형입니다. 고전적인 UI가 없으므로 백본이 전혀 필요 없습니다. 같은 차트에 툴바를 붙이는 순간 더 이상 표현 중립적이지 않습니다 — 그 버튼은 PrimeVue 버튼이고, 통합 전체가 따라옵니다.
결합 관계에 주목하세요. Tailwind 유틸리티는 primeVueCssUrl과 함께
제공됩니다. 별도의 Tailwind 호스트 CSS 키는 없으므로, 실제로 Tailwind가
필요한 컴포넌트는 PrimeVue 애셋도 로드하게 됩니다. (preflightCssUrl은 키
유니온의 일부가 아닙니다. 섀도우 루트 안에서 Tailwind preflight가 정말로
필요하다면 명령형으로 로드하세요 — 거의 필요하지 않습니다.)
이 페이지에서의 실질적 결론은 이렇습니다. 모듈이 원하는 것의 대부분은 이미 백본에 존재합니다. 공유 디자인 레이어는 그 위의 좁은 띠이지, PrimeVue와 Tailwind가 이미 다루는 것을 다시 만드는 자리가 아닙니다. 메커니즘은 CSS 주입을 참고하세요.
공유 디자인 레이어
어떤 개념들은 알려진 모듈 집합 전반에서 반복되지만 테마에는 컴포넌트가 없습니다. 콘텐츠 카드, 서피스 헤더 행, 서피스에 아무것도 없을 때 보여주는 것, 태그가 갖는 크기들. 실재하고, 공유되며, 갈 곳이 없습니다.
이들은 배포된 패키지로 제공되며 빌드 시점에 각 소비자에게 구체화됩니다. 소비자들이 서로 다른 저장소에 살기 때문에 경로 별칭이 아니라 패키지여야 합니다 — 이 레이어의 반증 가능한 기준은, 다른 저장소에 있고 생산자에 대한 경로 접근이 없는 모듈이 그 어휘를 소비하고 빌드된다는 것입니다.
생산 모듈은 그 패키지를 빌드 타임 아티팩트로 선언하고 각 소비자는 이를
자신의 트리로 구체화합니다. 선언 방법, node-package 포맷, 런타임이 대신
조정해주는 것, 그리고 빌드가 여전히 직접 제공해야 하는 접착 코드는
빌드 타임 아티팩트를 참고하세요.
모듈
그 밖의 모든 것, 그리고 공유 어휘로부터의 모든 의도적 이탈.
무엇이 어디에 속하는지 정하기
순서대로 물어보세요. 첫 번째 "예"가 답입니다.
- 값인가? 색상, 반경, 간격, 고도, 심각도. → 테마. 시맨틱 토큰을 읽으세요. 리터럴은 절대 안 됩니다.
- 테마가 이미 이것에 대한 컴포넌트를 제공하는가? Button, Dialog, Select, Tag. → 테마. 컴포넌트를 사용하세요. 스타일은 클래스를 그 위에 얹어서 지정하고, 절대 다시 만들지 마세요.
- 여러분의 모듈 둘 이상이 같은 개념을 필요로 하는데 뒷받침하는 테마 컴포넌트가 없는가? → 공유 디자인 레이어.
- 그 외 → 모듈.
사람들이 걸려 넘어지는 것은 2번이며, 그 뒤에는 날카로운 규칙이 있습니다.
실제 사례
아래 예시들은 이 레이어가 생기기 전 모듈 CSS의 15.4%가 완전 복제 중복이었던 Wippy 애플리케이션 Kickside에서 가져온 것입니다.
테마 컴포넌트를 절대 다시 만들지 말 것
PrimeVue는 Button을 제공합니다. Kickside의 아홉 개 모듈이 이를 쓰지 않고
네이티브 <button> 위에 .kx-btn을 직접 만들었고, 다른 일곱 모듈은
컴포넌트를 사용했습니다. 두 방언 모두 국소적으로는 합리적이었습니다 — 버튼을
둘 공유 장소가 없었기에 앱의 절반이 하나를 발명한 것입니다. 서로 비교해 보니
font-size와 line-height만 일치했고 나머지는 전부 달랐습니다.
나쁨: .kx-btn .kx-btn-primary를 단 네이티브 button 엘리먼트 — 테마가
이미 제공하는 컴포넌트의 두 번째 구현입니다. (여기서 일부러 셀렉터로
적었습니다. 문서 게이트는 예제 코드에서 네이티브 제품 컨트롤을 거부하는데,
이는 같은 규칙이 한 레이어 위에서 강제된 것입니다.)
좋음: 테마 컴포넌트를 쓰고, 조정이 필요하면 클래스를 얹습니다.
<Button label="Save" class="kx-save" />
테마 컴포넌트가 맞지 않는다고 해서 다시 만들어도 된다는 뜻은 아닙니다.
컴포넌트에 클래스를 얹고 그 클래스를 스타일링하세요 — 조정이 앱 전역이라면
파사드에서, 국소적이라면 모듈에서. Kickside의 knowledge 모듈은 여전히
네이티브 버튼에 .kn-btn / .kn-primary를 달고 있습니다. 그것은 남아 있는
마이그레이션이지 따라 할 패턴이 아닙니다.
심각도는 테마의 것이지 여러분의 것이 아니다
심각도 — success, danger, warn, info — 는 배포된 램프를 가진 테마
시맨틱입니다. Kickside는 이를 네 가지 명명 체계에 걸쳐 열여섯 번
재유도했습니다(tone-gn, t-ok, kx-tone-success, tone-success). 같은
클래스 이름이 세 모듈에서 세 가지 다른 색을 의미했으므로, 그중 하나를
배포했다면 나머지를 조용히 다시 칠했을 것입니다.
/* 나쁨 — 모듈 로컬 이름으로 심각도를 재유도 */
.tone-gn { color: #16a34a; }
/* 좋음 — 테마에서 가져온 심각도 */
.status-dot.success { background: var(--p-success-500); }
톤은 공유 레이어에 존재할 수 있지만, 오직 장식적 범주 색상으로서만 가능하며 심각도로서는 안 됩니다. "이것은 실패했다"를 의미할 수 있다면 그것은 심각도이고 테마의 것입니다.
테마에 자리가 없는 공유 어휘
/* 좋음 — PrimeVue는 Card도, 서피스 Header도, EmptyState도 제공하지 않습니다.
이들은 뒷받침하는 테마 요소 없이 모듈 전반에서 반복되므로,
정확히 공유 레이어가 존재하는 이유입니다. */
@import "@kickside/ui-kit/kx-card.css";
@import "@kickside/ui-kit/kx-state.css";
채택한다는 것은 import하고 삭제하는 것
CSS @import는 시트 안의 다른 모든 규칙보다 앞서야 합니다. 따라서 공유 시트는
항상 먼저 놓이고, 모듈이 그 뒤에 선언하는 것은 동일 명시도에서 이를
이깁니다. 패키지를 import하면서 자기 사본을 그대로 두는 모듈은 아무것도 바꾸지
않은 것입니다.
/* 나쁨 — import는 무효하고, 로컬 사본이 여전히 이깁니다 */
@import "@kickside/ui-kit/kx-card.css";
.kx-card { border-radius: 14px; border: 1px solid var(--p-content-border-color); }
/* 좋음 — import하고, 로컬 사본을 삭제하고, 문서화된 차이만 남깁니다 */
@import "@kickside/ui-kit/kx-card.css";
/* 이 서피스의 카드는 조밀한 목록 안에 인라인으로 놓이므로 떠오르는 효과를 뺍니다. */
.kx-card:hover { transform: none; }
차이만 남기고, 본문 전체를 다시 적지 마세요. 그리고 두 의도를 한 이름에 접어 넣지 마세요. 클래스 이름이 두 모듈에서 다른 것을 의미한다면 그것은 한 이름을 쓰고 있는 두 개념입니다. 이름을 나누세요. 승자를 골라 패자를 다시 칠하지 마세요.
테마에 대한 명시도
모듈의 CSS는 섀도우 루트에 먼저 주입되고, 테마의 PrimeVue 시트가 그 뒤에
추가됩니다. 둘 다 <style> 엘리먼트이므로 문서 순서가 결정하고 테마가
두 번째입니다. 테마 컴포넌트 클래스를 이겨야 하는 모듈 규칙에는 파일에서 더
뒤의 줄이 아니라 더 높은 명시도가 필요합니다. (adoptedStyleSheets는 테마가
아니라 파사드의 커스텀 CSS를 담으므로, 채택된 시트에 기대도 이 싸움에서는
이기지 못합니다.)
이 문제는 여러분의 클래스가 테마가 적용된 엘리먼트 위에 놓이는 패스스루 클래스에서 가장 크게 나타납니다.
/* 나쁨 — 이 클래스는 PrimeVue 자체의 footer 엘리먼트에 적용되므로,
동일 명시도에서 테마가 이기고 패딩은 결코 적용되지 않습니다. */
.kx-modal-foot { padding: 14px 18px; }
/* 좋음 — 다이얼로그 루트 아래로 범위를 좁혀 테마보다 명시도가 높습니다 */
.kx-modal > .kx-modal-foot { padding: 14px 18px; }
공유 레이어에 담을 수 있는 것
모듈 집합이 진정으로 공유하고 테마가 소유하지 않는 모든 것: CSS 어휘, 파생 토큰, 내부 컴포넌트, 헬퍼, 테스트 하네스. 중복의 성격은 동일합니다 — Kickside에는 복제된 CSS와 나란히 하나의 테스트 부트스트랩 사본이 열아홉 개 있었습니다.
시맨틱 단위로 나누어 제공하세요. 각 단위는 소비자가 이해할 수 있는 하나의
명명된 개념이어야 합니다 — kx-card, kx-state, kx-tag. 소비자가 필요한
것만 가져갈 수 있도록 더 잘게 나뉜 패키지를 선호하세요. 명확히 명명된 여러
단위를 담은 단일 패키지도 쓸 만하지만, 지향할 형태는 아닙니다.
잡동사니 단위는 절대 안 됩니다. common도, shared도, misc도, utils도
안 됩니다. 안에 무엇이 들었는지 이름이 말해주지 않는 단위는 갈 곳 없는 모든
것을 끌어모으게 되고, 결국 이 레이어가 해결하려던 문제를 다시 만들게 됩니다.
정규화는 시각적 변경이다
흩어진 사본을 통합하면 픽셀이 움직입니다. Kickside에는 열일곱 개의 서로 다른 본문에 걸쳐 열아홉 개의 정의를 가진 셀렉터가 하나 있었습니다. 모든 본문을 비교하고, 정본을 고르고, 왜 그것을 골랐는지 기록하고, 의도적 이탈은 문서화된 오버라이드로 남기세요 — 그리고 결과를 눈으로 확인하세요. 유닛 테스트는 레이아웃을 볼 수 없습니다.
관련 문서
- 테마 적용 — 토큰 카탈로그, 그리고 테마가 호스트와 자식 양쪽에 도달하는 방식
- 준수 체크리스트 — 프론트엔드가 검사받는 모듈별 규칙
- 빌드 타임 아티팩트 — 패키지를 선언하고 소비자에게 구체화하기
- 의존성 관리 — 모듈이 소비하는 것을 선언하고 해석하기