10-12일 ] Next.js 학습(Intercepting Routes, 리렌더링 최적화, Middlewar, 인증/권한

짱효·2025년 10월 6일

Next.js 학습 10-12일차 완벽 가이드

10일차: Intercepting Routes + Key 최적화 + 모달 라우팅

🎭 Intercepting Routes 완벽 정복

폴더 명명 규칙

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

기본 구조

app/
├── feed/
│   ├── page.tsx              # /feed
│   └── (..)photo/
│       └── [id]/
│           └── page.tsx      # 모달
└── photo/
    └── [id]/
        └── page.tsx          # 전체 페이지

실전 구현: 이커머스 상품 모달

// app/products/page.tsx - 상품 목록
import Link from 'next/link'

export default function ProductsPage() {
  const products = [
    { id: 1, name: '노트북', price: 1500000, image: '/products/1.jpg' },
    { id: 2, name: '마우스', price: 50000, image: '/products/2.jpg' },
    { id: 3, name: '키보드', price: 120000, image: '/products/3.jpg' },
  ]

  return (
    <div className="container mx-auto p-6">
      <h1 className="text-3xl font-bold mb-8">상품 목록</h1>
      <div className="grid grid-cols-3 gap-6">
        {products.map(product => (
          <Link 
            key={product.id}
            href={`/products/${product.id}`}
            className="border rounded-lg overflow-hidden hover:shadow-xl transition"
          >
            <img 
              src={product.image} 
              alt={product.name}
              className="w-full h-48 object-cover"
            />
            <div className="p-4">
              <h2 className="font-semibold text-lg">{product.name}</h2>
              <p className="text-blue-600 font-bold">
                {product.price.toLocaleString()}</p>
            </div>
          </Link>
        ))}
      </div>
    </div>
  )
}
// components/ProductDetail.tsx - 공통 컴포넌트
export default async function ProductDetail({ id }) {
  const product = await fetch(`/api/products/${id}`).then(r => r.json())

  return (
    <div className="space-y-6">
      <div className="grid grid-cols-2 gap-6">
        <img 
          src={product.image} 
          alt={product.name}
          className="w-full rounded-lg"
        />
        <div className="space-y-4">
          <h1 className="text-2xl font-bold">{product.name}</h1>
          <p className="text-3xl font-bold text-blue-600">
            {product.price.toLocaleString()}</p>
          <p className="text-gray-700">{product.description}</p>
          
          <div className="space-y-2">
            <button className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600">
              장바구니 담기
            </button>
            <button className="w-full border-2 border-blue-500 text-blue-500 py-3 rounded-lg hover:bg-blue-50">
              바로 구매
            </button>
          </div>
        </div>
      </div>
      
      {/* 리뷰 섹션 */}
      <div className="border-t pt-6">
        <h2 className="text-xl font-bold mb-4">리뷰 ({product.reviews.length})</h2>
        <div className="space-y-4">
          {product.reviews.slice(0, 3).map(review => (
            <div key={review.id} className="border-b pb-4">
              <div className="flex items-center gap-2 mb-2">
                <div className="text-yellow-500">{'★'.repeat(review.rating)}</div>
                <span className="text-sm text-gray-600">{review.author}</span>
              </div>
              <p className="text-gray-700">{review.content}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
// app/products/(.).[id]/page.tsx - 모달 (인터셉팅)
'use client'

import { useRouter } from 'next/navigation'
import ProductDetail from '@/components/ProductDetail'
import Link from 'next/link'

export default function ProductModal({ params }) {
  const router = useRouter()

  return (
    <>
      {/* 배경 오버레이 */}
      <div 
        className="fixed inset-0 bg-black/60 z-50"
        onClick={() => router.back()}
      />
      
      {/* 모달 컨테이너 */}
      <div className="fixed inset-0 z-50 overflow-y-auto">
        <div className="flex min-h-full items-center justify-center p-4">
          <div 
            className="bg-white rounded-lg w-full max-w-4xl max-h-[90vh] overflow-y-auto"
            onClick={(e) => e.stopPropagation()}
          >
            {/* 모달 헤더 */}
            <div className="sticky top-0 bg-white border-b px-6 py-4 flex justify-between items-center">
              <h2 className="text-lg font-semibold">상품 상세</h2>
              <div className="flex gap-3">
                <Link 
                  href={`/products/${params.id}`}
                  className="text-sm text-blue-500 hover:underline"
                >
                  전체 페이지로 보기 →
                </Link>
                <button 
                  onClick={() => router.back()}
                  className="text-2xl text-gray-400 hover:text-gray-600"
                >
                  ×
                </button>
              </div>
            </div>
            
            {/* 모달 본문 */}
            <div className="p-6">
              <ProductDetail id={params.id} />
            </div>
          </div>
        </div>
      </div>
    </>
  )
}
// app/products/[id]/page.tsx - 전체 페이지
import ProductDetail from '@/components/ProductDetail'
import Link from 'next/link'

export default function ProductPage({ params }) {
  return (
    <div className="container mx-auto p-6 max-w-6xl">
      <nav className="mb-6">
        <Link href="/products" className="text-blue-500 hover:underline">
          ← 상품 목록으로
        </Link>
      </nav>
      
      <ProductDetail id={params.id} />
      
      {/* 전체 페이지만의 추가 정보 */}
      <div className="mt-12 space-y-12">
        <section>
          <h2 className="text-2xl font-bold mb-6">상세 정보</h2>
          <div className="prose max-w-none">
            {/* 상세 설명 */}
          </div>
        </section>
        
        <section>
          <h2 className="text-2xl font-bold mb-6">배송 정보</h2>
          {/* 배송 정보 */}
        </section>
        
        <section>
          <h2 className="text-2xl font-bold mb-6">추천 상품</h2>
          {/* 추천 상품 */}
        </section>
      </div>
    </div>
  )
}

🔑 Key Prop 최적화

Key가 중요한 이유

// ❌ index를 key로 사용
function BadList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item.name}</li>
      ))}
    </ul>
  )
}

// 문제 상황:
// 초기: ['A', 'B', 'C']
// 맨 앞에 'D' 추가: ['D', 'A', 'B', 'C']
// React는 모든 항목을 업데이트함!
// index 0: A → D (업데이트)
// index 1: B → A (업데이트)
// index 2: C → B (업데이트)
// index 3: 없음 → C (생성)
// ✅ 고유한 ID를 key로 사용
function GoodList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

// 동작:
// 초기: [{ id: 'a', name: 'A' }, { id: 'b', name: 'B' }]
// 'D' 추가: [{ id: 'd', name: 'D' }, { id: 'a', name: 'A' }]
// React는 'd'만 새로 생성하고 나머지는 재사용!

실전 예제: 정렬 가능한 리스트

'use client'

import { useState } from 'react'

export default function SortableList() {
  const [items, setItems] = useState([
    { id: '1', name: '사과', price: 2000 },
    { id: '2', name: '바나나', price: 3000 },
    { id: '3', name: '오렌지', price: 2500 },
  ])
  
  const [sortBy, setSortBy] = useState('name')

  const sortedItems = [...items].sort((a, b) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name)
    }
    return a.price - b.price
  })

  return (
    <div className="p-6">
      <div className="mb-4 space-x-2">
        <button 
          onClick={() => setSortBy('name')}
          className={`px-4 py-2 rounded ${
            sortBy === 'name' ? 'bg-blue-500 text-white' : 'bg-gray-200'
          }`}
        >
          이름순
        </button>
        <button 
          onClick={() => setSortBy('price')}
          className={`px-4 py-2 rounded ${
            sortBy === 'price' ? 'bg-blue-500 text-white' : 'bg-gray-200'
          }`}
        >
          가격순
        </button>
      </div>
      
      <ul className="space-y-2">
        {sortedItems.map(item => (
          <li 
            key={item.id}  // ✅ 고유한 ID 사용
            className="border p-4 rounded-lg flex justify-between"
          >
            <span>{item.name}</span>
            <span className="font-bold">{item.price}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}

Key 선택 가이드

// ✅ 좋은 Key
- 데이터베이스 ID
- UUID
- 안정적인 고유 식별자

// ⚠️ 주의해서 사용
- 배열 인덱스 (항목이 추가/삭제/재정렬되지 않을 때만)

// ❌ 절대 사용 금지
- Math.random()
- Date.now()
- crypto.randomUUID() (매 렌더링마다 새로 생성)

🎨 모달 라우팅 고급 패턴

1. 다단계 모달

app/
├── dashboard/
│   ├── page.tsx
│   └── (..)settings/
│       ├── page.tsx              # 설정 모달
│       └── (..)profile/
│           └── page.tsx          # 프로필 모달 (모달 위의 모달)

2. 모달 + Parallel Routes

app/
├── shop/
│   ├── layout.tsx
│   ├── @modal/
│   │   └── (..)product/
│   │       └── [id]/
│   │           └── page.tsx
│   └── products/
│       └── page.tsx
// app/shop/layout.tsx
export default function ShopLayout({ children, modal }) {
  return (
    <>
      {children}
      {modal}
    </>
  )
}

3. 조건부 모달

'use client'

import { useSearchParams, useRouter } from 'next/navigation'

export default function ConditionalModal() {
  const searchParams = useSearchParams()
  const router = useRouter()
  const showModal = searchParams.get('modal') === 'true'

  if (!showModal) return null

  return (
    <div className="fixed inset-0 bg-black/50 z-50">
      <div className="bg-white p-6 rounded-lg">
        <button onClick={() => router.back()}>닫기</button>
        <h2>조건부 모달</h2>
      </div>
    </div>
  )
}

11일차: Route Groups + 리렌더링 최적화 + 라우트 구조

📁 Route Groups

기본 개념

app/
├── (marketing)/          # URL에 포함 안됨
│   ├── about/
│   │   └── page.tsx      # /about
│   └── contact/
│       └── page.tsx      # /contact
├── (shop)/
│   ├── products/
│   │   └── page.tsx      # /products
│   └── cart/
│       └── page.tsx      # /cart
└── (dashboard)/
    └── analytics/
        └── page.tsx      # /analytics

특징:

  • () 안의 이름은 URL에 포함되지 않음
  • 논리적 그룹화만 제공
  • 각 그룹마다 다른 레이아웃 가능

실전 예제: 멀티 레이아웃

app/
├── (auth)/
│   ├── layout.tsx        # 인증 레이아웃
│   ├── login/
│   │   └── page.tsx      # /login
│   └── signup/
│       └── page.tsx      # /signup
├── (dashboard)/
│   ├── layout.tsx        # 대시보드 레이아웃
│   ├── page.tsx          # /dashboard
│   └── analytics/
│       └── page.tsx      # /analytics
└── (marketing)/
    ├── layout.tsx        # 마케팅 레이아웃
    ├── page.tsx          # /
    ├── about/
    │   └── page.tsx      # /about
    └── pricing/
        └── page.tsx      # /pricing
// app/(auth)/layout.tsx - 인증 페이지 레이아웃
export default function AuthLayout({ children }) {
  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
      <div className="flex items-center justify-center min-h-screen p-4">
        <div className="bg-white rounded-lg shadow-xl p-8 w-full max-w-md">
          <div className="text-center mb-6">
            <h1 className="text-2xl font-bold">My App</h1>
          </div>
          {children}
        </div>
      </div>
    </div>
  )
}

// app/(dashboard)/layout.tsx - 대시보드 레이아웃
export default function DashboardLayout({ children }) {
  return (
    <div className="min-h-screen bg-gray-100">
      <nav className="bg-white shadow">
        <div className="container mx-auto px-6 py-4">
          <div className="flex justify-between items-center">
            <h1 className="text-xl font-bold">Dashboard</h1>
            <div className="flex gap-4">
              <a href="/analytics">Analytics</a>
              <a href="/settings">Settings</a>
            </div>
          </div>
        </div>
      </nav>
      <main className="container mx-auto p-6">
        {children}
      </main>
    </div>
  )
}

// app/(marketing)/layout.tsx - 마케팅 레이아웃
export default function MarketingLayout({ children }) {
  return (
    <div>
      <header className="bg-white shadow">
        <nav className="container mx-auto px-6 py-4">
          <div className="flex justify-between items-center">
            <h1 className="text-xl font-bold">My App</h1>
            <div className="flex gap-6">
              <a href="/">Home</a>
              <a href="/about">About</a>
              <a href="/pricing">Pricing</a>
              <a href="/login">Login</a>
            </div>
          </div>
        </nav>
      </header>
      <main>{children}</main>
      <footer className="bg-gray-800 text-white py-8 mt-12">
        <div className="container mx-auto px-6 text-center">
          © 2024 My App. All rights reserved.
        </div>
      </footer>
    </div>
  )
}

Route Groups + Parallel Routes

app/
├── (shop)/
│   ├── layout.tsx
│   ├── @modal/
│   │   └── default.tsx
│   └── products/
│       ├── page.tsx
│       └── (.).[id]/
│           └── page.tsx

⚡ 리렌더링 최적화

1. React.memo 심화

// ❌ 얕은 비교로는 감지 못함
const MemoComponent = React.memo(function Component({ user, onClick }) {
  return (
    <div onClick={onClick}>
      {user.name}
    </div>
  )
})

function Parent() {
  const [count, setCount] = useState(0)
  
  // 문제: 매번 새로운 객체/함수 생성
  const user = { name: 'John', age: 30 }
  const handleClick = () => console.log('clicked')
  
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <MemoComponent user={user} onClick={handleClick} />
    </>
  )
}

// ✅ 해결: 커스텀 비교 함수
const MemoComponent = React.memo(
  function Component({ user, onClick }) {
    return <div onClick={onClick}>{user.name}</div>
  },
  (prevProps, nextProps) => {
    // true: 리렌더링 스킵
    // false: 리렌더링
    return prevProps.user.id === nextProps.user.id
  }
)

2. 컴포넌트 분리 패턴

// ❌ 모든 것이 함께 리렌더링
function BadComponent() {
  const [input, setInput] = useState('')
  const [data] = useState(expensiveData)
  
  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <ExpensiveList data={data} /> {/* input 변경시마다 리렌더링! */}
    </div>
  )
}

// ✅ 컴포넌트 분리
function GoodComponent() {
  const [data] = useState(expensiveData)
  
  return (
    <div>
      <SearchInput />
      <ExpensiveList data={data} /> {/* 독립적 */}
    </div>
  )
}

function SearchInput() {
  const [input, setInput] = useState('')
  return <input value={input} onChange={e => setInput(e.target.value)} />
}

3. Children Prop 패턴

// ❌ 상태 변경시 children도 리렌더링
function Wrapper({ children }) {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </div>
  )
}

// 사용
<Wrapper>
  <ExpensiveComponent /> {/* count 변경시 리렌더링! */}
</Wrapper>

// ✅ children은 부모에서 생성되므로 리렌더링 안됨
function Parent() {
  return (
    <Wrapper>
      <ExpensiveComponent /> {/* 리렌더링 안됨! */}
    </Wrapper>
  )
}

4. useMemo vs memo 선택

// useMemo: 값 메모이제이션
function Component({ items }) {
  const total = useMemo(() => {
    return items.reduce((sum, item) => sum + item.price, 0)
  }, [items])
  
  return <div>{total}</div>
}

// React.memo: 컴포넌트 메모이제이션
const MemoizedComponent = React.memo(Component)

// 선택 기준:
// - 값 재사용 → useMemo
// - 컴포넌트 리렌더링 방지 → React.memo

🏗️ 라우트 구조 체계화

추천 구조

app/
├── (auth)/                  # 인증 관련
│   ├── login/
│   ├── signup/
│   └── forgot-password/
│
├── (marketing)/             # 마케팅 페이지
│   ├── page.tsx            # 홈
│   ├── about/
│   ├── pricing/
│   └── contact/
│
├── (app)/                   # 메인 앱
│   ├── dashboard/
│   ├── profile/
│   └── settings/
│
├── api/                     # API 라우트
│   ├── auth/
│   ├── users/
│   └── products/
│
└── (admin)/                 # 관리자
    ├── users/
    ├── orders/
    └── analytics/

12일차: Middleware + StrictMode + 인증/권한

🛡️ Middleware

기본 개념

// middleware.ts (프로젝트 루트)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  console.log('Middleware 실행:', request.nextUrl.pathname)
  
  // 요청 계속 진행
  return NextResponse.next()
}

// 특정 경로에만 적용
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/:path*',
  ]
}

인증 미들웨어

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value
  const { pathname } = request.nextUrl

  // 보호된 경로
  const protectedPaths = ['/dashboard', '/profile', '/settings']
  const isProtectedPath = protectedPaths.some(path => 
    pathname.startsWith(path)
  )

  // 인증 필요한 경로인데 토큰 없음
  if (isProtectedPath && !token) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(loginUrl)
  }

  // 이미 로그인했는데 로그인 페이지 접근
  if (pathname === '/login' && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
    '/login',
  ]
}

권한 체크 미들웨어

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyToken } from './lib/auth'

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value
  const { pathname } = request.nextUrl

  // 토큰 검증
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  try {
    const user = await verifyToken(token)

    // 관리자 전용 경로
    if (pathname.startsWith('/admin') && user.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', request.url))
    }

    // 헤더에 사용자 정보 추가 (서버 컴포넌트에서 사용)
    const requestHeaders = new Headers(request.headers)
    requestHeaders.set('x-user-id', user.id)
    requestHeaders.set('x-user-role', user.role)

    return NextResponse.next({
      request: {
        headers: requestHeaders,
      }
    })
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*']
}

IP 기반 제한

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const ALLOWED_IPS = ['127.0.0.1', '192.168.1.1']
const BLOCKED_IPS = ['1.2.3.4']

export function middleware(request: NextRequest) {
  const ip = request.ip || request.headers.get('x-forwarded-for')

  // IP 차단
  if (BLOCKED_IPS.includes(ip)) {
    return new NextResponse('Blocked', { status: 403 })
  }

  // 관리자 페이지는 특정 IP만
  if (request.nextUrl.pathname.startsWith('/admin')) {
    if (!ALLOWED_IPS.includes(ip)) {
      return new NextResponse('Unauthorized', { status: 401 })
    }
  }

  return NextResponse.next()
}

A/B 테스트 미들웨어

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 쿠키 확인
  let bucket = request.cookies.get('bucket')?.value

  // 쿠키 없으면 랜덤 할당
  if (!bucket) {
    bucket = Math.random() < 0.5 ? 'A' : 'B'
  }

  // 버전별 라우팅
  const url = request.nextUrl.clone()
  url.pathname = `/experiments/${bucket}${url.pathname}`

  const response = NextResponse.rewrite(url)
  
  // 쿠키 설정
  response.cookies.set('bucket', bucket)

  return response
}

export const config = {
  matcher: '/landing-page'
}

⚛️ React.StrictMode 활용

기본 설정

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {process.env.NODE_ENV === 'development' ? (
          <React.StrictMode>
            {children}
          </React.StrictMode>
        ) : (
          children
        )}
      </body>
    </html>
  )
}

StrictMode가 감지하는 것들

1. 예상치 못한 부작용

// ❌ StrictMode가 경고
function Component() {
  // 렌더링 중 외부 상태 변경
  someGlobalVariable = 'changed'
  
  return <div>Component</div>
}

// ✅ 올바른 방법
function Component() {
  useEffect(() => {
    someGlobalVariable = 'changed'
  }, [])
  
  return <div>Component</div>
}

2. 레거시 생명주기

// ❌ 더 이상 사용 안 함
class Component extends React.Component {
  componentWillMount() {
    // 경고 발생!
  }
  
  componentWillReceiveProps() {
    // 경고 발생!
  }
}

// ✅ 권장
function Component() {
  useEffect(() => {
    // 초기화 로직
  }, [])
}

3. 이중 렌더링

// StrictMode에서는 컴포넌트가 2번 렌더링됨
function Component() {
  console.log('렌더링!')  // 개발 모드: 2번 출력
  return <div>Component</div>
}

// 이유: 순수성 체크
// 프로덕션에서는 1번만 실행됨

🔐 인증/권한 실전 패턴

1. 서버 컴포넌트에서 인증 체크

// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { verifyToken } from '@/lib/auth'

export default async function DashboardPage() {
  const cookieStore = cookies()
  const token = cookieStore.get('token')?.value

  if (!token) {
    redirect('/login')
  }

  try {
    const user = await verifyToken(token)
    
    return (
      <div>
        <h1>환영합니다, {user.name}!</h1>
      </div>
    )
  } catch (error) {
    redirect('/login')
  }
}

2. 고차 컴포넌트 패턴

// components/withAuth.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { verifyToken } from '@/lib/auth'

export function withAuth(Component, options = {}) {
  return async function AuthenticatedComponent(props) {
    const cookieStore = cookies()
    const token = cookieStore.get('token')?.value

    if (!token) {
      redirect('/login')
    }

    const user = await verifyToken(token)

    // 권한 체크
    if (options.requiredRole && user.role !== options.requiredRole) {
      redirect('/unauthorized')
    }

    return <Component {...props} user={user} />
  }
}

// 사용
export default withAuth(DashboardPage, { requiredRole: 'admin' })

3. 권한 기반 UI

// components/RoleGuard.tsx
import { cookies } from 'next/headers'
import { verifyToken } from '@/lib/auth'

export async function RoleGuard({ 
  children, 
  allowedRoles,
  fallback = null 
}) {
  const cookieStore = cookies()
  const token = cookieStore.get('token')?.value

  if (!token) return fallback

  try {
    const user = await verifyToken(token)
    
    if (!allowedRoles.includes(user.role)) {
      return fallback
    }
    
    return children
  } catch {
    return fallback
  }
}

// 사용
export default function Page() {
  return (
    <div>
      <h1>대시보드</h1>
      
      <RoleGuard allowedRoles={['admin']}>
        <AdminPanel />
      </RoleGuard>
      
      <RoleGuard 
        allowedRoles={['admin', 'editor']}
        fallback={<p>권한이 없습니다</p>}
      >
        <EditorPanel />
      </RoleGuard>
    </div>
  )
}

💡 10-12일차 핵심 정리

필수 개념

  1. Intercepting Routes - 모달 라우팅 패턴
  2. Key Prop - 고유 ID 사용 필수
  3. Route Groups - 논리적 그룹화
  4. Middleware - 인증/권한 체크
  5. StrictMode - 개발 중 버그 조기 발견

최적화 원칙

  • Key는 항상 고유한 ID 사용
  • React.memo는 측정 후 적용
  • 컴포넌트 분리로 리렌더링 최소화
  • Children prop 패턴 활용

실무 패턴

  • 공통 컴포넌트로 코드 재사용
  • Middleware로 보안 강화
  • Route Groups로 구조화
  • StrictMode로 품질 관리

10-12일차 완료! 🚀

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

0개의 댓글