02 — Five Layers (5-레이어 모델)

디자인 시스템을 Foundations / Tokens / Recipes / Theming / Distribution 의 5층으로 쪼개는 이유는 우아함이 아니라 각 층의 변경 빈도와 변경 책임자가 다르기 때문이다. 변경 속도가 다른 것들을 한 파일에 두면 모든 변경의 비용이 가장 느린 변경의 비용에 맞춰진다. 5-레이어는 그 비용 폭발을 막는 구조적 격리벽이다.


Why — 왜 굳이 5층인가, 3층이면 안 되나

“디자인 시스템 = 토큰 + 컴포넌트” 라는 2층 모델은 그럴듯해 보이지만 6개월 안에 무너진다. 무너지는 지점은 항상 같다 — 언제 어느 값이 쓰여야 하는가가 토큰에 박혀버린다.

시도결과
color.blue.500 = #3370b8 만 두기다크모드 추가 시 “blue.500은 다크에서 더 밝아야 하는데…” → 토큰 이름과 의도가 충돌
color.primary.light.500, color.primary.dark.500 분리컴포넌트가 어느 토큰을 써야 하는지 매번 결정 → 컴포넌트 코드에 if-else 도배
3번째 브랜드 추가토큰이 N×M으로 폭발

근본 원인은 “이 색은 무엇인가(값)“와 “이 색을 언제 쓰는가(컨텍스트)“와 “이 색이 어떤 컴포넌트의 어떤 부위에 쓰는가(역할)” 가 한 곳에 뭉쳐 있다는 것. 5-레이어는 이 셋을 각각 다른 층으로 분리한다.

핵심: 위로 갈수록 느리게, 비싸게 변하고, 아래로 갈수록 자주, 싸게 변한다. 이 그래디언트를 한 파일에 섞으면 가장 자주 바뀌는 것의 변경조차 디자인 리드의 승인을 받아야 한다.


How — 각 레이어의 정의·책임·인터페이스

Layer 1 — Foundations (원칙·기초)

다루는 것: 디자인 원칙(voice & tone), 그리드 시스템(4px/8px), 접근성 기준(WCAG/APCA), 글로벌 레이아웃 규칙.

변경 빈도: 수년에 한 번 (브랜드 리프레시 수준).

산출물: 문서. 코드가 아니다.

# Foundations
 
- **그리드**: 4px 베이스 (모든 spacing은 4의 배수)
- **접근성**: WCAG 2.2 AA 필수, AAA 권장
- **반응형 단계**: 768 / 1024 / 1440 (3-step)
- **모션**: prefers-reduced-motion 존중

Layer 2 — Tokens (값)

다루는 것: 색·간격·타이포·shadow·radius·duration의 명명된 값.

변경 빈도: 분기마다 (디자인 리프레시·새 시멘틱 추가).

인터페이스: DTCG JSON (단일 출처) → 빌드 타임에 CSS variables / TS const / Swift / Kotlin으로 변환.

{
  "color": {
    "blue": {
      "500": { "$value": "#3370b8", "$type": "color" }
    },
    "primary": {
      "default": { "$value": "{color.blue.500}", "$type": "color" }
    }
  }
}

3계층 토큰 (사실상 표준):

  • Primitive (color.blue.500) — 순수한 값, 의미 없음.
  • Semantic (color.primary.default) — 의미·역할, primitive를 alias.
  • Component (button.primary.bg) — 특정 컴포넌트의 특정 부위, semantic을 alias.

자세한 내용은 01-tokens 챕터에서.

Layer 3 — Recipes & Variants (규칙)

다루는 것: 토큰을 어떻게 조합해서 컴포넌트로 만들 것인가.

변경 빈도: 주마다 (새 컴포넌트·새 variant 추가).

인터페이스: 도구별로 다름 — 이 지점에서 Panda CSS와 Tailwind가 갈린다.

Panda CSS의 recipe (타입 안전, zero-runtime):

import { cva } from 'styled-system/css'
 
export const button = cva({
  base: {
    px: '4',
    py: '2',
    rounded: 'md',
    fontWeight: 'medium',
  },
  variants: {
    intent: {
      primary: { bg: 'primary.default', color: 'white' },
      danger:  { bg: 'danger.default',  color: 'white' },
    },
    size: {
      sm: { fontSize: 'sm', px: '3' },
      lg: { fontSize: 'lg', px: '6' },
    },
  },
  defaultVariants: { intent: 'primary', size: 'sm' },
})

Tailwind의 utility 조합 (CVA 또는 tailwind-variants 사용):

import { cva } from 'class-variance-authority'
 
export const button = cva(
  'px-4 py-2 rounded-md font-medium',
  {
    variants: {
      intent: {
        primary: 'bg-primary-500 text-white',
        danger:  'bg-danger-500  text-white',
      },
      size: {
        sm: 'text-sm px-3',
        lg: 'text-lg px-6',
      },
    },
    defaultVariants: { intent: 'primary', size: 'sm' },
  }
)

두 코드의 구조는 거의 동일하다. 차이는 Panda는 토큰 객체를 직접 참조(bg: 'primary.default')하고, Tailwind는 토큰을 클래스 이름으로 인코딩(bg-primary-500)한다는 점.

Layer 4 — Theming (컨텍스트)

다루는 것: 같은 컴포넌트가 다른 컨텍스트에서 다른 토큰을 받는 메커니즘 — 다크모드, 멀티 브랜드, RTL.

변경 빈도: 브랜드/테마 추가 시 (드물지만, 큰 변경).

인터페이스: CSS variables의 cascade — [data-theme], :root, prefers-color-scheme.

:root {
  --color-primary: #3370b8;  /* light brand A */
}
:root[data-theme="dark"] {
  --color-primary: #5499d8;  /* dark brand A */
}
:root[data-brand="b"] {
  --color-primary: #b83370;  /* brand B */
}

핵심: Recipe(Layer 3)는 어느 토큰을 쓸지만 결정하고, 그 토큰의 실제 값은 Theming(Layer 4)이 컨텍스트에 따라 정한다. Recipe는 테마를 알 필요가 없다.

Layer 5 — Distribution (전달)

다루는 것: npm 패키지, 빌드 파이프라인, Storybook/docs 사이트, RFC 프로세스, SemVer, changelog.

변경 빈도: 매 릴리스 (1~2주 주기 권장).

인터페이스: @company/design-tokens, @company/ui npm 패키지 + 문서 사이트.

# 1) 토큰 빌드 (DTCG → CSS, TS)
pnpm tokens build
 
# 2) 컴포넌트 빌드
pnpm ui build
 
# 3) Storybook 빌드
pnpm storybook build
 
# 4) 릴리스 (changeset)
pnpm changeset publish

자세한 내용은 08-pipeline-distribution에서.


What — 레이어 간 인터페이스 표

Layer산출물단일 출처소비자변경 권한
1 Foundations원칙 문서 (.md)docs/ 폴더모든 레이어디자인 리드
2 TokensDTCG JSONtokens.jsonLayer 3, Layer 4디자이너 + 토큰 owner
3 Recipes.ts recipe 파일recipes/컴포넌트 코드개발자
4 ThemingCSS variablesthemes/*.css브라우저 cascade디자인+개발
5 Distributionnpm 패키지dist/다른 제품 팀인프라

Panda CSS와 Tailwind의 교차점 — Layer 2와 Layer 3

핵심 관찰: Panda CSS와 Tailwind는 Layer 3에서 갈리고, Layer 2(DTCG 토큰)에서 다시 만나고, Layer 4(CSS variables) 에서 한 번 더 만난다. 즉, 토큰 정의 + theming만 공유하면 두 도구는 한 앱에서 공존 가능하다. 이게 07-panda-tailwind-interop의 핵심 주장이다.

실제 디렉터리 예시

@company/design-system/
├── 1-foundations/        # 문서 (md)
│   ├── principles.md
│   ├── grid.md
│   └── accessibility.md
├── 2-tokens/             # DTCG JSON + 빌드 산출물
│   ├── tokens.json
│   ├── build/
│   │   ├── tokens.css
│   │   └── tokens.ts
│   └── style-dictionary.config.js
├── 3-recipes/            # Panda recipe + CVA
│   ├── button.recipe.ts
│   ├── card.recipe.ts
│   └── input.recipe.ts
├── 4-theming/            # 컨텍스트별 토큰 오버라이드
│   ├── light.css
│   ├── dark.css
│   └── brand-b.css
└── 5-distribution/       # 빌드·배포 설정
    ├── package.json
    ├── changeset/
    └── docs/

What-if — 레이어를 무너뜨리면

1) Layer 3에서 Layer 1의 결정을 한다 (가장 흔한 실수)

  • 증상: 새 컴포넌트마다 spacing 값이 12px, 13px, 15px처럼 들쭉날쭉.
  • 원인: Recipe 작성자가 4px grid(Layer 1)를 모르거나, 컴포넌트마다 자기 토큰을 만들어버림.
  • 대응: lint 룰로 px 리터럴 금지, 토큰만 참조 강제. Foundations 문서를 온보딩 필수로.

2) Layer 2에 컨텍스트가 박힌다

  • 증상: color.primary.dark.500, color.primary.light.500 같은 토큰이 증식.
  • 원인: Theming(Layer 4)을 토큰 이름으로 표현하려 함.
  • 대응: color.primary.500 한 이름만 두고, [data-theme] cascade로 전환.

3) Layer 4가 Layer 3에 침투한다

  • 증상: Recipe 안에 if (theme === 'dark') 같은 로직이 등장.
  • 원인: Recipe가 어느 테마인지를 알려 함. 잘못된 추상화.
  • 대응: Recipe는 시멘틱 토큰만 참조(primary.default), 실제 값은 Theming이 cascade로 주입.

4) Layer 5(Distribution) 없이 monorepo 직참조

  • 증상: 디자인 시스템 코드를 monorepo의 다른 패키지가 경로로 import (../../design-system/src/...).
  • 원인: 패키지 경계가 없으니 어떤 게 public API인지 불분명. 토큰 이름이 자유롭게 바뀜.
  • 대응: 디자인 시스템을 반드시 npm 패키지로 publish. monorepo 내부에서도 @company/ui 형태로 import.

5) 5개 레이어를 한 파일에 욱여넣기 (theme.ts)

// 잘못된 예 — 모든 레이어가 섞임
export const theme = {
  colors: { blue: '#3370b8' },          // Layer 2
  button: {                              // Layer 3
    primary: { bg: '#3370b8', ... },     // Layer 4를 모름 (다크모드 불가)
  },
  grid: 4,                               // Layer 1
  packageName: '@company/ui',            // Layer 5
}
  • 증상: 색 하나 바꾸려면 theme.ts 코드 리뷰가 7명 필요.
  • 원인: 변경 속도가 다른 것이 한 파일에 있음.
  • 대응: 위 디렉터리 예시처럼 물리적으로 분리.

Insight — 5-레이어는 어디서 왔는가

“5-레이어는 누가 처음 그렸나? 답: 명확하지 않다. 모두가 동시에 같은 그림에 도달했다.”

Brad Frost(2013)는 컴포넌트 수직 계층(AtomsPages)을 그렸지만 토큰 레이어가 없었다. Salesforce(2014)는 토큰을 도입했지만 Atomic Design을 따르지 않았다. Material Design 1.0(2014)에는 Principles(Layer 1)이 노골적이었지만 토큰은 theme 객체로 묶여 있었다(Layer 2와 4가 섞임). 20182020년 사이 Brad Frost의 Atomic Design 2판, Jina Anne의 DTCG 발족, Adam Wathan의 Tailwind 인기가 동시에 일어나면서, 업계는 암묵적 합의5-레이어 분리에 수렴했다. 누구도 “5-레이어 모델”이라고 공식 선언하지 않았지만, 2023년 이후 출간된 모든 디자인 시스템 책(Knapp의 Design Engineering, Salesforce의 Lightning DS handbook)은 이 5층을 전제한다. 분류는 발명되는 것이 아니라 수렴하는 것이다.

또 하나 흥미로운 관찰 — 5-레이어는 Conway’s Law의 산물이다. 디자인 시스템 팀의 역할 분담이 자연스럽게 5층으로 갈렸다 — 디자인 리드(Foundations), 디자이너(Tokens), 개발자(Recipes), 디자인+개발 협업(Theming), 인프라(Distribution). 이 5명이 서로 다른 속도로 일한다. 코드 구조가 조직 구조를 따라간다는 Conway의 1968년 관찰이 디자인 시스템에도 그대로 적용된다.

마지막으로 — 5-레이어 모델은 Panda CSS와 Tailwind 같은 도구가 공존 가능하다는 것을 증명한다. 두 도구가 한 앱에서 충돌하는 이유는 Layer 3에서 다른 어휘를 쓰기 때문이지, Layer 2의 토큰이 다르기 때문이 아니다. DTCG JSON을 단일 출처로 두고 두 도구의 config를 생성하면, 한 앱이 Panda recipe도 쓰고 Tailwind utility도 쓸 수 있다. 이게 07 챕터에서 풀어낼 핵심이다.


요약 + Mermaid

  • 5-레이어의 정당성은 우아함이 아니라 변경 속도의 격리.
  • Layer 1 Foundations(수년) → Layer 5 Distribution(매 릴리스)으로 갈수록 빠르게 변한다.
  • Panda CSS와 Tailwind는 Layer 3에서 갈리고, Layer 2/4에서 다시 만난다.
  • 레이어를 무너뜨리는 가장 흔한 실수: 컨텍스트(Layer 4)를 토큰 이름(Layer 2)에 박기.
  • 5-레이어는 누가 발명한 것이 아니라 업계가 수렴한 합의. Conway’s Law의 산물.