
Nextjs15 Init
- 39 installs
- 902 repo stars
- Updated June 22, 2026
- bear2u/my-skills
nextjs15-init is a Claude skill that scaffolds a new Next.js 15 App Router project with a chosen domain and modern TypeScript stack.
About
This skill creates a new Next.js 15 project using the App Router and a modern TypeScript stack. It asks the developer to pick a domain (Todo, Blog, Dashboard, E-commerce, or Custom) and a stack preset (Minimal, Essential, or Full Stack), then generates domain-specific pages, components, Zustand stores, and Zod validation. A developer uses it to bootstrap a full CRUD app instead of wiring the boilerplate by hand.
- Interactive prompts pick a domain and a stack preset before scaffolding
- Generates App Router pages, ShadCN UI, Zustand stores, and Zod schemas
- Runs lint and build to verify the generated project compiles
Nextjs15 Init by the numbers
- 39 all-time installs (skills.sh)
- Ranked #1,387 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
nextjs15-init capabilities & compatibility
- Capabilities
- project scaffolding · frontend setup · crud generation
- Use cases
- frontend · api development
What nextjs15-init says it does
Use when user wants to create a new Next.js 15 project (Todo/Blog/Dashboard/E-commerce/Custom domain) with App Router, ShadCN, Zustand, Tanstack Query, and modern Next.js stack
npx create-next-app@latest [폴더명] --typescript --tailwind --app --use-npm
npx skills add https://github.com/bear2u/my-skills --skill nextjs15-initAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 902 |
| Last updated | June 22, 2026 |
| Repository | bear2u/my-skills ↗ |
What it does
Scaffold a new Next.js 15 App Router project for a Todo, Blog, Dashboard, or E-commerce app with a chosen stack preset.
Who is it for?
Bootstrapping a fresh Next.js 15 CRUD app with a preset stack
Skip if: Adding features to an existing production codebase
When should I use this skill?
You want to create a new Next.js 15 project from scratch with a domain template
What you get
A generated Next.js 15 project with domain pages, components, stores, validation, and a passing lint and build.
- Next.js 15 App Router project
- domain pages and components
- Zustand stores
By the numbers
- 5 domain templates (Todo/Blog/Dashboard/E-commerce/Custom)
- 3 named stack presets (Minimal/Essential/Full Stack)
Files
NextJS 15 Init Skill
도메인 기반 Next.js 15 프로젝트를 생성하고 현대적인 스택으로 자동 설정합니다. Todo, Blog, Dashboard, E-commerce 또는 Custom 도메인을 선택하여 App Router 기반의 완전한 CRUD 앱을 즉시 생성할 수 있습니다.
Quick Start
스킬 실행 시 다음 정보를 입력받습니다:
- 폴더명 (예: my-todo-app)
- 프로젝트명 (예: todo-app)
- 도메인 선택 (Todo/Blog/Dashboard/E-commerce/Custom)
- 스택 프리셋 (Minimal/Essential/Full Stack/Custom)
그 후 자동으로 다음 단계가 실행됩니다:
# 1. Next.js 15 프로젝트 생성 (App Router, TypeScript, Tailwind)
npx create-next-app@latest [폴더명] --typescript --tailwind --app --use-npm
# 2. 패키지 설치
npm install
# 3. 도메인별 App Router 구조 자동 생성
# - app/[domain]/ (페이지)
# - components/[domain]/ (컴포넌트)
# - lib/stores/[domain].ts (Zustand 스토어)
# - lib/api/[domain].ts (API 로직)
# - lib/validations/[domain].ts (Zod 스키마)
# 4. 코드 품질 검증 (필수)
npm run lint
# 5. 개발 서버 실행
npm run devTask Instructions
IMPORTANT: 이 스킬은 대화형으로 진행됩니다.
Step 1: 도메인 및 프로젝트 설정 질문
먼저 사용자에게 이렇게 물어보세요:
"Next.js 15 앱을 생성합니다. 다음 정보를 알려주세요:
1. 도메인(엔티티) 선택
어떤 도메인의 앱을 만드시겠습니까?
A) Todo (할 일 관리)
- 필드: title, description, completed, createdAt, updatedAt
- 기능: CRUD, 필터링(전체/진행중/완료), 체크박스 토글
- API: /api/todos (GET, POST, PATCH, DELETE)
B) Blog (블로그/CMS)
- 필드: title, content, excerpt, slug, published, createdAt, updatedAt
- 기능: CRUD, 글 작성/수정, 목록/상세 페이지, 검색
- API: /api/posts (GET, POST, PATCH, DELETE)
C) Dashboard (대시보드/어드민)
- 필드: 통계, 차트 데이터, 사용자 관리
- 기능: 데이터 시각화, 테이블, 필터링, 페이지네이션
- API: /api/analytics, /api/users
D) E-commerce (쇼핑몰)
- 필드: name, price, description, images, stock, category
- 기능: 상품 목록/상세, 장바구니, 주문
- API: /api/products, /api/cart, /api/orders
E) Custom (직접 정의)
- 엔티티명과 필드를 직접 입력
2. 프로젝트 정보
- 폴더명: 프로젝트를 생성할 폴더 이름 (기본값: [도메인]-app, 예: todo-app)
- 이 폴더에 Next.js 프로젝트가 생성됩니다
- 프로젝트명: Next.js 프로젝트 이름 (기본값: 폴더명과 동일)
- package.json의 name 필드에 사용됩니다
3. 스택 프리셋 선택
다음 중 하나를 선택해주세요:
A) Essential (권장)
- ✅ Next.js 15 (App Router)
- ✅ TypeScript
- ✅ Tailwind CSS
- ✅ ShadCN/ui (UI 컴포넌트)
- ✅ Zustand (클라이언트 상태 관리)
- ✅ React Hook Form + Zod (폼 관리 + 검증)
- ✅ Lucide Icons
- ❌ Tanstack Query 제외
- ❌ Prisma 제외
- ❌ NextAuth 제외
B) Minimal (가장 단순)
- ✅ Next.js 15 (App Router)
- ✅ TypeScript
- ✅ Tailwind CSS
- ❌ ShadCN 제외
- ❌ Zustand 제외
- ❌ React Hook Form 제외
- ❌ 기타 라이브러리 제외
C) Full Stack (모든 기능)
- ✅ Next.js 15 (App Router)
- ✅ TypeScript
- ✅ Tailwind CSS
- ✅ ShadCN/ui
- ✅ Zustand (클라이언트 상태)
- ✅ Tanstack Query (서버 상태)
- ✅ React Hook Form + Zod
- ✅ Drizzle ORM (TypeScript-first ORM)
- ✅ Better Auth (인증)
- ✅ Framer Motion (애니메이션)
- ✅ Lucide Icons
D) Custom (직접 선택)
- 각 기능을 개별적으로 선택
어떤 도메인과 프리셋을 선택하시겠습니까? (도메인: A/B/C/D/E, 프리셋: A/B/C/D)"
Step 2: Custom 선택 시 추가 질문
2-1. Custom 도메인 (E) 선택 시:
1. 엔티티명: 엔티티 이름을 입력하세요 (예: Product, Post, User) 2. 필드 정의: 각 필드를 입력하세요 (형식: 필드명:타입, 예: title:string, price:number, isActive:boolean)
- 지원 타입: string, number, boolean, Date
- createdAt, updatedAt은 자동 추가됨
3. 주요 기능: 필터링/정렬 기준이 될 필드를 선택하세요
2-2. Custom 스택 프리셋 (D) 선택 시:
다음 질문들을 순차적으로 하세요:
1. UI 컴포넌트: ShadCN/ui를 사용하시겠습니까? (예/아니오) 2. 상태 관리: Zustand를 사용하시겠습니까? (예/아니오) 3. 서버 상태: Tanstack Query를 사용하시겠습니까? (예/아니오) 4. 폼 관리: React Hook Form + Zod를 사용하시겠습니까? (예/아니오) 5. 데이터베이스: Drizzle ORM을 사용하시겠습니까? (예/아니오) 6. 인증: Better Auth를 사용하시겠습니까? (예/아니오) 7. 애니메이션: Framer Motion을 사용하시겠습니까? (예/아니오)
Step 3: 선택된 도메인과 스택에 따라 프로젝트 생성
1. Next.js 15 프로젝트 생성:
- 사용자가 지정한 폴더명으로 프로젝트 생성
- 명령어:
npx create-next-app@latest [폴더명] --typescript --tailwind --app --use-npm - 폴더명과 프로젝트명이 다른 경우, 생성 후 package.json의
name필드를 수정
2. 선택된 패키지 설치: npm install [패키지들]
3. 폴더 구조 생성: App Router 기반 구조
app/
├── (auth)/
├── [domain]/
├── api/[domain]/
├── layout.tsx
└── page.tsx
components/
├── ui/ (ShadCN)
├── [domain]/
└── layouts/
lib/
├── db/ (Prisma)
├── stores/ (Zustand)
├── api/
├── utils/
└── validations/ (Zod)4. 도메인별 보일러플레이트 생성:
A) Todo: title, description, completed, createdAt, updatedAt
- API Routes: /api/todos (CRUD)
- Pages: /todos (리스트), /todos/[id] (상세)
- Components: TodoList, TodoItem, TodoForm
- Store: useTodoStore (Zustand)
- Validation: todoSchema (Zod)
B) Blog: title, content, excerpt, slug, published, createdAt, updatedAt
- API Routes: /api/posts (CRUD)
- Pages: /blog (목록), /blog/[slug] (상세), /blog/write (작성)
- Components: PostList, PostCard, PostEditor
- Store: usePostStore (Zustand)
- Validation: postSchema (Zod)
C) Dashboard: 통계, 차트, 사용자 관리
- API Routes: /api/analytics, /api/users
- Pages: /dashboard (메인), /dashboard/users (사용자)
- Components: Chart, StatsCard, DataTable
- Store: useDashboardStore (Zustand)
- Validation: userSchema (Zod)
D) E-commerce: name, price, description, images, stock, category
- API Routes: /api/products, /api/cart, /api/orders
- Pages: /products (목록), /products/[id] (상세), /cart (장바구니)
- Components: ProductCard, ProductGrid, Cart
- Store: useCartStore, useProductStore (Zustand)
- Validation: productSchema, cartSchema (Zod)
E) Custom: 사용자 정의 필드
- API Routes: 기본 CRUD
- Pages: 기본 List/Detail
- Components: 기본 CRUD 컴포넌트
- Store: 기본 store
- Validation: 기본 schema
5. ShadCN 설치 및 구성 (Essential 이상):
npx shadcn@latest init
npx shadcn@latest add button card input form table6. 코드 검증 및 오류 수정:
a. npm run lint 실행
b. 발견된 오류 수정:
- Import 경로: @/ alias 사용 (tsconfig.json 설정)
- TypeScript 타입: 모든 타입 명시
- ESLint 규칙: 사용하지 않는 변수, import 제거
- Next.js 규칙: metadata, generateStaticParams 등
- 'use client' 지시어: useState, useEffect 등 React Hooks 사용 시 파일 최상단에 추가
c. npm run build 또는 pnpm build 실행
d. 빌드 오류 수정:
- TypeScript 타입 오류: 타입 불일치, nullable 체크 누락 등
- 모듈 import 오류: 경로 확인, 패키지 설치 확인
- Server/Client Component 오류: 적절한 'use client' 지시어 추가
- Dynamic import 오류: server-only 코드가 클라이언트에서 사용되지 않도록 확인
e. 재검증: lint와 build 모두 성공할 때까지 반복
f. 목표:
npm run lint결과가 "0 errors"npm run build또는pnpm build성공
✅ CRITICAL: 이 단계는 필수입니다. lint와 build 모두 성공해야 다음 단계로 진행할 수 있습니다.
Step 4: 최종 검증 및 안내
✅ CRITICAL: 이 단계는 프로젝트 완료의 필수 조건입니다. lint와 build 모두 성공해야 합니다.
1. ESLint 검증:
npm run lint- ✅ 성공 예시:
✔ No ESLint warnings or errors- ❌ 실패 예시 (error가 있으면 반드시 수정):
Error: 'useState' is defined but never used
Error: Missing return type on function2. 프로덕션 빌드 검증:
npm run build또는 pnpm 사용 시:
pnpm build- ✅ 성공 예시:
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages- ❌ 실패 예시 (빌드 에러 발생 시 반드시 수정):
Type error: Property 'xyz' does not exist on type 'ABC'
Error: Module not found: Can't resolve '@/...'중요: 빌드가 실패하면 TypeScript 타입 오류나 import 경로 문제를 수정하고 다시 빌드해야 합니다.
3. 검증 결과 요약 (lint와 build 모두 성공 시):
✅ Next.js 15 프로젝트 생성 완료!
✅ 패키지 설치 완료 (ShadCN, Zustand, Tanstack Query 등)
✅ ESLint 검증 통과 (0 errors)
✅ TypeScript 빌드 성공
✅ 프로덕션 빌드 완료4. 프로젝트 정보 제공:
- 폴더명: [사용자 입력값] (예: my-todo-app)
- 프로젝트명: [사용자 입력값] (예: todo-app)
- 도메인: [선택된 도메인] (Todo/Blog/Dashboard/E-commerce/Custom)
- 선택된 스택: [프리셋명] (ShadCN, Zustand, Tanstack Query 등)
- 주요 기능: [도메인] CRUD, API Routes, 타입 안전성
- 생성된 파일: XX개 TypeScript 파일 (app, components, lib)
5. 실행 방법 안내:
cd [폴더명]
npm run dev
# http://localhost:3000 에서 확인6. 다음 단계 제안 (선택사항, 도메인별):
- Todo: 항목 추가/수정/삭제, 필터링(전체/진행중/완료), 완료 토글
- Blog: 글 작성/수정, 목록/상세 페이지, 검색, 태그
- Dashboard: 데이터 시각화, 차트, 사용자 관리, 필터링
- E-commerce: 상품 목록/상세, 장바구니, 주문, 카테고리
- 공통: TypeScript 타입 안전성, API 테스트, 배포 (Vercel)
Core Principles
- App Router: Next.js 15 App Router 기반 구조
- 타입 안전성: TypeScript Strict Mode
- 컴포넌트 재사용: ShadCN/ui 활용
- 상태 관리: Zustand (클라이언트), Tanstack Query (서버)
- 코드 품질: ESLint + Prettier
- 폼 검증: React Hook Form + Zod
Reference Files
references/setup-guide.md - 완전한 가이드
- 기본 셋업 (도메인별 CRUD, API Routes, 컴포넌트)
- 선택 옵션: ShadCN, Zustand, Tanstack Query, Drizzle ORM, Better Auth, Framer Motion
Notes
- 대화형 스킬: 사용자에게 도메인과 프리셋 선택을 통해 맞춤형 앱 구성
- 도메인 지원: Todo, Blog, Dashboard, E-commerce, Custom (사용자 정의)
- 프리셋 제공: Full Stack, Essential, Minimal, Custom
- 선택 가능 기능: ShadCN, Zustand, Tanstack Query, Drizzle ORM, Better Auth, Framer Motion
- 기본 포함: Next.js 15 (App Router), TypeScript, Tailwind CSS, ESLint
- 플랫폼: Web (Vercel 최적화)
- 품질 보증:
- 모든 프로젝트는
npm run lint통과 필수 - TypeScript Strict Mode
- 타입 안전성 보장
- App Router 패턴 준수
- 도메인별 최적화된 UI/UX
<!-- PROGRESSIVE DISCLOSURE GUIDELINES:
- Keep this file ~50 lines total (max ~150 lines)
- Use 1-2 code blocks only (recommend 1)
- Keep description <200 chars for Level 1 efficiency
- Move detailed docs to references/ for Level 3 loading
- This is Level 2 - quick reference ONLY, not a manual
-->
Next.js 15 Init
Next.js 15 프로젝트를 ShadCN, Zustand, Tanstack Query, Drizzle ORM과 함께 App Router 패턴으로 자동 세팅
주요 기능
- ✅ Next.js 15 (App Router)
- ✅ TypeScript (Strict Mode)
- ✅ Tailwind CSS + ShadCN/ui
- ✅ Zustand (클라이언트 상태 관리)
- ✅ Tanstack Query (서버 상태 관리)
- ✅ React Hook Form + Zod (폼 검증)
- ✅ Drizzle ORM (PostgreSQL 또는 SQLite)
- ✅ Better Auth (인증)
- ✅ Framer Motion (애니메이션)
- ✅ ESLint + Build 검증
검증 기준
모든 프로젝트는 다음 두 가지 검증을 반드시 통과해야 합니다:
1. ESLint: npm run lint 또는 pnpm lint → 0 errors 2. Build: npm run build 또는 pnpm build → 성공
Structure
SKILL.md- Main skill instructionsreferences/- Detailed documentation loaded as neededsetup-guide.md- 완전한 구현 가이드 (PostgreSQL/SQLite, Next.js 15 params Promise 등)scripts/- Executable code for deterministic operationsassets/- Templates, images, or other resources
Usage
This skill is automatically discovered by Claude when relevant to the task.
Next.js 15 Init 상세 가이드
전체 Setup 프로세스
1. 프로젝트 생성
중요: TypeScript, Tailwind CSS, App Router를 사용합니다.
npx create-next-app@latest my-app --typescript --tailwind --app --use-npm
cd my-app기본 설정:
- TypeScript: 타입 안전성
- Tailwind CSS: 유틸리티 퍼스트 CSS
- App Router: Next.js 15의 최신 라우팅 시스템
- Package Manager: npm
2. package.json 기본 의존성
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "^15.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5",
"tailwindcss": "^3.4.1",
"postcss": "^8",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "^15.0.0"
}
}3. 폴더 구조 생성 스크립트
mkdir -p app/api
mkdir -p components/ui
mkdir -p components/layouts
mkdir -p lib/stores
mkdir -p lib/api
mkdir -p lib/validations
mkdir -p lib/utils
mkdir -p lib/dbApp Router 구조:
app/
├── (auth)/ # 인증 관련 라우트 그룹
├── [domain]/ # 도메인별 페이지 (예: /todos, /blog)
├── api/[domain]/ # API 라우트
├── layout.tsx # 루트 레이아웃
└── page.tsx # 홈 페이지
components/
├── ui/ # ShadCN 컴포넌트
├── [domain]/ # 도메인별 컴포넌트
└── layouts/ # 레이아웃 컴포넌트
lib/
├── db/ # Prisma 클라이언트
├── stores/ # Zustand 스토어
├── api/ # API 클라이언트 로직
├── validations/ # Zod 스키마
└── utils/ # 유틸리티 함수기본 파일 템플릿
lib/utils.ts (shadcn 유틸리티)
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}tailwind.config.ts
import type { Config } from "tailwindcss"
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
}
export default configapp/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 262.1 83.3% 57.8%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 40% 98%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 263.4 70% 50.4%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}components.json (ShadCN 설정)
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}ShadCN/ui 설치 및 구성
# ShadCN 초기화
npx shadcn@latest init
# 기본 컴포넌트 추가
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add form
npx shadcn@latest add table
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
npx shadcn@latest add select
npx shadcn@latest add checkbox
npx shadcn@latest add toastEssential 프리셋 패키지
npm install zustand
npm install @tanstack/react-query
npm install react-hook-form
npm install zod
npm install @hookform/resolvers
npm install lucide-react
npm install clsx tailwind-merge
npm install tailwindcss-animatelib/stores/example-store.ts (Zustand)
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface ExampleState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useExampleStore = create<ExampleState>()(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}),
{
name: 'example-storage',
}
)
)
)app/providers.tsx (Tanstack Query)
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { useState } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1분
refetchOnWindowFocus: false,
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}app/layout.tsx (업데이트)
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { Providers } from './providers'
import { Toaster } from '@/components/ui/toaster'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'My App',
description: 'Next.js 15 App with ShadCN, Zustand, and Tanstack Query',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>
{children}
<Toaster />
</Providers>
</body>
</html>
)
}도메인별 보일러플레이트
Todo 도메인 (완전한 예제)
lib/validations/todo.ts
import { z } from 'zod'
export const todoSchema = z.object({
id: z.number().optional(),
title: z.string().min(1, 'Title is required'),
description: z.string().optional(),
completed: z.boolean().default(false),
createdAt: z.date().optional(),
updatedAt: z.date().optional(),
})
export type Todo = z.infer<typeof todoSchema>
export const createTodoSchema = todoSchema.omit({ id: true, createdAt: true, updatedAt: true })
export const updateTodoSchema = todoSchema.partial().required({ id: true })lib/stores/todo-store.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import type { Todo } from '@/lib/validations/todo'
export type TodoFilter = 'all' | 'active' | 'completed'
interface TodoState {
todos: Todo[]
filter: TodoFilter
setTodos: (todos: Todo[]) => void
addTodo: (todo: Todo) => void
updateTodo: (id: number, updates: Partial<Todo>) => void
deleteTodo: (id: number) => void
toggleTodo: (id: number) => void
setFilter: (filter: TodoFilter) => void
getFilteredTodos: () => Todo[]
}
export const useTodoStore = create<TodoState>()(
devtools(
persist(
(set, get) => ({
todos: [],
filter: 'all',
setTodos: (todos) => set({ todos }),
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
updateTodo: (id, updates) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, ...updates } : todo
),
})),
deleteTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
setFilter: (filter) => set({ filter }),
getFilteredTodos: () => {
const { todos, filter } = get()
if (filter === 'active') return todos.filter((t) => !t.completed)
if (filter === 'completed') return todos.filter((t) => t.completed)
return todos
},
}),
{
name: 'todo-storage',
}
)
)
)lib/api/todos.ts
import type { Todo } from '@/lib/validations/todo'
const API_BASE = '/api/todos'
export async function getTodos(): Promise<Todo[]> {
const res = await fetch(API_BASE)
if (!res.ok) throw new Error('Failed to fetch todos')
return res.json()
}
export async function getTodo(id: number): Promise<Todo> {
const res = await fetch(`${API_BASE}/${id}`)
if (!res.ok) throw new Error('Failed to fetch todo')
return res.json()
}
export async function createTodo(data: Omit<Todo, 'id'>): Promise<Todo> {
const res = await fetch(API_BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('Failed to create todo')
return res.json()
}
export async function updateTodo(id: number, data: Partial<Todo>): Promise<Todo> {
const res = await fetch(`${API_BASE}/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('Failed to update todo')
return res.json()
}
export async function deleteTodo(id: number): Promise<void> {
const res = await fetch(`${API_BASE}/${id}`, {
method: 'DELETE',
})
if (!res.ok) throw new Error('Failed to delete todo')
}app/api/todos/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { todoSchema } from '@/lib/validations/todo'
// 메모리 스토리지 (Prisma 사용 시 DB로 교체)
let todos = [
{ id: 1, title: 'Example Todo', description: 'This is an example', completed: false, createdAt: new Date(), updatedAt: new Date() },
]
let nextId = 2
export async function GET() {
return NextResponse.json(todos)
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = todoSchema.omit({ id: true }).parse(body)
const newTodo = {
id: nextId++,
...validated,
createdAt: new Date(),
updatedAt: new Date(),
}
todos.push(newTodo)
return NextResponse.json(newTodo, { status: 201 })
} catch (error) {
return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
}
}app/api/todos/[id]/route.ts
중요: Next.js 15부터 params는 Promise로 변경되었습니다. 반드시 await로 해제해야 합니다.
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params
const id = parseInt(idStr)
const todo = todos.find((t) => t.id === id)
if (!todo) {
return NextResponse.json({ error: 'Todo not found' }, { status: 404 })
}
return NextResponse.json(todo)
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params
const id = parseInt(idStr)
const todoIndex = todos.findIndex((t) => t.id === id)
if (todoIndex === -1) {
return NextResponse.json({ error: 'Todo not found' }, { status: 404 })
}
const body = await request.json()
const updatedTodo = {
...todos[todoIndex],
...body,
updatedAt: new Date(),
}
todos[todoIndex] = updatedTodo
return NextResponse.json(updatedTodo)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params
const id = parseInt(idStr)
const todoIndex = todos.findIndex((t) => t.id === id)
if (todoIndex === -1) {
return NextResponse.json({ error: 'Todo not found' }, { status: 404 })
}
todos.splice(todoIndex, 1)
return NextResponse.json({ message: 'Todo deleted' })
}components/todos/todo-list.tsx
'use client'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getTodos, deleteTodo, updateTodo } from '@/lib/api/todos'
import { useTodoStore } from '@/lib/stores/todo-store'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import { Trash2, Edit } from 'lucide-react'
export function TodoList() {
const queryClient = useQueryClient()
const filter = useTodoStore((state) => state.filter)
const { data: todos = [], isLoading } = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
})
const deleteMutation = useMutation({
mutationFn: deleteTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
const toggleMutation = useMutation({
mutationFn: ({ id, completed }: { id: number; completed: boolean }) =>
updateTodo(id, { completed }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
const filteredTodos = todos.filter((todo) => {
if (filter === 'active') return !todo.completed
if (filter === 'completed') return todo.completed
return true
})
if (isLoading) return <div>Loading...</div>
return (
<div className="space-y-4">
{filteredTodos.map((todo) => (
<Card key={todo.id}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={(checked) =>
toggleMutation.mutate({ id: todo.id!, completed: !!checked })
}
/>
<CardTitle className={todo.completed ? 'line-through' : ''}>
{todo.title}
</CardTitle>
</div>
<div className="flex space-x-2">
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(todo.id!)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardHeader>
{todo.description && (
<CardContent>
<p className="text-sm text-muted-foreground">{todo.description}</p>
</CardContent>
)}
</Card>
))}
</div>
)
}components/todos/todo-form.tsx
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createTodoSchema } from '@/lib/validations/todo'
import { createTodo } from '@/lib/api/todos'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { useToast } from '@/components/ui/use-toast'
export function TodoForm() {
const queryClient = useQueryClient()
const { toast } = useToast()
const form = useForm({
resolver: zodResolver(createTodoSchema),
defaultValues: {
title: '',
description: '',
completed: false,
},
})
const createMutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
form.reset()
toast({
title: 'Success',
description: 'Todo created successfully',
})
},
onError: () => {
toast({
title: 'Error',
description: 'Failed to create todo',
variant: 'destructive',
})
},
})
const onSubmit = form.handleSubmit((data) => {
createMutation.mutate(data)
})
return (
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter todo title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Enter description (optional)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating...' : 'Add Todo'}
</Button>
</form>
</Form>
)
}components/todos/todo-filter.tsx
'use client'
import { useTodoStore, type TodoFilter } from '@/lib/stores/todo-store'
import { Button } from '@/components/ui/button'
export function TodoFilterBar() {
const { filter, setFilter } = useTodoStore()
const filters: { value: TodoFilter; label: string }[] = [
{ value: 'all', label: 'All' },
{ value: 'active', label: 'Active' },
{ value: 'completed', label: 'Completed' },
]
return (
<div className="flex space-x-2">
{filters.map((f) => (
<Button
key={f.value}
variant={filter === f.value ? 'default' : 'outline'}
onClick={() => setFilter(f.value)}
>
{f.label}
</Button>
))}
</div>
)
}app/todos/page.tsx
import { TodoList } from '@/components/todos/todo-list'
import { TodoForm } from '@/components/todos/todo-form'
import { TodoFilterBar } from '@/components/todos/todo-filter'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export default function TodosPage() {
return (
<div className="container mx-auto py-10">
<div className="grid gap-6 md:grid-cols-[1fr_300px]">
<div>
<div className="mb-4 flex items-center justify-between">
<h1 className="text-3xl font-bold">Todos</h1>
<TodoFilterBar />
</div>
<TodoList />
</div>
<Card>
<CardHeader>
<CardTitle>Add New Todo</CardTitle>
</CardHeader>
<CardContent>
<TodoForm />
</CardContent>
</Card>
</div>
</div>
)
}---
선택 가능한 고급 기능
옵션 1: Drizzle ORM (TypeScript-first 데이터베이스 ORM)
설치 (PostgreSQL)
npm install drizzle-orm postgres
npm install -D drizzle-kit설치 (SQLite)
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3중요: SQLite를 사용하는 경우 next.config.js에 다음을 추가해야 합니다:
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: ['better-sqlite3'],
}
module.exports = nextConfiglib/db/schema.ts (PostgreSQL)
import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core'
export const todos = pgTable('todos', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
description: text('description'),
completed: boolean('completed').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
excerpt: text('excerpt'),
slug: text('slug').unique().notNull(),
published: boolean('published').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
export type Todo = typeof todos.$inferSelect
export type NewTodo = typeof todos.$inferInsert
export type Post = typeof posts.$inferSelect
export type NewPost = typeof posts.$inferInsertlib/db/schema.ts (SQLite)
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
export const todos = sqliteTable('todos', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
description: text('description'),
completed: integer('completed', { mode: 'boolean' }).default(false).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
})
export type Todo = typeof todos.$inferSelect
export type NewTodo = typeof todos.$inferInsertlib/db/index.ts (PostgreSQL)
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
const connectionString = process.env.DATABASE_URL!
// Disable prefetch as it is not supported for "Transaction" pool mode
const client = postgres(connectionString, { prepare: false })
export const db = drizzle(client, { schema })lib/db/index.ts (SQLite)
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from './schema'
import { join } from 'path'
const sqlite = new Database(join(process.cwd(), 'app.db'))
export const db = drizzle(sqlite, { schema })drizzle.config.ts (PostgreSQL)
import type { Config } from 'drizzle-kit'
export default {
schema: './lib/db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Configdrizzle.config.ts (SQLite)
import type { Config } from 'drizzle-kit'
export default {
schema: './lib/db/schema.ts',
out: './drizzle',
driver: 'better-sqlite',
dbCredentials: {
url: './app.db',
},
} satisfies ConfigDrizzle로 API 라우트 업데이트
// app/api/todos/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { todos } from '@/lib/db/schema'
import { todoSchema } from '@/lib/validations/todo'
import { desc } from 'drizzle-orm'
export async function GET() {
const allTodos = await db.select().from(todos).orderBy(desc(todos.createdAt))
return NextResponse.json(allTodos)
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = todoSchema.omit({ id: true }).parse(body)
const [newTodo] = await db
.insert(todos)
.values(validated)
.returning()
return NextResponse.json(newTodo, { status: 201 })
} catch (error) {
return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
}
}마이그레이션 실행 (PostgreSQL)
# 마이그레이션 파일 생성
npx drizzle-kit generate:pg
# 마이그레이션 실행
npx drizzle-kit push:pg
# Drizzle Studio 실행 (옵션)
npx drizzle-kit studio마이그레이션 실행 (SQLite)
# 마이그레이션 파일 생성
npx drizzle-kit generate:sqlite
# 마이그레이션 실행
npx drizzle-kit push:sqlite
# better-sqlite3 네이티브 바인딩 재빌드 (필요 시)
npm rebuild better-sqlite3
# Drizzle Studio 실행 (옵션)
npx drizzle-kit studio중요: SQLite를 사용하는 경우, better-sqlite3의 네이티브 바인딩 문제가 발생할 수 있습니다. 빌드 실패 시 npm rebuild better-sqlite3를 실행하세요.
옵션 2: Better Auth (TypeScript-first 인증)
설치
npm install better-auth
npm install bcryptjs
npm install -D @types/bcryptjslib/auth/auth.ts
import { betterAuth } from 'better-auth'
import { db } from '@/lib/db'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
}),
emailAndPassword: {
enabled: true,
},
socialProviders: {
// 필요 시 소셜 로그인 추가
// google: {
// clientId: process.env.GOOGLE_CLIENT_ID!,
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
// },
},
})
export type Session = typeof auth.$Infer.Sessionlib/db/schema.ts (Auth 테이블 추가)
import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core'
// 기존 테이블들...
// Better Auth 테이블
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name'),
email: text('email').unique().notNull(),
emailVerified: boolean('email_verified').default(false),
image: text('image'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
export const accounts = pgTable('accounts', {
id: serial('id').primaryKey(),
userId: serial('user_id').references(() => users.id, { onDelete: 'cascade' }),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
expiresAt: timestamp('expires_at'),
password: text('password'), // 이메일/비밀번호 로그인용
})
export const sessions = pgTable('sessions', {
id: serial('id').primaryKey(),
userId: serial('user_id').references(() => users.id, { onDelete: 'cascade' }),
token: text('token').unique().notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const verificationTokens = pgTable('verification_tokens', {
id: serial('id').primaryKey(),
identifier: text('identifier').notNull(),
token: text('token').unique().notNull(),
expiresAt: timestamp('expires_at').notNull(),
})app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth/auth'
export const { GET, POST } = auth.handlerlib/auth/auth-client.ts
import { createAuthClient } from 'better-auth/react'
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
})
export const { signIn, signUp, signOut, useSession } = authClientmiddleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { auth } from '@/lib/auth/auth'
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
})
const isAuthPage = request.nextUrl.pathname.startsWith('/login') ||
request.nextUrl.pathname.startsWith('/register')
if (isAuthPage && session) {
return NextResponse.redirect(new URL('/todos', request.url))
}
if (!isAuthPage && !session) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}app/login/page.tsx (예제)
'use client'
import { useState } from 'react'
import { signIn } from '@/lib/auth/auth-client'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
export default function LoginPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await signIn.email({
email,
password,
callbackURL: '/todos',
})
router.push('/todos')
}
return (
<div className="flex min-h-screen items-center justify-center">
<form onSubmit={handleSubmit} className="w-full max-w-md space-y-4">
<h1 className="text-2xl font-bold">Login</h1>
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button type="submit" className="w-full">
Sign In
</Button>
</form>
</div>
)
}옵션 3: Framer Motion (애니메이션)
설치
npm install framer-motioncomponents/animated-card.tsx
'use client'
import { motion } from 'framer-motion'
import { Card } from '@/components/ui/card'
export function AnimatedCard({ children, ...props }: React.ComponentProps<typeof Card>) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<Card {...props}>{children}</Card>
</motion.div>
)
}사용 예제
import { AnimatedCard } from '@/components/animated-card'
export function TodoList() {
return (
<div className="space-y-4">
{todos.map((todo) => (
<AnimatedCard key={todo.id}>
{/* Todo content */}
</AnimatedCard>
))}
</div>
)
}코드 검증 및 오류 수정
1. ESLint 실행
npm run lint2. 자주 발생하는 오류 및 해결 방법
Import 경로 오류
// ❌ 잘못된 예
import { Button } from '../../../components/ui/button'
// ✅ 올바른 예 (tsconfig.json의 paths 사용)
import { Button } from '@/components/ui/button'TypeScript 타입 오류
// ❌ 타입 명시 없음
export async function GET(request) {
// ...
}
// ✅ 타입 명시
export async function GET(request: NextRequest) {
// ...
}클라이언트 컴포넌트 표시
// useState, useEffect 등 React Hooks를 사용하는 컴포넌트는 반드시 'use client' 추가
'use client'
import { useState } from 'react'
export function MyComponent() {
const [count, setCount] = useState(0)
// ...
}Next.js 15 Route Handler params 오류
// ❌ Next.js 14 스타일 (빌드 실패)
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const id = params.id // Type error!
}
// ✅ Next.js 15 스타일 (params는 Promise)
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params // ✅ Correct!
}SQLite (better-sqlite3) 빌드 오류
# 오류: Could not locate the bindings file
# 해결 방법 1: next.config.js 설정
const nextConfig = {
serverExternalPackages: ['better-sqlite3'],
}
# 해결 방법 2: 네이티브 바인딩 재빌드
npm rebuild better-sqlite33. 성공 기준
✅ npm run lint 결과가 "0 errors" ✅ npm run build 또는 pnpm build 성공 ✅ TypeScript 컴파일 성공 ✅ 모든 import 경로 해결됨 ✅ 'use client' 지시어가 적절히 사용됨 ✅ Next.js 15 params Promise 패턴 사용
Full Stack package.json
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "^15.0.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-query": "^5.28.4",
"@tanstack/react-query-devtools": "^5.28.4",
"zustand": "^4.5.2",
"react-hook-form": "^7.51.1",
"zod": "^3.22.4",
"@hookform/resolvers": "^3.3.4",
"drizzle-orm": "^0.29.0",
"postgres": "^3.4.3",
"better-auth": "^0.8.0",
"bcryptjs": "^2.4.3",
"framer-motion": "^11.0.20",
"lucide-react": "^0.358.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"class-variance-authority": "^0.7.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/bcryptjs": "^2.4.6",
"drizzle-kit": "^0.20.0",
"typescript": "^5",
"tailwindcss": "^3.4.1",
"postcss": "^8",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "^15.0.0"
}
}환경 변수 (.env.local)
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# Better Auth
BETTER_AUTH_SECRET="your-secret-key-here"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"---
대화형 선택 가이드
사용자가 각 프리셋을 선택하면:
1. Essential (권장): ShadCN, Zustand, React Hook Form + Zod 설치 2. Minimal: Next.js + TypeScript + Tailwind만 설치 3. Full Stack: 모든 기능 (Drizzle ORM, Better Auth, Framer Motion 포함) 설치 4. Custom: 각 기능을 개별적으로 선택
모든 선택은 독립적으로 작동하며, 원하는 조합으로 구성 가능합니다.
린트 설정 (ESLint)
Next.js 프로젝트는 기본적으로 ESLint를 사용합니다.
기본 설정 (.eslintrc.json)
{
"extends": "next/core-web-vitals"
}린트 실행
# 린트 검사
npm run lint
# 린트 자동 수정
npm run lint -- --fix추가 린트 규칙 (선택사항)
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"prefer-const": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}Prettier 통합 (선택사항)
npm install -D prettier eslint-config-prettier.prettierrc:
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}.eslintrc.json (업데이트):
{
"extends": ["next/core-web-vitals", "prettier"]
}Related skills
FAQ
Which domains does the skill scaffold?
It offers Todo, Blog, Dashboard, E-commerce, and Custom domain templates.
What stack presets are available?
Minimal, Essential, and Full Stack presets, plus a Custom option to pick each library individually.