๐Ÿ”— Tailwind 13์žฅ: Next.js + Tailwind ์™„๋ฒฝ ์—ฐ๋™

2026๋…„ 3์›” 5์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

Next.js App Router ํ™˜๊ฒฝ์—์„œ Tailwind ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ†ตํ•ฉํ•˜๋Š” ๋ฒ• โ€” ์„ค์น˜๋ถ€ํ„ฐ next/font, Server Components, shadcn/ui ๊นŒ์ง€

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 20๋ถ„

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • create-next-app ์œผ๋กœ Tailwind ํฌํ•จ ํ”„๋กœ์ ํŠธ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • next/font ๋กœ ํ•œ๊ตญ์–ด ํฐํŠธ๋ฅผ ์ตœ์ ํ™”ํ•ด์„œ Tailwind ์™€ ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค
  • Server Components ์™€ Client Components ์—์„œ Tailwind ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค
  • shadcn/ui ๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ  ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ๐Ÿฃ ์˜์ฒ  (์‹ ์ž…): "๋ฆฌ๋“œ ๋‹˜! Next.js ์ƒˆ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ํ•˜๋Š”๋ฐ, Tailwind ์„ค์ •์„ ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•ด์š”? ๊ณต์‹ ๋ฌธ์„œ ๋ณด๋‹ˆ๊นŒ ๋ฒ„์ „๋งˆ๋‹ค ์„ค์ •์ด ๋‹ฌ๋ผ์„œ ํ—ท๊ฐˆ๋ ค์š”."
  • ๐Ÿฆ ์˜ํ˜ธ (๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜, create-next-app ์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด Tailwind ์„ค์ •๊นŒ์ง€ ์ž๋™์œผ๋กœ ๋ผ์š”. ๊ทผ๋ฐ ํ•œ๊ตญ์–ด ํฐํŠธ๋ž‘ ๋‹คํฌ ๋ชจ๋“œ, shadcn/ui ์ถ”๊ฐ€ํ•˜๋Š” ๋ถ€๋ถ„์ด ์ข€ ๊นŒ๋‹ค๋กญ๊ฑฐ๋“ ์š”. ํ•œ ๋ฒˆ์— ์ œ๋Œ€๋กœ ์žก์•„๋ด์š”."
  • ๐Ÿ‘” ์˜์ˆ˜ (PM): "์„ค์ • ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ ค์š”? ๋นจ๋ฆฌ ๊ธฐ๋Šฅ ๋งŒ๋“ค๊ณ  ์‹ถ์€๋ฐ."
  • ๐Ÿฆ ์˜ํ˜ธ: "์˜ค๋Š˜ ์ค‘์œผ๋กœ ์™„๋ฒฝํ•˜๊ฒŒ ์„ธํŒ…ํ•ด๋“œ๋ฆด๊ฒŒ์š”. ์ดˆ๋ฐ˜์— ์ œ๋Œ€๋กœ ์žก์œผ๋ฉด ๋‚˜์ค‘์— ํ›จ์”ฌ ํŽธํ•ด์š”."

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

Tailwind ๋ฅผ Next.js ์™€ ํ†ตํ•ฉํ•  ๋•Œ๋Š” ๋ช‡ ๊ฐ€์ง€ ๊ณ ๋ คํ•  ์ ์ด ์žˆ์–ด:

  1. ํฐํŠธ ์ตœ์ ํ™”: next/font ์™€ CSS ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด Tailwind ํฐํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ์™€ ์—ฐ๊ฒฐ
  2. ๋‹คํฌ ๋ชจ๋“œ: Server/Client Component ๊ฒฝ๊ณ„์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
  3. CSS ํŒŒ์ผ ์œ„์น˜: App Router ์—์„œ๋Š” app/globals.css ์— ์ž„ํฌํŠธ
  4. shadcn/ui: Tailwind ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์ถฉ๋Œ ์—†์ด ์„ค์ •

์ด๊ฒƒ๋“ค์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์žก์ง€ ์•Š์œผ๋ฉด ๋‚˜์ค‘์— ๋Œ€๊ทœ๋ชจ ๋ฆฌํŒฉํ† ๋ง์ด ํ•„์š”ํ•ด.


โš™๏ธ ์„ค์น˜์™€ ์ดˆ๊ธฐ ์„ค์ •

์ƒˆ ํ”„๋กœ์ ํŠธ: create-next-app (๊ถŒ์žฅ)

# Tailwind ํฌํ•จ ์ž๋™ ์„ค์ •
npx create-next-app@latest my-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*"

์ด ๋ช…๋ น์œผ๋กœ ์ƒ์„ฑ๋œ ํ”„๋กœ์ ํŠธ์—๋Š” ์ด๋ฏธ ํฌํ•จ๋ผ ์žˆ์–ด:

  • tailwindcss (v4 ๋˜๋Š” v3, Next.js ๋ฒ„์ „์— ๋”ฐ๋ผ)
  • postcss, autoprefixer
  • tailwind.config.ts ๋˜๋Š” globals.css ์— @import "tailwindcss"

๊ธฐ์กด ํ”„๋กœ์ ํŠธ์— Tailwind ์ถ”๊ฐ€

# Tailwind v4 (์ตœ์‹ )
npm install tailwindcss @tailwindcss/postcss postcss
 
# postcss.config.mjs
# { plugins: { '@tailwindcss/postcss': {} } }
/* app/globals.css */
@import "tailwindcss";

์ƒ์„ฑ๋œ ํŒŒ์ผ ๊ตฌ์กฐ

my-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ globals.css     โ† Tailwind ์ž„ํฌํŠธ ์œ„์น˜
โ”‚   โ”œโ”€โ”€ layout.tsx      โ† globals.css ์ž„ํฌํŠธ
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ tailwind.config.ts  โ† Tailwind v3 ์„ค์ • (v4 ์—์„œ๋Š” ๋ถˆํ•„์š”)
โ”œโ”€โ”€ postcss.config.mjs  โ† PostCSS ์„ค์ •
โ””โ”€โ”€ package.json

๐Ÿ–‹๏ธ next/font ์™€ Tailwind ํ†ตํ•ฉ

ํ•œ๊ตญ์–ด Noto Sans KR ์ ์šฉ

// app/layout.tsx
import { Noto_Sans_KR, Inter } from 'next/font/google';
import './globals.css';
 
// ์˜์–ด ํฐํŠธ
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',   // CSS ๋ณ€์ˆ˜ ์ด๋ฆ„
  display: 'swap',
});
 
// ํ•œ๊ตญ์–ด ํฐํŠธ (ํ•„์š”ํ•œ ๊ตต๊ธฐ๋งŒ ๋กœ๋“œํ•ด์„œ ์ตœ์ ํ™”)
const notoSansKr = Noto_Sans_KR({
  subsets: ['latin'],
  weight: ['400', '500', '700'],  // ํ•„์š”ํ•œ ๊ตต๊ธฐ๋งŒ
  variable: '--font-noto-sans-kr',
  display: 'swap',
});
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    // className ์— ๋‘ ํฐํŠธ ๋ณ€์ˆ˜ ๋ชจ๋‘ ์ถ”๊ฐ€
    <html lang="ko" className={`${inter.variable} ${notoSansKr.variable}`}>
      <body className="font-sans antialiased">
        {children}
      </body>
    </html>
  );
}
/* app/globals.css */
@import "tailwindcss";
 
@theme {
  /* next/font ๊ฐ€ ์ƒ์„ฑํ•œ CSS ๋ณ€์ˆ˜๋ฅผ Tailwind ํฐํŠธ๋กœ ๋“ฑ๋ก */
  --font-sans: var(--font-noto-sans-kr), var(--font-inter), system-ui, sans-serif;
  --font-mono: ui-monospace, 'JetBrains Mono', monospace;
}

์ด์ œ font-sans ํด๋ž˜์Šค๋ฅผ ์“ฐ๋ฉด Noto Sans KR โ†’ Inter โ†’ system-ui ํด๋ฐฑ ์ˆœ์œผ๋กœ ์ ์šฉ๋ผ.

๋กœ์ปฌ ํฐํŠธ (Pretendard) ์ ์šฉ

// app/layout.tsx
import localFont from 'next/font/local';
 
const pretendard = localFont({
  src: [
    { path: '../public/fonts/Pretendard-Regular.woff2',  weight: '400' },
    { path: '../public/fonts/Pretendard-Medium.woff2',   weight: '500' },
    { path: '../public/fonts/Pretendard-SemiBold.woff2', weight: '600' },
    { path: '../public/fonts/Pretendard-Bold.woff2',     weight: '700' },
  ],
  variable: '--font-pretendard',
  display: 'swap',
});

๐Ÿ–ฅ๏ธ Server Components ์—์„œ Tailwind

Server vs Client Component ์—์„œ์˜ ์ฐจ์ด

// โœ… Server Component โ€” Tailwind ํด๋ž˜์Šค ๊ทธ๋ƒฅ ์‚ฌ์šฉ (๊ฐ€์žฅ ์ผ๋ฐ˜์ )
// app/components/StudyCard.tsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ)
export function StudyCard({ title, description }: Props) {
  return (
    <div className="rounded-xl border border-gray-200 bg-white p-5 shadow-md">
      <h3 className="text-lg font-bold text-gray-900">{title}</h3>
      <p className="text-sm text-gray-500">{description}</p>
    </div>
  );
}
// โœ… Client Component โ€” ์ธํ„ฐ๋ž™์…˜์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ
// 'use client' ๊ฐ€ ๋ถ™์–ด๋„ Tailwind ํด๋ž˜์Šค๋Š” ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉ
'use client';
 
import { useState } from 'react';
 
export function ExpandableCard({ title, content }: Props) {
  const [expanded, setExpanded] = useState(false);
 
  return (
    <div className="rounded-xl border border-gray-200 bg-white shadow-md">
      <button
        onClick={() => setExpanded(!expanded)}
        className="flex w-full items-center justify-between p-5 text-left"
      >
        <h3 className="font-bold text-gray-900">{title}</h3>
        <span className={`transition-transform ${expanded ? 'rotate-180' : ''}`}>โ–ผ</span>
      </button>
      {expanded && (
        <div className="border-t border-gray-100 p-5 text-sm text-gray-600">
          {content}
        </div>
      )}
    </div>
  );
}

๐Ÿ’ก Server Component ์—์„œ Tailwind ๋ฅผ ์“ฐ๋ฉด ์„œ๋ฒ„์—์„œ HTML ๋ Œ๋”๋ง ์‹œ ์ด๋ฏธ ํด๋ž˜์Šค๊ฐ€ ํฌํ•จ๋ผ. ๋ณ„๋„ JS ๋ฒˆ๋“ค ์—†์ด ์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์–ด FCP(First Contentful Paint) ๊ฐ€ ๋นจ๋ผ์ ธ.

Conditional Classes in Server Components

// Server Component ์—์„œ๋„ cn() ํŒจํ„ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ
import { cn } from '@/lib/utils';
 
// async Server Component
async function StudyStatus({ studyId }: { studyId: string }) {
  const study = await getStudy(studyId);  // ์„œ๋ฒ„์—์„œ ์ง์ ‘ DB ์กฐํšŒ
 
  return (
    <span className={cn(
      'rounded-full px-3 py-1 text-xs font-semibold',
      study.status === 'recruiting' && 'bg-green-100 text-green-700',
      study.status === 'ongoing'    && 'bg-blue-100 text-blue-700',
      study.status === 'completed'  && 'bg-gray-100 text-gray-500',
    )}>
      {study.statusLabel}
    </span>
  );
}

๐ŸŒ™ next-themes ๋‹คํฌ ๋ชจ๋“œ ํ†ตํ•ฉ

(07์žฅ์—์„œ ๋‹ค๋ค˜์ง€๋งŒ Next.js ์ปจํ…์ŠคํŠธ์—์„œ ๋‹ค์‹œ ์ •๋ฆฌ)

npm install next-themes
// app/providers.tsx
'use client';
 
import { ThemeProvider } from 'next-themes';
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider
      attribute="class"       // <html class="dark"> ๋ฐฉ์‹ ์‚ฌ์šฉ
      defaultTheme="system"   // ์‹œ์Šคํ…œ ์„ค์ • ๊ธฐ๋ณธ๊ฐ’
      enableSystem            // ์‹œ์Šคํ…œ ์„ค์ • ๊ฐ์ง€
      disableTransitionOnChange // ํ…Œ๋งˆ ์ „ํ™˜ ์‹œ transition ์ผ์‹œ ๋น„ํ™œ์„ฑ (๊นœ๋นก์ž„ ๋ฐฉ์ง€)
    >
      {children}
    </ThemeProvider>
  );
}
// app/layout.tsx
import { Providers } from './providers';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" suppressHydrationWarning>  {/* ํ•„์ˆ˜! */}
      <body className="bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-white antialiased">
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}

๐Ÿงฉ shadcn/ui: Tailwind ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

shadcn/ui ๋Š” Tailwind + Radix UI ๊ธฐ๋ฐ˜์˜ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, ์ฝ”๋“œ๋ฅผ ๋ณต์‚ฌํ•ด์„œ ํ”„๋กœ์ ํŠธ์— ์ง์ ‘ ์†Œ์œ ํ•˜๋Š” ๋…ํŠนํ•œ ๋ฐฉ์‹์ด์•ผ.

์„ค์น˜

npx shadcn@latest init

์„ค์น˜ ๊ณผ์ •์—์„œ:

  • TypeScript ์‚ฌ์šฉ ์—ฌ๋ถ€
  • ์Šคํƒ€์ผ (Default or New York)
  • ๊ธฐ๋ณธ ์ƒ‰์ƒ (๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ)
  • CSS ๋ณ€์ˆ˜ ์‚ฌ์šฉ ์—ฌ๋ถ€
  • components.json ์ƒ์„ฑ

์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€

# ์›ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋งŒ ์„ ํƒ์ ์œผ๋กœ ์ถ”๊ฐ€
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
npx shadcn@latest add toast

์ถ”๊ฐ€๋œ ํŒŒ์ผ ์œ„์น˜: components/ui/button.tsx, components/ui/card.tsx ๋“ฑ

shadcn/ui ์™€ ์ปค์Šคํ…€ Tailwind ํ…Œ๋งˆ ์—ฐ๋™

/* globals.css */
@import "tailwindcss";
 
@theme {
  /* shadcn/ui ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” CSS ๋ณ€์ˆ˜์™€ ๋งคํ•‘ */
  --color-background: hsl(0 0% 100%);
  --color-foreground: hsl(222.2 84% 4.9%);
  --color-primary: hsl(222.2 47.4% 11.2%);
  --color-primary-foreground: hsl(210 40% 98%);
  --color-secondary: hsl(210 40% 96.1%);
  --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
  --color-muted: hsl(210 40% 96.1%);
  --color-muted-foreground: hsl(215.4 16.3% 46.9%);
  --color-accent: hsl(210 40% 96.1%);
  --color-accent-foreground: hsl(222.2 47.4% 11.2%);
  --color-border: hsl(214.3 31.8% 91.4%);
  --color-ring: hsl(222.2 84% 4.9%);
 
  /* ๋‹คํฌ ๋ชจ๋“œ */
  /* .dark ํด๋ž˜์Šค ๋‚ด์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ */
}

shadcn/ui ์ปดํฌ๋„ŒํŠธ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

shadcn/ui ์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ : ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๊ฐ€ ๋‚ด ํ”„๋กœ์ ํŠธ์— ์žˆ์–ด์„œ ์ž์œ ๋กญ๊ฒŒ ์ˆ˜์ • ๊ฐ€๋Šฅ!

// components/ui/button.tsx (shadcn/ui ๊ฐ€ ์ƒ์„ฑํ•œ ํŒŒ์ผ โ€” ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅ)
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
 
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default:     'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline:     'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
        secondary:   'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost:       'hover:bg-accent hover:text-accent-foreground',
        link:        'text-primary underline-offset-4 hover:underline',
        // ๐ŸŽจ ์ปค์Šคํ…€ variant ์ถ”๊ฐ€ โ€” ์˜์ˆ˜๋„ค ๋ธŒ๋žœ๋“œ ๋ฒ„ํŠผ
        brand:       'bg-brand-600 text-white hover:bg-brand-700 focus-visible:ring-brand-500',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm:      'h-9 rounded-md px-3',
        lg:      'h-11 rounded-md px-8',
        icon:    'h-10 w-10',
      },
    },
    defaultVariants: { variant: 'default', size: 'default' },
  }
);
 
// ์‚ฌ์šฉ ์˜ˆ
<Button variant="brand">์˜์ˆ˜๋„ค ๋ธŒ๋žœ๋“œ ๋ฒ„ํŠผ</Button>

๐Ÿ’ป ์‹ค์ „: ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ Next.js ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

์ถ”์ฒœ ํด๋” ๊ตฌ์กฐ

src/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ globals.css          โ† Tailwind @import + @theme + @layer
โ”‚   โ”œโ”€โ”€ layout.tsx           โ† next/font + Providers + HTML ๊ตฌ์กฐ
โ”‚   โ”œโ”€โ”€ providers.tsx        โ† ThemeProvider (Client Component)
โ”‚   โ”œโ”€โ”€ page.tsx             โ† ๋ฉ”์ธ ํŽ˜์ด์ง€ (Server Component)
โ”‚   โ””โ”€โ”€ studies/
โ”‚       โ”œโ”€โ”€ page.tsx         โ† ์Šคํ„ฐ๋”” ๋ชฉ๋ก (Server Component)
โ”‚       โ””โ”€โ”€ [id]/
โ”‚           โ””โ”€โ”€ page.tsx     โ† ์Šคํ„ฐ๋”” ์ƒ์„ธ (Server Component)
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ ui/                  โ† shadcn/ui ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚   โ”œโ”€โ”€ button.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ card.tsx
โ”‚   โ”‚   โ””โ”€โ”€ input.tsx
โ”‚   โ”œโ”€โ”€ layout/              โ† ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚   โ”œโ”€โ”€ Navbar.tsx
โ”‚   โ”‚   โ””โ”€โ”€ Footer.tsx
โ”‚   โ””โ”€โ”€ features/            โ† ๋„๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ
โ”‚       โ”œโ”€โ”€ StudyCard.tsx
โ”‚       โ””โ”€โ”€ StudyGrid.tsx
โ””โ”€โ”€ lib/
    โ””โ”€โ”€ utils.ts             โ† cn() ์œ ํ‹ธ๋ฆฌํ‹ฐ

์™„์„ฑ๋œ layout.tsx

// app/layout.tsx
import type { Metadata } from 'next';
import { Noto_Sans_KR, Inter } from 'next/font/google';
import { Providers } from './providers';
import './globals.css';
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
});
 
const notoSansKr = Noto_Sans_KR({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  variable: '--font-noto-sans-kr',
  display: 'swap',
});
 
export const metadata: Metadata = {
  title: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ',
  description: '๊ฐœ๋ฐœ์ž ์Šคํ„ฐ๋”” ๋งค์นญ ํ”Œ๋žซํผ',
};
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html
      lang="ko"
      className={`${inter.variable} ${notoSansKr.variable}`}
      suppressHydrationWarning  // next-themes ๋‹คํฌ ๋ชจ๋“œ์šฉ
    >
      <body className={`
        min-h-screen
        bg-gray-50 text-gray-900
        dark:bg-gray-900 dark:text-white
        antialiased font-sans
      `}>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}

์™„์„ฑ๋œ globals.css

/* app/globals.css */
@import "tailwindcss";
 
/* ===== ๋””์ž์ธ ํ† ํฐ ===== */
@theme {
  /* ํฐํŠธ (next/font CSS ๋ณ€์ˆ˜ ์ฐธ์กฐ) */
  --font-sans: var(--font-noto-sans-kr), var(--font-inter), system-ui, sans-serif;
 
  /* ๋ธŒ๋žœ๋“œ ์ƒ‰์ƒ */
  --color-brand-500: #8b5cf6;
  --color-brand-600: #7c3aed;
  --color-brand-700: #6d28d9;
 
  /* ์ปค์Šคํ…€ Breakpoint */
  --breakpoint-xs: 30rem;
}
 
/* ===== Base ์Šคํƒ€์ผ ===== */
@layer base {
  html { scroll-behavior: smooth; }
  body { word-break: keep-all; }
 
  /* ์ „์—ญ ํฌ์ปค์Šค ์Šคํƒ€์ผ */
  *:focus-visible {
    outline: 2px solid theme('colors.brand.600');
    outline-offset: 2px;
  }
}

๐Ÿšจ Next.js + Tailwind ํ”ํ•œ ํ•จ์ •

ํ•จ์ • 1: globals.css ์ž„ํฌํŠธ ์œ„์น˜

// โŒ ๊ฐœ๋ณ„ ํŽ˜์ด์ง€์—์„œ ์ž„ํฌํŠธ โ€” Next.js ๋Š” layout ์—์„œ๋งŒ ํ—ˆ์šฉ
// app/studies/page.tsx
import '../globals.css';  // ๊ฒฝ๊ณ  ๋ฐœ์ƒ!
 
// โœ… ๋ฃจํŠธ layout ์—์„œ ํ•œ ๋ฒˆ๋งŒ ์ž„ํฌํŠธ
// app/layout.tsx
import './globals.css';  // ์—ฌ๊ธฐ์„œ๋งŒ!

ํ•จ์ • 2: Server Component ์—์„œ useState ์™€ Tailwind

// โŒ Server Component ์—์„œ ๋™์  ํด๋ž˜์Šค๋ฅผ useState ๋กœ ๊ด€๋ฆฌ ๋ถˆ๊ฐ€
export default function StudyCard() {
  const [isHovered, setIsHovered] = useState(false);  // Server Component ์—์„œ ์—๋Ÿฌ!
  return <div className={isHovered ? 'bg-blue-50' : 'bg-white'}>...</div>;
}
 
// โœ… hover ๋Š” CSS ๋กœ (Tailwind variant ์‚ฌ์šฉ) โ€” JS ๋ถˆํ•„์š”
export default function StudyCard() {
  return <div className="bg-white hover:bg-blue-50 transition-colors">...</div>;
}
 
// โœ… ์ง„์งœ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด 'use client' ์ถ”๊ฐ€
'use client';
export default function StudyCard() {
  const [isExpanded, setIsExpanded] = useState(false);
  // ...
}

ํ•จ์ • 3: ๋‹คํฌ ๋ชจ๋“œ Hydration ๋ถˆ์ผ์น˜

// โŒ ์„œ๋ฒ„์—์„œ ๋‹คํฌ ๋ชจ๋“œ ์ƒํƒœ๋ฅผ ์•Œ ์ˆ˜ ์—†์–ด์„œ ๊นœ๋นก์ž„ ๋ฐœ์ƒ
<html className={isDark ? 'dark' : ''}>
 
// โœ… next-themes + suppressHydrationWarning ์œผ๋กœ ํ•ด๊ฒฐ
<html suppressHydrationWarning>  {/* ThemeProvider ๊ฐ€ ์•Œ์•„์„œ ์ฒ˜๋ฆฌ */}

ํ•จ์ • 4: Tailwind v3 vs v4 ์„ค์ • ์ฐจ์ด

v3: tailwind.config.ts ํ•„์š” + postcss.config.js
v4: globals.css ์— @import "tailwindcss" ๋งŒ ์žˆ์œผ๋ฉด ๋จ (config ํŒŒ์ผ ๋ถˆํ•„์š”)
/* v4 ์„ค์ • ํ™•์ธ: globals.css ์— ์ด๊ฒŒ ์žˆ์œผ๋ฉด v4 */
@import "tailwindcss";
 
/* v3 ํ™•์ธ: tailwind.config.ts ์— ์ด๊ฒŒ ์žˆ์œผ๋ฉด v3 */
/* content: ['./src/**/*.{ts,tsx}', './app/**/*.{ts,tsx}'] */

๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

ํ•ญ๋ชฉ๋ฐฉ๋ฒ•์ฃผ์˜์‚ฌํ•ญ
์ƒˆ ํ”„๋กœ์ ํŠธcreate-next-app --tailwind์ž๋™ ์„ค์ •๋จ
next/font ํ†ตํ•ฉCSS ๋ณ€์ˆ˜ โ†’ @theme ๋“ฑ๋กdisplay: swap ํ•„์ˆ˜
globals.css ์œ„์น˜app/layout.tsx ์—์„œ ์ž„ํฌํŠธ๋‹ค๋ฅธ ๊ณณ X
๋‹คํฌ ๋ชจ๋“œnext-themes + suppressHydrationWarningmounted ์ฒดํฌ
shadcn/uinpx shadcn@latest add์ฝ”๋“œ ์ง์ ‘ ์†Œ์œ 
Server Componenthover ๋Š” CSS variant, ์ƒํƒœ๋Š” 'use client'JS ์ตœ์†Œํ™”

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. Next.js App Router ์—์„œ globals.css ๋ฅผ ์ž„ํฌํŠธํ•ด์•ผ ํ•˜๋Š” ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜๋Š”?

โœ… ์ •๋‹ต: ๋ฃจํŠธ app/layout.tsx ์—์„œ ํ•œ ๋ฒˆ๋งŒ ์ž„ํฌํŠธํ•œ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • Next.js App Router ์—์„œ CSS ํŒŒ์ผ์€ ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ์—์„œ ์ž„ํฌํŠธํ•ด์•ผ ์ „์ฒด ์•ฑ์— ์ ์šฉ๋ผ. ๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ ์ž„ํฌํŠธํ•˜๋ฉด ๊ฒฝ๊ณ  ๋˜๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด.
  • import './globals.css' ๋Š” app/layout.tsx ๋งจ ์œ„์— ํ•œ ๋ฒˆ๋งŒ ์œ„์น˜ํ•ด์•ผ ํ•ด.
  • ํŽ˜์ด์ง€๋ณ„ CSS Modules (.module.css)๋Š” ๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ž„ํฌํŠธ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์ „์—ญ CSS ๋Š” ๋ฃจํŠธ์—์„œ๋งŒ.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "์ „์—ญ CSS = ๋ฃจํŠธ layout ์—์„œ ํ•œ ๋ฒˆ๋งŒ."

Q2. next/font ๋กœ ๋กœ๋“œํ•œ ํฐํŠธ๋ฅผ Tailwind font-sans ํด๋ž˜์Šค์™€ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์€?

โœ… ์ •๋‹ต: next/font ์—์„œ variable ์˜ต์…˜์œผ๋กœ CSS ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ์ง€์ •ํ•˜๊ณ , @theme ์—์„œ ๊ทธ ๋ณ€์ˆ˜๋ฅผ --font-sans ์— ๋“ฑ๋กํ•œ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  1. next/font ์„ค์ •: variable: '--font-noto-sans-kr'
  2. HTML ์— ๋ณ€์ˆ˜ ์ฃผ์ž…: <html className={notoSansKr.variable}>
  3. @theme ๋“ฑ๋ก: --font-sans: var(--font-noto-sans-kr), sans-serif;
  4. ์‚ฌ์šฉ: <body className="font-sans">

์ด ํ๋ฆ„์ด next/font ์™€ Tailwind ๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๊ณต์‹ ํŒจํ„ด์ด์•ผ.

  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "next/font variable โ†’ html ํด๋ž˜์Šค โ†’ @theme โ†’ font-sans ํด๋ž˜์Šค."

Q3. shadcn/ui ๊ฐ€ ๊ธฐ์กด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(@mui/material, @chakra-ui ๋“ฑ)์™€ ๋‹ค๋ฅธ ํ•ต์‹ฌ ์ฐจ์ด์ ์€?

โœ… ์ •๋‹ต: shadcn/ui ๋Š” ์„ค์น˜ ํ›„ ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๋ฅผ ํ”„๋กœ์ ํŠธ ์•ˆ์— ์ง์ ‘ ๋ณต์‚ฌํ•ด์„œ ์™„์ „ํ•œ ์†Œ์œ ๊ถŒ์„ ๊ฐ€์ง„๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—…๋ฐ์ดํŠธ์˜ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๊ณ , ์ž์œ ๋กญ๊ฒŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • MUI, Chakra UI ๋Š” node_modules ์— ์žˆ์–ด์„œ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†๊ณ , ์—…๋ฐ์ดํŠธ ์‹œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๊ทธ๋Œ€๋กœ ๋ฐ›์•„์•ผ ํ•ด.
  • shadcn/ui ๋Š” npx shadcn@latest add button ์„ ํ•˜๋ฉด components/ui/button.tsx ํŒŒ์ผ์ด ๋‚ด ํ”„๋กœ์ ํŠธ์— ์ƒ์„ฑ๋ผ. ์ด ํŒŒ์ผ์€ ๋‚ด ๊ฒƒ์ด๋ผ ๋งˆ์Œ๋Œ€๋กœ ์ˆ˜์ • ๊ฐ€๋Šฅ.
  • ๋‹จ์ : ์ƒˆ ๋ฒ„์ „์˜ ๋ฒ„๊ทธ ์ˆ˜์ •์ด๋‚˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€๋ฅผ ์ž๋™์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์—†์–ด. ์ˆ˜๋™์œผ๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ ์ง์ ‘ ์ˆ˜์ •ํ•ด์•ผ ํ•ด.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "shadcn = ๋ ˆ์‹œํ”ผ ์ œ๊ณต. ์š”๋ฆฌ๋Š” ๋„ค๊ฐ€ ํ•ด. MUI = ์™„์„ฑ ์š”๋ฆฌ ๋ฐฐ๋‹ฌ."

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜๋กœ Tailwind ์‹œ๋ฆฌ์ฆˆ๊ฐ€ ์ง„์งœ ๋๋‚ฌ๋‹ค. ์ฒ˜์Œ 1์žฅ์—์„œ "ํด๋ž˜์Šค๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์•„์„œ ๋ถˆํŽธํ•ด์š”" ๋ผ๊ณ  ํˆฌ๋œ๊ฑฐ๋ฆฌ๋˜ ๋‚ด๊ฐ€, ์ด์ œ๋Š” cva, cn(), @theme, next-themes, shadcn/ui ๊นŒ์ง€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค.

Next.js ์—ฐ๋™ ํŒŒํŠธ๊ฐ€ ์‚ฌ์‹ค ์ œ์ผ ํ—ท๊ฐˆ๋ ธ๋Š”๋ฐ, ์˜ํ˜ธ ๋‹˜์ด globals.css ์ž„ํฌํŠธ ์œ„์น˜, next/font CSS ๋ณ€์ˆ˜ ์—ฐ๊ฒฐ, suppressHydrationWarning ์ด์œ ๊นŒ์ง€ ํ•˜๋‚˜์”ฉ ์„ค๋ช…ํ•ด์ฃผ๋‹ˆ๊นŒ ํผ์ฆ์ด ๋งž์ถฐ์ง€๋Š” ๋А๋‚Œ์ด์—ˆ๋‹ค.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "Next.js + Tailwind ์˜ ํ•ต์‹ฌ์€ '์„œ๋ฒ„์—์„œ ์ตœ๋Œ€ํ•œ, ํด๋ผ์ด์–ธํŠธ๋Š” ์ตœ์†Œํ•œ'. hover/focus ๋Š” CSS ๋กœ, ์‹ค์ œ ์ƒํƒœ ๋ณ€ํ™”๋งŒ use client ๋กœ."

์ด์ œ ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฐฐ์šด ๊ฑธ ์ „๋ถ€ ์ ์šฉํ•ด๋ณผ ์ˆ˜ ์žˆ๊ฒ ๋‹ค. ์˜ํ˜ธ ๋‹˜ํ•œํ…Œ ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ ๋ฐ”ํƒ•์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ PR ์˜ฌ๋ฆฌ๋ฉด, ๋“œ๋””์–ด "์ž˜ ๋๋„ค์š”" ๋ผ๋Š” ๋ง ๋“ค์„ ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ? ๋‘๊ทผ๋‘๊ทผํ•˜๋ฉด์„œ ๊ธฐ๋Œ€๋„ ๋œ๋‹ค. ๋‚ด์ผ๋„ ํŒŒ์ดํŒ…! ๐Ÿš€


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ