디자인 시스템
시스템은 결과물 그 자체입니다. 그 외 모든 것은 시스템으로부터 만들어집니다. 고유한 색상과 간격을 임의로 정한 산출물은 일회성에 불과하지만, 토큰 기반으로 구축된 결과물은 확장 가능하고, 리브랜딩이 가능하며, 일관성을 유지할 수 있습니다.
출력물
세 개의 파일이며, 분할 자체에 의미가 있습니다:
design/tokens.css— 토큰만 포함하고 그 외 아무것도 없습니다. 일반 CSS 커스텀 속성입니다.design/components.css— 의미론적 토큰만으로 구축된 클래스 형태의 컴포넌트입니다.design/Design System.html— 견본 페이지입니다. 위 두 파일을<link>로 가져와 문서화합니다. 문서화만 담당하며, 자체적으로 값을 선언하지 않습니다.
이 두 CSS 파일이 시스템입니다. 다른 앱이 복사해 가는 대상입니다. 견본 페이지는 프로젝트 내에 문서로 남습니다.
견본 페이지의 섹션 순서:
- 의도(Intent) — 세 문장: 이 시스템이 누구를 위한 것인지, 어떤 느낌을 주어야 하는지, 명시적으로 무엇이 아닌지.
- 색상(Colour) — 모든 스와치와 토큰 이름, 실제로 해석되는 값, 명도 대비를 통과하는 쌍.
- 타이포그래피(Type) — 폰트 패밀리, 굵기, 각 단계별 명명된 역할이 있는 스케일, 본문 크기의 단락 샘플.
- 공간과 형태(Space and shape) — 간격 스케일, 모서리 둥글기, 그림자, 테두리.
- 컴포넌트(Components) — 각 컴포넌트를 실제 상태로 렌더링하고, 앱이 복사할 마크업을 함께 표시.
- 규칙(Rules) — 그렇지 않으면 실수할 수 있는 다섯 또는 여섯 가지 결정 사항.
- 앱에서 사용하기(Use it in an app) — 버전, 복사할 두 파일, 로드 순서, 폰트
<link>, 루트 클래스, 이전 버전과 달라진 점.
토큰
두 계층입니다. 기본(Primitive)은 원시 값을 보유하고, 의미론적(Semantic) 토큰은 역할을 명명하고 기본 값을 가리킵니다. 컴포넌트는 항상 의미론적 토큰만 참조합니다. 이것이 리브랜딩을 한 줄 변경으로 만드는 핵심입니다.
design/tokens.css에 작성합니다. 이 파일은 프로젝트에서 원시 값이 나타날 수 있는 유일한 파일입니다. acme는 시스템의 프리픽스를 나타냅니다(공유 섹션 참조):
/* Acme 디자인 시스템 1.0.0 — 토큰. 이 파일을 그대로 복사하세요; 원본 프로젝트에서 변경하세요. */
:root {
/* 기본 */
--acme-ink-900: oklch(0.22 0.02 260);
--acme-ink-600: oklch(0.48 0.02 260);
--acme-paper-50: oklch(0.99 0.005 90);
--acme-paper-100: oklch(0.96 0.008 90);
--acme-line-200: oklch(0.90 0.006 90);
--acme-accent-500: oklch(0.55 0.14 35);
--acme-accent-600: oklch(0.45 0.15 35);
--acme-accent-100: oklch(0.94 0.03 35);
/* 의미론적 */
--acme-text: var(--acme-ink-900);
--acme-text-muted: var(--acme-ink-600);
--acme-bg: var(--acme-paper-50);
--acme-surface: var(--acme-paper-100);
--acme-border: var(--acme-line-200);
--acme-accent: var(--acme-accent-500);
--acme-accent-hover: var(--acme-accent-600);
--acme-accent-wash: var(--acme-accent-100);
/* 타이포그래피 */
--acme-font-display: "Fraunces", Georgia, serif;
--acme-font-text: "Public Sans", system-ui, sans-serif;
--acme-step--1: 0.875rem;
--acme-step-0: 1rem;
--acme-step-1: 1.25rem;
--acme-step-2: 1.5625rem;
--acme-step-3: 1.953rem;
--acme-step-4: 2.441rem;
--acme-step-5: 3.052rem;
/* 간격, 4px 기준 */
--acme-s-1: 4px; --acme-s-2: 8px; --acme-s-3: 12px; --acme-s-4: 16px;
--acme-s-5: 24px; --acme-s-6: 32px; --acme-s-7: 48px; --acme-s-8: 64px; --acme-s-9: 96px;
/* 형태와 모션 */
--acme-radius-sm: 4px; --acme-radius: 8px; --acme-radius-lg: 16px;
--acme-border-width: 1px; --acme-ring-width: 2px;
--acme-shadow-1: 0 1px 2px oklch(0.2 0.02 260 / 0.06);
--acme-shadow-2: 0 4px 16px oklch(0.2 0.02 260 / 0.08);
--acme-ease: cubic-bezier(0.2, 0.7, 0.3, 1);
--acme-dur: 160ms;
}
해당 블록은 시작점이며, 맹목적으로 복사할 템플릿이 아닙니다. 요구사항에 맞게 강조 색조, 폰트 조합, 잉크 온도를 변경하되 구조는 유지하세요.
토큰 규칙
- 대비는 측정하며, 눈대중으로 하지 않습니다. 본문 텍스트는 실제 배경 대비 4.5:1; 큰 텍스트와 UI 테두리는 3:1. 모든 스와치 페이지에 그 대비쌍을 명시합니다.
- Oklch 사용, 임의의 hex 사용 금지. 균일한 명도 단계는 팔레트가 뿌옇게 보이는 것을 방지합니다. 색조 계열 내에서 채도를 대략 일정하게 유지하세요.
- 사용되지 않는 토큰 금지. 아무 것도 참조하지 않는 토큰은 삭제하세요.
- 이 파일 외부에 값 금지. 다른 곳에 있는 hex,
oklch()리터럴, px 크기 또는 그림자는 아직 이름이 지정되지 않은 토큰입니다. - 두 개의 폰트 패밀리. 하나는 디스플레이, 하나는 텍스트용. 폴백 스택은
sans-serif단독이 아닌 실제 폰트여야 합니다. - 애드리브 금지, 스케일 사용. 간격 또는 타이포그래피 스케일에 없는 값이라면 의도적으로 추가하거나 가장 가까운 값을 사용하세요.
- 다크 모드는 요청 시에만 — 하지만 색상을 의미론적으로 유지하여 나중에 추가할 때 토큰 교체로 가능하게 하고, 재작성하지 않도록 합니다.
컴포넌트
design/components.css에 작성하고, 견본 페이지에서 각 컴포넌트를 기본, 호버, 포커스, 활성, 비활성 상태(해당하는 경우)로 렌더링합니다:
- 버튼 — 기본, 보조, 고스트. 하나의 크기면 충분합니다.
- 텍스트 입력과 선택(select), 레이블, 도움말 텍스트, 오류 상태 포함.
- 카드, "Lorem ipsum"이 아닌 실제 콘텐츠 예시 포함.
- 네비게이션, 데스크톱 및 좁은 너비 모두.
- 헤더가 있는 테이블 또는 리스트 행.
- 배지와 태그.
- 모달 또는 다이얼로그.
- 빈 상태(empty state).
- 토스트 또는 인라인 알림.
각 컴포넌트는 의미론적 토큰만 사용합니다. 컴포넌트에 토큰이 없는 값이 필요하다면 시스템에 갭이 있는 것입니다 — 토큰을 추가하고 값을 하드코딩하지 마세요.
/* Acme 디자인 시스템 1.0.0 — 컴포넌트. 같은 버전의 tokens.css가 먼저 로드되어야 합니다. */
.acme {
font-family: var(--acme-font-text);
font-size: var(--acme-step-0);
color: var(--acme-text);
background: var(--acme-bg);
}
.acme-button {
font: inherit;
padding: var(--acme-s-2) var(--acme-s-4);
border: var(--acme-border-width) solid transparent;
border-radius: var(--acme-radius);
background: var(--acme-accent);
color: var(--acme-bg);
transition: background var(--acme-dur) var(--acme-ease);
}
.acme-button:hover { background: var(--acme-accent-hover); }
.acme-button:focus-visible { outline: var(--acme-ring-width) solid var(--acme-accent); outline-offset: var(--acme-ring-width); }
.acme-button--ghost { background: transparent; color: var(--acme-accent); border-color: var(--acme-border); }
공유
다른 앱은 tokens.css와 components.css를 복사하여 시스템을 가져갑니다. 아래 규칙은 복사본이 어디서나 작동하도록 합니다.
- 모든 것에 프리픽스를 붙이세요. 첫 번째 토큰 전에 시스템의 짧은 이름 하나를 선택하고 모든 커스텀 속성과 모든 클래스에 적용합니다:
--acme-accent,.acme-button. 그러면 복사본이 앱 자체 CSS와 충돌 없이 공존할 수 있습니다. 프리픽스는 API의 일부입니다; 절대 변경하지 마세요. - 두 파일 모두 순수 CSS. Tailwind 지시어, 전처리기 문법, 중첩,
@layer요구사항, 파일 간@import없음. React 앱, 정적 페이지, 또는 다른 디자인 도구에서 동일한 결과를 얻을 수 있어야 합니다. - 모든 것은 런타임에 해석됩니다. 빌드 타임 색상 계산, 컴파일된 출력 없음.
components.css에 전역 스타일 없음. 리셋, 요소 선택자,html또는body에 대한 스타일 없음: 파일을 앱에 추가해도 기존 스타일을 변경하지 않아야 합니다. 기본 텍스트 스타일은 루트 클래스.acme에 있으며, 앱이 원하는 곳에 추가합니다.- 상태는 마크업에서 옵니다. 앱이 토글해야 하는 상태 클래스 대신
:hover,:focus-visible,:disabled,[aria-invalid="true"],[aria-current],[aria-expanded]를 스타일링하여 모든 프레임워크의 HTML과 작동하도록 합니다. - 재정의가 쉽습니다. 가능하면 규칙당 하나의 클래스, ID 없음,
!important없음. - 두 파일 모두 동일한 버전. 각 파일의 첫 줄은 시스템과 버전을 명시합니다; 변경 시마다 두 파일 모두 버전을 올립니다. 값 변경은 패치, 새 토큰이나 컴포넌트는 마이너, 제거는 메이저. 이름 변경은 새 토큰을 추가하고 이전 토큰을 별칭으로 유지합니다(
--acme-old: var(--acme-new);), 앱이 이동할 때까지; 별칭 제거가 메이저 범프입니다. - 프로젝트 관련 사항은 견본 페이지에 남깁니다. 폰트
<link>태그, 컨테이너 너비, 에셋 경로는 토큰이 아닙니다. 견본 페이지는 폰트<link>를 보여주어 앱이 파일과 함께 복사할 수 있도록 합니다.
대상 프로젝트에 이미 토큰 파일이 있다면, 임의로 새 이름을 만들기 전에 그 명명 규칙을 따르세요. 공유된 어휘는 더 나은 이름보다 가치가 있습니다.
견본 페이지
견본 페이지는 감상용 컴포넌트 갤러리가 아니라 사람을 위한 문서입니다. 스와치 옆에 토큰 이름을 표시하세요. 각 컴포넌트를 앱이 복사할 마크업과 함께 보여주세요. 보기 어려운 상태(포커스 링, 비활성화)를 주목하세요.
두 파일을 연결하되 토큰 파일을 먼저, 루트 클래스를 <body>에 추가합니다. 시스템 자체의 타이포그래피와 간격을 사용하여 견본 페이지를 배치하세요 — 이것이 시스템이 작동한다는 첫 번째 증거입니다.
값을 파일에서 읽어와서 입력하지 않도록 하여 문서가 틀어지지 않게 하세요:
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components.css">
…
<body class="acme">
<figure class="swatch" data-token="--acme-accent-500"></figure>
…
<script>
for (const el of document.querySelectorAll('[data-token]')) {
const name = el.dataset.token;
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
el.dataset.value = value; // CSS로 출력
el.style.setProperty('--swatch', value); // CSS로 칠함
}
</script>
스와치는 토큰에서 스스로 색을 칠하고 브라우저가 해석한 값을 출력합니다. 수동으로 입력한 hex는 미래의 거짓말입니다.
마무리
견본 페이지에서 design-verify를 실행합니다. 견본 페이지나 components.css에 있는 모든 색상 리터럴을 나열합니다. 없어야 합니다. 모든 스와치 레이블이 읽기 쉬운지, 모든 컴포넌트가 동일한 베이스라인 그리드에 정렬되었는지, 1024px 너비에서 샘플 텍스트가 컨테이너를 넘치지 않는지 확인하세요.
시스템이 다른 앱으로 이동할 때, tokens.css와 components.css를 변경하지 않고 복사하고, 견본 페이지의 폰트 <link>도 함께 복사합니다 — 견본 페이지 자체는 복사하지 않습니다. 견본 페이지의 독립 실행형 복사본은 폴더 없이 읽어야 하는 경우에만 내보냅니다.