{"id":11463,"date":"2026-03-08T13:51:42","date_gmt":"2026-03-08T04:51:42","guid":{"rendered":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=11463"},"modified":"2026-03-13T17:45:21","modified_gmt":"2026-03-13T08:45:21","slug":"%eb%aa%a8%eb%8d%98-react-%ec%8a%a4%ed%83%9d-%ec%99%84%ec%a0%84-%ec%a0%95%eb%b3%b5-zustand-tanstack-query-react-hook-form-zod-tailwind-shadcn-ui","status":"publish","type":"post","link":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=11463","title":{"rendered":"\ubaa8\ub358 React \uc2a4\ud0dd \uc644\uc804 \uc815\ubcf5: Zustand + TanStack Query + React Hook Form + Zod + Tailwind + shadcn\/ui"},"content":{"rendered":"<h1>\ubaa8\ub358 React \uc2a4\ud0dd \uc644\uc804 \uc815\ubcf5: Zustand + TanStack Query + React Hook Form + Zod + Tailwind + shadcn\/ui<\/h1>\n<blockquote>\n<p>\uc2e4\ubb34\uc5d0\uc11c \uac00\uc7a5 \ub9ce\uc774 \uc4f0\uc774\ub294 6\uac00\uc9c0 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ub2e8\uacc4\ubcc4 \uc0d8\ud50c \ucf54\ub4dc\uc640 \ud568\uaed8 \uc124\uba85\ud569\ub2c8\ub2e4.<br \/>\n\uac01 \ub2e8\uacc4\ub9c8\ub2e4 \uc774\uc804 \ucf54\ub4dc\uc5d0 \uc0c8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ub367\ubd99\uc774\ub294 \ubc29\uc2dd\uc73c\ub85c \uc9c4\ud589\ud569\ub2c8\ub2e4.<br \/>\n<strong>\uc774 \ubb38\uc11c\ub294 Tailwind CSS v4 \uae30\uc900\uc73c\ub85c \uc791\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<\/strong><\/p>\n<\/blockquote>\n<h2>0. \ud504\ub85c\uc81d\ud2b8 \uc138\ud305<\/h2>\n<pre><code class=\"language-bash\">npx create-next-app@latest my-app --typescript --eslint --app --no-tailwind --src-dir\ncd my-app<\/code><\/pre>\n<pre><code class=\"language-bash\">npm run dev<\/code><\/pre>\n<p>\ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c <a href=\"http:\/\/localhost:3000\">http:\/\/localhost:3000<\/a> \uc73c\ub85c \uc811\uc18d\ud558\uba74 \ub429\ub2c8\ub2e4.<\/p>\n<h2>1. Tailwind CSS (v4)<\/h2>\n<h3>\uc124\uce58<\/h3>\n<pre><code class=\"language-bash\">npm install tailwindcss @tailwindcss\/postcss postcss<\/code><\/pre>\n<blockquote>\n<p>v4\ub294 <code>tailwind.config.ts<\/code> \ud30c\uc77c\uc774 <strong>\ud544\uc694 \uc5c6\uc2b5\ub2c8\ub2e4<\/strong>. \ubaa8\ub4e0 \uc124\uc815\uc740 CSS \ud30c\uc77c\uc5d0\uc11c \uc9c1\uc811 \ud569\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<p><code>postcss.config.mjs<\/code> \ud30c\uc77c\uc744 \ud504\ub85c\uc81d\ud2b8 \ub8e8\ud2b8\uc5d0 \uc0dd\uc131\ud569\ub2c8\ub2e4:<\/p>\n<pre><code class=\"language-js\">\/\/ postcss.config.mjs\nconst config = {\n  plugins: {\n    &quot;@tailwindcss\/postcss&quot;: {},\n  },\n};\nexport default config;<\/code><\/pre>\n<p><code>src\/app\/globals.css<\/code>\uc5d0 Tailwind\ub97c \uc784\ud3ec\ud2b8\ud569\ub2c8\ub2e4:<\/p>\n<pre><code class=\"language-css\">@import &quot;tailwindcss&quot;;<\/code><\/pre>\n<blockquote>\n<p>v4\uc5d0\uc11c\ub294 <code>@tailwind base\/components\/utilities<\/code> 3\uc904 \ub300\uc2e0 <code>@import &quot;tailwindcss&quot;<\/code> \ud55c \uc904\uc774\uba74 \ub429\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<h3>\ucee4\uc2a4\ud140 \ud14c\ub9c8 \uc124\uc815<\/h3>\n<p>v4\uc5d0\uc11c\ub294 <code>tailwind.config.ts<\/code> \ub300\uc2e0 CSS \ud30c\uc77c \uc548\uc758 <code>@theme<\/code> \ube14\ub85d\uc5d0\uc11c \ub514\uc790\uc778 \ud1a0\ud070\uc744 \uc815\uc758\ud569\ub2c8\ub2e4.<\/p>\n<pre><code class=\"language-css\">\/* src\/app\/globals.css *\/\n@import &quot;tailwindcss&quot;;\n\n@theme {\n  \/* \ucee4\uc2a4\ud140 \uc0c9\uc0c1 *\/\n  --color-brand: oklch(0.62 0.19 255);\n\n  \/* \ucee4\uc2a4\ud140 \ud3f0\ud2b8 *\/\n  --font-sans: &quot;Pretendard&quot;, sans-serif;\n\n  \/* \ucee4\uc2a4\ud140 \uac04\uaca9 *\/\n  --spacing-18: 4.5rem;\n}<\/code><\/pre>\n<p><code>@theme<\/code>\uc5d0 \uc815\uc758\ud55c \ud1a0\ud070\uc740 \uc790\ub3d9\uc73c\ub85c CSS \ubcc0\uc218\ub85c \ub178\ucd9c\ub418\uace0, <code>bg-brand<\/code>, <code>font-sans<\/code> \uac19\uc740 \uc720\ud2f8\ub9ac\ud2f0 \ud074\ub798\uc2a4\ub85c \ubc14\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n<h3>\uc0ac\uc6a9\ubc95<\/h3>\n<p>Tailwind \uc720\ud2f8\ub9ac\ud2f0 \ud074\ub798\uc2a4 \uc0ac\uc6a9\ubc95\uc740 v3\uc640 \ub3d9\uc77c\ud569\ub2c8\ub2e4.<\/p>\n<pre><code class=\"language-tsx\">\/\/ src\/app\/page.tsx - Step 1: Tailwind\ub9cc \uc0ac\uc6a9\ud55c \uc0ac\uc6a9\uc790 \ud3fc UI\nexport default function Page() {\n  return (\n    &lt;main className=&quot;min-h-screen bg-gray-50 flex items-center justify-center p-4&quot;&gt;\n      &lt;div className=&quot;bg-white rounded-2xl shadow-md w-full max-w-md p-8&quot;&gt;\n        &lt;h1 className=&quot;text-2xl font-bold text-gray-800 mb-6&quot;&gt;\ud68c\uc6d0\uac00\uc785&lt;\/h1&gt;\n\n        &lt;div className=&quot;space-y-4&quot;&gt;\n          &lt;div&gt;\n            &lt;label className=&quot;block text-sm font-medium text-gray-700 mb-1&quot;&gt;\n              \uc774\ub984\n            &lt;\/label&gt;\n            &lt;input\n              type=&quot;text&quot;\n              placeholder=&quot;\ud64d\uae38\ub3d9&quot;\n              className=&quot;w-full border border-gray-300 rounded-lg px-3 py-2 text-sm\n                         focus:outline-none focus:ring-2 focus:ring-blue-500&quot;\n            \/&gt;\n          &lt;\/div&gt;\n\n          &lt;div&gt;\n            &lt;label className=&quot;block text-sm font-medium text-gray-700 mb-1&quot;&gt;\n              \uc774\uba54\uc77c\n            &lt;\/label&gt;\n            &lt;input\n              type=&quot;email&quot;\n              placeholder=&quot;hong@example.com&quot;\n              className=&quot;w-full border border-gray-300 rounded-lg px-3 py-2 text-sm\n                         focus:outline-none focus:ring-2 focus:ring-blue-500&quot;\n            \/&gt;\n          &lt;\/div&gt;\n\n          &lt;div&gt;\n            &lt;label className=&quot;block text-sm font-medium text-gray-700 mb-1&quot;&gt;\n              \ube44\ubc00\ubc88\ud638\n            &lt;\/label&gt;\n            &lt;input\n              type=&quot;password&quot;\n              placeholder=&quot;\ucd5c\uc18c 8\uc790 \uc774\uc0c1&quot;\n              className=&quot;w-full border border-gray-300 rounded-lg px-3 py-2 text-sm\n                         focus:outline-none focus:ring-2 focus:ring-blue-500&quot;\n            \/&gt;\n          &lt;\/div&gt;\n\n          &lt;button\n            className=&quot;w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold\n                       py-2 rounded-lg transition-colors&quot;\n          &gt;\n            \uac00\uc785\ud558\uae30\n          &lt;\/button&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/main&gt;\n  );\n}<\/code><\/pre>\n<h3>\ud575\uc2ec \ud3ec\uc778\ud2b8<\/h3>\n<table>\n<thead>\n<tr>\n<th>\uac1c\ub150<\/th>\n<th>\ud074\ub798\uc2a4 \uc608\uc2dc<\/th>\n<th>\uc124\uba85<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\ub808\uc774\uc544\uc6c3<\/td>\n<td><code>flex<\/code>, <code>grid<\/code>, <code>items-center<\/code><\/td>\n<td>Flexbox \/ Grid<\/td>\n<\/tr>\n<tr>\n<td>\uac04\uaca9<\/td>\n<td><code>p-4<\/code>, <code>m-2<\/code>, <code>space-y-4<\/code><\/td>\n<td>padding \/ margin \/ \uc790\uc2dd \uac04\uaca9<\/td>\n<\/tr>\n<tr>\n<td>\ubc18\uc751\ud615<\/td>\n<td><code>sm:<\/code>, <code>md:<\/code>, <code>lg:<\/code> \uc811\ub450\uc0ac<\/td>\n<td>\ube0c\ub808\uc774\ud06c\ud3ec\uc778\ud2b8<\/td>\n<\/tr>\n<tr>\n<td>\uc0c1\ud0dc<\/td>\n<td><code>hover:<\/code>, <code>focus:<\/code>, <code>disabled:<\/code><\/td>\n<td>\uc778\ud130\ub799\uc158 \uc2a4\ud0c0\uc77c<\/td>\n<\/tr>\n<tr>\n<td>\ub2e4\ud06c\ubaa8\ub4dc<\/td>\n<td><code>dark:<\/code> \uc811\ub450\uc0ac<\/td>\n<td>\ub2e4\ud06c \ud14c\ub9c8<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>2. shadcn\/ui<\/h2>\n<h3>\uac1c\uc694<\/h3>\n<p>shadcn\/ui\ub294 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \uc544\ub2d9\ub2c8\ub2e4. <strong>\ucef4\ud3ec\ub10c\ud2b8 \ucf54\ub4dc\ub97c \uc9c1\uc811 \ud504\ub85c\uc81d\ud2b8\uc5d0 \ubcf5\uc0ac\ud574 \ub123\ub294<\/strong> \ubc29\uc2dd\uc73c\ub85c, Radix UI + Tailwind \uae30\ubc18\uc758 \uc644\uc131\ub3c4 \ub192\uc740 UI\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4. Tailwind v4\uc640 React 19\ub97c \uc644\uc804\ud788 \uc9c0\uc6d0\ud569\ub2c8\ub2e4.<\/p>\n<h3>\uc124\uce58<\/h3>\n<pre><code class=\"language-bash\">npx shadcn@latest init<\/code><\/pre>\n<blockquote>\n<p>CLI\uac00 \uc790\ub3d9\uc73c\ub85c <code>globals.css<\/code>\uc5d0 shadcn \ud14c\ub9c8 \ubcc0\uc218\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4. Tailwind v4 \ubc29\uc2dd(<code>@theme inline<\/code>)\uc744 \uc0ac\uc6a9\ud569\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<p>\ud544\uc694\ud55c \ucef4\ud3ec\ub10c\ud2b8\ub97c \uac1c\ubcc4 \uc124\uce58:<\/p>\n<pre><code class=\"language-bash\">npx shadcn@latest add button input label card form<\/code><\/pre>\n<h3>globals.css \u2014 shadcn + Tailwind v4 \uad6c\uc870<\/h3>\n<p><code>npx shadcn init<\/code> \uc2e4\ud589 \ud6c4 <code>globals.css<\/code>\ub294 \uc544\ub798\uc640 \uac19\uc740 \uad6c\uc870\uac00 \ub429\ub2c8\ub2e4:<\/p>\n<pre><code class=\"language-css\">\/* src\/app\/globals.css *\/\n@import &quot;tailwindcss&quot;;\n@import &quot;tw-animate-css&quot;; \/* shadcn \uc560\ub2c8\uba54\uc774\uc158 (v4\uc5d0\uc11c tailwindcss-animate \ub300\uccb4) *\/\n\n@custom-variant dark (&amp;:is(.dark *));\n\n:root {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --radius: 0.625rem;\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --primary: oklch(0.985 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  \/* ... *\/\n}\n\n\/* v4 \ud575\uc2ec: @theme inline\uc73c\ub85c CSS \ubcc0\uc218\ub97c Tailwind \uc720\ud2f8\ub9ac\ud2f0\uc5d0 \uc5f0\uacb0 *\/\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n}<\/code><\/pre>\n<blockquote>\n<p><strong><code>@theme inline<\/code>\uc774 \ud575\uc2ec\uc785\ub2c8\ub2e4.<\/strong> <code>:root<\/code>\uc5d0\uc11c \uc815\uc758\ud55c CSS \ubcc0\uc218\ub97c Tailwind \uc720\ud2f8\ub9ac\ud2f0(<code>bg-background<\/code>, <code>text-primary<\/code> \ub4f1)\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uac8c \uc5f0\uacb0\ud569\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<h3>\ucee4\uc2a4\ud140 \uc0c9\uc0c1 \ucd94\uac00<\/h3>\n<p>\uc0c8 \uc0c9\uc0c1\uc744 \ucd94\uac00\ud560 \ub54c\ub3c4 \ub3d9\uc77c\ud55c \ud328\ud134\uc744 \ub530\ub985\ub2c8\ub2e4:<\/p>\n<pre><code class=\"language-css\">:root {\n  --warning: oklch(0.84 0.16 84);\n  --warning-foreground: oklch(0.28 0.07 46);\n}\n\n.dark {\n  --warning: oklch(0.41 0.11 46);\n  --warning-foreground: oklch(0.99 0.02 95);\n}\n\n@theme inline {\n  --color-warning: var(--warning);\n  --color-warning-foreground: var(--warning-foreground);\n}<\/code><\/pre>\n<p>\uc774\ud6c4 <code>bg-warning<\/code>, <code>text-warning-foreground<\/code> \ud074\ub798\uc2a4\ub85c \ubc14\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n<h3>\uc0ac\uc6a9\ubc95<\/h3>\n<pre><code class=\"language-tsx\">\/\/ src\/app\/page.tsx - Step 2: shadcn\/ui \ucef4\ud3ec\ub10c\ud2b8 \uc801\uc6a9\nimport { Button } from &quot;@\/components\/ui\/button&quot;;\nimport { Input } from &quot;@\/components\/ui\/input&quot;;\nimport { Label } from &quot;@\/components\/ui\/label&quot;;\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from &quot;@\/components\/ui\/card&quot;;\n\nexport default function Page() {\n  return (\n    &lt;main className=&quot;min-h-screen bg-gray-50 flex items-center justify-center p-4&quot;&gt;\n      &lt;Card className=&quot;w-full max-w-md&quot;&gt;\n        &lt;CardHeader&gt;\n          &lt;CardTitle&gt;\ud68c\uc6d0\uac00\uc785&lt;\/CardTitle&gt;\n          &lt;CardDescription&gt;\n            \uc544\ub798 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uacc4\uc815\uc744 \ub9cc\ub4dc\uc138\uc694.\n          &lt;\/CardDescription&gt;\n        &lt;\/CardHeader&gt;\n        &lt;CardContent&gt;\n          &lt;div className=&quot;space-y-4&quot;&gt;\n            &lt;div className=&quot;space-y-1&quot;&gt;\n              &lt;Label htmlFor=&quot;name&quot;&gt;\uc774\ub984&lt;\/Label&gt;\n              &lt;Input id=&quot;name&quot; placeholder=&quot;\ud64d\uae38\ub3d9&quot; \/&gt;\n            &lt;\/div&gt;\n\n            &lt;div className=&quot;space-y-1&quot;&gt;\n              &lt;Label htmlFor=&quot;email&quot;&gt;\uc774\uba54\uc77c&lt;\/Label&gt;\n              &lt;Input id=&quot;email&quot; type=&quot;email&quot; placeholder=&quot;hong@example.com&quot; \/&gt;\n            &lt;\/div&gt;\n\n            &lt;div className=&quot;space-y-1&quot;&gt;\n              &lt;Label htmlFor=&quot;password&quot;&gt;\ube44\ubc00\ubc88\ud638&lt;\/Label&gt;\n              &lt;Input id=&quot;password&quot; type=&quot;password&quot; placeholder=&quot;\ucd5c\uc18c 8\uc790 \uc774\uc0c1&quot; \/&gt;\n            &lt;\/div&gt;\n\n            &lt;Button className=&quot;w-full&quot;&gt;\uac00\uc785\ud558\uae30&lt;\/Button&gt;\n          &lt;\/div&gt;\n        &lt;\/CardContent&gt;\n      &lt;\/Card&gt;\n    &lt;\/main&gt;\n  );\n}<\/code><\/pre>\n<h3>\uc790\uc8fc \uc4f0\ub294 \ucef4\ud3ec\ub10c\ud2b8<\/h3>\n<pre><code class=\"language-bash\">npx shadcn@latest add dialog sonner select checkbox badge table<\/code><\/pre>\n<blockquote>\n<p>v4\uc5d0\uc11c\ub294 <code>toast<\/code> \ub300\uc2e0 <code>sonner<\/code>\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4. <code>tailwindcss-animate<\/code>\ub3c4 <code>tw-animate-css<\/code>\ub85c \ub300\uccb4\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<hr \/>\n<h2>3. Zod<\/h2>\n<h3>\uac1c\uc694<\/h3>\n<p>Zod\ub294 <strong>TypeScript-first \uc2a4\ud0a4\ub9c8 \uc120\uc5b8 &amp; \uc720\ud6a8\uc131 \uac80\uc0ac<\/strong> \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4. \uc2a4\ud0a4\ub9c8\ub97c \uc815\uc758\ud558\uba74 \ud0c0\uc785 \ucd94\ub860\uc774 \uc790\ub3d9\uc73c\ub85c \ub429\ub2c8\ub2e4.<\/p>\n<h3>\uc124\uce58<\/h3>\n<pre><code class=\"language-bash\">npm install zod<\/code><\/pre>\n<h3>\uc0ac\uc6a9\ubc95<\/h3>\n<p>\ud3fc\uc5d0 \uc0ac\uc6a9\ud560 \uc2a4\ud0a4\ub9c8\ub97c \ubcc4\ub3c4 \ud30c\uc77c\ub85c \ubd84\ub9ac\ud569\ub2c8\ub2e4.<\/p>\n<pre><code class=\"language-ts\">\/\/ lib\/schemas\/auth.ts - Step 3: Zod \uc2a4\ud0a4\ub9c8 \uc815\uc758\nimport { z } from &quot;zod&quot;;\n\nexport const signUpSchema = z\n  .object({\n    name: z\n      .string()\n      .min(2, &quot;\uc774\ub984\uc740 2\uc790 \uc774\uc0c1\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.&quot;)\n      .max(20, &quot;\uc774\ub984\uc740 20\uc790 \uc774\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4.&quot;),\n\n    email: z\n      .string()\n      .email(&quot;\uc62c\ubc14\ub978 \uc774\uba54\uc77c \ud615\uc2dd\uc774 \uc544\ub2d9\ub2c8\ub2e4.&quot;),\n\n    password: z\n      .string()\n      .min(8, &quot;\ube44\ubc00\ubc88\ud638\ub294 8\uc790 \uc774\uc0c1\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.&quot;)\n      .regex(\n        \/^(?=.*[a-zA-Z])(?=.*[0-9])\/,\n        &quot;\ube44\ubc00\ubc88\ud638\ub294 \uc601\ubb38\uacfc \uc22b\uc790\ub97c \ud3ec\ud568\ud574\uc57c \ud569\ub2c8\ub2e4.&quot;\n      ),\n\n    confirmPassword: z.string(),\n  })\n  .refine((data) =&gt; data.password === data.confirmPassword, {\n    message: &quot;\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.&quot;,\n    path: [&quot;confirmPassword&quot;],\n  });\n\n\/\/ \uc2a4\ud0a4\ub9c8\uc5d0\uc11c \ud0c0\uc785 \uc790\ub3d9 \ucd94\ub860\nexport type SignUpInput = z.infer&lt;typeof signUpSchema&gt;;<\/code><\/pre>\n<h3>Zod \ud575\uc2ec API<\/h3>\n<pre><code class=\"language-ts\">import { z } from &quot;zod&quot;;\n\n\/\/ \uae30\ubcf8 \ud0c0\uc785\nz.string()\nz.number()\nz.boolean()\nz.date()\nz.undefined()\nz.null()\n\n\/\/ \ubb38\uc790\uc5f4 \uac80\uc99d\nz.string().min(1).max(100).email().url().uuid().regex(\/pattern\/)\n\n\/\/ \uc22b\uc790 \uac80\uc99d\nz.number().min(0).max(100).int().positive().nonnegative()\n\n\/\/ \ubc30\uc5f4 &amp; \uac1d\uccb4\nz.array(z.string()).min(1)\nz.object({ key: z.string() })\n\n\/\/ \uc120\ud0dd &amp; \uae30\ubcf8\uac12\nz.string().optional()          \/\/ string | undefined\nz.string().nullable()          \/\/ string | null\nz.string().default(&quot;\uae30\ubcf8\uac12&quot;)\n\n\/\/ \ucee4\uc2a4\ud140 \uac80\uc99d\nz.string().refine((val) =&gt; val !== &quot;\uae08\uc9c0\uc5b4&quot;, {\n  message: &quot;\uae08\uc9c0\ub41c \ub2e8\uc5b4\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.&quot;,\n})\n\n\/\/ \ud0c0\uc785 \ucd94\ucd9c\ntype MyType = z.infer&lt;typeof mySchema&gt;<\/code><\/pre>\n<hr \/>\n<h2>4. React Hook Form + Zod<\/h2>\n<h3>\uac1c\uc694<\/h3>\n<p>React Hook Form(RHF)\uc740 <strong>\ube44\uc81c\uc5b4 \ucef4\ud3ec\ub10c\ud2b8 \ubc29\uc2dd<\/strong>\uc73c\ub85c \ud3fc \uc0c1\ud0dc\ub97c \uad00\ub9ac\ud569\ub2c8\ub2e4. \ub9ac\ub80c\ub354\ub9c1\uc744 \ucd5c\uc18c\ud654\ud574 \uc131\ub2a5\uc774 \ub6f0\uc5b4\ub0a9\ub2c8\ub2e4. Zod\uc640 \uacb0\ud569\ud558\uba74 \uc2a4\ud0a4\ub9c8 \uae30\ubc18 \uc720\ud6a8\uc131 \uac80\uc0ac\uac00 \uc644\uc131\ub429\ub2c8\ub2e4.<\/p>\n<h3>\uc124\uce58<\/h3>\n<pre><code class=\"language-bash\">npm install react-hook-form @hookform\/resolvers<\/code><\/pre>\n<blockquote>\n<p><code>@hookform\/resolvers<\/code>\ub294 RHF\uc640 Zod\ub97c \uc5f0\uacb0\ud574\uc8fc\ub294 \uc5b4\ub311\ud130\uc785\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<h3>form.tsx \uc218\ub3d9\uc0dd\uc131<\/h3>\n<p>form \uc774 \uc124\uce58\uac00 \ub418\uc9c0 \uc54a\uc544\uc11c \uc218\ub3d9\uc73c\ub85c \uc0dd\uc131\ud574 \uc90d\ub2c8\ub2e4.<\/p>\n<pre><code class=\"language-tsx\">\/\/ src\/components\/form.tsx\n&quot;use client&quot;\n\nimport * as React from &quot;react&quot;\nimport { Slot } from &quot;radix-ui&quot;\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  useFormContext,\n} from &quot;react-hook-form&quot;\n\nimport { cn } from &quot;@\/lib\/utils&quot;\nimport { Label } from &quot;@\/components\/ui\/label&quot;\n\nconst Form = FormProvider\n\ntype FormFieldContextValue&lt;\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath&lt;TFieldValues&gt; = FieldPath&lt;TFieldValues&gt;,\n&gt; = {\n  name: TName\n}\n\nconst FormFieldContext = React.createContext&lt;FormFieldContextValue&gt;(\n  {} as FormFieldContextValue\n)\n\nfunction FormField&lt;\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath&lt;TFieldValues&gt; = FieldPath&lt;TFieldValues&gt;,\n&gt;({ ...props }: ControllerProps&lt;TFieldValues, TName&gt;) {\n  return (\n    &lt;FormFieldContext.Provider value={{ name: props.name }}&gt;\n      &lt;Controller {...props} \/&gt;\n    &lt;\/FormFieldContext.Provider&gt;\n  )\n}\n\nconst useFormField = () =&gt; {\n  const fieldContext = React.useContext(FormFieldContext)\n  const itemContext = React.useContext(FormItemContext)\n  const { getFieldState, formState } = useFormContext()\n\n  const fieldState = getFieldState(fieldContext.name, formState)\n\n  if (!fieldContext) {\n    throw new Error(&quot;useFormField should be used within &lt;FormField&gt;&quot;)\n  }\n\n  const { id } = itemContext\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  }\n}\n\ntype FormItemContextValue = {\n  id: string\n}\n\nconst FormItemContext = React.createContext&lt;FormItemContextValue&gt;(\n  {} as FormItemContextValue\n)\n\nfunction FormItem({ className, ...props }: React.ComponentProps&lt;&quot;div&quot;&gt;) {\n  const id = React.useId()\n\n  return (\n    &lt;FormItemContext.Provider value={{ id }}&gt;\n      &lt;div\n        data-slot=&quot;form-item&quot;\n        className={cn(&quot;grid gap-2&quot;, className)}\n        {...props}\n      \/&gt;\n    &lt;\/FormItemContext.Provider&gt;\n  )\n}\n\nfunction FormLabel({\n  className,\n  ...props\n}: React.ComponentProps&lt;typeof Label&gt;) {\n  const { error, formItemId } = useFormField()\n\n  return (\n    &lt;Label\n      data-slot=&quot;form-label&quot;\n      data-error={!!error}\n      className={cn(&quot;data-[error=true]:text-destructive&quot;, className)}\n      htmlFor={formItemId}\n      {...props}\n    \/&gt;\n  )\n}\n\nfunction FormControl({ ...props }: React.ComponentProps&lt;typeof Slot.Root&gt;) {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n  return (\n    &lt;Slot.Root\n      data-slot=&quot;form-control&quot;\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    \/&gt;\n  )\n}\n\nfunction FormDescription({\n  className,\n  ...props\n}: React.ComponentProps&lt;&quot;p&quot;&gt;) {\n  const { formDescriptionId } = useFormField()\n\n  return (\n    &lt;p\n      data-slot=&quot;form-description&quot;\n      id={formDescriptionId}\n      className={cn(&quot;text-muted-foreground text-sm&quot;, className)}\n      {...props}\n    \/&gt;\n  )\n}\n\nfunction FormMessage({ className, children, ...props }: React.ComponentProps&lt;&quot;p&quot;&gt;) {\n  const { error, formMessageId } = useFormField()\n  const body = error ? String(error?.message ?? &quot;&quot;) : children\n\n  if (!body) {\n    return null\n  }\n\n  return (\n    &lt;p\n      data-slot=&quot;form-message&quot;\n      id={formMessageId}\n      className={cn(&quot;text-destructive text-sm&quot;, className)}\n      {...props}\n    &gt;\n      {body}\n    &lt;\/p&gt;\n  )\n}\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormMessage,\n}<\/code><\/pre>\n<h3>\uc0ac\uc6a9\ubc95<\/h3>\n<pre><code class=\"language-tsx\">\/\/ app\/page.tsx - Step 4: React Hook Form + Zod \uc5f0\uacb0\n&quot;use client&quot;;\n\nimport { useForm } from &quot;react-hook-form&quot;;\nimport { zodResolver } from &quot;@hookform\/resolvers\/zod&quot;;\nimport { Button } from &quot;@\/components\/ui\/button&quot;;\nimport { Input } from &quot;@\/components\/ui\/input&quot;;\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from &quot;@\/components\/ui\/form&quot;;\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from &quot;@\/components\/ui\/card&quot;;\nimport { signUpSchema, type SignUpInput } from &quot;@\/lib\/schemas\/auth&quot;;\n\nexport default function Page() {\n  const form = useForm&lt;SignUpInput&gt;({\n    resolver: zodResolver(signUpSchema),\n    defaultValues: {\n      name: &quot;&quot;,\n      email: &quot;&quot;,\n      password: &quot;&quot;,\n      confirmPassword: &quot;&quot;,\n    },\n  });\n\n  const onSubmit = (data: SignUpInput) =&gt; {\n    \/\/ \uc720\ud6a8\uc131 \uac80\uc0ac \ud1b5\uacfc \ud6c4 \uc2e4\ud589\n    console.log(&quot;\ud3fc \ub370\uc774\ud130:&quot;, data);\n  };\n\n  return (\n    &lt;main className=&quot;min-h-screen bg-gray-50 flex items-center justify-center p-4&quot;&gt;\n      &lt;Card className=&quot;w-full max-w-md&quot;&gt;\n        &lt;CardHeader&gt;\n          &lt;CardTitle&gt;\ud68c\uc6d0\uac00\uc785&lt;\/CardTitle&gt;\n          &lt;CardDescription&gt;\n            \uc544\ub798 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uacc4\uc815\uc744 \ub9cc\ub4dc\uc138\uc694.\n          &lt;\/CardDescription&gt;\n        &lt;\/CardHeader&gt;\n        &lt;CardContent&gt;\n          {\/* shadcn Form\uc740 RHF FormProvider\ub97c \ub0b4\ubd80\uc801\uc73c\ub85c \uc0ac\uc6a9 *\/}\n          &lt;Form {...form}&gt;\n            &lt;form onSubmit={form.handleSubmit(onSubmit)} className=&quot;space-y-4&quot;&gt;\n\n              &lt;FormField\n                control={form.control}\n                name=&quot;name&quot;\n                render={({ field }) =&gt; (\n                  &lt;FormItem&gt;\n                    &lt;FormLabel&gt;\uc774\ub984&lt;\/FormLabel&gt;\n                    &lt;FormControl&gt;\n                      &lt;Input placeholder=&quot;\ud64d\uae38\ub3d9&quot; {...field} \/&gt;\n                    &lt;\/FormControl&gt;\n                    &lt;FormMessage \/&gt; {\/* \uc5d0\ub7ec \uba54\uc2dc\uc9c0 \uc790\ub3d9 \ud45c\uc2dc *\/}\n                  &lt;\/FormItem&gt;\n                )}\n              \/&gt;\n\n              &lt;FormField\n                control={form.control}\n                name=&quot;email&quot;\n                render={({ field }) =&gt; (\n                  &lt;FormItem&gt;\n                    &lt;FormLabel&gt;\uc774\uba54\uc77c&lt;\/FormLabel&gt;\n                    &lt;FormControl&gt;\n                      &lt;Input type=&quot;email&quot; placeholder=&quot;hong@example.com&quot; {...field} \/&gt;\n                    &lt;\/FormControl&gt;\n                    &lt;FormMessage \/&gt;\n                  &lt;\/FormItem&gt;\n                )}\n              \/&gt;\n\n              &lt;FormField\n                control={form.control}\n                name=&quot;password&quot;\n                render={({ field }) =&gt; (\n                  &lt;FormItem&gt;\n                    &lt;FormLabel&gt;\ube44\ubc00\ubc88\ud638&lt;\/FormLabel&gt;\n                    &lt;FormControl&gt;\n                      &lt;Input type=&quot;password&quot; placeholder=&quot;\ucd5c\uc18c 8\uc790 \uc774\uc0c1&quot; {...field} \/&gt;\n                    &lt;\/FormControl&gt;\n                    &lt;FormMessage \/&gt;\n                  &lt;\/FormItem&gt;\n                )}\n              \/&gt;\n\n              &lt;FormField\n                control={form.control}\n                name=&quot;confirmPassword&quot;\n                render={({ field }) =&gt; (\n                  &lt;FormItem&gt;\n                    &lt;FormLabel&gt;\ube44\ubc00\ubc88\ud638 \ud655\uc778&lt;\/FormLabel&gt;\n                    &lt;FormControl&gt;\n                      &lt;Input type=&quot;password&quot; placeholder=&quot;\ube44\ubc00\ubc88\ud638 \uc7ac\uc785\ub825&quot; {...field} \/&gt;\n                    &lt;\/FormControl&gt;\n                    &lt;FormMessage \/&gt;\n                  &lt;\/FormItem&gt;\n                )}\n              \/&gt;\n\n              &lt;Button\n                type=&quot;submit&quot;\n                className=&quot;w-full&quot;\n                disabled={form.formState.isSubmitting}\n              &gt;\n                {form.formState.isSubmitting ? &quot;\ucc98\ub9ac \uc911...&quot; : &quot;\uac00\uc785\ud558\uae30&quot;}\n              &lt;\/Button&gt;\n\n            &lt;\/form&gt;\n          &lt;\/Form&gt;\n        &lt;\/CardContent&gt;\n      &lt;\/Card&gt;\n    &lt;\/main&gt;\n  );\n}<\/code><\/pre>\n<h3>RHF \ud575\uc2ec API<\/h3>\n<pre><code class=\"language-ts\">const form = useForm&lt;T&gt;({\n  resolver: zodResolver(schema),  \/\/ Zod \uc5f0\uacb0\n  defaultValues: { ... },         \/\/ \ucd08\uae30\uac12\n  mode: &quot;onChange&quot;,               \/\/ \uac80\uc99d \uc2dc\uc810: onChange | onBlur | onSubmit\n});\n\n\/\/ \ud3fc \uc0c1\ud0dc\nform.formState.isSubmitting   \/\/ \uc81c\ucd9c \uc911 \uc5ec\ubd80\nform.formState.errors         \/\/ \uc5d0\ub7ec \uac1d\uccb4\nform.formState.isDirty        \/\/ \uac12\uc774 \ubcc0\uacbd\ub410\ub294\uc9c0\nform.formState.isValid        \/\/ \uc720\ud6a8\uc131 \ud1b5\uacfc \uc5ec\ubd80\n\n\/\/ \uc720\ud2f8\nform.reset()                  \/\/ \ud3fc \ucd08\uae30\ud654\nform.setValue(&quot;field&quot;, value) \/\/ \uac12 \uc124\uc815\nform.getValues()              \/\/ \ud604\uc7ac \uac12 \uc77d\uae30\nform.watch(&quot;field&quot;)           \/\/ \uc2e4\uc2dc\uac04 \uac12 \uad6c\ub3c5\nform.setError(&quot;field&quot;, { message: &quot;...&quot; }) \/\/ \uc218\ub3d9 \uc5d0\ub7ec \uc124\uc815<\/code><\/pre>\n<h2>5. TanStack Query<\/h2>\n<h3>\uac1c\uc694<\/h3>\n<p>TanStack Query(\uad6c React Query)\ub294 <strong>\uc11c\ubc84 \uc0c1\ud0dc(server state) \uad00\ub9ac<\/strong> \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4. \ub370\uc774\ud130 \ud328\uce6d, \uce90\uc2f1, \ub3d9\uae30\ud654, \ubc30\uacbd \uac31\uc2e0\uc744 \uc790\ub3d9\uc73c\ub85c \ucc98\ub9ac\ud569\ub2c8\ub2e4.<\/p>\n<h3>\uc124\uce58<\/h3>\n<pre><code class=\"language-bash\">npm install @tanstack\/react-query @tanstack\/react-query-devtools<\/code><\/pre>\n<h3>Provider \uc124\uc815<\/h3>\n<pre><code class=\"language-tsx\">\/\/ app\/providers.tsx - QueryClient Provider \uc124\uc815\n&quot;use client&quot;;\n\nimport { QueryClient, QueryClientProvider } from &quot;@tanstack\/react-query&quot;;\nimport { ReactQueryDevtools } from &quot;@tanstack\/react-query-devtools&quot;;\nimport { useState } from &quot;react&quot;;\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(\n    () =&gt;\n      new QueryClient({\n        defaultOptions: {\n          queries: {\n            staleTime: 60 * 1000,   \/\/ 1\ubd84 \ub3d9\uc548 fresh \uc0c1\ud0dc \uc720\uc9c0\n            retry: 1,               \/\/ \uc2e4\ud328 \uc2dc 1\ubc88 \uc7ac\uc2dc\ub3c4\n          },\n        },\n      })\n  );\n\n  return (\n    &lt;QueryClientProvider client={queryClient}&gt;\n      {children}\n      &lt;ReactQueryDevtools initialIsOpen={false} \/&gt;\n    &lt;\/QueryClientProvider&gt;\n  );\n}<\/code><\/pre>\n<pre><code class=\"language-tsx\">\/\/ app\/layout.tsx - Provider \uc801\uc6a9\nimport { Providers } from &quot;.\/providers&quot;;\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    &lt;html lang=&quot;ko&quot;&gt;\n      &lt;body&gt;\n        &lt;Providers&gt;{children}&lt;\/Providers&gt;\n      &lt;\/body&gt;\n    &lt;\/html&gt;\n  );\n}<\/code><\/pre>\n<h3>API \ud568\uc218 \ubd84\ub9ac<\/h3>\n<pre><code class=\"language-ts\">\/\/ lib\/api\/auth.ts - API \ud568\uc218 \uc815\uc758\nimport { SignUpInput } from &quot;@\/lib\/schemas\/auth&quot;;\n\nexport interface User {\n  id: string;\n  name: string;\n  email: string;\n  createdAt: string;\n}\n\n\/\/ \uc0ac\uc6a9\uc790 \ubaa9\ub85d \uc870\ud68c\nexport async function fetchUsers(): Promise&lt;User[]&gt; {\n  const res = await fetch(&quot;\/api\/users&quot;);\n  if (!res.ok) throw new Error(&quot;\uc0ac\uc6a9\uc790 \ubaa9\ub85d\uc744 \ubd88\ub7ec\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.&quot;);\n  return res.json();\n}\n\n\/\/ \ud68c\uc6d0\uac00\uc785 API\nexport async function signUp(data: Omit&lt;SignUpInput, &quot;confirmPassword&quot;&gt;): Promise&lt;User&gt; {\n  const res = await fetch(&quot;\/api\/auth\/signup&quot;, {\n    method: &quot;POST&quot;,\n    headers: { &quot;Content-Type&quot;: &quot;application\/json&quot; },\n    body: JSON.stringify(data),\n  });\n  if (!res.ok) {\n    const error = await res.json();\n    throw new Error(error.message || &quot;\ud68c\uc6d0\uac00\uc785\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4.&quot;);\n  }\n  return res.json();\n}<\/code><\/pre>\n<h3>useQuery &amp; useMutation \uc801\uc6a9<\/h3>\n<pre><code class=\"language-tsx\">\/\/ app\/page.tsx - Step 5: TanStack Query \uc5f0\uacb0\n&quot;use client&quot;;\n\nimport { useQuery, useMutation, useQueryClient } from &quot;@tanstack\/react-query&quot;;\nimport { useForm } from &quot;react-hook-form&quot;;\nimport { zodResolver } from &quot;@hookform\/resolvers\/zod&quot;;\nimport { Button } from &quot;@\/components\/ui\/button&quot;;\nimport { Input } from &quot;@\/components\/ui\/input&quot;;\nimport {\n  Form, FormControl, FormField, FormItem, FormLabel, FormMessage,\n} from &quot;@\/components\/ui\/form&quot;;\nimport {\n  Card, CardContent, CardDescription, CardHeader, CardTitle,\n} from &quot;@\/components\/ui\/card&quot;;\nimport { signUpSchema, type SignUpInput } from &quot;@\/lib\/schemas\/auth&quot;;\nimport { fetchUsers, signUp } from &quot;@\/lib\/api\/auth&quot;;\n\nexport default function Page() {\n  const queryClient = useQueryClient();\n\n  \/\/ \u2705 \ub370\uc774\ud130 \uc870\ud68c - \uc790\ub3d9 \uce90\uc2f1, \ubc31\uadf8\ub77c\uc6b4\ub4dc \uac31\uc2e0\n  const { data: users, isLoading, isError } = useQuery({\n    queryKey: [&quot;users&quot;],        \/\/ \uce90\uc2dc \ud0a4 (\ubc30\uc5f4)\n    queryFn: fetchUsers,        \/\/ \ub370\uc774\ud130 \ud328\uce6d \ud568\uc218\n    staleTime: 5 * 60 * 1000,  \/\/ 5\ubd84 \ub3d9\uc548 fresh \uc0c1\ud0dc\n  });\n\n  \/\/ \u2705 \ub370\uc774\ud130 \ubcc0\uacbd - \uc131\uacf5\/\uc2e4\ud328 \ud578\ub4e4\ub9c1\n  const signUpMutation = useMutation({\n    mutationFn: signUp,\n    onSuccess: (newUser) =&gt; {\n      \/\/ \uce90\uc2dc \ubb34\ud6a8\ud654 \u2192 users \ucffc\ub9ac \uc790\ub3d9 \uc7ac\ud328\uce6d\n      queryClient.invalidateQueries({ queryKey: [&quot;users&quot;] });\n      alert(`${newUser.name}\ub2d8, \ud658\uc601\ud569\ub2c8\ub2e4!`);\n      form.reset();\n    },\n    onError: (error: Error) =&gt; {\n      form.setError(&quot;root&quot;, { message: error.message });\n    },\n  });\n\n  const form = useForm&lt;SignUpInput&gt;({\n    resolver: zodResolver(signUpSchema),\n    defaultValues: { name: &quot;&quot;, email: &quot;&quot;, password: &quot;&quot;, confirmPassword: &quot;&quot; },\n  });\n\n  const onSubmit = (data: SignUpInput) =&gt; {\n    const { confirmPassword, ...payload } = data;\n    signUpMutation.mutate(payload);\n  };\n\n  return (\n    &lt;main className=&quot;min-h-screen bg-gray-50 flex items-center justify-center p-4&quot;&gt;\n      &lt;div className=&quot;w-full max-w-md space-y-6&quot;&gt;\n\n        {\/* \uc0ac\uc6a9\uc790 \ubaa9\ub85d *\/}\n        &lt;Card&gt;\n          &lt;CardHeader&gt;\n            &lt;CardTitle&gt;\uac00\uc785\ud55c \uc0ac\uc6a9\uc790&lt;\/CardTitle&gt;\n          &lt;\/CardHeader&gt;\n          &lt;CardContent&gt;\n            {isLoading &amp;&amp; &lt;p className=&quot;text-sm text-gray-500&quot;&gt;\ubd88\ub7ec\uc624\ub294 \uc911...&lt;\/p&gt;}\n            {isError &amp;&amp; &lt;p className=&quot;text-sm text-red-500&quot;&gt;\ubd88\ub7ec\uc624\uae30 \uc2e4\ud328&lt;\/p&gt;}\n            {users?.map((user) =&gt; (\n              &lt;div key={user.id} className=&quot;text-sm py-1 border-b last:border-0&quot;&gt;\n                &lt;span className=&quot;font-medium&quot;&gt;{user.name}&lt;\/span&gt;\n                &lt;span className=&quot;text-gray-500 ml-2&quot;&gt;{user.email}&lt;\/span&gt;\n              &lt;\/div&gt;\n            ))}\n          &lt;\/CardContent&gt;\n        &lt;\/Card&gt;\n\n        {\/* \ud68c\uc6d0\uac00\uc785 \ud3fc *\/}\n        &lt;Card&gt;\n          &lt;CardHeader&gt;\n            &lt;CardTitle&gt;\ud68c\uc6d0\uac00\uc785&lt;\/CardTitle&gt;\n            &lt;CardDescription&gt;\uc544\ub798 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uacc4\uc815\uc744 \ub9cc\ub4dc\uc138\uc694.&lt;\/CardDescription&gt;\n          &lt;\/CardHeader&gt;\n          &lt;CardContent&gt;\n            &lt;Form {...form}&gt;\n              &lt;form onSubmit={form.handleSubmit(onSubmit)} className=&quot;space-y-4&quot;&gt;\n                {form.formState.errors.root &amp;&amp; (\n                  &lt;p className=&quot;text-sm text-red-500&quot;&gt;\n                    {form.formState.errors.root.message}\n                  &lt;\/p&gt;\n                )}\n\n                &lt;FormField control={form.control} name=&quot;name&quot;\n                  render={({ field }) =&gt; (\n                    &lt;FormItem&gt;\n                      &lt;FormLabel&gt;\uc774\ub984&lt;\/FormLabel&gt;\n                      &lt;FormControl&gt;&lt;Input placeholder=&quot;\ud64d\uae38\ub3d9&quot; {...field} \/&gt;&lt;\/FormControl&gt;\n                      &lt;FormMessage \/&gt;\n                    &lt;\/FormItem&gt;\n                  )}\n                \/&gt;\n                &lt;FormField control={form.control} name=&quot;email&quot;\n                  render={({ field }) =&gt; (\n                    &lt;FormItem&gt;\n                      &lt;FormLabel&gt;\uc774\uba54\uc77c&lt;\/FormLabel&gt;\n                      &lt;FormControl&gt;&lt;Input type=&quot;email&quot; placeholder=&quot;hong@example.com&quot; {...field} \/&gt;&lt;\/FormControl&gt;\n                      &lt;FormMessage \/&gt;\n                    &lt;\/FormItem&gt;\n                  )}\n                \/&gt;\n                &lt;FormField control={form.control} name=&quot;password&quot;\n                  render={({ field }) =&gt; (\n                    &lt;FormItem&gt;\n                      &lt;FormLabel&gt;\ube44\ubc00\ubc88\ud638&lt;\/FormLabel&gt;\n                      &lt;FormControl&gt;&lt;Input type=&quot;password&quot; {...field} \/&gt;&lt;\/FormControl&gt;\n                      &lt;FormMessage \/&gt;\n                    &lt;\/FormItem&gt;\n                  )}\n                \/&gt;\n                &lt;FormField control={form.control} name=&quot;confirmPassword&quot;\n                  render={({ field }) =&gt; (\n                    &lt;FormItem&gt;\n                      &lt;FormLabel&gt;\ube44\ubc00\ubc88\ud638 \ud655\uc778&lt;\/FormLabel&gt;\n                      &lt;FormControl&gt;&lt;Input type=&quot;password&quot; {...field} \/&gt;&lt;\/FormControl&gt;\n                      &lt;FormMessage \/&gt;\n                    &lt;\/FormItem&gt;\n                  )}\n                \/&gt;\n\n                &lt;Button\n                  type=&quot;submit&quot;\n                  className=&quot;w-full&quot;\n                  disabled={signUpMutation.isPending}\n                &gt;\n                  {signUpMutation.isPending ? &quot;\ucc98\ub9ac \uc911...&quot; : &quot;\uac00\uc785\ud558\uae30&quot;}\n                &lt;\/Button&gt;\n              &lt;\/form&gt;\n            &lt;\/Form&gt;\n          &lt;\/CardContent&gt;\n        &lt;\/Card&gt;\n      &lt;\/div&gt;\n    &lt;\/main&gt;\n  );\n}<\/code><\/pre>\n<p>\uac80\uc0c9\uc2dc \ud30c\ub77c\ubbf8\ud130\ub97c \uc804\ub2ec\ud558\uace0\uc790 \ud55c\ub2e4\uba74<\/p>\n<pre><code class=\"language-tsx\">\/\/ src\/lib\/api\/auth.ts\nexport interface FetchUsersParams {\n  search?: string;\n  limit?: number;\n  page?: number;\n}\n\nexport async function fetchUsers(params?: FetchUsersParams): Promise&lt;User[]&gt; {\n  const query = new URLSearchParams();\n  if (params?.search) query.set(&quot;search&quot;, params.search);\n  if (params?.limit)  query.set(&quot;limit&quot;,  String(params.limit));\n  if (params?.page)   query.set(&quot;page&quot;,   String(params.page));\n\n  const res = await fetch(`\/api\/users?${query}`);\n  if (!res.ok) throw new Error(&quot;\uc0ac\uc6a9\uc790 \ubaa9\ub85d\uc744 \ubd88\ub7ec\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.&quot;);\n  return res.json();\n}<\/code><\/pre>\n<pre><code class=\"language-tsx\">\/\/ page.tsx\nconst [params, setParams] = useState&lt;FetchUsersParams&gt;({\n  page: 1,\n  limit: 20\n});\n\nconst { data: users } = useQuery({\n  queryKey: [&quot;users&quot;, params],   \/\/ params \uac1d\uccb4 \uc804\uccb4\ub97c key\ub85c\n  queryFn: () =&gt; fetchUsers(params),\n});<\/code><\/pre>\n<h3>TanStack Query \ud575\uc2ec API<\/h3>\n<pre><code class=\"language-ts\">\/\/ useQuery - \ub370\uc774\ud130 \uc870\ud68c\nconst { data, isLoading, isError, error, refetch, isFetching } = useQuery({\n  queryKey: [&quot;users&quot;, userId],  \/\/ \ud0a4\uac00 \ubc14\ub00c\uba74 \uc790\ub3d9 \uc7ac\ud328\uce6d\n  queryFn: () =&gt; fetchUser(userId),\n  enabled: !!userId,            \/\/ \uc870\uac74\ubd80 \uc2e4\ud589\n  staleTime: 1000 * 60,         \/\/ \uce90\uc2dc \uc720\ud6a8 \uc2dc\uac04 (ms)\n  gcTime: 1000 * 60 * 5,        \/\/ \uac00\ube44\uc9c0 \uceec\ub809\uc158 \uc2dc\uac04 (\uad6c cacheTime)\n  select: (data) =&gt; data.users, \/\/ \ub370\uc774\ud130 \ubcc0\ud658\n});\n\n\/\/ useMutation - \ub370\uc774\ud130 \ubcc0\uacbd\nconst mutation = useMutation({\n  mutationFn: createPost,\n  onMutate: (variables) =&gt; {},   \/\/ \uc694\uccad \uc804 (\ub099\uad00\uc801 \uc5c5\ub370\uc774\ud2b8\uc5d0 \ud65c\uc6a9)\n  onSuccess: (data, variables) =&gt; {},\n  onError: (error, variables, context) =&gt; {},\n  onSettled: () =&gt; {},           \/\/ \uc131\uacf5\/\uc2e4\ud328 \uc0c1\uad00\uc5c6\uc774 \ud56d\uc0c1 \uc2e4\ud589\n});\n\nmutation.mutate(payload);\nmutation.mutateAsync(payload);  \/\/ Promise \ubc18\ud658\n\n\/\/ \uce90\uc2dc \uc870\uc791\nqueryClient.invalidateQueries({ queryKey: [&quot;users&quot;] })  \/\/ \ubb34\ud6a8\ud654 \u2192 \uc7ac\ud328\uce6d\nqueryClient.setQueryData([&quot;users&quot;], newData)             \/\/ \uc9c1\uc811 \uce90\uc2dc \uc5c5\ub370\uc774\ud2b8\nqueryClient.prefetchQuery({ queryKey, queryFn })         \/\/ \ubbf8\ub9ac \ud328\uce6d<\/code><\/pre>\n<h2>6. Zustand<\/h2>\n<h3>\uac1c\uc694<\/h3>\n<p>Zustand\ub294 <strong>\ud074\ub77c\uc774\uc5b8\ud2b8 \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac<\/strong> \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4. Redux\ubcf4\ub2e4 \ud6e8\uc52c \uac04\ub2e8\ud558\uace0, Context API\ubcf4\ub2e4 \uc131\ub2a5\uc774 \uc88b\uc2b5\ub2c8\ub2e4. TanStack Query\uac00 \uc11c\ubc84 \uc0c1\ud0dc\ub97c \ub2f4\ub2f9\ud55c\ub2e4\uba74, Zustand\ub294 UI \uc0c1\ud0dc(\ubaa8\ub2ec, \ud14c\ub9c8, \uc0ac\uc774\ub4dc\ubc14 \ub4f1)\ub97c \ub2f4\ub2f9\ud569\ub2c8\ub2e4.<\/p>\n<h3>\uc124\uce58<\/h3>\n<pre><code class=\"language-bash\">npm install zustand<\/code><\/pre>\n<h3>\uc2a4\ud1a0\uc5b4 \uc815\uc758<\/h3>\n<pre><code class=\"language-ts\">\/\/ store\/useAuthStore.ts - Step 6: Zustand \uc2a4\ud1a0\uc5b4\nimport { create } from &quot;zustand&quot;;\nimport { persist, devtools } from &quot;zustand\/middleware&quot;;\n\ninterface AuthUser {\n  id: string;\n  name: string;\n  email: string;\n}\n\ninterface AuthStore {\n  \/\/ \uc0c1\ud0dc\n  user: AuthUser | null;\n  isAuthenticated: boolean;\n\n  \/\/ \uc561\uc158\n  setUser: (user: AuthUser) =&gt; void;\n  logout: () =&gt; void;\n}\n\nexport const useAuthStore = create&lt;AuthStore&gt;()(\n  devtools(            \/\/ Redux DevTools \uc5f0\ub3d9\n    persist(           \/\/ localStorage\uc5d0 \uc790\ub3d9 \uc800\uc7a5\n      (set) =&gt; ({\n        user: null,\n        isAuthenticated: false,\n\n        setUser: (user) =&gt;\n          set({ user, isAuthenticated: true }, false, &quot;setUser&quot;),\n\n        logout: () =&gt;\n          set({ user: null, isAuthenticated: false }, false, &quot;logout&quot;),\n      }),\n      {\n        name: &quot;auth-storage&quot;,   \/\/ localStorage \ud0a4\n        partialize: (state) =&gt; ({ user: state.user }), \/\/ \uc800\uc7a5\ud560 \uc0c1\ud0dc \uc120\ud0dd\n      }\n    )\n  )\n);<\/code><\/pre>\n<pre><code class=\"language-ts\">\/\/ store\/useUIStore.ts - UI \uc0c1\ud0dc \uc2a4\ud1a0\uc5b4 \uc608\uc2dc\nimport { create } from &quot;zustand&quot;;\n\ninterface UIStore {\n  isSignUpModalOpen: boolean;\n  theme: &quot;light&quot; | &quot;dark&quot;;\n  openSignUpModal: () =&gt; void;\n  closeSignUpModal: () =&gt; void;\n  toggleTheme: () =&gt; void;\n}\n\nexport const useUIStore = create&lt;UIStore&gt;((set) =&gt; ({\n  isSignUpModalOpen: false,\n  theme: &quot;light&quot;,\n\n  openSignUpModal: () =&gt; set({ isSignUpModalOpen: true }),\n  closeSignUpModal: () =&gt; set({ isSignUpModalOpen: false }),\n  toggleTheme: () =&gt;\n    set((state) =&gt; ({ theme: state.theme === &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot; })),\n}));<\/code><\/pre>\n<h3>Zustand \ud575\uc2ec \ud328\ud134<\/h3>\n<pre><code class=\"language-ts\">\/\/ \u2705 \ud544\uc694\ud55c \uc0c1\ud0dc\/\uc561\uc158\ub9cc \uad6c\ub3c5 (\uc131\ub2a5 \ucd5c\uc801\ud654)\nconst user = useAuthStore((state) =&gt; state.user);\nconst setUser = useAuthStore((state) =&gt; state.setUser);\n\n\/\/ \u2705 \uc5ec\ub7ec \uac12 \ub3d9\uc2dc \uad6c\ub3c5\nconst { user, isAuthenticated } = useAuthStore(\n  (state) =&gt; ({ user: state.user, isAuthenticated: state.isAuthenticated })\n);\n\n\/\/ \u2705 \uc2a4\ud1a0\uc5b4 \uc678\ubd80\uc5d0\uc11c \uc9c1\uc811 \uc811\uadfc (React \ucef4\ud3ec\ub10c\ud2b8 \ubc16)\nconst { logout } = useAuthStore.getState();\n\n\/\/ \u2705 \ubbf8\ub4e4\uc6e8\uc5b4 \uc870\ud569\nimport { create } from &quot;zustand&quot;;\nimport { persist, devtools, immer } from &quot;zustand\/middleware&quot;;\n\nconst useStore = create&lt;State&gt;()(\n  devtools(\n    persist(\n      immer((set) =&gt; ({\n        \/\/ immer\ub97c \uc4f0\uba74 \ubd88\ubcc0\uc131 \uc5c6\uc774 \uc9c1\uc811 \uc218\uc815 \uac00\ub2a5\n        users: [],\n        addUser: (user) =&gt;\n          set((state) =&gt; {\n            state.users.push(user); \/\/ \uc9c1\uc811 push \uac00\ub2a5\n          }),\n      })),\n      { name: &quot;my-storage&quot; }\n    )\n  )\n);<\/code><\/pre>\n<h2>7. \ucd5c\uc885 \uc644\uc131 \ucf54\ub4dc<\/h2>\n<p>\ubaa8\ub4e0 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ud1b5\ud569\ud55c \ucd5c\uc885 \ucf54\ub4dc\uc785\ub2c8\ub2e4.<\/p>\n<h3>\ud30c\uc77c \uad6c\uc870<\/h3>\n<pre><code>my-app\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 app\/\n\u2502   \u2502   \u251c\u2500\u2500 globals.css         \u2190 Tailwind v4 + shadcn \ud14c\ub9c8\n\u2502   \u2502   \u251c\u2500\u2500 layout.tsx          \u2190 QueryClient Provider\n\u2502   \u2502   \u251c\u2500\u2500 page.tsx            \u2190 \uba54\uc778 \ud398\uc774\uc9c0\n\u2502   \u2502   \u2514\u2500\u2500 providers.tsx       \u2190 \uc804\uc5ed Provider \ubaa8\uc74c\n\u2502   \u251c\u2500\u2500 components\/\n\u2502   \u2502   \u2514\u2500\u2500 ui\/                 \u2190 shadcn \ucef4\ud3ec\ub10c\ud2b8\n\u2502   \u251c\u2500\u2500 lib\/\n\u2502   \u2502   \u251c\u2500\u2500 api\/\n\u2502   \u2502   \u2502   \u2514\u2500\u2500 auth.ts         \u2190 API \ud568\uc218\n\u2502   \u2502   \u2514\u2500\u2500 schemas\/\n\u2502   \u2502       \u2514\u2500\u2500 auth.ts         \u2190 Zod \uc2a4\ud0a4\ub9c8\n\u2502   \u2514\u2500\u2500 store\/\n\u2502       \u251c\u2500\u2500 useAuthStore.ts     \u2190 Zustand \uc778\uc99d \uc2a4\ud1a0\uc5b4\n\u2502       \u2514\u2500\u2500 useUIStore.ts       \u2190 Zustand UI \uc2a4\ud1a0\uc5b4\n\u251c\u2500\u2500 postcss.config.mjs          \u2190 @tailwindcss\/postcss \uc124\uc815\n\u2514\u2500\u2500 (tailwind.config.ts \uc5c6\uc74c)   \u2190 v4\uc5d0\uc11c\ub294 \ubd88\ud544\uc694<\/code><\/pre>\n<h3><code>src\/app\/globals.css<\/code> \u2014 \uc644\uc131\ubcf8<\/h3>\n<pre><code class=\"language-css\">@import &quot;tailwindcss&quot;;\n@import &quot;tw-animate-css&quot;;\n\n@custom-variant dark (&amp;:is(.dark *));\n\n:root {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --radius: 0.625rem;\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.985 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.396 0.141 25.723);\n  --border: oklch(1 0 0 \/ 10%);\n  --input: oklch(1 0 0 \/ 15%);\n  --ring: oklch(0.556 0 0);\n}\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n}<\/code><\/pre>\n<h3><code>src\/app\/providers.tsx<\/code><\/h3>\n<pre><code class=\"language-tsx\">&quot;use client&quot;;\n\nimport { QueryClient, QueryClientProvider } from &quot;@tanstack\/react-query&quot;;\nimport { ReactQueryDevtools } from &quot;@tanstack\/react-query-devtools&quot;;\nimport { useState } from &quot;react&quot;;\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(() =&gt; new QueryClient({\n    defaultOptions: { queries: { staleTime: 60 * 1000, retry: 1 } },\n  }));\n\n  return (\n    &lt;QueryClientProvider client={queryClient}&gt;\n      {children}\n      &lt;ReactQueryDevtools initialIsOpen={false} \/&gt;\n    &lt;\/QueryClientProvider&gt;\n  );\n}<\/code><\/pre>\n<h3><code>src\/app\/page.tsx<\/code> \u2014 \ucd5c\uc885 \uc644\uc131<\/h3>\n<pre><code class=\"language-tsx\">\/\/ src\/app\/api\/auth\/routes.tsx\nimport { NextRequest, NextResponse } from &quot;next\/server&quot;;\nimport { users } from &quot;@\/lib\/store&quot;;\n\nexport async function POST(req: NextRequest) {\n  const { name, email, password } = await req.json();\n\n  if (users.find((u) =&gt; u.email === email)) {\n    return NextResponse.json({ message: &quot;\uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc778 \uc774\uba54\uc77c\uc785\ub2c8\ub2e4.&quot; }, { status: 409 });\n  }\n\n  const newUser = {\n    id: crypto.randomUUID(),\n    name,\n    email,\n    createdAt: new Date().toISOString(),\n  };\n  users.push(newUser);\n\n  return NextResponse.json(newUser, { status: 201 });\n}\n<\/code><\/pre>\n<pre><code class=\"language-tsx\">\/\/ src\/app\/users\/routes.tsx\nimport { NextResponse } from &quot;next\/server&quot;;\nimport { users } from &quot;@\/lib\/store&quot;;\n\nexport async function GET() {\n  return NextResponse.json(users);\n}\n<\/code><\/pre>\n<pre><code class=\"language-tsx\">\/\/ src\/lib\/store.tsx\n\/\/ \uc11c\ubc84 \uba54\ubaa8\ub9ac \uc784\uc2dc \uc800\uc7a5\uc18c (\uac1c\ubc1c\uc6a9)\nexport const users: { id: string; name: string; email: string; createdAt: string }[] = [];<\/code><\/pre>\n<pre><code class=\"language-tsx\">&quot;use client&quot;;\n\nimport { useQuery, useMutation, useQueryClient } from &quot;@tanstack\/react-query&quot;;\nimport { useForm } from &quot;react-hook-form&quot;;\nimport { zodResolver } from &quot;@hookform\/resolvers\/zod&quot;;\nimport { Button } from &quot;@\/components\/ui\/button&quot;;\nimport { Input } from &quot;@\/components\/ui\/input&quot;;\nimport { Badge } from &quot;@\/components\/ui\/badge&quot;;\nimport {\n  Form, FormControl, FormField, FormItem, FormLabel, FormMessage,\n} from &quot;@\/components\/ui\/form&quot;;\nimport {\n  Card, CardContent, CardDescription, CardHeader, CardTitle,\n} from &quot;@\/components\/ui\/card&quot;;\nimport {\n  Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,\n} from &quot;@\/components\/ui\/dialog&quot;;\nimport { signUpSchema, type SignUpInput } from &quot;@\/lib\/schemas\/auth&quot;;\nimport { fetchUsers, signUp } from &quot;@\/lib\/api\/auth&quot;;\nimport { useAuthStore } from &quot;@\/store\/useAuthStore&quot;;\nimport { useUIStore } from &quot;@\/store\/useUIStore&quot;;\n\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ \ud68c\uc6d0\uac00\uc785 \ud3fc \ucef4\ud3ec\ub10c\ud2b8\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction SignUpForm({ onSuccess }: { onSuccess: () =&gt; void }) {\n  const queryClient = useQueryClient();\n  const setUser = useAuthStore((state) =&gt; state.setUser);\n\n  const form = useForm&lt;SignUpInput&gt;({\n    resolver: zodResolver(signUpSchema),\n    defaultValues: { name: &quot;&quot;, email: &quot;&quot;, password: &quot;&quot;, confirmPassword: &quot;&quot; },\n  });\n\n  const signUpMutation = useMutation({\n    mutationFn: signUp,\n    onSuccess: (newUser) =&gt; {\n      queryClient.invalidateQueries({ queryKey: [&quot;users&quot;] });\n      setUser(newUser);  \/\/ Zustand\uc5d0 \ub85c\uadf8\uc778 \uc720\uc800 \uc800\uc7a5\n      onSuccess();\n    },\n    onError: (error: Error) =&gt; {\n      form.setError(&quot;root&quot;, { message: error.message });\n    },\n  });\n\n  const onSubmit = (data: SignUpInput) =&gt; {\n    const { confirmPassword, ...payload } = data;\n    signUpMutation.mutate(payload);\n  };\n\n  return (\n    &lt;Form {...form}&gt;\n      &lt;form onSubmit={form.handleSubmit(onSubmit)} className=&quot;space-y-4&quot;&gt;\n        {form.formState.errors.root &amp;&amp; (\n          &lt;p className=&quot;text-sm text-destructive&quot;&gt;\n            {form.formState.errors.root.message}\n          &lt;\/p&gt;\n        )}\n        {([&quot;name&quot;, &quot;email&quot;, &quot;password&quot;, &quot;confirmPassword&quot;] as const).map((fieldName) =&gt; (\n          &lt;FormField\n            key={fieldName}\n            control={form.control}\n            name={fieldName}\n            render={({ field }) =&gt; (\n              &lt;FormItem&gt;\n                &lt;FormLabel&gt;\n                  {{ name: &quot;\uc774\ub984&quot;, email: &quot;\uc774\uba54\uc77c&quot;, password: &quot;\ube44\ubc00\ubc88\ud638&quot;, confirmPassword: &quot;\ube44\ubc00\ubc88\ud638 \ud655\uc778&quot; }[fieldName]}\n                &lt;\/FormLabel&gt;\n                &lt;FormControl&gt;\n                  &lt;Input\n                    type={fieldName.toLowerCase().includes(&quot;password&quot;) ? &quot;password&quot; : &quot;text&quot;}\n                    {...field}\n                  \/&gt;\n                &lt;\/FormControl&gt;\n                &lt;FormMessage \/&gt;\n              &lt;\/FormItem&gt;\n            )}\n          \/&gt;\n        ))}\n        &lt;Button type=&quot;submit&quot; className=&quot;w-full&quot; disabled={signUpMutation.isPending}&gt;\n          {signUpMutation.isPending ? &quot;\ucc98\ub9ac \uc911...&quot; : &quot;\uac00\uc785\ud558\uae30&quot;}\n        &lt;\/Button&gt;\n      &lt;\/form&gt;\n    &lt;\/Form&gt;\n  );\n}\n\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ \uba54\uc778 \ud398\uc774\uc9c0\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nexport default function Page() {\n  \/\/ Zustand: \uc804\uc5ed \uc0c1\ud0dc \uc77d\uae30\n  const { user, isAuthenticated, logout } = useAuthStore();\n  const { isSignUpModalOpen, openSignUpModal, closeSignUpModal } = useUIStore();\n\n  \/\/ TanStack Query: \uc11c\ubc84 \uc0c1\ud0dc \uc870\ud68c\n  const { data: users, isLoading } = useQuery({\n    queryKey: [&quot;users&quot;],\n    queryFn: fetchUsers,\n  });\n\n  return (\n    &lt;main className=&quot;min-h-screen bg-background p-8&quot;&gt;\n      &lt;div className=&quot;max-w-2xl mx-auto space-y-6&quot;&gt;\n\n        {\/* \ud5e4\ub354 *\/}\n        &lt;div className=&quot;flex items-center justify-between&quot;&gt;\n          &lt;h1 className=&quot;text-3xl font-bold text-foreground&quot;&gt;\ubaa8\ub358 React \uc2a4\ud0dd&lt;\/h1&gt;\n          &lt;div className=&quot;flex items-center gap-3&quot;&gt;\n            {isAuthenticated ? (\n              &lt;&gt;\n                &lt;Badge variant=&quot;secondary&quot;&gt;{user?.name}&lt;\/Badge&gt;\n                &lt;Button variant=&quot;outline&quot; onClick={logout}&gt;\ub85c\uadf8\uc544\uc6c3&lt;\/Button&gt;\n              &lt;\/&gt;\n            ) : (\n              &lt;Dialog open={isSignUpModalOpen} onOpenChange={(open) =&gt; open ? openSignUpModal() : closeSignUpModal()}&gt;\n                &lt;DialogTrigger asChild&gt;\n                  &lt;Button onClick={openSignUpModal}&gt;\ud68c\uc6d0\uac00\uc785&lt;\/Button&gt;\n                &lt;\/DialogTrigger&gt;\n                &lt;DialogContent&gt;\n                  &lt;DialogHeader&gt;\n                    &lt;DialogTitle&gt;\ud68c\uc6d0\uac00\uc785&lt;\/DialogTitle&gt;\n                  &lt;\/DialogHeader&gt;\n                  &lt;SignUpForm onSuccess={closeSignUpModal} \/&gt;\n                &lt;\/DialogContent&gt;\n              &lt;\/Dialog&gt;\n            )}\n          &lt;\/div&gt;\n        &lt;\/div&gt;\n\n        {\/* \uc0ac\uc6a9\uc790 \ubaa9\ub85d *\/}\n        &lt;Card&gt;\n          &lt;CardHeader&gt;\n            &lt;CardTitle&gt;\uac00\uc785\ud55c \uc0ac\uc6a9\uc790&lt;\/CardTitle&gt;\n            &lt;CardDescription&gt;\n              \ucd1d {users?.length ?? 0}\uba85\n            &lt;\/CardDescription&gt;\n          &lt;\/CardHeader&gt;\n          &lt;CardContent&gt;\n            {isLoading ? (\n              &lt;p className=&quot;text-sm text-muted-foreground&quot;&gt;\ubd88\ub7ec\uc624\ub294 \uc911...&lt;\/p&gt;\n            ) : (\n              &lt;ul className=&quot;divide-y divide-border&quot;&gt;\n                {users?.map((u) =&gt; (\n                  &lt;li key={u.id} className=&quot;flex items-center justify-between py-2&quot;&gt;\n                    &lt;div&gt;\n                      &lt;span className=&quot;font-medium text-sm text-foreground&quot;&gt;{u.name}&lt;\/span&gt;\n                      &lt;span className=&quot;text-muted-foreground text-sm ml-2&quot;&gt;{u.email}&lt;\/span&gt;\n                    &lt;\/div&gt;\n                    {u.id === user?.id &amp;&amp; (\n                      &lt;Badge&gt;\ub098&lt;\/Badge&gt;\n                    )}\n                  &lt;\/li&gt;\n                ))}\n              &lt;\/ul&gt;\n            )}\n          &lt;\/CardContent&gt;\n        &lt;\/Card&gt;\n\n      &lt;\/div&gt;\n    &lt;\/main&gt;\n  );\n}<\/code><\/pre>\n<blockquote>\n<p><strong>v4 \ubcc0\uacbd \ud3ec\uc778\ud2b8:<\/strong> <code>text-gray-500<\/code> \u2192 <code>text-muted-foreground<\/code>, <code>bg-gray-50<\/code> \u2192 <code>bg-background<\/code>, <code>border-b<\/code> \u2192 <code>divide-border<\/code> \ub4f1 shadcn \ud14c\ub9c8 \ubcc0\uc218\ub97c \ud65c\uc6a9\ud558\uba74 \ub2e4\ud06c\ubaa8\ub4dc\uac00 \uc790\ub3d9\uc73c\ub85c \ub3d9\uc791\ud569\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<h2>\ub77c\uc774\ube0c\ub7ec\ub9ac \uc5ed\ud560 \uc815\ub9ac<\/h2>\n<table>\n<thead>\n<tr>\n<th>\ub77c\uc774\ube0c\ub7ec\ub9ac<\/th>\n<th>\uc5ed\ud560<\/th>\n<th>\ub2e4\ub8e8\ub294 \uc0c1\ud0dc<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Tailwind CSS v4<\/strong><\/td>\n<td>\uc720\ud2f8\ub9ac\ud2f0 \uae30\ubc18 \uc2a4\ud0c0\uc77c\ub9c1 (CSS-first \uc124\uc815)<\/td>\n<td>\u2014<\/td>\n<\/tr>\n<tr>\n<td><strong>shadcn\/ui<\/strong><\/td>\n<td>\uc7ac\uc0ac\uc6a9 \uac00\ub2a5\ud55c UI \ucef4\ud3ec\ub10c\ud2b8 (OKLCH \uc0c9\uc0c1)<\/td>\n<td>\u2014<\/td>\n<\/tr>\n<tr>\n<td><strong>Zod<\/strong><\/td>\n<td>\uc2a4\ud0a4\ub9c8 \uc815\uc758 + \ud0c0\uc785 \ucd94\ub860<\/td>\n<td>\u2014<\/td>\n<\/tr>\n<tr>\n<td><strong>React Hook Form<\/strong><\/td>\n<td>\ud3fc \uc0c1\ud0dc + \uc720\ud6a8\uc131 \uac80\uc0ac<\/td>\n<td>\ub85c\uceec(\ud3fc) \uc0c1\ud0dc<\/td>\n<\/tr>\n<tr>\n<td><strong>TanStack Query<\/strong><\/td>\n<td>\uc11c\ubc84 \ub370\uc774\ud130 \ud328\uce6d + \uce90\uc2f1<\/td>\n<td><strong>\uc11c\ubc84 \uc0c1\ud0dc<\/strong><\/td>\n<\/tr>\n<tr>\n<td><strong>Zustand<\/strong><\/td>\n<td>\uc804\uc5ed \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc<\/td>\n<td><strong>\ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<blockquote>\n<p>\ud83d\udca1 <strong>\ud575\uc2ec \uc6d0\uce59<\/strong>: TanStack Query\ub294 \uc11c\ubc84\uc5d0\uc11c \uc624\ub294 \ub370\uc774\ud130\ub97c, Zustand\ub294 UI\uc5d0\ub9cc \uc874\uc7ac\ud558\ub294 \ub370\uc774\ud130\ub97c \uad00\ub9ac\ud569\ub2c8\ub2e4. \uc774 \ub458\uc744 \ud63c\uc6a9\ud558\uc9c0 \uc54a\ub294 \uac83\uc774 \uc88b\uc740 \uc124\uacc4\uc785\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<h2>\ucc38\uace0 \uc790\ub8cc<\/h2>\n<ul>\n<li><a href=\"https:\/\/tailwindcss.com\/docs\">Tailwind CSS v4 \uacf5\uc2dd \ubb38\uc11c<\/a><\/li>\n<li><a href=\"https:\/\/tailwindcss.com\/docs\/upgrade-guide\">Tailwind CSS v4 \uc5c5\uadf8\ub808\uc774\ub4dc \uac00\uc774\ub4dc<\/a><\/li>\n<li><a href=\"https:\/\/ui.shadcn.com\/docs\/tailwind-v4\">shadcn\/ui Tailwind v4 \uac00\uc774\ub4dc<\/a><\/li>\n<li><a href=\"https:\/\/zod.dev\">Zod \uacf5\uc2dd \ubb38\uc11c<\/a><\/li>\n<li><a href=\"https:\/\/react-hook-form.com\">React Hook Form \uacf5\uc2dd \ubb38\uc11c<\/a><\/li>\n<li><a href=\"https:\/\/tanstack.com\/query\/latest\">TanStack Query \uacf5\uc2dd \ubb38\uc11c<\/a><\/li>\n<li><a href=\"https:\/\/zustand-demo.pmnd.rs\">Zustand \uacf5\uc2dd \ubb38\uc11c<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>\ubaa8\ub358 React \uc2a4\ud0dd \uc644\uc804 \uc815\ubcf5: Zustand + TanStack Query + React Hook Form + Zod + Tailwind + shadcn\/ui \uc2e4\ubb34\uc5d0\uc11c \uac00\uc7a5 \ub9ce\uc774 \uc4f0\uc774\ub294 6\uac00\uc9c0 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ub2e8\uacc4\ubcc4 \uc0d8\ud50c \ucf54\ub4dc\uc640 \ud568\uaed8 \uc124\uba85\ud569\ub2c8\ub2e4. \uac01 \ub2e8\uacc4\ub9c8\ub2e4 \uc774\uc804 \ucf54\ub4dc\uc5d0 \uc0c8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ub367\ubd99\uc774\ub294 \ubc29\uc2dd\uc73c\ub85c \uc9c4\ud589\ud569\ub2c8\ub2e4. \uc774 \ubb38\uc11c\ub294 Tailwind CSS v4 \uae30\uc900\uc73c\ub85c \uc791\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. 0. \ud504\ub85c\uc81d\ud2b8 \uc138\ud305 npx create-next-app@latest my-app &#8211;typescript &#8211;eslint &#8211;app\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=11463\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[22],"tags":[],"class_list":["post-11463","post","type-post","status-publish","format-standard","hentry","category-web"],"_links":{"self":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/11463","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11463"}],"version-history":[{"count":21,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/11463\/revisions"}],"predecessor-version":[{"id":11485,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/11463\/revisions\/11485"}],"wp:attachment":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11463"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11463"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11463"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}