최근에 디자이너분과 함께 디자인 시스템을 새롭게 구성했다.
기획 초기부터 Figma ↔ 코드 ↔ 실제 앱 간의 연결성을 고려했고,
특히 Nx 기반 모노레포 구조를 사용하는 만큼 확장성과 테마 유연성에 초점을 맞췄다.
이 글에서는 내가 어떻게 설계했는지, 어떤 구조로 잡았는지, 그리고 실제 적용 사례까지 정리해본다!!!!🤖
• 하나의 시스템으로 여러 앱 커버하기
• Figma 기준에 최대한 맞추기
• 테마별 커스터마이징이 쉽게 가능할 것
• 디자이너와 개발자가 같은 언어로 말할 수 있도록
디자이너분와 여러 차례의 회의 끝에 다음과 같은 계층 구조를 정리했다:
1. Primitive
디자인 토큰 또는 가장 기본이 되는 원자 스타일들
2. Semantic
의미 있는 역할을 가진 클래스 이름들 (ex. badge-role-primary-bg)
3. Theme
각 앱에 따라 다르게 설정 가능한 CSS 변수 값들
결국 Primitive는 Figma에서 정의된 디자인 토큰,
Semantic은 클래스 네이밍을 통한 역할 지정,
Theme은 실제 변수값을 오버라이드하는 구조다.
디자인 시스템 관련 코드는 libs/ds/ 하위에 모아뒀다.
libs/
└── ds/
└── src/
└── semantic/
├── badge.ts
├── button.ts
├── ...
└── plugin.js
각 파일은 tailwindcss/plugin을 활용해 다음처럼 만들었다.
// badge.ts 예시..
import plugin from 'tailwindcss/plugin'
export const badge = plugin(({ addUtilities }) => {
addUtilities({
// role
'.badge-role-primary-bg': {
backgroundColor: 'var(--color-badge-primary-bg)',
},
'.badge-role-primary-border': {
borderStyle: 'var(--border-style-badge)',
borderWidth: 'var(--border-width-badge)',
borderColor: 'var(--color-badge-primary-border)',
},
'.badge-role-primary-text': {
color: 'var(--color-badge-primary-text)',
},
// size
'.badge-size-xs-height': {
height: 'var(--height-badge-xs)',
},
'.badge-size-xs-padding': {
padding: 'var(--spacing-padding-badge-xs)',
},
'.badge-size-xs-radius': {
borderRadius: 'var(--radius-badge-radius)',
},
})
})
이렇게 작성한 semantic을 plugin.js 에 import시켰다.
const plugin = require('tailwindcss/plugin')
// 1. 작성한 semantic 파일 가져오기
const { badge } = require('./semantic/badge')
// 2. 가져온 파일을 통해 유틸리티 추가
module.exports = plugin(({ addUtilities }) => {
badge.handler({ addUtilities })
})
그리고 root에 있는 tailwind.confing.js에 plugins에 등록했다.
plugins: [
...,
...,
// 디자인시스템 유틸리티 추가
require('./libs/ds/src/plugin.js'),
],
tailwind plugin으로 선언해두면 앱에서 재사용이 편하고, IDE 자동완성도 잘 먹는다.
각 앱의 apps/{앱이름}/_theme/ 폴더에 테마 정의 파일을 넣었다.
theme에서는 primitive (원시값) 을 넣는다.
# apps/my-app/_theme/badge.css
@tailwind base;
@layer base {
:root {
--border-style-badge: solid;
--border-width-badge: 1px;
--color-badge-primary-bg: #85efac;
--color-badge-primary-border: transparent;
--color-badge-primary-text: #121212;
--color-badge-secondary-bg: #f2f2f2;
--color-badge-secondary-border: transparent;
--color-badge-secondary-text: #121212;
--color-badge-tertiary-bg: #121212;
--color-badge-tertiary-border: transparent;
--color-badge-tertiary-text: #ffffff;
--height-badge-xs: 18px;
--height-badge-sm: 24px;
--spacing-padding-badge-xs: 4px;
--spacing-padding-badge-sm: 8px;
--radius-badge-radius: 2px;
}
}
앱마다 다르게 지정할 수 있으므로, 브랜드별 테마, 다크모드 등 대응이 유연해진다.
실제는 Badge컴포넌트를 shadcn 라이브러리를 쓰므로,
해당 컴포넌트 안에 디자인시스템을 적용해주면 된다.
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '../utils'
const badgeVariants = cva(
'inline-flex items-center gap-2.5 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
size: {
xs: 'badge-size-xs-height badge-size-xs-padding badge-size-xs-radius text-label-2-r',
sm: 'badge-size-sm-height badge-size-sm-padding badge-size-sm-radius text-subhead-2-b',
},
// rds
theme: {
primary: 'badge-role-primary-bg badge-role-primary-border badge-role-primary-text',
secondary: 'badge-role-secondary-bg badge-role-secondary-border badge-role-secondary-text',
tertiary: 'badge-role-tertiary-bg badge-role-tertiary-border badge-role-tertiary-text',
},
},
},
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, size, theme, ...props }: BadgeProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <div className={cn(badgeVariants({ size, theme }), className)} {...props} />
}
export { Badge, badgeVariants }
그럼 이제 각 앱의 ui 에서 사용할때 정말 간단해진다..
<Badge size="xs" theme="secondary">
New
</Badge>
아직 다 완성된건 아니지만, 이렇게 설계를 한것에 대해서 많이 성장했다고 느꼈다. 각 앱별로 theme이 다르니까, 코드는 동일해도 보여지는 ui가 다르게 표현될 수 있어서 코드에 일관성을 유지할 수 있었다.
이렇게 정리를 한 부분의 장점을 정리해봤다!
• libs/ds/에 의미 기반 유틸리티 클래스(semantic) 정의
• 각 앱 apps/{앱}/_theme에 실제 변수 값(Theme) 정의
• 디자이너와 합의된 naming rule로 Figma ↔ 코드 매핑 용이
• 확장성과 유연성을 동시에 잡은 구조
제일 편했던건 디자이너분과의 소통이 엄청나게 편해졌다는 부분이다.
디자인 시스템은 한번에 완성되지 않는다고 생각한다.
하지만 구조를 잘 잡아두면, 새 기능이 생기거나 앱이 늘어나도
“어디에 정의해야 하지?” 고민이 줄어든다.
그리고 커뮤니케이션도 엄청 개선이 될 수 있다!
chatGpt codex agent를 활용해서, 우리의 디자인 시스템을 학습시키고, figma mcp로 화면을 디자인시스템이 적용된 코드로 변환하는 방법을 찾아보고 있다. 아직 시스템이 적용이 안된 화면도 많아서 쉽진 않겠지만 그래도 업무가 훨씬 빨라질 것 같다!
끝~~
시대에 맞춰서 협업 방식을 고민하는 부분이 인상 깊어요! 👏👏