Next.js 학습 6-9일차 핵심 정리

짱효·2025년 10월 1일

Next.js 학습 6-9일차 핵심 정리

6일차: CSS 통합 + 상태 관리 + 스타일링

🎨 CSS Modules

// 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 CSS

// 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()
  // 깔끔!
}

7일차: Error Handling + 에러 페이지

⚠️ error.tsx

// 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>
  )
}

🔍 not-found.tsx

// 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>
}

⏳ loading.tsx

// 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

8일차: Loading UI + Streaming + Virtual DOM

🌊 Streaming (Suspense)

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의 장점:

  • 전체 페이지를 기다리지 않음
  • 준비된 부분부터 보여줌
  • 사용자 경험 향상

🔍 Virtual DOM

// 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>
  )
}

9일차: Parallel Routes + Reconciliation + 복잡한 라우팅

🔀 Parallel Routes

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 장점:

  • 여러 페이지를 동시에 렌더링
  • 각각 독립적인 로딩/에러 처리
  • 대시보드, 소셜 피드에 유용

🔄 Reconciliation

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)
}, [])

🎭 인터셉팅 라우트 (Intercepting Routes)

폴더 이름 규칙:

(.)      - 같은 레벨
(..)     - 한 단계 위
(..)(..) - 두 단계 위
(...)    - 루트부터

구조:

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>
  )
}

동작 방식:

  • 피드에서 링크 클릭 → 모달로 열림
  • URL 직접 입력 → 전체 페이지로 열림

🔧 컴포넌트 공유 패턴

// 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>
  )
}

💡 6-9일차 핵심 정리

필수 개념

  1. CSS Modules / Tailwind - 스타일링 방법 선택
  2. error.tsx / not-found.tsx - 에러 처리 필수
  3. Suspense - 점진적 로딩으로 UX 개선
  4. Parallel Routes - 복잡한 레이아웃 구현
  5. 인터셉팅 라우트 - 모달 라우팅 패턴

실무 팁

  • Suspense로 코드 스플리팅
  • 스켈레톤 UI로 로딩 개선
  • Key 올바르게 사용
  • 공통 컴포넌트 분리로 코드 재사용
  • memo, useMemo, useCallback으로 최적화

파일 구조 예시

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의 고급 기능들을 마스터했습니다! 🚀

profile
✨🌏확장해 나가는 프론트엔드 개발자입니다✏️

0개의 댓글