탐색

스타일 작성

패키지 작성

공유 CSS 소스를 게시하고 각 앱에서 공개 토큰을 재정의하며 프로덕션 사용 앱에서 패키징한 패키지를 검증합니다.

이 페이지의 내용

#개요

CSS 소스를 게시하여 안정적인 스타일 어휘를 공유하세요. 각 사용 앱은 자체 Master CSS 진입점에 소스를 가져오고 필요한 토큰, 유틸리티, 네이티브 컴포넌트를 컴파일합니다. 한 앱 안에서 어휘가 정상 동작한 뒤 여기서 시작하세요. 스타일 재사용이 그 첫 단계를 다룹니다.

이 패키지 형태에서는 앱의 생성 스타일시트나 매니페스트가 아닌 소스 정의를 게시하세요. 그러면 앱이 자체 클래스 사용, 토큰 재정의, 전달 모드를 선택할 수 있습니다. 런타임 통합도 제공하는 패키지는 별도의 산출물 계약을 가집니다.

정의는 공유하고 앱마다 컴파일하기

두 앱은 같은 CSS 소스를 가져옵니다. 네이티브 컴포넌트는 기본적으로 포함되며 각 앱은 자체 마크업이 요청하는 유틸리티를 생성합니다.

텍스트 설명
  1. 소스 공유. 패키지는 네이티브 .btn 컴포넌트와 필요할 때 생성되는 content-auto 유틸리티를 정의합니다.
  2. 앱 A. 마크업은 btn을 사용합니다. 네이티브 컴포넌트는 포함되지만 사용하지 않는 content-auto 유틸리티는 출력되지 않습니다.
  3. 앱 B. 마크업은 btn과 content-auto를 사용합니다. 출력에 네이티브 컴포넌트와 요청한 유틸리티가 포함됩니다.

#패키지 만들기

이 예제는 @acme/ui-theme을 사용합니다. 게시 전에 자신이 소유한 scope로 바꾸세요. 사용 측은 패키지 이름으로 가져오며 내보내는 CSS 파일은 master.css입니다.

  • my-master-theme
    • package.json
    • master.css
    • README.md

패키지 루트가 master.css를 가리키게 하세요:

package.json
{  "name": "@acme/ui-theme",  "version": "0.1.0",  "files": ["master.css"],  "style": "./master.css",  "exports": {    ".": {      "style": "./master.css",      "default": "./master.css"    }  },  "sideEffects": ["./master.css"]}

이 패키지 형태에는 번들러, TypeScript 빌드, 매니페스트 생성, 컴파일된 CSS 출력이 필요하지 않습니다. 패키지는 소스 CSS를 게시하며 Master CSS를 지원하는 프로젝트가 자체 스타일시트 그래프의 일부로 컴파일할 수 있습니다.


#master.css 작성하기

사용 프로젝트에 제공할 공유 어휘를 master.css에 두세요.

master.css
@theme {  --color-brand: #4f46e5;  --color-text-action: var(--color-text-strong);  --color-focus-action: var(--color-text-strong);  --spacing-action-x: 1rem;  --radius-action: 0.375rem;}@custom-media --theme-motion (prefers-reduced-motion: no-preference);@utility content-auto {  content-visibility: auto;  contain-intrinsic-size: auto 32rem;}@layer components {  .btn {    display: inline-flex;    align-items: center;    justify-content: center;    gap: var(--spacing-xs);    min-height: 2.375rem;    max-width: 100%;    padding: .5rem var(--spacing-action-x);    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-action);    font-size: var(--font-size-sm);    font-weight: 400;    line-height: 1.2;    background-color: var(--color-surface-raised);    color: var(--color-text-action);    box-shadow: var(--shadow-sm);    &:hover {      box-shadow: var(--shadow-md);    }    &:active {      background-color: var(--color-surface-inset);      box-shadow: none;    }    &:focus-visible {      outline: 2px solid var(--color-focus-action);      outline-offset: 3px;    }  }}

이 예제는 색상 표시, 네이티브 .btn 컴포넌트, 필요할 때 생성되는 content-auto 유틸리티, 이름이 있는 theme-motion 조건을 제공합니다. 컴포넌트는 공유 상태를 소유하고 앱은 전환을 요청할지 선택합니다. 네이티브 컴포넌트 CSS는 기본적으로 포함되며 유틸리티는 사용할 때만 규칙을 생성합니다.

사용 측에 필요한 최소 공개 어휘를 선택하세요:

  • 색상, 간격, 반경, 타이포그래피, 그림자, 모션, 컨테이너 토큰 같은 공유 값에는 @theme을 사용합니다.
  • 미디어 이름에는 @custom-media를, 다른 재사용 래퍼에는 @contents를 가진 @mixin을 사용합니다.
  • 필요할 때 생성되는 클래스에는 @utility를, 네이티브 CSS 안의 재사용에는 @mixin을 사용합니다.
  • 패키지가 btn, field, card, toolbar 같은 의미 있는 UI 어휘를 의도적으로 소유할 때만 네이티브 @layer components를 사용합니다.

토큰 명명은 테마 토큰, 지시문 문법은 CSS 지시문, 추상화 선택은 전역 스타일을 참고하세요.


#패키지 사용하기

패키지를 설치한 뒤 기본 Master CSS 진입점 다음에 가져오세요:

app.css
@import '@master/css';@import '@acme/ui-theme';

가져온 소스는 프로젝트 CSS 진입점 그래프의 일부가 됩니다. 프로젝트 마크업은 공유 어휘를 유틸리티와 네이티브 CSS 클래스로 사용할 수 있습니다:

공유 패키지 어휘

<article class="p-lg b-solid b(1) b-line-divider r-lg content-auto">  <p class="flex items-center gap-xs m(0) text-xs fg-text-muted"><span aria-hidden="true" class="h(8) w(8) pill bg-brand"></span>Package brand</p>  <h2 class="mb-xs mt-sm text-lg font-regular">Shared vocabulary</h2>  <p class="mt(0) mb-md text-sm fg-text-muted">The app supplies the markup; the package supplies the shared action.</p>  <button type="button" class="btn (transition-property:box-shadow,background-color)@theme-motion (transition-duration:var(--duration-fast))@theme-motion (transition-timing-function:var(--easing-smooth))@theme-motion">Preview action</button></article>
생성된 CSS
@layer components {  .btn {    justify-content: center;    align-items: center;    gap: var(--spacing-xs);    max-width: 100%;    min-height: 2.375rem;    padding: .5rem var(--spacing-action-x);    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-action);    font-size: var(--font-size-sm);    background-color: var(--color-surface-raised);    color: var(--color-text-action);    box-shadow: var(--shadow-sm);    font-weight: 400;    line-height: 1.2;    display: inline-flex;    &:hover {      box-shadow: var(--shadow-md);    }    &:active {      background-color: var(--color-surface-inset);      box-shadow: none;    }    &:focus-visible {      outline: 2px solid var(--color-focus-action);      outline-offset: 3px;    }  }}@layer theme {  :root,  :host {    --color-zinc-5: oklch(96.5% .0011 265);    --color-zinc-40: oklch(68.6% .0028 265);    --color-zinc-60: oklch(48.6% .003 265);    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-inset: light-dark(var(--color-zinc-5), var(--color-zinc-100));    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white));    --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-weight-regular: 400;    --font-size-xs: .75rem;    --font-size-sm: .875rem;    --font-size-lg: 1.125rem;    --text-xs: var(--font-size-xs);    --text-xs--line-height: 1.5;    --text-xs--letter-spacing: 0;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --text-lg: var(--font-size-lg);    --text-lg--line-height: 1.5;    --text-lg--letter-spacing: 0;    --radius-lg: .5rem;    --spacing-xs: .5rem;    --spacing-sm: .75rem;    --spacing-md: 1rem;    --spacing-lg: 1.5rem;    --duration-fast: .15s;    --easing-smooth: cubic-bezier(.4, 0, .2, 1)  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24));    --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26))  }  :root,  :host {    --color-brand: #4f46e5;    --color-text-action: var(--color-text-strong);    --color-focus-action: var(--color-text-strong);    --spacing-action-x: 1rem;    --radius-action: .375rem  }}@layer utilities {  .text-lg {    font-size: var(--text-lg);    line-height: var(--text-lg--line-height, normal);    letter-spacing: var(--text-lg--letter-spacing, normal)  }  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .text-xs {    font-size: var(--text-xs);    line-height: var(--text-xs--line-height, normal);    letter-spacing: var(--text-xs--letter-spacing, normal)  }  .content-auto {    content-visibility: auto;    contain-intrinsic-size: auto 32rem  }  .items-center {    align-items: center  }  .bg-brand {    background-color: var(--color-brand)  }  .b-line-divider {    border-color: var(--color-line-divider)  }  .r-lg {    border-radius: var(--radius-lg)  }  .pill {    border-radius: calc(infinity * 1px)  }  .b-solid {    border-style: solid  }  .b\(1\) {    border-width: calc(1 * 1px)  }  .fg-text-muted {    color: var(--color-text-muted)  }  .flex {    display: flex  }  .font-regular {    font-weight: var(--font-weight-regular)  }  .gap-xs {    gap: var(--spacing-xs)  }  .h\(8\) {    height: calc(8 * 1px)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .mb-md {    margin-bottom: var(--spacing-md)  }  .mb-xs {    margin-bottom: var(--spacing-xs)  }  .mt-sm {    margin-top: var(--spacing-sm)  }  .mt\(0\) {    margin-top: calc(0 * 1px)  }  .p-lg {    padding: var(--spacing-lg)  }  .w\(8\) {    width: calc(8 * 1px)  }  @media (prefers-reduced-motion: no-preference) {    .\(transition-duration\:var\(--duration-fast\)\)\@theme-motion {      transition-duration: var(--duration-fast)    }  }  @media (prefers-reduced-motion: no-preference) {    .\(transition-property\:box-shadow\,background-color\)\@theme-motion {      transition-property: box-shadow, background-color    }  }  @media (prefers-reduced-motion: no-preference) {    .\(transition-timing-function\:var\(--easing-smooth\)\)\@theme-motion {      transition-timing-function: var(--easing-smooth)    }  }}
버튼에 마우스를 올리거나 누르거나 초점을 맞춰 패키지의 공유 상태를 확인하세요. 저장 동작은 수행하지 않습니다. 앱은 패키지의 theme-motion 조건으로 동작 줄이기 설정을 존중합니다.

프로젝트 진입점에서 가져온 패키지는 이미 로컬 스타일시트에서 사용할 수 있습니다. 한 스타일시트의 컴파일 컨텍스트에만 패키지를 추가하려면 @reference를 사용하세요:

components/Button.module.css
@reference '@acme/ui-theme';.button {  padding-inline: var(--spacing-action-x);  border-radius: var(--radius-action);  background-color: var(--color-brand);}

로컬 .button은 패키지의 간격, 반경, 브랜드 토큰을 사용합니다. @reference는 이 스타일시트를 컴파일할 때 정의를 제공하며 출력에는 필요한 테마 리소스가 포함됩니다. 패키지의 네이티브 .btn 규칙이나 생성 유틸리티 클래스는 복사하지 않습니다. 해당 규칙이 필요하면 패키지 CSS를 별도로 불러오세요. 로컬 스타일시트 패턴은 라우트 수준 스타일을 참고하세요.


#사용 측에서 공개 값 재정의하기

앱별 선택은 패키지 가져오기 뒤에 두세요. 예를 들어 이 앱은 컴포넌트 CSS를 복사하지 않고 공유 동작 버튼을 넓힙니다:

app.css
@import '@master/css';@import '@acme/ui-theme';@theme {  --spacing-action-x: 1.5rem;}

.btn은 여전히 var(--spacing-action-x)를 읽습니다. 앱은 가로 안쪽 여백을 1rem에서 1.5rem으로 바꾸고 다른 앱은 각자 값을 유지합니다. 사용 측이 의존할 수 있는 토큰 이름, 컴포넌트 선택자, 유틸리티 이름을 문서화하세요. 패키지에 JavaScript가 없어도 공개 이름을 바꾸면 사용 측의 마크업이나 스타일시트에 영향을 줍니다.

값을 재정의하고 컴포넌트 유지하기

사용 측 재정의는 패키지 가져오기 뒤에 둡니다. 안쪽 안내선은 루트 16px에서 1rem과 1.5rem을 비교하며 .btn 규칙은 계속 공유됩니다.

텍스트 설명

패키지와 앱이 하나의 .btn 정의를 공유합니다. 앱은 --spacing-action-x를 바꾸어 버튼의 정체성을 유지하면서 가로 안쪽 여백을 늘립니다.

#앱 정책은 앱 안에 유지하기

재사용 가능한 작성 패키지가 특정 앱의 소스 스캔, 필터링, 배포 방식을 결정해서는 안 됩니다.

다음은 사용 앱에 유지하세요:

  • 앱별 @source와 @source not 규칙.
  • 앱별 @safelist와 @blocklist 정책.
  • 라우트, 페이지 또는 컴포넌트의 일회성 클래스.
  • 생성된 매니페스트 JSON과 컴파일된 생성 CSS 출력.

패키지는 공유 어휘에 사용하고 앱 CSS 진입점은 로컬 소스 경계, 전달 선택, 앱별 정책에 사용하세요.


#예제로 검증하기

패키지 디렉터리에서 소스를 패키징하세요. npm pack은 로컬 아카이브를 만들며 레지스트리에 게시하지는 않습니다:

npm pack

예제 이름과 버전의 아카이브는 acme-ui-theme-0.1.0.tgz입니다. master.css와 예상한 package.json 내보내기가 포함되었는지 확인하세요. files 목록은 관련 없는 소스와 빌드 산출물을 제외하며 npm은 표준 메타데이터 파일도 포함합니다.

Master CSS가 이미 통합된 작은 사용 앱에 아카이브를 설치하세요:

npm install ../my-master-theme/acme-ui-theme-0.1.0.tgznpm run build

위 마크업을 스캔되는 소스 파일에 넣으세요. 프로덕션 출력에서 네이티브 .btn 규칙, 요청한 content-auto 유틸리티, 필요한 테마 리소스를 확인합니다. 앱을 열어 버튼의 계산된 안쪽 여백을 검사하고 로컬 재정의로 반복하세요. 기본 상태뿐 아니라 호버, 키보드 초점, 동작 줄이기도 검증합니다.

패키지 해석은 프레임워크 통합의 빌드로 검증하세요. 현재 독립형 CLI 검사에서는 앱 진입점을 불러와도 서드 파티 베어 패키지 가져오기가 스타일시트 그래프 밖에 남을 수 있습니다. 나열된 스타일시트 의존성을 확인하세요. 패키지의 master.css가 빠진 검사는 해당 어휘를 검증한 것이 아닙니다. 프로덕션 사용 앱은 로컬 소스 전용 미리보기로 확인할 수 없는 파일과 내보내기 경로도 검증합니다.