프로젝트를 진행하다 보면 디자인이 계속 바뀌는데, 그때마다 색상, 폰트, 간격 등을 일일이 수정하는 일이 번거로웠다.
브랜드 컬러 하나만 바꿔도 앱 전체가 한 번에 바뀌고, 버튼의 둥글기를 바꾸면 전부 반영되며, 폰트를 바꿔도 전역에서 따라오도록 만들고 싶었다.
이걸 해결하기 위해 디자인 시스템을 중앙 관리하는 구조를 설계했고, 그 과정을 정리해보았다.
먼저 디자인 토큰(Design Token)이 뭔지 간단히 설명하면,
디자인의 결정값을 의미 있는 이름으로 변수화해서 코드로 관리하는 방식이다.
예를 들어 브랜드 메인 색을 그냥 #FF4D8D로 박아 넣는 대신:
export const Colors = {
brandPrimary: '#FF4D8D',
};
이렇게 변수로 관리하면 이 변수만 바꿨을 때 전역에서 색이 모두 바뀌도록 만들 수 있었다.
중앙 관리가 가능했다. 한 군데서 정의하면 앱 전체에 반영됐다.
의미가 명확했다. brandPrimary라는 이름만 봐도 쓰임새를 알 수 있었다.
팀 작업에서 일관성을 유지할 수 있었다.
런타임 변수라서 CSS 클래스에서 직접 못 썼다.
인라인 스타일이나 styled-components 같은 데서는 바로 쓸 수 있었지만
Tailwind 유틸리티 클래스에서는 적용이 안 됐다.
실수로 하드코딩된 색상값을 넣을 가능성이 있었다.
코드리뷰나 규칙으로 강제해야 했다.
이 방식의 가장 큰 문제는
“변수 기반으로 중앙 관리하면 좋은데, 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.config.js의 extend.colors에 넣으면 bg-primary, text-primary 같은 클래스가 생긴다. 이 정도면 색상을 유틸리티로 쓰기에 충분하다.
그런데 디자인 시스템 명세가 이렇게 오면 곤란하다.
버튼 border는 primary, ring-color도 primary, outline-color도 primary로 맞춰주세요.
Tailwind가 기본적으로 ring-primary나 outline-primary는 만들어주지 않는다.
bg, text, border까지만 지원하고 ring-color는 따로 지정해줘야 한다.
이렇게 되면 다음 문제를 만나게 된다.
✅ colors에는 primary가 정의되어 있는데
❌ ring-primary 클래스는 존재하지 않는다.
이 문제를 해결하기 위해 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 커스텀화를 고려해볼 만하다.
디자인 시스템은 아예 별도 폴더로 관리하도록 설계했다.
design-system/
├── tokens/
│ ├── colors.ts
│ ├── typography.ts
│ ├── spacing.ts
│ └── ...
├── themes/
│ └── light.ts
├── components/
│ ├── Button.tsx
│ └── ...
└── index.ts
✅ tokens: 디자인 결정값 (색상, 간격, 폰트) → 싱글 소스 오브 트루스로 관리했다.
✅ themes: tokens을 조합해 테마를 정의했다.
✅ components: 반드시 tokens/테마에서 값을 가져와서 스타일을 적용하도록 했다.
이게 디자인 시스템 설계에서 진짜 핵심이었다.
“디자인 결정값을 한 군데서만 관리하고, 거기가 바뀌면 앱 전체가 바뀌도록 한다.”
색상 팔레트가 바뀌면 → Colors.ts만 수정했다.
폰트 크기를 바꾸면 → Typography.ts만 수정했다.
Spacing 단위를 변경하면 → Spacing.ts만 수정했다.
결국 토큰이 디자인의 "진실의 원천(=Single Source of Truth)"이 되었다.
✅ 다른 말로는
디자인 토큰 저장소
디자인 시스템 변수
디자인 정의 계층
어떻게 부르든 중요한 건 → 중앙에서만 관리한다는 점이었다.
✔ 브랜드 리디자인이 필요하면 → 토큰 파일만 수정하면 됐다.
✔ 팀원들이 각자 색상을 새로 정의하지 못하게 강제할 수 있었다.
✔ 다크모드 / 라이트모드 테마 전환이 훨씬 쉽도록 설계할 수 있었다.
✔ Tailwind 유틸리티를 그대로 써서 → 퍼포먼스 최적화, Purge도 가능했다.
내가 권장하는 방식은:
✅ 디자인 토큰을 TypeScript 객체로 관리했다.
✅ design-system 폴더에서 모든 디자인 결정을 중앙 관리했다.
✅ tailwind.config.js에서 이 토큰을 import해 → Tailwind 클래스 생성까지 자동화했다.
✅ 앱 컴포넌트에서는 클래스와 변수 둘 다 자유롭게 활용했다.
결국
"변수 기반 중앙 관리"와 "Tailwind의 퍼포먼스와 편의성"을 둘 다 잡을 수 있었다.
이 구조를 만들면서 나도 처음에는
그냥 colors.ts 하나 만들어서 변수만 쓰면 될 줄 알았다 → Tailwind에서 못 썼다.
tailwind.config.js만 쓰자 → JS 코드에서 못 썼다.
둘 다 유지보수는 어떻게 하지?
이런 고민을 거쳤다.
결국 “토큰 → Tailwind config 연동” 이 가장 깔끔하고 표준적인 해법이었다.