[Design system] 디자인 시스템 제작하기

Innes·2025년 6월 30일
0

🎨 디자인 시스템 제대로 만들기: 토큰 기반 중앙 관리부터 Tailwind 연동까지

프로젝트를 진행하다 보면 디자인이 계속 바뀌는데, 그때마다 색상, 폰트, 간격 등을 일일이 수정하는 일이 번거로웠다.

브랜드 컬러 하나만 바꿔도 앱 전체가 한 번에 바뀌고, 버튼의 둥글기를 바꾸면 전부 반영되며, 폰트를 바꿔도 전역에서 따라오도록 만들고 싶었다.

이걸 해결하기 위해 디자인 시스템을 중앙 관리하는 구조를 설계했고, 그 과정을 정리해보았다.


✅ 1️⃣ 디자인 토큰 객체 만들기

먼저 디자인 토큰(Design Token)이 뭔지 간단히 설명하면,

디자인의 결정값을 의미 있는 이름으로 변수화해서 코드로 관리하는 방식이다.

예를 들어 브랜드 메인 색을 그냥 #FF4D8D로 박아 넣는 대신:

export const Colors = {
  brandPrimary: '#FF4D8D',
};

이렇게 변수로 관리하면 이 변수만 바꿨을 때 전역에서 색이 모두 바뀌도록 만들 수 있었다.

✅ 장점

중앙 관리가 가능했다. 한 군데서 정의하면 앱 전체에 반영됐다.

의미가 명확했다. brandPrimary라는 이름만 봐도 쓰임새를 알 수 있었다.

팀 작업에서 일관성을 유지할 수 있었다.

✅ 단점

런타임 변수라서 CSS 클래스에서 직접 못 썼다.

인라인 스타일이나 styled-components 같은 데서는 바로 쓸 수 있었지만

Tailwind 유틸리티 클래스에서는 적용이 안 됐다.

실수로 하드코딩된 색상값을 넣을 가능성이 있었다.

코드리뷰나 규칙으로 강제해야 했다.


✅ 2️⃣ 단점 보완: Tailwind Config에 연동하기

이 방식의 가장 큰 문제는

“변수 기반으로 중앙 관리하면 좋은데, Tailwind 클래스에서는 못 쓰잖아?”

라는 부분이었다.

이를 해결하기 위해 Tailwind의 tailwind.config.js에서 토큰 객체를 불러와서 확장하도록 만들었다.

// tailwind.config.js
const { Colors } = require('./design-system/tokens/colors');

module.exports = {
  theme: {
    extend: {
      colors: Colors,
    },
  },
};

이렇게 하면:

Tailwind가 Colors 객체를 읽어서

bg-brandPrimary, text-brandPrimary 같은 유틸리티 클래스를 자동으로 생성하고

CSS 빌드 시 포함하게 만들었다.

✅ 런타임 이슈가 해결되는 이유

Tailwind는 빌드타임에 config를 읽고 → CSS 클래스를 생성한다.

JS 변수를 config에 넣으면 → 변수 기반으로 클래스를 미리 생성한다.

빌드된 CSS에는 단순히 클래스 이름과 색상값만 들어간다.

브라우저가 읽을 때는 변수 참조가 필요 없다.

결국

“JS 변수 기반 중앙 관리하면서도, Tailwind 클래스화 → 퍼포먼스 최적화 + 일관성 유지”

를 달성할 수 있었다.


👩🏻‍💻 심화 : tailwind plugin api 사용하기

보통은 tailwind.config.js의 extend.colors에 넣으면 bg-primary, text-primary 같은 클래스가 생긴다. 이 정도면 색상을 유틸리티로 쓰기에 충분하다.

문제: ring-color, outline-color는 자동 생성이 안 된다

그런데 디자인 시스템 명세가 이렇게 오면 곤란하다.

버튼 border는 primary, ring-color도 primary, outline-color도 primary로 맞춰주세요.

Tailwind가 기본적으로 ring-primary나 outline-primary는 만들어주지 않는다.
bg, text, border까지만 지원하고 ring-color는 따로 지정해줘야 한다.

이렇게 되면 다음 문제를 만나게 된다.

✅ colors에는 primary가 정의되어 있는데
❌ ring-primary 클래스는 존재하지 않는다.

해결책: Plugin API로 커스텀 유틸리티 생성

이 문제를 해결하기 위해 Tailwind의 Plugin API를 활용했다.

Tailwind의 plugin 함수에서 addUtilities를 쓰면, 원하는 규칙을 따라 유틸리티 클래스를 추가할 수 있다.

아래는 디자인 토큰의 색상을 border, ring-color 유틸리티로 변환해주는 플러그인 예시 코드다.

import plugin from 'tailwindcss/plugin';
import { colors } from './design-system/tokens/colors.js';

export default {
  theme: {
    extend: {
      colors,
    },
  },
  plugins: [
    plugin(({ addUtilities }) => {
      const colorUtilities = Object.entries(colors).reduce((acc, [name, value]) => {
        acc[`.border-${name}`] = { borderColor: value };
        acc[`.ring-${name}`] = { '--tw-ring-color': value };
        return acc;
      }, {});
      
      addUtilities(colorUtilities);
    }),
  ],
}

이 코드가 하는 일

colors.js에 아래처럼 디자인 시스템 토큰을 정의했다고 가정했다.

export const colors = {
  primary: '#4F46E5',
  secondary: '#7C3AED',
  error: '#EF4444',
};

Tailwind의 기본 확장으로는 이런 클래스만 만들어진다.

✅ bg-primary
✅ text-primary
✅ border-primary

하지만 ring-color 유틸리티는 없다.

플러그인 코드가 실행되면:

✅ .border-primary → border-color: #4F46E5
✅ .ring-primary → --tw-ring-color: #4F46E5

이런 규칙을 자동으로 생성해준다.

결과적으로 얻은 것

디자인 토큰을 바꾸면 Tailwind 유틸리티가 한 번에 바뀐다.
ring, border 등 모든 상태에서 디자인 시스템 색상을 일관되게 사용할 수 있게 됐다.

이제 버튼을 이렇게 쓰면 된다.

<button class="border-primary ring-primary">
  Confirm
</button>

✅ 디자인 시스템의 색상이 수정되면 버튼의 border와 ring도 전부 바뀐다.

한 단계 더 확장하기

원한다면 outline, focus-visible 같은 상태까지 지원할 수도 있다.
예를 들어 아래처럼 규칙을 추가하면 된다.

acc[`.outline-${name}`] = { outlineColor: value };
acc[`.focus-visible-${name}`] = { outlineColor: value };

이렇게 하면:

✅ .outline-primary
✅ .focus-visible-primary

클래스도 자동 생성된다.

Tailwind CSS는 기본적으로 bg, text, border 색상 유틸리티만 토큰을 반영한다.
하지만 디자인 시스템에서는 ring, outline 같은 다양한 상태색상도 통일해야 할 때가 많다.

이걸 해결하기 위해 Plugin API를 써서 addUtilities로 규칙을 생성할 수 있다.
이 방법으로 디자인 토큰 변경이 Tailwind 전역 유틸리티 클래스에 일괄 반영되도록 만들 수 있다.

디자인 시스템을 코드화하고, 색상 토큰의 일관된 사용을 강제하고 싶다면 Tailwind Plugin 커스텀화를 고려해볼 만하다.


✅ 3️⃣ 폴더 구조 예시

디자인 시스템은 아예 별도 폴더로 관리하도록 설계했다.

design-system/
  ├── tokens/
  │     ├── colors.ts
  │     ├── typography.ts
  │     ├── spacing.ts
  │     └── ...
  ├── themes/
  │     └── light.ts
  ├── components/
  │     ├── Button.tsx
  │     └── ...
  └── index.ts

✅ tokens: 디자인 결정값 (색상, 간격, 폰트) → 싱글 소스 오브 트루스로 관리했다.
✅ themes: tokens을 조합해 테마를 정의했다.
✅ components: 반드시 tokens/테마에서 값을 가져와서 스타일을 적용하도록 했다.


✅ 4️⃣ 싱글 소스 오브 트루스(Single Source of Truth)

이게 디자인 시스템 설계에서 진짜 핵심이었다.

“디자인 결정값을 한 군데서만 관리하고, 거기가 바뀌면 앱 전체가 바뀌도록 한다.”

색상 팔레트가 바뀌면 → Colors.ts만 수정했다.

폰트 크기를 바꾸면 → Typography.ts만 수정했다.

Spacing 단위를 변경하면 → Spacing.ts만 수정했다.

결국 토큰이 디자인의 "진실의 원천(=Single Source of Truth)"이 되었다.

✅ 다른 말로는
디자인 토큰 저장소

디자인 시스템 변수

디자인 정의 계층

어떻게 부르든 중요한 건 → 중앙에서만 관리한다는 점이었다.


✅ 5️⃣ 결과적으로 얻은 이점

✔ 브랜드 리디자인이 필요하면 → 토큰 파일만 수정하면 됐다.
✔ 팀원들이 각자 색상을 새로 정의하지 못하게 강제할 수 있었다.
✔ 다크모드 / 라이트모드 테마 전환이 훨씬 쉽도록 설계할 수 있었다.
✔ Tailwind 유틸리티를 그대로 써서 → 퍼포먼스 최적화, Purge도 가능했다.


✅ 6️⃣ 최종 정리

내가 권장하는 방식은:

✅ 디자인 토큰을 TypeScript 객체로 관리했다.
✅ design-system 폴더에서 모든 디자인 결정을 중앙 관리했다.
✅ tailwind.config.js에서 이 토큰을 import해 → Tailwind 클래스 생성까지 자동화했다.
✅ 앱 컴포넌트에서는 클래스와 변수 둘 다 자유롭게 활용했다.

결국

"변수 기반 중앙 관리"와 "Tailwind의 퍼포먼스와 편의성"을 둘 다 잡을 수 있었다.


✅ 마무리

이 구조를 만들면서 나도 처음에는

그냥 colors.ts 하나 만들어서 변수만 쓰면 될 줄 알았다 → Tailwind에서 못 썼다.

tailwind.config.js만 쓰자 → JS 코드에서 못 썼다.

둘 다 유지보수는 어떻게 하지?

이런 고민을 거쳤다.

결국 “토큰 → Tailwind config 연동” 이 가장 깔끔하고 표준적인 해법이었다.

profile
무서운 속도로 흡수하는 스펀지 개발자 🧽

0개의 댓글