← 글 목록

notes

style - design system

스타일·디자인 시스템의 토큰·컴포넌트 구성과 일관성 유지 메모.

  • 토큰 → 컴포넌트 → 화면 순서로 쌓고, 화면에서는 색 코드를 직접 쓰지 않는다.
  • 토큰은 CSS 변수로 두고 Tailwind theme에 연결한다. 모노레포면 UI 패키지 경로를 content에 꼭 넣는다.
  • Storybook은 컴포넌트 상태를 스토리로 고정해서 디자인 리뷰용으로 쓴다.

디자인 시스템, Storybook, Tailwind를 각각 어느 층에 두어야 하는지 헷갈려서 정리했다. 도구 설명보다는 내가 실제로 고르는 기준 위주다.

디자인 시스템이 하는 일

색·간격·타이포·컴포넌트 규칙을 토큰 → 컴포넌트 → 화면으로 한곳에 모은다. 앱마다 버튼 스타일을 새로 정하지 않게. 순서는 Foundation(색/타입/스페이싱)이 먼저고 그다음이 컴포넌트다. thankq 쪽은 @monorepo/ui + semantic-* / atomic-unit-* 토큰이 그 역할.

도구별로 맡는 자리

Tailwind

유틸 클래스만으로 빠르게 칠한다. 설치는 프레임워크별 방법대로 하면 되고, 내가 실제로 막혔던 건 모노레포 content 경로 하나였다. 모노레포면 shared UI 패키지의 content 경로를 앱 tailwind.config에 넣어야 클래스가 안 날아간다. (Architect - 모노레포 글에 적어 둔 content glob이랑 같음.)

토큰을 CSS 변수로 두고 Tailwind theme에 이름으로 연결하면 DS와 Tailwind가 덜 싸운다. 다크 모드도 변수 값만 바꾸면 끝이라 컴포넌트 코드는 안 건드린다.

토큰을 Tailwind에 연결하는 코드 (v3 기준)
/* packages/ui/src/tokens.css */
:root {
  --color-primary: 222 89% 55%;
  --radius-md: 8px;
}
.dark {
  --color-primary: 222 89% 65%;
}
// apps/web/tailwind.config.ts
export default {
  content: ['./app/**/*.{ts,tsx}', '../../packages/ui/src/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: { primary: 'hsl(var(--color-primary) / <alpha-value>)' },
      borderRadius: { md: 'var(--radius-md)' },
    },
  },
};

Tailwind v4로 올리면 config 파일 대신 CSS에서 @theme으로 토큰을 선언하고, UI 패키지 경로는 @source "../../packages/ui/src";로 알려 준다.

Storybook

UI를 앱 라우트·API 없이 스토리 단위로 띄운다. Next 프로젝트에서 init을 돌리면 @storybook/nextjs 프레임워크를 알아서 잡아 줘서, next/image나 라우터를 쓰는 컴포넌트도 따로 목을 안 만들고 띄울 수 있었다.

npx storybook@latest init

컴포넌트 variant·상태(로딩/에러/빈)를 스토리로 고정해 두면 디자인 리뷰·회귀에 유리하다. Testing 글의 Storybook 섹션이랑 목적이 같다 — 여기는 “DS 문서화 도구” 관점.

Button 스토리 예시
// packages/ui/src/button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';

const meta: Meta<typeof Button> = { component: Button, title: 'Foundation/Button' };
export default meta;

export const Primary: StoryObj<typeof Button> = { args: { variant: 'primary', children: '저장' } };
export const Loading: StoryObj<typeof Button> = { args: { variant: 'primary', loading: true, children: '저장' } };
export const Disabled: StoryObj<typeof Button> = { args: { disabled: true, children: '저장' } };

로딩, 비활성 같은 상태를 스토리로 박아 두면 디자이너랑 같은 화면을 보면서 얘기할 수 있다.

이 모노레포에서 고르는 법

내 기준은 단순하다. 이미 회사 DS가 있으면 거기 맞추고, 없으면 토큰부터 CSS 변수로 잡은 다음 컴포넌트를 하나씩 늘린다. Foundation을 색, 타이포, 간격, 그림자 정도로만 쪼개도 초반엔 충분했다.