// Button.module.css
.button {
padding: 10px 20px;
background-color: blue;
}
.primary {
background-color: green;
}
// Button.jsx
import styles from './Button.module.css'
export default function Button({ variant = 'primary' }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
클릭
</button>
)
}
장점:
// tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: '#3b82f6',
},
},
},
}
// 사용 예시
export default function Button({ variant = 'primary' }) {
return (
<button className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">
버튼
</button>
)
}
// Context로 전역 상태 정리
const CartContext = createContext()
export function CartProvider({ children }) {
const [cart, setCart] = useState([])
const addToCart = (item) => {
setCart(prev => [...prev, item])
}
return (
<CartContext.Provider value={{ cart, addToCart }}>
{children}
</CartContext.Provider>
)
}
export const useCart = () => useContext(CartContext)
// 사용
function ProductPage() {
const { addToCart } = useCart()
// 깔끔!
}
// app/error.tsx - 전역 에러 처리
'use client'
export default function Error({ error, reset }) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">문제가 발생했습니다</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded-lg"
>
다시 시도
</button>
</div>
)
}
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-6xl font-bold mb-4">404</h1>
<h2 className="text-2xl mb-4">페이지를 찾을 수 없습니다</h2>
<Link href="/" className="px-6 py-3 bg-blue-500 text-white rounded-lg">
홈으로 돌아가기
</Link>
</div>
)
}
// 페이지에서 트리거
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const data = await fetchData(params.id)
if (!data) {
notFound() // not-found.tsx 렌더링
}
return <div>{data.title}</div>
}
// app/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
)
}
// 스켈레톤 UI
export default function BlogLoading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-full"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
)
}
app/
├── error.tsx # 전역 에러
├── not-found.tsx # 전역 404
├── loading.tsx # 전역 로딩
└── blog/
├── error.tsx # 블로그 에러
├── loading.tsx # 블로그 로딩
└── [slug]/
└── page.tsx
import { Suspense } from 'react'
// 느린 컴포넌트
async function SlowData() {
await new Promise(resolve => setTimeout(resolve, 3000))
const data = await fetchData()
return <div>{data}</div>
}
// 빠른 컴포넌트
async function FastData() {
const data = await fetchFastData()
return <div>{data}</div>
}
export default function Dashboard() {
return (
<div className="grid grid-cols-2 gap-4">
{/* 빠른 데이터는 즉시 보임 */}
<Suspense fallback={<div>로딩 중...</div>}>
<FastData />
</Suspense>
{/* 느린 데이터는 나중에 보임 */}
<Suspense fallback={<div>로딩 중...</div>}>
<SlowData />
</Suspense>
</div>
)
}
Streaming의 장점:
// React의 동작 방식
// 1. 상태 변경
setCount(count + 1)
// 2. 새로운 Virtual DOM 생성
// 3. 이전 Virtual DOM과 비교 (Diffing)
// 4. 변경된 부분만 실제 DOM에 반영
// 최적화
import { memo } from 'react'
const OptimizedChild = memo(function Child({ data }) {
return <div>{data}</div>
})
// components/Skeleton.jsx
export function SkeletonCard() {
return (
<div className="border rounded-lg p-6 animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
)
}
export function SkeletonList({ count = 3 }) {
return (
<div className="space-y-4">
{Array.from({ length: count }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
)
}
app/
└── dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/
│ └── page.tsx
├── @stats/
│ └── page.tsx
└── @notifications/
└── page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
stats,
notifications
}) {
return (
<div className="p-6">
<div className="grid grid-cols-2 gap-6">
<div>{analytics}</div>
<div>{stats}</div>
</div>
<div>{notifications}</div>
<main>{children}</main>
</div>
)
}
Parallel Routes 장점:
Key의 중요성:
// ❌ 나쁜 예: index를 key로
items.map((item, index) => <div key={index}>{item}</div>)
// ✅ 좋은 예: 고유한 ID 사용
items.map(item => <div key={item.id}>{item}</div>)
성능 최적화:
import { memo, useMemo, useCallback } from 'react'
// 불필요한 리렌더링 방지
const OptimizedComponent = memo(function Component({ data }) {
return <div>{data}</div>
})
// 비용이 큰 계산 메모이제이션
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item, 0)
}, [items])
// 함수 메모이제이션
const handleClick = useCallback(() => {
setCount(c => c + 1)
}, [])
폴더 이름 규칙:
(.) - 같은 레벨
(..) - 한 단계 위
(..)(..) - 두 단계 위
(...) - 루트부터
구조:
app/
├── feed/
│ ├── page.tsx # /feed
│ └── (..)photo/ # photo 인터셉팅
│ └── [id]/
│ └── page.tsx # 모달 버전
└── photo/
└── [id]/
└── page.tsx # 전체 페이지 버전
모달 구현:
// app/feed/(..)photo/[id]/page.tsx - 모달
'use client'
import { useRouter } from 'next/navigation'
export default function PhotoModal({ params }) {
const router = useRouter()
return (
<>
<div
className="fixed inset-0 bg-black/70 z-50"
onClick={() => router.back()}
/>
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50">
<div className="bg-white rounded-lg p-6 w-[600px]">
<button onClick={() => router.back()}>×</button>
<img src={`/photos/${params.id}.jpg`} />
</div>
</div>
</>
)
}
// app/photo/[id]/page.tsx - 전체 페이지
export default function PhotoPage({ params }) {
return (
<div className="container mx-auto p-6">
<h1>사진 {params.id}</h1>
<img src={`/photos/${params.id}.jpg`} />
<p>이것은 전체 페이지입니다</p>
</div>
)
}
동작 방식:
// components/PhotoDetail.tsx - 공통 컴포넌트
export default function PhotoDetail({ id }) {
return (
<div>
<h1>사진 제목</h1>
<img src={`/photos/${id}.jpg`} />
<p>설명...</p>
</div>
)
}
// 모달에서 사용
function PhotoModal({ params }) {
return (
<Modal>
<PhotoDetail id={params.id} />
</Modal>
)
}
// 전체 페이지에서 사용
function PhotoPage({ params }) {
return (
<div>
<PhotoDetail id={params.id} />
</div>
)
}
app/
├── error.tsx
├── not-found.tsx
├── loading.tsx
├── dashboard/
│ ├── layout.tsx
│ ├── @analytics/
│ ├── @stats/
│ └── page.tsx
├── feed/
│ ├── page.tsx
│ └── (..)photo/
│ └── [id]/
│ └── page.tsx
└── photo/
└── [id]/
└── page.tsx
6-9일차 완료! Next.js의 고급 기능들을 마스터했습니다! 🚀