자신만의 Shadcn UI 레지스트리 구축 방법
Source: Dev.to
4주 동안 30개 이상의 랜딩 페이지 블록(히어로, 가격, FAQ, CTA, 고객 후기)을 구축한 뒤, 마침내 완벽한 Shadcn 레지스트리 설정을 찾아냈습니다.
registry.json에 블록(Hero, Pricing, FAQ 등)을 모델링하고 연결하며, 서버 친화적인 미리보기를 렌더링하고, npx shadcn add에 사용할 수 있도록 준비하는 과정을 안내합니다.
1. 소개
Shadcn UI는 공식 레지스트리 컴포넌트와 블록을 제공하지만, 레이아웃과 디자인 시스템에 맞게 직접 레지스트리를 만들 수도 있습니다.
**4주간의 반복 작업 끝에, 히어로, 가격표, FAQ, CTA 등 30개 이상의 블록을 포함하는 랜딩‑페이지‑전용 레지스트리를 구축했습니다. 모든 블록은 registry.json에 정의되고, 라이브 프리뷰와 구문‑하이라이트된 코드를 제공하는 일반적인 /blocks/[name] 페이지에서 렌더링됩니다.
지표: 12개 카테고리에 걸친 30개 이상의 블록, 50 k+ 라인의 Tailwind, 100 % 서버‑컴포넌트 호환.
다룰 내용:
- 단일 블록(
Hero01)이 어떻게 구조화되는지 - 해당 블록이
registry.json에 어떻게 기술되는지 - 일반적인 Next.js 페이지
/blocks/[name]가 이름으로 블록을 어떻게 렌더링하는지 BlockProvider가 앱의 전역 테마를 건드리지 않고 테마, 화면 크기, 프리뷰 동작을 어떻게 주입하는지- 이 설계가 React Server Components와 Shadcn CLI와 어떻게 잘 어울리는지
2. Shadcn “registry block”이란?
높은 수준에서 Shadcn 레지스트리 항목은 하나 이상의 코드 파일을 가리키고 그들의 의존성을 설명하는 JSON 메타데이터입니다.
| 유형 | 역할 | 예시 값 |
|---|---|---|
| Component | Low‑level primitive (button, input) | registry:component |
| Block | Page section (hero, pricing, FAQ) | registry:block |
In my registry: 30 page sections (hero ×5, pricing ×3, features ×5, CTA ×4, 등) 을 registry:block 으로 모델링했습니다.
Metric: 각 블록은 registryDependencies 를 통해 3–5 Shadcn primitives (button, badge, card) 를 평균적으로 포함합니다.
3. 구체적인 블록: Hero01
Here’s my actual Hero01 – a two‑column hero with badge, title, description, two CTAs, and an image:
interface Hero01Props {
badge?: string;
heading?: string;
description?: string;
buttons?: {
primary?: { text: string; url: string; icon?: React.ReactNode };
secondary?: { text: string; url: string; icon?: React.ReactNode };
};
image?: string;
className?: string;
}
핵심 설계 결정
- 마케팅 props만 – 저수준 레이아웃 조정 옵션 없음
- 순수 프레젠테이션 – 훅 0개, 데이터 페칭 0개
- 서버 컴포넌트 준비 –
use client지시어 없음 - Shadcn 구성 –
Button,Badge와 Tailwind 그리드 사용
사용법
Metric: Hero01 = 187 라인, 12 Tailwind 유틸리티 클래스, 2 Shadcn 컴포넌트.

4. registry.json에서 블록 설명하기
My hero-01 registry entry:
{
"name": "hero-01",
"type": "registry:block",
"title": "Hero 01",
"description": "Two-column hero with badge + CTAs",
"dependencies": ["lucide-react"],
"registryDependencies": ["button", "badge"],
"categories": ["hero"],
"meta": { "image": "/r/previews/hero-01.webp" },
"files": [
{
"path": "src/registry/blocks/hero-01/hero.tsx",
"target": "components/hero-01.tsx"
}
]
}
핵심 필드
registryDependencies→ CLI가button과badge를 자동 설치합니다categories→/blocks?category=hero필터링을 지원합니다files.target→ 예측 가능한components/hero-01.tsx위치
Metric: 30 blocks = 12 categories, 47 total dependencies (npm + registry).
5. BlockProvider: 격리된 프리뷰 매직
Problem: 테마 전환 + 반응형 브레이크포인트를 30개 이상의 블록에서 사이트 테마를 깨뜨리지 않고 미리 보기.
Solution: BlockProvider는 자체 포함된 프리뷰 유니버스를 생성합니다:
interface BlockContextValue {
block: SerializableRegistryBlock;
theme: Theme;
screenSize: ScreenSize; // 'mobile' | 'tablet' | 'desktop'
setTheme: (theme: Theme) => void;
setScreenSize: (size: ScreenSize) => void;
}
왜 작동하나요
- ✅ 테마 변경이 /blocks/hero-01 내부에 머무릅니다
- ✅ 화면 크기 토글 = 즉시 적용되는 CSS 뷰포트 클래스
- ✅ 30개 이상의 블록이 하나의 프리뷰 시스템을 공유합니다
- ✅ 전역 사이트 테마는 전혀 건드리지 않습니다
Metric: Provider는 5가지 테마 × 3가지 브레이크포인트 = 블록당 15개의 프리뷰 변형을 처리합니다.
6. 서버 컴포넌트 + 레지스트리 = ⚡ 성능
서버‑우선 설계
- Blocks = 서버 컴포넌트 (
use client없음) - Registry JSON = 정적, 빌드 시 읽기 가능
- Preview shell = 최소한의 클라이언트 경계
결과
✅ Bundle size: Hero01 = 2.1 KB (gzipped)
✅ TTFB: /blocks/hero-01 = 89 ms
✅ Static pages: 30+ generated at build time
프리뷰 셸(BlockProvider 및 해당 컨트롤)은 필요한 경우에만 클라이언트 측에 남습니다.
7. 전체 레지스트리를 위한 하나의 라우트
단일 동적 라우트(pages/blocks/[name].tsx 또는 app/blocks/[name]/page.tsx)를 사용하면, 모든 블록을 필요할 때마다 렌더링할 수 있습니다:
// pages/blocks/[name].tsx
import { getBlockByName } from '@/registry';
import BlockProvider from '@/components/BlockProvider';
export default async function BlockPage({ params }) {
const block = await getBlockByName(params.name);
return (
<BlockProvider block={block}>
{/* Render the block component dynamically */}
</BlockProvider>
);
}
- 블록당 별도의 페이지가 필요 없습니다.
- 코드베이스를 DRY하게 유지하고 새 블록을 추가하는 것이
registry.json을 업데이트하는 것만큼 간단합니다.
TL;DR
- Define each UI section as a
registry:blockinregistry.json. →registry.json에서 각 UI 섹션을registry:block으로 정의합니다. - Implement the block as a server component that only accepts marketing‑level props. → 마케팅 수준의 props만 받는 서버 컴포넌트로 블록을 구현합니다.
- Render any block through a single dynamic route wrapped in
BlockProviderfor isolated theme & viewport previews. → 격리된 테마 및 뷰포트 미리보기를 위해BlockProvider로 감싼 단일 동적 라우트를 통해 모든 블록을 렌더링합니다. - Enjoy fast builds, tiny bundles, and a workflow that meshes perfectly with the Shadcn CLI. → 빠른 빌드, 작은 번들, 그리고 Shadcn CLI와 완벽히 맞는 워크플로우를 즐기세요.
즐거운 빌딩 되세요! 🚀
카탈로그: /blocks/[name]
단일 Next.js 페이지가 30개 이상의 모든 블록을 구동합니다
export async function generateStaticParams() {
return getBlocks().map(block => ({ name: block.name })); // 30+ pages!
}
export default async function BlockPage({ params }) {
const block = getBlock(params.name);
const { component, ...serializableBlock } = block; // Server‑safe
return (
<BlockProvider block={serializableBlock}>
{/* Render the block component */}
</BlockProvider>
);
}
자동 생성: /blocks/hero-01, /blocks/pricing-01, /blocks/cta-01, …
지표: 22 정적 페이지, 100 % 서버‑렌더링, 전체 SEO 메타데이터.
8️⃣ CLI 통합
npx shadcn add @shadcnship/hero-01
내 레지스트리는 Shadcn 스키마를 정확히 따르기 때문에 CLI 통합이 매끄럽습니다:
# 직접 URL (현재 작동)
npx shadcn add https://mysite.com/r/hero-01.json
# 네임스페이스 방식 (PR 승인 후)
npx shadcn add @shadcnship/hero-01
CLI가 수행하는 작업:
hero.tsx→components/hero-01.tsx복사lucide-react설치button+badge자동 설치
✅ 총 30초
9️⃣ Lessons learned (after 4 weeks + 30 blocks)
✅ Do
- 마케팅 prop만 (
heading,buttons) 사용하기. registryDependencies를 철저히 사용하기.- 블록을 서버‑컴포넌트 우선으로 유지하기.
- 블록을 확장하기 전에 프리뷰 시스템 구축하기.
❌ Don’t
- 레이아웃 prop(
padding,gap)을 노출하지 않기. - 프리뷰와 사이트‑테마 상태를 혼합하지 않기.
- 프리뷰 페이지에 블록 이름을 하드코딩하지 않기.
- 오버라이드를 위한
classNameprop을 잊지 않기.
Biggest win: Generic /blocks/[name] + BlockProvider = 블록당 추가 코드가 전혀 없음.
전체 코드 받기
30개 이상의 프로덕션‑레디 블록을 프리뷰 시스템, 테마 전환 및 레지스트리와 함께 제공:
👉 GitHub:
👉 Live demo:
시간을 내 주셔서 감사합니다.
즐기세요! 🚀
