shadcn/ui 시작 실습: Vite 설정·components.json·입력 폼·Sonner

2026.09.14·수정 2026.09.15·약 11분·작성: 해비·블로그 소개
shadcn/ui 시작 실습: Vite 설정·components.json·입력 폼·Sonner 학습 표지

이 글은 프론트엔드 라이브러리 연결 과정의 일부입니다. React 컴포넌트·props·state·이벤트와 TypeScript 객체·배열 타입을 먼저 익혀 주세요. 원본 강의의 문장을 옮기는 대신 독립적인 예제와 확인 과제를 구성했습니다.

학습 목표: shadcn/ui의 코드 소유 방식을 이해하고 입력 값과 제출 알림을 연결합니다.

예상 학습 시간: 45분. 개인별 차이가 있으며 설치 시간은 제외합니다.

컴포넌트 코드를 프로젝트 안에서 관리합니다

shadcn/ui는 미리 정해진 모양만 가져다 쓰는 방식으로 이해하면 부족합니다. 프로젝트에 들어온 UI 파일을 직접 읽고 수정할 수 있다는 점이 중요합니다. 대신 사용자 정의한 코드의 변경과 업데이트도 프로젝트가 관리합니다. 다른 UI 패키지가 항상 모든 컴포넌트를 번들에 포함한다거나, shadcn을 쓰면 무조건 용량이 작다는 비교는 피합니다.

공식 설치 절차와 경로 확인

기존 Vite 프로젝트의 Tailwind 설정을 먼저 확인합니다. 다음 명령은 공식 설치 흐름을 따라 별도의 프로젝트에서 수행할 절차입니다. 이 작업 환경에서는 CLI 레지스트리 연결이 실패했으므로 명령 성공을 주장하지 않습니다.

npm install tailwindcss @tailwindcss/vite
npx shadcn@latest init
npx shadcn@latest add button input textarea sonner

Vite plugins에 Tailwind 플러그인을 연결하고 전역 CSS에서 @import “tailwindcss”를 불러옵니다. TypeScript paths는 타입 검사 도구의 경로이고 Vite alias는 실제 번들러 경로입니다. 둘 다 @/를 src로 연결해야 합니다. 프로젝트에 tsconfig.app.json이 있다면 앱 코드에 적용되는 설정을 확인하세요. 과거 예제의 baseUrl을 무조건 추가하기보다 사용하는 TypeScript 버전의 설정을 따릅니다.

파일 확인할 내용
components.json UI 설치 위치와 스타일·CSS 변수 사용 설정
src/lib/utils.ts clsx의 조건부 조합과 tailwind-merge의 충돌 정리
src/index.css background·foreground·primary 등 의미 기반 색상
vite.config.ts / tsconfig.json @/가 같은 src를 가리키는지

cn은 클래스 생성기가 아닙니다

cn은 클래스 문자열을 합치고 알려진 Tailwind 충돌을 정리하는 도우미입니다. 서버에서 온 임의 값으로 text-${size}를 만들었을 때 누락된 CSS를 생성해 주지는 않습니다. 클래스 감지는 빌드 단계의 문제입니다. text-primary가 항상 본문용 색상이라는 뜻도 아닙니다. 배경과 함께 쓰는 foreground 토큰을 구분하고 대비를 확인합니다.

입력값·제출·피드백

Input과 Textarea는 React의 제어 입력 방식으로 사용합니다. value와 onChange를 함께 연결하고 label의 htmlFor와 입력 id를 맞춥니다. placeholder는 필드 이름을 대신하지 않습니다. Button의 variant는 시각적 역할이며, 폼 제출 의도는 type=”submit”으로 따로 지정합니다. destructive는 삭제 등 파괴적 작업을 구분할 때 사용합니다.

Sonner의 Toaster는 공통 진입점에 한 번 배치합니다. 페이지에서는 toast 함수를 호출합니다. 토스트는 잠시 후 사라지므로 중요한 결과는 화면에도 남깁니다. 실습은 서버 저장 없이 입력 확인만 수행하므로 저장 완료라는 잘못된 성공 문구를 사용하지 않습니다. 실제 저장 기능에서는 요청이 성공한 뒤 성공 토스트를 표시하고, 실패하면 입력을 보존해야 합니다.

직접 해보기와 확인 질문

제목을 입력하고 Enter로 제출한 다음 메모를 비워 다시 제출하세요. UI 파일의 기본 버튼 색상을 바꾸면 어떤 화면이 함께 바뀌는지 확인하세요.

풀이 기준 보기

form의 onSubmit으로 Enter와 클릭을 같은 경로에서 처리합니다. 메모가 비면 메모 없음이 표시됩니다. 공통 Button을 수정하면 이를 가져다 쓰는 여러 화면이 영향을 받으므로 개별 화면의 임시 변경과 구분해야 합니다.

공통 실습 실행

아래 공통 ZIP을 풀고 Node.js 24 환경에서 실행합니다. package-lock.json에 실습 버전을 고정했습니다. 본문의 경로와 ZIP의 경로는 같습니다.

npm ci
npm run dev

터미널에 표시된 주소를 열고 이 글에 해당하는 메뉴를 선택하세요. 서버 Todo만 별도 터미널의 npm run server가 필요합니다. 본문 코드에서 생략한 공통 설정과 UI 파일까지 ZIP에 포함되어 있습니다.

프론트엔드 라이브러리 공통 실습 ZIP

components.json

공통 실습 ZIP 안의 전체 파일입니다. 이 파일 하나만으로 독립 실행되지는 않으며 import하는 파일은 ZIP에 함께 들어 있습니다.

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/index.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

src/lib/utils.ts

공통 실습 ZIP 안의 전체 파일입니다. 이 파일 하나만으로 독립 실행되지는 않으며 import하는 파일은 ZIP에 함께 들어 있습니다.

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

src/pages/form-page.tsx

공통 실습 ZIP 안의 전체 파일입니다. 이 파일 하나만으로 독립 실행되지는 않으며 import하는 파일은 ZIP에 함께 들어 있습니다.

import { useState, type FormEvent } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";

export default function FormPage() {
  const [title, setTitle] = useState("");
  const [memo, setMemo] = useState("");
  const [result, setResult] = useState("");
  function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!title.trim()) {
      setResult("제목을 입력하세요.");
      return;
    }
    setResult(`입력 확인: ${title.trim()} / ${memo.trim() || "메모 없음"}`);
    toast.success("입력을 확인했습니다. 서버에는 저장하지 않습니다.");
  }
  return (
    <section>
      <h2>입력과 알림</h2>
      <form onSubmit={submit}>
        <label htmlFor="lesson-title">제목</label>
        <Input
          id="lesson-title"
          value={title}
          onChange={(event) => setTitle(event.target.value)}
          required
        />
        <label htmlFor="lesson-memo">메모</label>
        <Textarea
          id="lesson-memo"
          value={memo}
          onChange={(event) => setMemo(event.target.value)}
        />
        <Button type="submit">입력 확인</Button>
      </form>
      <p role="status">{result}</p>
    </section>
  );
}

UI 파일 범위: ZIP의 src/components/ui는 shadcn CLI 생성물이 아닙니다. 레지스트리 접속 실패로 Radix·Sonner·Embla를 사용하는 축약 학습용 구현을 작성했습니다. 공식 설치 절차는 별도 프로젝트에서 비교하세요. 공식 컴포넌트 전체 기능과 동일하다고 보장하지 않습니다.

검증 범위

Node.js 24.19.0, React 19.3.0, React Router 7.18.3, Zustand 5.0.15, TanStack Query 5.102.8, Tailwind CSS 4.3.3에서 타입 검사와 Vite 빌드를 확인했습니다. 자동 테스트는 jsdom 기반입니다. 실제 브라우저의 포커스·레이아웃·화면낭독기와 모든 네트워크 경쟁 상황은 별도 확인 대상입니다.

npm test
npm run build

공식 문서

2026-09-14 확인. 공식 최신 문서의 버전과 실습 고정 버전이 다를 수 있으므로 설치 버전은 ZIP을 기준으로 비교합니다.

이 글이 도움이 되었나요?

조회 중

React 학습 순서

필수 19개 · 전체 23개

읽음 기록 관리

전체 과정 목차 (23개)
  1. 필수 길잡이 · React 학습 순서: 컴포넌트·props·state부터 상태관리까지
  2. 필수 학습 · React Vite 사용법: Vite 8 프로젝트 생성·실행·빌드
  3. 필수 학습 · React 컴포넌트 개념 정리: UI 재사용 구조 잡기
  4. 필수 학습 · React JSX 문법 사용법: 조건부 렌더링과 리스트 처리
  5. 필수 학습 · React props 사용법: 부모에서 자식으로 데이터 전달하는 구조
  6. 필수 학습 · React useState 사용법: state와 객체 배열 업데이트 기준
  7. 선택 참고 · React state 업데이트 안됨 문제 해결
  8. 필수 선수 · React 리스트 key 경고 해결 기준: index key를 피해야 하는 이유
  9. 필수 학습 · React 제어 컴포넌트 폼과 상태 끌어올리기 첫 실습 가이드
  10. 필수 학습 · React useRef 실습: 입력 포커스와 state의 역할 나누기
  11. 필수 선수 · React useEffect 두 번 실행되는 이유: StrictMode·API 중복 해결
  12. 선택 참고 · React Maximum update depth exceeded 오류 해결: 무한 렌더링 원인 찾기
  13. 선택 참고 · React Cannot update 오류 해결: 렌더링 중 setState 원인
  14. 필수 학습 · React useReducer와 Context 실습: 작업 목록 상태를 여러 컴포넌트에서 공유하기
  15. 필수 학습 · React 컴포넌트 props 타입 지정하기: 부모와 자식 사이의 값 구조 잡기
  16. 선택 참고 · React Hook Form 에러 메시지 표시 문제 해결: validation이 안 보일 때 체크리스트
  17. 필수 길잡이 · React 라이브러리 조합 가이드: Zustand·TanStack Query·shadcn/ui 선택 기준
  18. 필수 학습 · Next.js 커스텀 훅 설계: 프론트엔드 상태 관리 구조 잡기
  19. 필수 학습 · React Compiler 기준: useMemo useCallback 언제 줄일까
  20. 필수 학습 · React·TypeScript 검색 필터 만들기: 상태와 결과 목록 연결
  21. 필수 학습 · React Router v7 실습: BrowserRouter부터 Layout·Outlet·상세 경로까지
  22. 필수 학습 · shadcn/ui 시작 실습: Vite 설정·components.json·입력 폼·Sonner 현재 글
  23. 필수 학습 · shadcn/ui 복합 컴포넌트 실습: Dialog·Popover·Carousel과 접근성

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기