디자인 시스템 설계기 😃 (with Nx)

seoyeonpp·2025년 10월 28일

Frontend

목록 보기
12/15
post-thumbnail

최근에 디자이너분과 함께 디자인 시스템을 새롭게 구성했다.
기획 초기부터 Figma ↔ 코드 ↔ 실제 앱 간의 연결성을 고려했고,
특히 Nx 기반 모노레포 구조를 사용하는 만큼 확장성과 테마 유연성에 초점을 맞췄다.

이 글에서는 내가 어떻게 설계했는지, 어떤 구조로 잡았는지, 그리고 실제 적용 사례까지 정리해본다!!!!🤖


목표는 단순했다!!

•	하나의 시스템으로 여러 앱 커버하기
•	Figma 기준에 최대한 맞추기
•	테마별 커스터마이징이 쉽게 가능할 것
•	디자이너와 개발자가 같은 언어로 말할 수 있도록

구조 설계: Primitive → Semantic → Theme

디자이너분와 여러 차례의 회의 끝에 다음과 같은 계층 구조를 정리했다:
1. Primitive
디자인 토큰 또는 가장 기본이 되는 원자 스타일들
2. Semantic
의미 있는 역할을 가진 클래스 이름들 (ex. badge-role-primary-bg)
3. Theme
각 앱에 따라 다르게 설정 가능한 CSS 변수 값들

결국 Primitive는 Figma에서 정의된 디자인 토큰,
Semantic은 클래스 네이밍을 통한 역할 지정,
Theme은 실제 변수값을 오버라이드하는 구조다.


Nx 모노레포 구조에 녹여내기 !!

디자인 시스템 관련 코드는 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 자동완성도 잘 먹는다.


Theme 정의는 앱 단에서..

각 앱의 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로 화면을 디자인시스템이 적용된 코드로 변환하는 방법을 찾아보고 있다. 아직 시스템이 적용이 안된 화면도 많아서 쉽진 않겠지만 그래도 업무가 훨씬 빨라질 것 같다!

끝~~

1개의 댓글

comment-user-thumbnail
2025년 10월 28일

시대에 맞춰서 협업 방식을 고민하는 부분이 인상 깊어요! 👏👏

답글 달기