콘텐츠로 이동

1. 지형도

셋은 경쟁하지 않는다. 완전히 다른 층에 있다

초심자가 가장 많이 하는 오해가 “Next.js vs Tailwind vs shadcn 중에 뭘 골라야 하나”다. 셋은 고르는 대상이 아니라 쌓는 층이다.

flowchart TB
    A["shadcn/ui — 컴포넌트<br/>버튼 · 다이얼로그 · 폼이 어떻게 생기고 동작하는가"]
    B["Tailwind CSS — 스타일 언어<br/>그 생김새를 어떤 문법으로 기술하는가"]
    C["Next.js — 실행 환경<br/>그 컴포넌트를 언제 · 어디서 실행하는가"]
    D["React — UI 모델<br/>상태가 바뀌면 화면이 바뀐다"]

    A --> B --> C --> D

    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef ok   fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef key  fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class A warn
    class B ok
    class C key
    class D mute

각 층은 아래 층을 전제하지 대체하지 않는다. 한 문장씩 하면 —

  • React — “상태가 바뀌면 화면을 다시 그린다”는 규칙 하나로 UI를 함수로 만든다
  • Next.js — 그 React 컴포넌트를 서버에서도 실행하고, 라우팅·번들링·캐싱·배포까지 묶는다
  • Tailwind CSS — CSS를 클래스 이름을 새로 짓지 않고 마크업 안에서 직접 조립한다
  • shadcn/ui — 그 Tailwind로 스타일링된 접근성 있는 컴포넌트 소스코드를 내 레포에 복사해 준다

세 도구는 전부 실제로 아팠던 문제에서 나왔다. 그 통증을 모르면 왜 이렇게 생겼는지 이해가 안 된다.

sequenceDiagram
    participant B as 브라우저
    participant C as CDN
    participant A as API 서버
    B->>C: index.html 요청
    C-->>B: 거의 빈 HTML + 1.2MB JS
    Note over B: 흰 화면. JS 다운로드·파싱 대기
    B->>A: /api/user
    A-->>B: JSON
    B->>A: /api/posts (user 를 받은 뒤에야 가능)
    A-->>B: JSON
    Note over B: 이제서야 첫 화면
  • 번들이 크다 — 화면에 안 보이는 코드까지 전부 브라우저로 간다
  • 워터폴 — 컴포넌트가 렌더돼야 fetch가 시작되고, 그 결과로 또 fetch한다
  • SEO — 크롤러가 받은 HTML에는 내용이 없다
  • API 키를 브라우저에 둘 수 없으니 결국 서버를 하나 더 만들게 된다

Tailwind 이전 — 이름 짓기가 병목

섹션 제목: “Tailwind 이전 — 이름 짓기가 병목”
/* 이 클래스 이름을 뭐라고 지을 것인가로 30분 */
.card { }
.card__header { }
.card__header--highlighted { }
.card__header--highlighted-compact { } /* 여기서부터 무너진다 */
  • 이름 짓기가 병목 — BEM, OOCSS, SMACSS… 방법론 논쟁이 10년 넘게 이어졌다
  • 삭제가 불가능 — 이 클래스를 지워도 되는지 아무도 확신하지 못해 CSS 파일은 계속 커진다
  • 전역 네임스페이스 — 어딘가의 .title이 내 .title을 덮어쓴다
  • 파일 왕복 — 마크업 고치고, CSS 파일 열고, 다시 마크업으로

전통적인 컴포넌트 라이브러리(MUI, Ant Design, Chakra)는 node_modules에 들어온다.

  • 처음엔 빠르다. 디자인이 라이브러리와 다른 순간 문제가 시작된다
  • 커스터마이징은 라이브러리가 허락한 만큼만 가능하다 (sx, theme.overrides, !important)
  • v5 → v6 업그레이드가 전사 프로젝트급 작업이 된다
  • 결국 “라이브러리를 감싸는 우리 래퍼”를 만들고, 그 래퍼가 또 부채가 된다

근본 원인은 하나다 — 내가 쓰는 코드를 내가 소유하지 않았다.

셋은 같은 사람들이 같은 시기에 만든 게 아닌데도 유독 잘 맞는다.

궁합이 맞는 지점

shadcn/ui는 Tailwind로 스타일링되어 있다. Tailwind는 빌드 타임에 클래스를 스캔하니 Next.js 파이프라인에 그대로 얹힌다. shadcn 컴포넌트는 RSC를 전제로 만들어져 use client가 필요한 것에만 붙어 있다.

결과

shadcn add button 한 줄이면 끝. 디자인 변경 = 내 레포의 파일 수정. 토큰 하나 바꾸면 앱 전체가 따라 바뀐다.

공통점은 **“추상화를 얇게 유지한다”**는 태도다. 어느 쪽도 아래 층을 가리지 않기 때문에 겹쳐 써도 서로 부딪히지 않는다.

정직하게 짚고 간다. 아래에 해당하면 다른 선택이 낫다.

  • 디자인 시스템이 이미 완성돼 있고 웹 컴포넌트로 배포 중 → 그걸 쓰는 게 맞다
  • 사내 표준이 Vue/Angular → Next.js가 아니라 Nuxt/Analog (Tailwind와 shadcn 포팅본은 있다)
  • 관리자 화면만 빠르게 → 데이터 그리드가 완성품으로 있는 MUI/AG Grid가 더 빠를 수 있다
  • 정적 문서 사이트 → Astro나 VitePress가 더 가볍다
  • 팀에 React 경험자가 없다 → 학습 곡선 세 개를 동시에 오르게 된다

반대로 제품을 오래 만들 예정이고 디자인이 계속 바뀔 것이라면 이 조합의 이점이 가장 크게 나타난다.

이 덱의 선택 주요 대안 언제 대안이 낫나
메타 프레임워크 Next.js Remix/React Router, TanStack Start, Astro 서버 중심 폼 / 콘텐츠 사이트
스타일 Tailwind CSS Modules, Panda CSS, vanilla-extract 런타임 zero + 타입 안전 CSS가 최우선
컴포넌트 shadcn/ui MUI, Ant Design, Mantine, Chakra 완성된 복합 위젯이 당장 필요
프리미티브 Base UI Radix, React Aria, Headless UI 이미 쓰고 있는 것이 있음

Panda CSS와 vanilla-extract는 Tailwind와 철학이 비슷하지만 타입 안전성을 더 중시한다. 빌드 복잡도가 올라가는 대신 CSS 값에 타입이 붙는다.

덱 전체를 관통하는 질문을 여기서 한 번 더 심어둔다.

Tailwind의 클래스 스캔, generateStaticParams, 정적 껍데기 생성.

특징 — 사용자 요청과 무관하게 미리 끝난다. 그래서 동적인 값을 알 수 없다. Tailwind가 `text-${color}-500` 을 못 찾는 이유가 정확히 이것이다. (10장)

  • 세 도구는 다른 층에 있다 — 컴포넌트 / 스타일 언어 / 실행 환경
  • 각각은 실제 통증에서 나왔다 — 번들과 워터폴 / 이름 짓기 / 소유권 없음
  • 조합의 핵심은 추상화를 얇게 유지한다는 공통 태도
  • 정답은 아니다. 디자인이 자주 바뀌는 장기 제품에서 이점이 가장 크다
  • 앞으로 계속 물을 질문: “이 코드는 어디서 실행되는가?”