๐ Tailwind 13์ฅ: Next.js + Tailwind ์๋ฒฝ ์ฐ๋
๐ ๊ฐ์
Next.js App Router ํ๊ฒฝ์์ Tailwind ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ํตํฉํ๋ ๋ฒ โ ์ค์น๋ถํฐ next/font, Server Components, shadcn/ui ๊น์ง
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- โ๏ธ ์ค์น์ ์ด๊ธฐ ์ค์
- ๐๏ธ next/font ์ Tailwind ํตํฉ
- ๐ฅ๏ธ Server Components ์์ Tailwind
- ๐ next-themes ๋คํฌ ๋ชจ๋ ํตํฉ
- ๐งฉ shadcn/ui: Tailwind ๊ธฐ๋ฐ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ๐ป ์ค์ : ์์๋ค ์ปค๋ฎค๋ํฐ Next.js ํ๋ก์ ํธ ๊ตฌ์กฐ
- ๐จ Next.js + Tailwind ํํ ํจ์
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- [๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ](#-์์ฒ ์ด์-ํด๊ทผ ์ผ๊ธฐ)
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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 ์ ํตํฉํ ๋๋ ๋ช ๊ฐ์ง ๊ณ ๋ คํ ์ ์ด ์์ด:
- ํฐํธ ์ต์ ํ:
next/font์ CSS ๋ณ์๋ฅผ ํตํด Tailwind ํฐํธ ์ ํธ๋ฆฌํฐ์ ์ฐ๊ฒฐ - ๋คํฌ ๋ชจ๋: Server/Client Component ๊ฒฝ๊ณ์์ ์์ ํ๊ฒ ์ฒ๋ฆฌ
- CSS ํ์ผ ์์น: App Router ์์๋
app/globals.css์ ์ํฌํธ - 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,autoprefixertailwind.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 + suppressHydrationWarning | mounted ์ฒดํฌ |
| shadcn/ui | npx shadcn@latest add | ์ฝ๋ ์ง์ ์์ |
| Server Component | hover ๋ 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 ์ ๋ฑ๋กํ๋ค.
๐ก ์์ธ ํด์ค:
next/font์ค์ :variable: '--font-noto-sans-kr'- HTML ์ ๋ณ์ ์ฃผ์
:
<html className={notoSansKr.variable}> @theme๋ฑ๋ก:--font-sans: var(--font-noto-sans-kr), sans-serif;- ์ฌ์ฉ:
<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 ์ฌ๋ฆฌ๋ฉด, ๋๋์ด "์ ๋๋ค์" ๋ผ๋ ๋ง ๋ค์ ์ ์์ง ์์๊น? ๋๊ทผ๋๊ทผํ๋ฉด์ ๊ธฐ๋๋ ๋๋ค. ๋ด์ผ๋ ํ์ดํ ! ๐