그냥 npm install shadcn/ui 하면 되는 거 아니야?라고 했던 내가 CLI를 이해하기까지
1. 문제 상황
shadcn/ui를 처음 접했을 때 가장 먼저 든 의문은 이것이었다.
“왜 npm install로 설치를 안 하지?”
대부분의 UI 라이브러리는 패키지를 설치하는 방식이다. MUI, Chakra UI, Mantine — 전부 npm install로 시작한다. node_modules에 들어가고, import해서 쓴다. 이게 당연한 흐름이었다.
그런데 shadcn/ui는 달랐다. 공식 문서를 열면 npx shadcn@latest init으로 시작하고, 컴포넌트 하나를 추가할 때도 npx shadcn@latest add button이라고 한다. npm install이 없다.
처음엔 “그냥 특이한 방식인가 보다”라고 넘겼다. 그런데 쓰다 보니 의문이 쌓였다.
- 버전 관리를 어떻게 하지?
- 업데이트는 어떻게 받지?
- 팀원들과 코드를 어떻게 맞추지?
2. 내가 처음 한 생각
@shadcn/ui 같은 패키지가 npm에 없는지 찾아봤다. 있긴 했다. 그런데 공식 문서 어디에도 그걸 쓰라는 말이 없었다.
“이상하다. 왜 굳이 CLI를 써야 하지?”
그때의 내 이해는 이랬다. 라이브러리는 npm으로 설치하는 것이고, shadcn도 라이브러리니까 npm으로 설치할 수 있어야 한다. CLI는 그냥 편의를 위한 도구일 뿐이라고 생각했다.
틀렸다. shadcn/ui는 라이브러리가 아니었다.
3. 실제로 해 본 선택
처음엔 그냥 썼다
이해 없이 일단 썼다. npx shadcn@latest add button을 치면 components/ui/button.tsx 파일이 생겼다. “아, 파일을 복사해주는 도구구나” 정도로만 알고 넘어갔다.
그런데 문제가 생겼다. 버튼 컴포넌트 스타일을 프로젝트에 맞게 바꿨는데, 팀원이 같은 명령어를 다시 실행해서 내 수정 내용이 덮어씌워졌다. 버전 관리 개념이 없으니 생긴 혼란이었다.
왜 npm install이 아닌지 제대로 파고들었다
이 사고 이후 shadcn/ui가 왜 이런 방식을 택했는지를 찾아봤다.
핵심은 소유권이었다.
npm으로 설치한 패키지는 node_modules 안에 있다. 그 코드는 내 것이 아니다. 수정하면 다음 install 때 사라진다. 추상화된 컴포넌트를 가져다 쓰는 구조다.
shadcn/ui는 반대 방향을 택했다. CLI가 컴포넌트 소스 코드를 직접 프로젝트 안에 복사해준다. 그 순간부터 코드는 완전히 내 것이다. 수정해도 된다. 삭제해도 된다. 팀의 디자인 시스템에 맞게 뜯어고쳐도 된다.
Radix UI의 접근성 로직과 Tailwind의 스타일링을 가져오되, 그것을 추상화 뒤에 숨기지 않고 소스 코드 그대로 노출시키는 방식이었다.
동작 방식을 직접 확인했다
npx shadcn@latest add button을 실행했을 때 실제로 무슨 일이 일어나는지 따라가 봤다.
- shadcn CLI가 레지스트리(shadcn의 GitHub 또는 자체 서버)에서 컴포넌트 정의를 가져온다.
- 프로젝트의
components.json설정을 읽어 경로, 스타일, alias를 확인한다. - 컴포넌트 소스 파일을
components/ui/디렉토리에 직접 쓴다. - 필요한 외부 패키지(
@radix-ui/react-slot,class-variance-authority등)가 없으면 npm install까지 실행해준다.
npm install이 없는 게 아니었다. npm install 대상이 달랐다. 컴포넌트 코드 자체는 파일로, 컴포넌트가 의존하는 유틸리티들은 패키지로 — 두 가지를 분리해서 처리하고 있었다.
4. 결과
잘 된 것: 커스터마이징 자유도
기존 UI 라이브러리에서 가장 불편했던 것은 스타일 오버라이드였다. MUI 같은 경우 sx prop이나 styled로 감싸는 작업이 번거로웠고, 라이브러리 내부 구조를 모르면 원하는 부분에 정확히 접근하기 어려웠다.
shadcn은 달랐다. 버튼 컴포넌트가 마음에 안 들면 components/ui/button.tsx를 열어서 직접 고치면 됐다. className 로직, variant 정의, 기본 스타일 — 전부 내 코드 안에 있었다. 외부 라이브러리의 구조를 이해하려고 씨름할 필요가 없었다.
잘 된 것: 번들 크기
npm 패키지로 UI 라이브러리를 설치하면 쓰지 않는 컴포넌트까지 포함될 수 있다. shadcn은 쓸 컴포넌트만 골라서 추가한다. 번들에는 실제로 사용하는 코드만 들어간다.
어려웠던 것: 업데이트 관리
컴포넌트를 커스터마이징하고 나면 shadcn 공식 업데이트를 적용하기가 까다로워진다. 새 버전의 button.tsx가 나와도 그냥 덮어씌울 수 없다. 내 수정 내용과 충돌할 수 있기 때문이다.
이건 트레이드오프다. 소유권을 얻는 대신 업데이트 추적 책임도 가져가는 것이다. 처음엔 불편하게 느껴졌는데, 생각해보면 당연한 구조였다.
5. 지금 다시 해본다면?
처음부터 shadcn이 “라이브러리가 아니다”라는 것을 이해하고 시작했을 것 같다.
공식 문서에는 이렇게 쓰여 있다. “This is not a component library. It’s a collection of reusable components that you can copy and paste into your apps.” npm으로 설치하지 않는 이유가 여기 있었다. 배포하는 패키지가 아니라, 복사해서 소유하는 코드이기 때문이다.
이 전제를 알고 시작했다면 팀원과의 덮어쓰기 사고도 없었을 것이다. “CLI가 파일을 복사해주는 도구”라는 이해를 더 일찍 가졌다면, 처음 추가할 때부터 해당 파일을 git으로 관리하고 수정 이력을 남겼을 것이다.
도구를 도입할 때 다음엔 이 순서로 먼저 생각해볼 것 같다.
- 이 도구는 어떤 문제를 해결하려고 만들어졌는가?
- 기존 방식과 무엇이 다른가?
- 그 차이가 우리 프로젝트에 이득인가, 부담인가?
“npm install이 없네, 이상하다”라고 넘어갔던 내가 틀리지는 않았다. 다만 그 이상함의 이유를 더 빨리 파고들었다면 시행착오가 줄었을 것이다. shadcn의 선택은 특이한 게 아니었다. 소유권에 대한 명확한 철학이었다.