Skip to content

Latest commit

Β 

History

History
340 lines (266 loc) Β· 6.62 KB

File metadata and controls

340 lines (266 loc) Β· 6.62 KB

⚑ μ„±λŠ₯ μ΅œμ ν™”

πŸ“‹ λͺ©μ°¨


μ„±λŠ₯ μ§€ν‘œ

πŸ“Š 핡심 μ„±κ³Ό

μ§€ν‘œ κ°œμ„  μ „ κ°œμ„  ν›„ ν–₯상λ₯ 
νŽ˜μ΄μ§€ λ‘œλ“œ 127.6초 0.1초 1,276λ°°
API 응닡 500ms 50ms 10λ°°
λ²ˆλ“€ 크기 2MB 500KB 75% κ°μ†Œ
LCP 3.2초 1.8초 44% κ°œμ„ 
FID 120ms 45ms 63% κ°œμ„ 

μ„œλ²„ μ»΄ν¬λ„ŒνŠΈ μ΅œμ ν™”

πŸš€ 1,276λ°° μ„±λŠ₯ ν–₯상 λΉ„κ²°

κΈ°μ‘΄ 방식 (느림)

// ❌ ν΄λΌμ΄μ–ΈνŠΈ β†’ API β†’ DB (127.6초)
'use client'

export default function PostList() {
  const [posts, setPosts] = useState([])
  
  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => setPosts(data))
  }, [])
  
  return <div>{/* λ Œλ”λ§ */}</div>
}

κ°œμ„ λœ 방식 (빠름)

// βœ… μ„œλ²„μ—μ„œ 직접 DB μ ‘κ·Ό (0.1초)
export default async function PostList() {
  const posts = await prisma.mainPost.findMany({
    include: {
      author: true,
      category: true,
      _count: { select: { comments: true, likes: true }}
    }
  })
  
  return <div>{/* μ¦‰μ‹œ λ Œλ”λ§ */}</div>
}

🎯 병렬 데이터 페칭

// 3개 쿼리 λ™μ‹œ μ‹€ν–‰
export default async function DashboardPage() {
  const [posts, users, stats] = await Promise.all([
    prisma.mainPost.findMany({ take: 10 }),
    prisma.user.findMany({ take: 5 }),
    prisma.siteStats.findFirst()
  ])
  
  // 순차 μ‹€ν–‰ λŒ€λΉ„ 3λ°° 빠름
  return <Dashboard data={{ posts, users, stats }} />
}

λ²ˆλ“€ μ‚¬μ΄μ¦ˆ μ΅œμ ν™”

πŸ“¦ 75% 크기 κ°μ†Œ μ „λž΅

1. 동적 μž„ν¬νŠΈ

// ν•„μš”ν•  λ•Œλ§Œ λ‘œλ“œ
const SearchModal = lazy(() => import('@/components/search/SearchModal'))

// μ‚¬μš©μžκ°€ 클릭할 λ•Œ λ‘œλ“œ
{isSearchOpen && (
  <Suspense fallback={<Loading />}>
    <SearchModal />
  </Suspense>
)}

2. νŒ¨ν‚€μ§€ μ΅œμ ν™”

// ❌ 전체 μž„ν¬νŠΈ (200KB)
import _ from 'lodash'

// βœ… ν•„μš”ν•œ κ²ƒλ§Œ (5KB)
import debounce from 'lodash/debounce'

3. 이미지 μ΅œμ ν™”

// Next.js Image μ»΄ν¬λ„ŒνŠΈ + WebP/AVIF
<Image
  src={url}
  alt="Post"
  width={800}
  height={400}
  quality={85}
  placeholder="blur"
  formats={['webp', 'avif']}
/>

λ°μ΄ν„°λ² μ΄μŠ€ μ΅œμ ν™”

πŸ—„οΈ 쿼리 μ΅œμ ν™”

1. Select ν•„λ“œ μ œν•œ

// ❌ λͺ¨λ“  ν•„λ“œ κ°€μ Έμ˜€κΈ°
const posts = await prisma.mainPost.findMany()

// βœ… ν•„μš”ν•œ ν•„λ“œλ§Œ
const posts = await prisma.mainPost.findMany({
  select: {
    id: true,
    title: true,
    createdAt: true,
    author: { select: { name: true, image: true }}
  }
})

2. 볡합 인덱슀 ν™œμš©

model MainPost {
  // 자주 μ‘°νšŒν•˜λŠ” 쑰합에 인덱슀
  @@index([status, categoryId, createdAt])
  @@index([authorId, status])
  @@index([slug])
}

3. N+1 문제 ν•΄κ²°

// ❌ N+1 문제 λ°œμƒ
const posts = await prisma.mainPost.findMany()
for (const post of posts) {
  const author = await prisma.user.findUnique({ where: { id: post.authorId }})
}

// βœ… Include둜 ν•œ λ²ˆμ— 쑰회
const posts = await prisma.mainPost.findMany({
  include: { author: true }
})

캐싱 μ „λž΅

πŸš€ Redis 캐싱

1. νŽ˜μ΄μ§€ 캐싱

export async function getPostsByCategory(categoryId: string) {
  const cacheKey = `posts:category:${categoryId}`
  
  // μΊμ‹œ 확인
  const cached = await redis.get(cacheKey)
  if (cached) return JSON.parse(cached)
  
  // DB 쑰회
  const posts = await prisma.mainPost.findMany({
    where: { categoryId },
    take: 20
  })
  
  // μΊμ‹œ μ €μž₯ (1μ‹œκ°„)
  await redis.set(cacheKey, JSON.stringify(posts), 'EX', 3600)
  
  return posts
}

2. 슀마트 μΊμ‹œ λ¬΄νš¨ν™”

// κ²Œμ‹œκΈ€ μ—…λ°μ΄νŠΈ μ‹œ κ΄€λ ¨ μΊμ‹œλ§Œ μ‚­μ œ
async function updatePost(postId: string, data: any) {
  const post = await prisma.mainPost.update({ where: { id: postId }, data })
  
  // κ΄€λ ¨ μΊμ‹œ λ¬΄νš¨ν™”
  await Promise.all([
    redis.del(`post:${postId}`),
    redis.del(`posts:category:${post.categoryId}`),
    redis.del('posts:recent')
  ])
  
  return post
}

🎯 Core Web Vitals μ΅œμ ν™”

LCP (Largest Contentful Paint)

// 1. μ€‘μš” λ¦¬μ†ŒμŠ€ ν”„λ¦¬λ‘œλ“œ
<link rel="preload" href="/fonts/main.woff2" as="font" crossOrigin="" />

// 2. 이미지 μš°μ„ μˆœμœ„
<Image priority src={heroImage} alt="Hero" />

// 3. μ„œλ²„ μ»΄ν¬λ„ŒνŠΈ ν™œμš©
export default async function Hero() {
  const data = await getHeroData() // μ„œλ²„μ—μ„œ μ‹€ν–‰
  return <HeroSection data={data} />
}

FID (First Input Delay)

// 1. μ½”λ“œ μŠ€ν”Œλ¦¬νŒ…
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <Skeleton />,
  ssr: false
})

// 2. 이벀트 ν•Έλ“€λŸ¬ μ΅œμ ν™”
const handleClick = useCallback(() => {
  // μ΅œμ ν™”λœ 둜직
}, [dependency])

CLS (Cumulative Layout Shift)

/* 1. 이미지 크기 λͺ…μ‹œ */
.post-image {
  aspect-ratio: 16/9;
  width: 100%;
}

/* 2. 폰트 λ‘œλ”© μ΅œμ ν™” */
@font-face {
  font-display: swap; /* FOUT λ°©μ§€ */
}

/* 3. μŠ€μΌˆλ ˆν†€ UI */
.skeleton {
  animation: pulse 2s infinite;
}

πŸ“ˆ λͺ¨λ‹ˆν„°λ§ 도ꡬ

Vercel Analytics 톡합

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

μ„±λŠ₯ μΈ‘μ • μ½”λ“œ

// μ»€μŠ€ν…€ μ„±λŠ₯ μΈ‘μ •
export function measurePerformance(name: string, fn: () => Promise<any>) {
  return async (...args: any[]) => {
    const start = performance.now()
    const result = await fn(...args)
    const end = performance.now()
    
    console.log(`${name}: ${(end - start).toFixed(2)}ms`)
    
    return result
  }
}

πŸš€ 배포 μ΅œμ ν™”

Vercel μ„€μ •

{
  "functions": {
    "app/api/*": {
      "maxDuration": 10
    }
  },
  "images": {
    "formats": ["image/avif", "image/webp"],
    "minimumCacheTTL": 31536000
  }
}

λΉŒλ“œ μ΅œμ ν™”

# Turbopack μ‚¬μš© (개발)
npm run dev --turbo

# ν”„λ‘œλ•μ…˜ λΉŒλ“œ
npm run build -- --debug # λΉŒλ“œ 뢄석

# Bundle Analyzer
ANALYZE=true npm run build