5 min read

5년 된 프로젝트를 새로 만들기로 한 이유 #2 — 디자인 시스템을 처음부터

이 시리즈는 2024년에 있었던 일을 기점으로 회고한 기록입니다.


Element UI를 걷어내야 했다

기존 프로젝트는 Element UI 위에 커스텀 스타일을 덮어씌운 형태였습니다. 처음엔 빠르게 만들 수 있어서 좋았는데, 시간이 지나면서 문제가 드러났어요.

  • Element UI의 기본 스타일을 override하는 CSS가 곳곳에 흩어져 있음
  • 같은 버튼인데 화면마다 미묘하게 다른 스타일
  • 디자이너가 시안을 줘도 "이건 Element UI에서 지원 안 하는데요"가 반복
  • Vue 3로 가려면 Element Plus로 바꿔야 하는데, API가 달라서 결국 전부 수정해야 함

새로 만드는 김에 UI 프레임워크 의존을 완전히 끊고, 프리미티브 컴포넌트부터 직접 만들기로 했습니다.

진작부터 그랬었으면 더 수월했을텐데 말이죠..


토큰 체계: 3계층 구조

디자인 시스템의 기반은 토큰입니다. 색상, 타이포그래피, 간격 같은 값을 하드코딩하지 않고 변수로 관리하는 거죠.

3계층으로 설계했습니다:

Reference (ref)  →  System (sys)  →  UI (ui)
원시값              시맨틱 의미        컴포넌트 용도

예를 들어 색상:

/* Reference: 원시 색상 팔레트 */
--ref-color-neutral-45: #676A7E;
--ref-color-primary-55: #4255D5;

/* System: 의미 부여 */
--sys-color-text-secondary: var(--ref-color-neutral-45);
--sys-color-action-primary: var(--ref-color-primary-55);

/* UI: 컴포넌트에서 사용 */
--ui-button-color-primary: var(--sys-color-action-primary);

타이포그래피도 마찬가지:

/* Reference */
--ref-fontsize-14px: 14px;
--ref-lineheight-22px: 22px;
--ref-fontweight-medium: 500;

/* System: 용도별 조합 */
--sys-typography-text-sm: var(--ref-fontweight-regular) var(--ref-fontsize-14px)/var(--ref-lineheight-22px) var(--ref-fontfamily-pretendard);
--sys-typography-title-lg-bold: var(--ref-fontweight-bold) var(--ref-fontsize-16px)/var(--ref-lineheight-24px) var(--ref-fontfamily-pretendard);

/* UI: 페이지 구조에 매핑 */
--ui-typography-headline-h1-page: var(--sys-typography-headline-lg);
--ui-typography-body-large-bold: var(--sys-typography-text-lg-bold);

이렇게 하면 나중에 "primary 색상을 바꿔야 해"가 ref 한 곳만 수정하면 전체에 반영됩니다. 다크 테마를 추가할 때도 sys 레이어만 오버라이드하면 되고요.


Figma → 코드: Style Dictionary 파이프라인

토큰을 수작업으로 관리하면 디자이너가 Figma에서 바꾼 값과 코드가 어긋나는 문제가 생깁니다. 그래서 파이프라인을 만들었어요.

Figma Tokens Plugin → tokens.json 내보내기 → Style Dictionary → CSS Variables 자동 생성

Style Dictionary 빌드 스크립트가 tokens.json을 읽어서 카테고리별로 SCSS 파일을 생성합니다:

variables/light-theme/
├── _color.scss
├── _font.scss
├── _typography.scss
├── _spacing.scss
├── _border-radius.scss
├── _border-width.scss
├── _border.scss
├── _box-shadow.scss
├── _opacity.scss
├── _sizing.scss
└── _line-height.scss

디자이너가 Figma에서 토큰을 수정하면, JSON을 내보내고 빌드 스크립트를 돌리면 끝. 수동으로 CSS 값을 맞추는 작업이 사라졌습니다.


프리미티브 컴포넌트: 어디까지 직접 만들었나

Element UI를 걷어내면 버튼부터 다이얼로그까지 전부 직접 만들어야 합니다. 범위를 정해야 했어요.

기준은 우리 제품에서 반복적으로 쓰이는 UI 패턴이었습니다.

당연하지만 우리가 직접 만들지 않았던 것들

  • 인터랙션: Button, IconButton, Input, Select, Checkbox, Radio, Switch, Tabs
  • 피드백: Dialog, Drawer, Alert, Loading, Tooltip, Popover
  • 데이터: Table (커스텀 컬럼, 필터, 정렬 포함), Tag, Badge, Avatar
  • 레이아웃: Card, FormItem, Accordion

이제야 Storybook이..

기존에는 디자이너가 시안을 주면 개발자가 구현하고, 결과물을 스테이징에 배포해야 확인할 수 있었어요. 피드백 루프가 길었습니다.

사실 요즘 Storybook을 안쓰는 상용 프로젝트가 있을까? 싶을 정도로 오히려 도입이 늦었던거죠..

Storybook이 있으면?

  • 디자이너가 컴포넌트 단위로 현재 구현 상태를 바로 확인 가능
  • 변경사항이 생기면 Storybook 링크로 "이거 이렇게 바뀌었는데 확인해주세요" 가능
  • props 조합을 인터랙티브하게 테스트할 수 있어서 엣지케이스 발견이 빠름
  • 새 컴포넌트 프로토타입을 Storybook에서 먼저 보여주고 합의한 뒤 본 프로젝트에 반영

autodocs로 props 문서가 자동 생성됩니다. 컴포넌트를 만들면 스토리도 같이 만드는 걸 규칙으로
가져갔습니다.


돌이켜보면

디자인 시스템을 처음부터 만드는 건 시간이 많이 듭니다. 초반 2개월은 화면 하나 못 만들고 인프라만 깔았어요. 단기적으로는 Element UI를 그냥 쓰는 게 빠릅니다.

하지만 조금만 지나고 보면 확실히 달랐습니다:

  • 디자인 변경 요청이 와도 토큰 한 곳만 수정하면 전체 반영
  • 새 화면을 만들 때 기존 컴포넌트 조합으로 최소 절반은 해결
  • 디자이너와 개발자가 같은 언어(토큰 이름)로 소통
  • UI 프레임워크 버전 업데이트에 종속되지 않음