shadcn ui 자세히 알아보기

shadcn ui 자세히 알아보기

May 13, 2024

naver/fe-news를 보던 중 2023년 한 해 동안 가장 많은 Github Star를 받은 JavaScript 프로젝트들에 대한 글을 발견했다.

제일 Star를 많이 받은 JavaScript 프로젝트는 무엇이엇을까? Bun? Excalidraw? StyleX? 모두 훌륭하지만, 정답은 shadcn/ui이다.

UI 도구가 Excalidraw와 Bun을 제쳤다는 게 굉장히 신기하지 않은가? 사실 shadcn/ui의 인기에는 몇 가지 특수성이 있었기 때문에 2024년의 2분기인 현재에도 여전히 사용해 보지 않았거나 친숙하지 않은 React 개발자들이 꽤 많을 거라 생각한다.

이 글은 shadcn/ui의 정체와 개념, 동작 방식을 탐구하는 글이다.

Star History Chart
Star History Chart

shadcn/ui

shadcn/ui는 Vercel의 shadcn이 만든 UI 도구로, Radix UI(Primitives)Tailwind CSS라는 상당히 최신 기술을 기반으로 하는 Component Collection이다. shadcn/ui가 많은 관심을 받은 이유 중 하나가 바로 스스로 "component library"를 거부하고 "component collection"이라 소개하기 때문이다.

This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps.
- shadcn/ui (Introduction)

component collection? library? 단순한 말장난 같지만, 제작자의 의도를 명확하게 하자면 이렇다.

기존의 전통적인 컴포넌트 library들은 번들된 소스 코드를 패키지 매니저를 통해 프로젝트 의존성에 추가해 사용한다. 이와 달리 shadcn/ui는 번들되지 않은 컴포넌트 소스 코드를 프로젝트에 의존성으로 추가하지 않고, 말 그대로 복붙해서 컴포넌트 코드를 온전히 사용할 수 있다.
이런 독특한 방식으로 딱 사용자가 원하는 컴포넌트만 골라서 가져가면 되기 때문에 "library"가 아닌 "collection"이라 칭하는 것이다.

사용을 위해서는 npm 설치 없이 npx shadcn-ui <command> 형태의 npx 명령어만 사용하거나 직접 사이트에서 가져가면 된다. (npx 명령어를 실행될 때 컴포넌트 복붙과 의존성 추가가 자동으로 이루어진다)

Library vs shadcn/ui

컴포넌트 설치를 의존성으로 하지 않는다는 건 결국 컴포넌트 설치(복붙)이후의 코드와 번들링에 대한 관리 책임이 패키지 사용자에게로 넘어간다는 것인데, 이는 사용자와 제작자 모두에게 유의미한 장점을 제공한다.

  • 패키지 사용자 입장: 책임을 넘겨받는다는 게 부정적으로 보일 수 있지만, 기존 외부 lib 컴포넌트를 사용할 때 존재하던 커스터마이징 제약, 불필요한 번들 크기 증가와 같은 pain point가 사라진다. 필요한 컴포넌트만 복붙하고 자유롭게 커스터마이징하며 사용하면 된다.
  • 패키지 제작자 입장: 컴포넌트 커스터마이징 지원, 번들링 및 배포에 대한 부담이 사라진다. 커스텀 컴포넌트를 제작하고, 소스 코드를 어딘가에 올리고, 이를 다운로드할 수 있는 npx script만 제공하면 된다.

shadcn/ui의 컴포넌트들이 복잡도가 낮기 때문에 이 모든 게 가능하다.

외부 lib 컴포넌트에 대한 커스텀이 필요한데 지원이 잘 안될 때 CSS !important를 사용해 오버라이드하거나, 프로젝트를 clone(or patch)해서 직접 수정하거나, 정 안되면 직접 비슷한 커스텀 컴포넌트를 만드는 경험은 프론트엔드 개발자라면 대부분 가지고 있을 것이다.

이를 디자인과 마크업을 제거해서 해결한 게 Headless UI로 오직 UI에 대한 로직만 library에 포함하고 있다.
shadcn/ui는 여기서 접근 방식을 달리해 컴포넌트 관리 책임을 사용자에게 넘기는 것으로 컴포넌트를 소스 코드 레벨에서 커스터마이징이 가능하게 만들었다. 외부 컴포넌트의 문제점을 컴포넌트 라이브러리를 잘 만드는 방식(Headless UI)이 아니라 설치 방식을 바꾸어 해결한 게 놀랍다.

shadcn/ui의 방향성은 외부에서 컴포넌트 기능을 제공하는 것이 아니라 온전히 컴포넌트 소유권을 넘겨주는 것이다.

어떻게 2023년 가장 많은 Star를 받았을까

그렇다면 어떻게 Excalidraw 같은 훌륭한 프로젝트를 제치고 가장 많은 Star를 받을 수 있었을까? 심지어 2024년 5월 현재까지도 Star 상승세가 엄청난데 여기에는 여러 가지 이유가 있다고 생각한다.

v0 UI Generator에서 shadcn/ui를 사용

Vercel이 제작한 v0 AI UI Generator는 프롬프트에 맞는 UI와 코드를 생성하는 도구로, shadcn/ui와 Tailwind CSS를 사용해 코드를 생성한다. AI가 생성하는 소스 코드에 사용된다는 사실만으로 흥미롭지 않은가?

Tailwind CSS를 기반으로 한 UI 도구

Tailwind CSS는 프론트엔드 생태계에서 뜨거운 감자 같은 녀석이였지만 결국 주류 기술로 자리를 잡았고, 이를 사용한다는 게 인기에 영향을 줬을 것이다. (Radix UI 또한 훌륭한 headless UI 라이브러리이다)

독특한 컴포넌트 설치 방식

기존에도 이런 방식으로 설치하던 프로젝트가 있었는지 모르곘지만, 필자의 길지 않은 개발자 인생에서는 shadcn/ui를 통해 처음 접했다.
이 방식은 프로젝트 기반의 Primitive Component, 유틸리티, hooks 같은 것들에 매우 효과적인 방식인 것 같고 재밌는 방식이다.

잘 만들어진 UI Component Collection

이전에 MUI, Ant Design, Chakra UI 같은 잘 만들어진 UI 컴포넌트 도구들이 인기가 있었던 것처럼, shadcn/ui 또한 기본 스타일은 약간 부족해 보일 수 있지만 Radix UI를 기반으로 잘 만든 UI 컴포넌트 도구이다.

어떻게 사용하는가

사용법은 공식 문서에 빌드 환경별로 잘 설명되어 있지만 간략하게 남기자면 아래와 같다.

1. initialize

npx shadcn-ui@latest init 명령어를 실행하면 아래 질문 중에 프로젝트 환경에 맞는 적절한 것들이 프롬프트로 표시된다. 각 질문에 맞는 값을 입력하면 설정이 완료된다.

1- Would you like to use TypeScript (recommended)? › no / yes
2- Which style would you like to use? › Default
3- Which color would you like to use as base color? › Slate
4- Where is your global CSS file? › src/index.css
5- Do you want to use CSS variables for colors? › no / yes
6- Are you using a custom tailwind prefix eg. tw-?
7- Where is your tailwind.config.js located? › tailwind.config.js Leave blank if not) ›
8- Configure the import alias for components: › @/components
9- Configure the import alias for utils: › @/lib/utils
10- Are you using React Server Components? › no / yes
  • 순서대로 아래와 같은 의미를 지닌다.
    • TypeScript 사용 여부
    • component style (default와 new-work이 존재함)
    • 기반 color pallete
    • tailwind import 등이 들어갈 global css file
    • CSS variable 사용 여부
    • 사용하는 tailwind prefix
    • tailwind config 위치
    • component가 저장될 path
    • utils 파일이 저장될 path
    • RSC 사용 여부

init 과정에서 tailwindcss-animate, class-variance-authority, clsx 같은 필수 library들이 자동으로 설치된다.

2. add component

설정이 완료되면 npx shadcn-ui@latest add 명령어로 원하는 컴포넌트를 설치하면 자동으로 컴포넌트가 init에서 설정한 컴포넌트 경로에 생성된다. 이제 자유롭게 설치된 컴포넌트를 사용하면 된다.

예를 들어 npx shadcn-ui@latest add button을 실행하면 설치되는 컴포넌트의 모습은 아래와 같다.

1// src/components/ui/button.tsx
2
3import * as React from "react"
4import { Slot } from "@radix-ui/react-slot"
5import { cva, type VariantProps } from "class-variance-authority"
6
7import { cn } from "@/lib/utils"
8
9const buttonVariants = cva(
10 "inline-flex items-center ...",
11 {
12 variants: {
13 variant: {
14 default: "bg-primary text-primary-foreground hover:bg-primary/90",
15 ...
16 },
17 size: {
18 default: "h-10 px-4 py-2",
19 ...
20 },
21 },
22 defaultVariants: {
23 variant: "default",
24 size: "default",
25 },
26 }
27)
28
29export interface ButtonProps
30 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
31 VariantProps<typeof buttonVariants> {
32 asChild?: boolean
33}
34
35const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
36 ({ className, variant, size, asChild = false, ...props }, ref) => {
37 const Comp = asChild ? Slot : "button"
38 return (
39 <Comp
40 className={cn(buttonVariants({ variant, size, className }))}
41 ref={ref}
42 {...props}
43 />
44 )
45 }
46)
47Button.displayName = "Button"
48
49export { Button, buttonVariants }
  • @radix-ui/react-slot는 add 명령어 실행 과정에서 자동으로 설치된다.

아래처럼 바로 꺼내 쓰면 된다.

1// Home.tsx
2
3import { Button } from "@/components/ui/button";
4
5export default function Home() {
6 const [count, setCount] = useState(0);
7
8 return (
9 <div>
10 <Button onClick={() => setCount(count + 1)}>Easy</Button>
11 </div>
12 );
13}

Button 컴포넌트 기반을 닦는 과정을 npx 명령어로 해결했다. 이제 스타일만 디자인에 맞게 손보면 된다.

컴포넌트의 구성 방향

컴포넌트 구성은 스타일은 Tailwind CSS를 기반으로 하고, 기능은 Radix UI를 기반으로 한다고 단순하게 볼 수 있다.

component architecture - anantomy of shadcn ui

컴포넌트 코드를 살펴보면 특이한 점을 한가지 발견할 수 있는데, 대부분 컴포넌트가 기능을 외부 React 라이브러리에 위임해 직접적인 구현을 최소로 하고 스타일링도 Tailwind CSS를 기반으로 기본적인 것만 처리한다.

이러한 구현 방식은 프로젝트 방향성을 생각하면 적절하면서도 어쩔 수 없는 부분이기도 하다. shadcn/ui 제작자의 직접적인 구현이 많아지게 되면 코드의 소유권을 사용자에게 넘기기 까다롭게 된다. 코드 인수인계하듯이 직접적인 구현 코드에 대한 설명이 shadcn/ui 레벨에서 필요하기 때문이다. 하지만 외부 React 라이브러리는 이미 그들이 잘 만들어둔 문서와 예제들이 많기 때문에, 설명할 필요 없이 사용자는 이를 직접 찾아보면 된다.

Don't reinvent the wheel

컴포넌트의 구현에 대한 자세한 내용은 아래 글에 잘 소개되어 있다. 자세한 내용을 알고 싶은 분들은 참조하면 좋을 것 같다.

The anatomy of shadcn/ui (https://manupa.dev/blog/anatomy-of-shadcn-ui)

shadcn/ui 작동을 위한 프로젝트 구성 요소

shadcn/ui의 내부 코드가 궁금하지 않다면, 이 부분은 넘어가고 바로 고찰로 이동을 추천한다.

shadcn/ui 프로젝트를 작동할 수 있게 해주는 요소는 다음 네 가지로 나눠볼 수 있다. (CLI 사용 기준)
  1. shadcn/ui config를 저장하는 components.json (Config)
  2. 컴포넌트가 기본적으로 사용하는 Tailwind CSS와 관련 라이브러리 및 유틸리티 함수 (Style)
  3. 원본 컴포넌트 코드 (Source)
  4. init, add 명령어를 위한 script (CLI)

shadcn/ui의 시스템은 위 네 가지 요소로 작동하며 일반적인 컴포넌트 라이브러리와 비교하면 요소가 많다고 볼 수 있다. 하지만 전체 구성의 크기는 작기 때문에 복잡도가 높지 않다. 이제 하나씩 살펴보자.

components.json

components.jsoninit 명령어에서 입력받은 프로젝트 설정(alias, 관련 파일들의 위치, rsc, tsx 사용 여부, 테마 등)을 담고 있다. CLI를 사용하려면 필요한 파일이다.

1{
2 "$schema": "https://ui.shadcn.com/schema.json",
3 "style": "default",
4 "rsc": false,
5 "tsx": true,
6 "tailwind": {
7 "config": "tailwind.config.js",
8 "css": "src/index.css",
9 "baseColor": "slate",
10 "cssVariables": true,
11 "prefix": ""
12 },
13 "aliases": {
14 "components": "@/components",
15 "utils": "@/lib/utils"
16 }
17}

위 설정의 style, rsc, tsx, aliases, tailwind 같은 프로젝트 설정에 맞춰서 shadcn/ui에서 컴포넌트를 가져올 때 적절하게 transform 해서 설치를 하게 된다.

e.g. tsx: false면 소스 코드를 ts -> js로 변환해 jsx 확장자 파일로 저장한다.

components.json 파일의 구성에 대한 자세한 설명은 공식 문서에서 확인할 수 있다.

shadcn/ui (Docs > components.json)

Tailwind CSS

styling에 사용되는 Tailwind CSS도 컴포넌트 사용에 필수적이며 기본적인 config, css, 연관 라이브러리, utils는 init 과정에서 세팅되므로, 크게 직접 설정할 부분은 없다.

프로젝트에서 관장하는 Tailwind CSS 연관 파일은 총 3개이다. 각 파일 모두 shadcn/ui가 관리하는 템플릿이 존재하며, init 과정에서 자동으로 템플릿이 설치된다.

  1. tailwind config file
  2. global css file
  3. cn helper가 들어있는 utils file

1.2.는 Tailwind CSS 사용 시 필수적인 부분이므로 설명을 생략하고, 3.에 대해 알아보자면 아래와 같은 cn 함수 하나 들어있는 유틸리티 파일이다.

1import { type ClassValue, clsx } from "clsx"
2import { twMerge } from "tailwind-merge"
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs))
6}
  • 특별한 기능은 없고 조건부로 Tailwind CSS 클래스를 쉽게 추가하기 위해 사용한다.
  • clsx는 객체, 배열 지원과 조건부 추가 등 className 사용을 편리하게 해주며 동적으로 className string을 생성한다.
    (e.g. ['c', 'b', 0, true && 'disabled', { foo: true }] -> 'c b disabled foo')
  • tailwind-merge는 Tailwind 유틸리티 클래스의 중복과 충돌 제거 등을 처리한다.

각 파일의 템플릿 소스는 아래와 같다.

tailwind config
1export const TAILWIND_CONFIG_TS = `
2import type { Config } from "tailwindcss"
3
4const config = {
5 darkMode: ["class"],
6 content: [
7 ...
8 ],
9 prefix: "<%- prefix %>",
10 theme: {
11 container: {
12 ...
13 },
14 extend: {
15 keyframes: {
16 ...
17 },
18 animation: {
19 ...
20 },
21 },
22 },
23 plugins: [require("tailwindcss-animate")],
24} satisfies Config
25
26export default config`
tailwind utils
1export const UTILS = `import { type ClassValue, clsx } from "clsx"
2import { twMerge } from "tailwind-merge"
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs))
6}
7`
tailwind global css
1const BASE_STYLES = `@tailwind base;
2 @tailwind components;
3 @tailwind utilities;
4 `

v0.8.0 기준 템플릿이 설치될 때 아래처럼 그냥 fs.writeFile을 때려버리기 때문에 기존 내용이 날아간다.

1await fs.writeFile(
2 config.resolvedPaths.tailwindConfig,
3 template(tailwindConfigTemplate)({
4 extension,
5 prefix: config.tailwind.prefix,
6 }),
7 "utf8"
8)

원본 컴포넌트

원본 컴포넌트는 TypeScript와 빌드 결과물 JSON 두 가지 형태로 관리된다.

1. TypeScript 원본 소스

원본 컴포넌트들은 Radix UI를 기반으로 RSC, TypeScript, Tailwind CSS, Path Alias가 모두 적용되어 있으며, registry/<style>/ui/<name>.tsx 경로에 저장된다. 여기서 <style>은 컴포넌트 스타일(default or new-york), <name>은 컴포넌트 이름을 나타낸다.

스타일별로 컴포넌트 소스 코드(스타일링)가 다르기 때문에, 동일한 타입의 컴포넌트여도 분리되어 있다.

e.g. registry/default/ui/button.tsx

1import * as React from "react"
2import { Slot } from "@radix-ui/react-slot"
3import { cva, type VariantProps } from "class-variance-authority"
4
5import { cn } from "@/lib/utils"
6
7const buttonVariants = cva(
8 "inline-flex ...",
9 {
10 variants: {
11 ...
12 },
13 defaultVariants: {
14 ...
15 },
16 }
17)
18
19export interface ButtonProps
20 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
21 VariantProps<typeof buttonVariants> {
22 asChild?: boolean
23}
24
25const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
26 ({ className, variant, size, asChild = false, ...props }, ref) => {
27 ...
28 }
29)
30Button.displayName = "Button"
31
32export { Button, buttonVariants }

컴포넌트들을 프로젝트에 복붙하려면 컴포넌트 이름, 소스 코드, 필요한 의존성 등의 정보가 필요하다. 이를 효율적으로 관리하기 위해 별도 registry array를 만들고, 컴포넌트 이름과 의존성 내역, 컴포넌트 구성 파일들의 경로를 관리한다.
(배열이 파일 정적 분석을 통해 자동 생성되는 건 아니고 직접 작성하고 있다)

registry/ui.ts
1import { Registry } from "@/registry/schema"
2
3export const ui: Registry = [
4 {
5 name: "accordion",
6 type: "components:ui",
7 dependencies: ["@radix-ui/react-accordion"],
8 files: ["ui/accordion.tsx"],
9 },
10 {
11 name: "alert",
12 type: "components:ui",
13 files: ["ui/alert.tsx"],
14 },
15 {
16 name: "alert-dialog",
17 type: "components:ui",
18 dependencies: ["@radix-ui/react-alert-dialog"],
19 registryDependencies: ["button"],
20 files: ["ui/alert-dialog.tsx"],
21 },
22 ...
23]
  • name: 컴포넌트 이름
  • type: registry type (컴포넌트는 모두 components:ui이다)
  • dependencies: 컴포넌트 의존성 라이브러리 배열
  • registryDependencies: 컴포넌트 의존성 registry 배열 (e.g. alert-dialog의 경우 button을 사용하기 때문에 button이 포함되어 있다)
  • files: 컴포넌트에 필요한 파일들의 경로 (e.g. hooks가 추가로 필요하다면 해당 hooks 파일 경로도 files에 추가된다)

이후, 위 TypeScript 소스들을 기반으로 build 과정에서 실제로 CLI에 사용할 JSON 파일이 생성된다.

2. Transformed JSON

component 코드와 registry array는 JSON 파일들로 변환되며, CLI의 파일 복붙과 의존성 설치에 사용되게 된다.

component json은 아래처럼 registry 정보에 files만 실제 소스 코드로 변환한 형태이다.

public/registry/styles/default/button.json
1{
2 "name": "button",
3 "dependencies": [
4 "@radix-ui/react-slot"
5 ],
6 "files": [
7 {
8 "name": "button.tsx",
9 "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\" ... export { Button, buttonVariants }\n"
10 }
11 ],
12 "type": "components:ui"
13}

registry array는 원본과 동일하다.

registry/index.json
1[
2 {
3 "name": "accordion",
4 "dependencies": [
5 "@radix-ui/react-accordion"
6 ],
7 "files": [
8 "ui/accordion.tsx"
9 ],
10 "type": "components:ui"
11 },
12 {
13 "name": "alert",
14 "files": [
15 "ui/alert.tsx"
16 ],
17 "type": "components:ui"
18 },
19 {
20 "name": "alert-dialog",
21 "dependencies": [
22 "@radix-ui/react-alert-dialog"
23 ],
24 "registryDependencies": [
25 "button"
26 ],
27 "files": [
28 "ui/alert-dialog.tsx"
29 ],
30 "type": "components:ui"
31 },
32 ...
33]

위 JSON 정보를 기반으로 컴포넌트를 추가할 때 registry array에서 필요한 컴포넌트 정보를 찾고, 해당 정보를 바탕으로 프로젝트 style에 맞춰서 component json을 가져와서 의존성과 컴포넌트 files를 설치하게 되는 것이다.

CLI script

CLI script는 project initialize, component add, component diff 총 세 가지 기능이 있다. init과 add 둘 다 shadcn/ui 문서에 수동으로 처리하는 방법이 나와 있기 때문에, 사실 꼭 사용할 필요는 없다. (Initialize > Manual, Component Installation > Manual)

diff는 experimental 상태로 업데이트가 있는지(로컬 컴포넌트와 코드가 다른지) 체크하는 기능이다. 사실 컴포넌트 직접 수정을 해버리면 의미가 없긴 하다.

init

init 과정은 config 세팅과 템플릿, 필수 의존성 설치로 이루어져 있다.

init process

1. 프로젝트 설정을 입력받는다.

prompts 라이브러리를 사용해서 initialize의 질문들이 프롬프트로 표시된다.

2. tailwind config, global css, utils를 템플릿으로 세팅해 준다.

각각의 템플릿은 #tailwind-css에서 확인할 수 있다.

3. 필수 의존성을 설치해 준다.

사용에 필요한 필수 의존성을 설치한다.

  • tailwindcss-animate
  • class-variance-authority
  • clsx
  • tailwind-merge
  • project style이 defaultlucide-react, new-work이면 @radix-ui/react-icons을 설치한다.

위와 같이 과정이 매우 간단하며 실제 소스 코드는 cli/src/commands/init.ts에서 확인 가능하다.

add

add process

add도 필요한 컴포넌트 정보를 가져와서 설치만 하면 되기 때문에 간단하다.

1. 입력받은 컴포넌트 정보를 registry array json에서 가져온다.

예를 들어 npx shadcn-ui@latest add alert-dialog를 실행하면 다음처럼 registryDependency인 button도 함께 정보를 가져온다.

1[
2 {
3 "name": "alert-dialog",
4 "dependencies": [
5 "@radix-ui/react-alert-dialog"
6 ],
7 "registryDependencies": [
8 "button"
9 ],
10 "files": [
11 "ui/alert-dialog.tsx"
12 ],
13 "type": "components:ui"
14 },
15 {
16 "name": "button",
17 "dependencies": [
18 "@radix-ui/react-slot"
19 ],
20 "files": [
21 "ui/button.tsx"
22 ],
23 "type": "components:ui"
24 }
25]

2. project style을 기반으로 컴포넌트 json을 가져온다.

registry array에서 추가할 컴포넌트 정보들을 찾은 뒤에는 project style에 맞춰 컴포넌트 json을 가져온다.

1// registry/styles/default/alert-dialog.json
2{
3 "name": "alert-dialog",
4 "dependencies": [
5 "@radix-ui/react-alert-dialog"
6 ],
7 "registryDependencies": [
8 "button"
9 ],
10 "files": [
11 {
12 "name": "alert-dialog.tsx",
13 "content": "import * as React from \"react\"\n...const Button = React.forwardRef...export { Button, buttonVariants }\n"
14 }
15 ],
16 "type": "components:ui"
17}
18// registry/styles/default/button.json
19{
20 "name": "button",
21 "dependencies": [
22 "@radix-ui/react-slot"
23 ],
24 "files": [
25 {
26 "name": "button.tsx",
27 "content": "\"use client\"\n\n...export {\n AlertDialog..."
28 }
29 ],
30 "type": "components:ui"
31}

3. files를 순회하며 파일 저장 및 의존성 설치를 한다.

각 파일의 내용을 프로젝트에 저장하고, 의존성을 설치한다.

1for (const file of files) {
2 let filePath = path.resolve(targetDir, file.name)
3
4 // Run transformers.
5 const content = await transform({
6 filename: file.name,
7 raw: file.content,
8 config,
9 baseColor,
10 })
11
12 if (!config.tsx) {
13 filePath = filePath.replace(/\.tsx$/, ".jsx")
14 filePath = filePath.replace(/\.ts$/, ".js")
15 }
16
17 await fs.writeFile(filePath, content)
18}
  • targetDir은 사실상 components.jsonaliases.components 경로를 나타낸다.
  • transform 함수에서 config에 맞게 file content를 변환한다.
1const packageManager = await getPackageManager(cwd)
2
3if (item.dependencies?.length) {
4 await execa(
5 packageManager,
6 [
7 packageManager === "npm" ? "install" : "add",
8 ...item.dependencies,
9 ],
10 {
11 cwd,
12 }
13 )
14}
15
16// Install devDependencies.
17if (item.devDependencies?.length) {
18 await execa(
19 packageManager,
20 [
21 packageManager === "npm" ? "install" : "add",
22 "-D",
23 ...item.devDependencies,
24 ],
25 {
26 cwd,
27 }
28 )
29}
  • execa는 command를 실행해 주는 lib이다.
  • packageManager를 가져와서 그에 맞는 명령어로 dependencies를 설치한다.

실제 소스는 cli/src/commands/add.ts에서 확인 가능하다.

고찰

shadcn/ui 프로젝트가 어떻게 시작된 건지는 알 수 없지만, 컴포넌트를 복붙해서 쓰는 컬렉션 형태로 React 컴포넌트 프로젝트를 생각했다는 게 대단하다. shadcn/ui를 알아보면서 이런 형태의 프로젝트 컨셉이 기존에 가지고 있던 생각의 프레임을 많이 벗어나 있어서 생각의 성장에 도움이 되었다.

대부분의 현업 프로젝트는 내부적인 디자인 시스템과 컴포넌트가 존재하는데 그런 환경의 기반을 닦는데 shadcn/ui가 매우 최적화된 프로젝트라 생각한다. 새로운 웹 프로젝트를 만드는데 디자인이 존재한다면 shadcn/ui로 시작하지 않을까? 물론 단순 internal이거나 디자인 맞출 필요가 크게 없는 프로젝트면 장점이 많이 사라져서 다른 기술들과 차용을 고민해 볼 것 같다.

참고