콘텐츠로 이동
Study Noteshadcn/ui

11. 테마를 팀에 배포하기

앱이 하나면 필요 없다. 둘이 되는 순간 필요해진다.

이 장에서 처음 나오는 말4개
레지스트리Registry
컴포넌트와 테마를 JSON으로 담아 둔 배포처. shadcn CLI가 URL을 받아 여기서 코드를 가져온다.
업스트림Upstream
내가 복사해 온 원본. shadcn/ui 공식 레지스트리가 이 덱에서의 업스트림이다.
`--diff`
내가 고친 파일과 레지스트리의 현재 항목 차이를 보여주는 CLI 옵션. 업스트림 변경을 검토하는 핵심 도구다.
드리프트Drift
같은 컴포넌트가 앱마다 조금씩 달라져 결국 서로 다른 물건이 되는 현상.

앱이 둘이 되면 이런 일이 시작된다.

같은 공식 컴포넌트를 받은 앱 A와 앱 B가 각자 수정하다 6개월 뒤 같은 이름의 다른 물건이 되는 드리프트

소유권 모델의 대가다. 자유롭게 고칠 수 있다는 건 자유롭게 갈라진다는 뜻이기도 하다. 레지스트리는 이 갈라짐에 공통 출발점을 다시 만들어 준다.

레지스트리는 컴포넌트만 배포하는 게 아니다.

종류담기는 것언제
registry:themeCSS 변수 한 벌팀 테마를 작은 단위로 배포할 때
registry:ui컴포넌트 소스 파일우리 팀만의 컴포넌트를 공유할 때
registry:block여러 파일로 된 화면 조각로그인 화면 같은 완성 조합
registry:hook훅 파일컴포넌트가 필요로 하는 훅
registry:style프로젝트 초기 설정 한 벌새 앱 시작 템플릿
registry:base기반·컴포넌트·토큰·폰트·설정 전체디자인 시스템 전체를 한 번에 고정할 때
registry:font폰트 설정폰트를 별도 항목으로 배포할 때

테마부터 시작하는 것을 권한다. 파일이 CSS 하나라 만들기 쉽고 효과가 즉시 보인다.

  1. registry.json을 만든다

    {
    "$schema": "https://ui.shadcn.com/schema/registry.json",
    "name": "acme",
    "homepage": "https://ui.acme.com",
    "items": [
    {
    "name": "acme-theme",
    "type": "registry:theme",
    "title": "Acme 테마",
    "description": "브랜드 색 · 반경 · 폰트 한 벌",
    "cssVars": {
    "theme": {
    "radius": "0.5rem",
    "font-sans": "'Pretendard Variable', system-ui, sans-serif"
    },
    "light": {
    "primary": "oklch(0.55 0.20 264)",
    "primary-foreground": "oklch(0.99 0 0)",
    "ring": "oklch(0.55 0.20 264)"
    },
    "dark": {
    "primary": "oklch(0.72 0.16 264)",
    "primary-foreground": "oklch(0.20 0.04 264)",
    "ring": "oklch(0.72 0.16 264)"
    }
    }
    }
    ]
    }
  2. 빌드한다

    터미널 창
    pnpm dlx shadcn@latest build

    public/r/acme-theme.json이 생긴다.

  3. 정적 파일로 올린다

    Vercel·Cloudflare Pages·S3 — 어디든 된다. JSON 파일 하나를 제공하는 게 전부라 서버가 필요 없다. 사내용이면 사내망 정적 호스팅으로 충분하다.

  4. 다른 앱에서 받는다

    터미널 창
    pnpm dlx shadcn@latest add https://ui.acme.com/r/acme-theme.json

    그 앱의 globals.css에 토큰이 병합된다. 기존 키를 바꿀 수 있으므로 먼저 --dry-run이나 --diff로 적용 범위를 확인하고, git diff를 리뷰한다.

components.json에 등록해 두면 URL 대신 짧은 이름을 쓸 수 있다.

{
"registries": {
"@acme": "https://ui.acme.com/r/{name}.json"
}
}
터미널 창
pnpm dlx shadcn@latest add @acme/acme-theme
pnpm dlx shadcn@latest add @acme/data-table

사내 레지스트리라 인증이 필요하면 헤더를 함께 설정할 수 있다.

{
"registries": {
"@acme": {
"url": "https://ui.acme.com/r/{name}.json",
"headers": { "Authorization": "Bearer ${ACME_TOKEN}" }
}
}
}

${ACME_TOKEN}은 환경변수에서 읽는다. 토큰을 파일에 직접 쓰지 않는다.

우리 팀만의 컴포넌트가 생기면 같은 방식으로 배포한다.

{
"name": "user-avatar",
"type": "registry:ui",
"title": "사용자 아바타",
"files": [
{ "path": "registry/user-avatar.tsx", "type": "registry:ui" }
],
"registryDependencies": ["avatar", "tooltip"],
"dependencies": ["date-fns"]
}
  • registryDependencies — 다른 레지스트리 항목. 공식 컴포넌트 이름을 쓰면 그것도 같이 받아진다
  • dependencies — npm 패키지. 자동으로 설치된다

받는 쪽은 명령 한 줄이다.

터미널 창
pnpm dlx shadcn@latest add @acme/user-avatar
# → user-avatar.tsx + avatar.tsx + tooltip.tsx + date-fns 설치
  • 디렉터리registry/ 배포할 소스를 모아 두는 곳
    • user-avatar.tsx
    • data-table.tsx
  • registry.json 배포 정의
  • 디렉터리public/
    • 디렉터리r/ build가 만드는 결과 — 이걸 호스팅한다
      • acme-theme.json
      • user-avatar.json

레지스트리와 별개로, 업스트림 변경을 확인하는 핵심 도구가 이것이다. git 이력·테스트·시각 회귀 검사도 함께 안전망이 된다.

터미널 창
pnpm dlx shadcn@latest add button --diff
components/ui/button.tsx
"focus-visible:ring-2 focus-visible:ring-ring"
"focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring"

업스트림에서 포커스 링 스타일이 개선됐다는 뜻이다. 내 파일에는 이 개선이 없다 — --diff는 이 차이를 직접 드러내 준다.

shadcn/ui 컴포넌트는 계속 개선된다. 대부분이 접근성 수정이다.

  • 포커스 표시 방식 개선
  • aria-* 속성 추가
  • 키보드 조작 버그 수정
  • 프리미티브 API 변경 대응

라이브러리를 썼다면 npm update 한 번에 받았을 것들이다. 소유권을 가져온 대가로, 이걸 직접 확인해야 한다.

이만큼만 정해도 드리프트가 크게 준다.

  1. components/ui/는 함부로 고치지 않는다

    토큰으로 되는 일은 토큰으로, 도메인 로직은 감싸는 컴포넌트로. 진짜 고쳐야 하면 파일 맨 위에 [수정] 주석을 남긴다.

  2. 테마는 레지스트리 한 곳에서만 온다

    앱마다 globals.css를 손으로 편집하지 않는다. 브랜드 색을 바꾸고 싶으면 레지스트리를 고치고 각 앱이 받아 간다.

  3. 분기마다 --diff를 돌린다

    담당자를 정해 둔다. 안 정해 두면 아무도 안 한다.

  • 앱이 둘 이상이 되면 드리프트가 시작된다. 레지스트리가 공통 출발점을 만든다
  • 배포 단위는 여럿이지만 테마(registry:theme)부터 시작하는 게 쉽고 효과가 크다
  • 레지스트리는 JSON 파일을 정적으로 제공하는 것뿐이다. 서버가 필요 없다
  • components.json의 registries에 등록하면 @acme/이름으로 짧게 쓴다. 토큰은 환경변수로
  • 컴포넌트 배포는 registryDependencies(레지스트리 항목)와 dependencies(npm)를 구분해 적는다
  • --diff는 업스트림 변경을 확인하는 핵심 도구다. git 이력과 테스트를 함께 쓴다
  • 팀 규칙 셋 — ui/ 함부로 안 고침 · 테마는 한 곳에서 · 분기마다 --diff