Design System 구축기

이 글에서는 디자인 시스템을 기획하면서, 당연하게 여겨지던 'npm 패키지 배포' 대신 'shadcn 스타일의 Registry 배포 방식'을 선택한 이유와, 프론트엔드 API 설계에 대한 치열했던 고민을 공유합니다.

#design-system#shadcn#tokens-studio#frontend#architecture

버튼 하나를 만들 때마다 "이 버튼의 disabled 상태에서 hover 처리는 어떻게 하죠?", "텍스트 필드의 상태 이름은 뭘로 할까요?" 같은 질문이 제품마다 반복되었습니다. 심지어 Figma의 Variant 이름과 코드의 Prop 이름이 달라 매번 어느 쪽이 진짜 기준인지 묻는 일도 잦았습니다.

빠르게 생성되고 배포되는 마이크로 SaaS 프로젝트가 늘어날수록 UI 파편화 문제는 심화되었고, 조직 내에 공통의 기준인 디자인 시스템이 필요하다는 공감대가 형성되었습니다. 이 글에서는 디자인 시스템을 기획하며 당연하게 여겨지던 'npm 패키지 배포' 방식을 버리고 'Registry 배포 방식'을 선택한 이유와, 프론트엔드 API 설계 과정에서의 치열했던 고민을 공유합니다.

1. npm 패키지라는 관성을 버리다

디자인 시스템을 처음 기획할 때는 여러 프로젝트에서 동일한 구현을 유지하기 위해 당연히 npm 패키지 형태로 배포해야 한다고 생각했습니다. 중앙에서 일관성을 통제하고 패키지 버전만 올리면 전체 프로젝트가 업데이트되는 편리한 방식이기 때문입니다.

하지만 우리 조직의 현실은 달랐습니다. 빠르게 런칭했다가 사라지기도 하는 마이크로 SaaS 생태계에서는 중앙 집중적인 통제보다, 각 프로젝트 상황에 맞는 빠른 시작점과 유연한 커스터마이징이 압도적으로 중요했습니다. 패키지 방식을 고수할 경우, 각 프로젝트의 예외적인 디자인 요구를 수용하기 위해 컴포넌트의 Prop이 끝도 없이 늘어나는 병목 현상이 발생할 것이 분명했습니다.

따라서 우리는 npm 패키지가 아닌, CLI를 통해 컴포넌트 소스 코드를 프로젝트 내부로 직접 복사해 오는 Registry 방식(shadcn 스타일)을 선택했습니다.

코드가 복사된 순간부터 컴포넌트의 소유권은 각 프로젝트가 가집니다. 공통 컴포넌트를 기반으로 프로젝트 입맛에 맞게 코드를 즉각적으로 수정할 수 있어 개발 속도와 유연성이 극대화됩니다. 우리의 목표는 모든 프로젝트를 완벽히 통제하는 것이 아니라, 마이크로 SaaS가 빠르게 시작할 수 있는 훌륭한 초기값을 제공하는 것이기 때문입니다.

2. API 설계: Flat과 Compound 사이의 딜레마

배포 방식을 Registry로 결정하여 수정의 자유를 확보했지만, 컴포넌트를 설계하는 API 방식 자체도 확장성을 고려해야 했습니다. 컴포넌트 API는 크게 두 가지 패턴으로 나뉩니다.

첫째, Flat 패턴입니다. 내부 구조를 숨기고 모든 변형을 props로 제어하는 방식입니다. <Card actionLabel="다운로드" title="리포트"/>처럼 사용법이 직관적입니다. 하지만 예측하지 못한 요구사항이 생길 때마다 Prop이 기형적으로 늘어나는 단점이 있습니다.

둘째, Compound 패턴입니다. 하위 컴포넌트를 제공하여 조합해 쓰는 방식입니다. <Card><Card.Title>리포트</Card.Title></Card>처럼 구조를 직접 제어하므로 확장에 매우 유연합니다. 반면, 코드가 길어지고 단순한 기능만 필요할 때도 무거운 보일러플레이트를 작성해야 합니다.

2-1. 결정: Primitive 기반의 Flat API 하이브리드 전략

우리는 두 패턴의 장점을 모두 취하는 하이브리드 방식을 선택했습니다. 내부적으로는 Radix UI와 결합된 Compound 패턴(Primitive)으로 유연한 기본 조각들을 만듭니다. 하지만 마이크로 SaaS의 핵심인 빠른 시작을 위해, 이 조각들을 미리 조립한 Flat 패턴의 API를 기본으로 노출합니다.

// 1. 내부 구조: 확장에 유연한 Compound 패턴(Primitive)으로 구성
const CardRoot = ({ children }) => <div className="card">{children}</div>;
const CardTitle = ({ children }) => <h2 className="title">{children}</h2>;
 
// 2. 외부 제공: 프로젝트에서 가져다 쓸 때는 직관적인 Flat API로 조립하여 제공
export function Card({ title, content }) {
  return (
    <CardRoot>
      <CardTitle>{title}</CardTitle>
      <p>{content}</p>
    </CardRoot>
  );
}

만약 특정 프로젝트에서 CardTitle 옆에 아이콘을 넣는 등 특수한 구조 변경이 필요하다면, Registry로 복사해 온 위 코드를 열어서 Compound 구조를 직접 입맛대로 수정하면 됩니다.

3. 기술 스택과 토큰 파이프라인

Registry 배포 방식과 유연한 API 설계를 뒷받침할 기술 스택은 현재 조직의 주력 환경을 고려하여 구성했습니다.

  • Vite: 컴포넌트를 빠르게 띄워보고 검증하기 위한 개발 및 문서화 환경입니다.
  • CLI 배포 방식 + Emotion 스타일링: shadcn이 보여준 훌륭한 'CLI 배포 방식(npx 툴로 코드를 프로젝트에 직접 복사해주는 구조)'은 차용하되, Tailwind 기반 스타일링은 제외했습니다. 기존 앱들이 이미 Emotion을 사용하고 있는 점을 고려해 혼선을 줄이기 위함입니다.
  • Radix UI: Dialog, DropdownMenu 등 복잡한 WAI-ARIA 상호작용은 직접 구현하지 않고 Radix UI Primitive에 위임하여 접근성과 안정성을 확보했습니다.

3-1. CSS 변수와 Tokens Studio 연동

가장 중요한 시각적 일관성은 토큰(Token)으로 관리합니다. 색상, 간격, 모서리 반경 등의 토큰을 Emotion theme에 가두지 않고 순수 CSS 변수로 추출했습니다. 이를 통해 다크모드 전환을 유연하게 처리하고, 향후 다른 프레임워크에서도 공통 토큰을 재사용할 수 있는 기반을 마련했습니다.

특히 디자이너와 프론트엔드 개발자 간의 싱크를 맞추기 위해 Tokens Studio의 GitHub 연동 기능을 활용했습니다.

Figma 내의 Tokens Studio 플러그인에는 디자인 속성을 JSON 형태로 변환하여 GitHub Repository로 직접 커밋하고 Push할 수 있는 UI가 내장되어 있습니다.

초기 기획에서는 디자이너가 Push하면 GitHub Actions가 이를 감지해 CSS 변수 파일로 자동 변환하는 CI 파이프라인을 구상했습니다. 하지만 실제 운영을 고민해 보니, 극단적인 자동화 과정에서 발생한 예기치 않은 토큰 변경이 제품 전체의 UI를 의도치 않게 망가뜨릴 리스크가 컸습니다.

따라서 현재는 수동 동기화(Manual Sync) 방식으로 운영하고 있습니다. 디자이너가 GitHub에 토큰 JSON을 Push하여 PR을 올리면, 프론트엔드 개발자가 변경 사항이 UI에 미칠 영향을 직접 리뷰하고 스크립트를 돌려 CSS 변수로 변환한 뒤 머지(Merge)합니다. 자동화의 편리함보다는 프로덕트의 안정성을 택한 결과입니다.

4. 제공 단위의 재정의와 현실적인 리스크 관리

디자인 시스템은 Registry를 통해 복사해 갈 수 있는 단위를 두 가지로 나누어 제공합니다. Button, TextField 같은 범용적인 UI 요소인 Component와 Login Page, Dashboard Shell 등 여러 컴포넌트가 조합된 완성형 화면인 Block입니다. 도메인 의존성이 높아 재사용성은 떨어지더라도 당장 화면을 그릴 수 있는 Block을 제공하는 것은 마이크로 SaaS의 빠른 시작이라는 목표에 완벽하게 부합합니다.

물론 Registry 방식이 주는 '수정의 자유'는 양날의 검이었습니다. 이를 통제하기 위해 구체적인 방어 전략을 두었습니다.

첫째, 파편화라는 빚에 대한 방어입니다. 각 프로젝트에서 코드를 마음대로 뜯어고치다 보면, 1년 뒤에는 완전히 다른 컴포넌트가 될 위험이 컸습니다. 이를 방어하기 위해 사내 문서에 컴포넌트별로 '절대 수정해선 안 되는 Core 영역'과 '자유롭게 수정해도 되는 Style 영역'을 명시했습니다. 또한, 특정 커스텀 컴포넌트가 범용적으로 훌륭하게 쓰인다고 판단되면, 이를 디자인 시스템의 표준으로 역반영하여 별도의 패키지로 분리하는 프로세스를 마련했습니다.

둘째, 디자인 토큰 네이밍 불안정성에 대한 방어입니다. Figma에서 디자이너가 이름을 바꾸면 프론트엔드 코드는 에러를 뱉으며 무너집니다. 이를 막기 위해 Figma의 네이밍을 코드가 맹목적으로 따라가지 않도록 번역 계층(Mapping Layer)을 두었습니다. 디자인 토큰은 Semantic Name을 엄격하게 유지하도록 디자이너와 협의(WEB Syntax 룰 수립)하고, 피치 못하게 변수명이 변경될 때는 코드상에서 기존 이름과 새 이름을 연결하는 Alias를 두어 기존 코드가 즉시 깨지지 않도록 유예 기간을 확보합니다.

5. 마치며

이번 디자인 시스템은 완벽한 추상화와 중앙 통제라는 전통적인 미덕을 과감히 내려놓았습니다. 대신 속도, 프로젝트별 소유권, 그리고 유연한 커스터마이징이라는 마이크로 SaaS 생태계에 가장 현실적이고 강력한 무기를 쥐기로 했습니다. 이 기획과 치열한 고민의 결과물이 앞으로 탄생할 수많은 프로젝트들의 든든한 베이스캠프가 되기를 기대합니다.

    관련 글

    댓글 0

    닉네임과 댓글은 공개됩니다.

    0/1000

    댓글을 불러오는 중입니다…