완전한 Next.js 성능 및 SEO 최적화 가이드

발행: (2025년 12월 16일 오전 04:27 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

내장 컴포넌트 최적화

Next.js는 자동으로 성능 모범 사례를 적용하는 최적화된 내장 컴포넌트를 제공하여 수동 최적화가 필요 없게 하고 일반적인 함정을 줄여줍니다.

next/image

Image 컴포넌트는 Next.js에서 가장 강력한 성능 도구 중 하나입니다. 다음을 제공합니다:

  • 자동 레이지 로딩
  • 디바이스 기반 이미지 리사이징
  • 포맷 최적화 (WebP, AVIF)
  • 누적 레이아웃 이동(CLS) 방지
  • 대역폭 사용량 감소

기본 구현

import Image from 'next/image';

export default function ProductCard() {
  return (
    
      
    
  );
}

고급 설정 (next.config.js)

module.exports = {
  images: {
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    domains: ['example.com', 'cdn.example.com'], // 허용된 외부 도메인
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
        pathname: '/images/**',
      },
    ],
    formats: ['image/webp', 'image/avif'],
    minimumCacheTTL: 60,
    dangerouslyAllowSVG: true,
    contentSecurityPolicy:
      "default-src 'self'; script-src 'none'; sandbox;",
  },
};

유의 사항

  • 화면에 바로 보이는 이미지에는 priority={true}를 사용합니다.
  • 레이아웃 이동을 방지하려면 항상 widthheight를 지정합니다.
  • 반응형 컨테이너에는 fill prop을 사용합니다.
  • 외부 이미지는 도메인 화이트리스트가 필요합니다.
  • WebP 포맷은 JPEG보다 약 30 % 작은 파일 크기를 제공합니다.

전체 페이지 새로고침 없이 클라이언트‑사이드 네비게이션을 가능하게 합니다.

import Link from 'next/link';

export default function Navigation() {
  return (
    
      About
      Blog
    
  );
}

유의 사항

  • 방문 가능성이 낮은 페이지 링크에는 prefetch={false}를 설정합니다.

스크립트 로딩 전략

서드파티 스크립트 로딩을 최적화하면 메인 스레드 차단을 방지하고 페이지 로드 성능을 향상시킵니다. Script 컴포넌트를 사용하면 언제, 어떻게 스크립트를 로드할지 세밀하게 제어할 수 있습니다.

중요한 스크립트 – beforeInteractive

Next.js 코드와 페이지 하이드레이션 이전에 로드됩니다. 사용자 인터랙션 전에 반드시 실행돼야 하는 스크립트에 사용합니다.

import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    
      
        
        {children}
      
    
  );
}

분석 및 비핵심 스크립트 – afterInteractive

페이지가 인터랙티브해진 뒤 로드됩니다. 분석, 광고 등 필수적이지 않은 스크립트에 적합합니다.

import Script from 'next/script';

export default function Analytics() {
  return (
     console.log('Analytics loaded')}
    />
  );
}

레이지 로드 스크립트 – lazyOnload

브라우저가 유휴 상태일 때 로드됩니다. 채팅 위젯, 소셜 미디어 버튼 등 비핵심 스크립트에 이상적입니다.

import Script from 'next/script';

export default function ChatWidget() {
  return (
     console.log('Chat widget ready')}
    />
  );
}

워커 전략 – worker

웹 워커에서 스크립트를 실행해 성능을 향상시킵니다.

import Script from 'next/script';

export default function Page() {
  return (
    
  );
}

유의 사항

  • 정말 중요한 스크립트에만 beforeInteractive를 사용합니다.
  • 대부분의 분석 스크립트는 afterInteractive를 사용합니다.
  • 소셜 미디어 위젯과 채팅은 lazyOnload를 사용합니다.
  • 인라인 스크립트보다 onLoad / onReady 콜백을 선호합니다.

의존성 관리

사용되지 않는 의존성을 제거하면 번들 크기가 줄고 빌드 시간이 단축되며 보안 취약점도 최소화됩니다.

사용되지 않는 패키지 식별

# depcheck 전역 설치
npm install -g depcheck   # 또는: yarn global add depcheck

# 프로젝트 루트에서 실행
depcheck

# 옵션 사용
depcheck --ignores="eslint-*,@types/*"

# 디렉터리 지정
depcheck ./src --ignore-dirs=build,dist

예시 출력

Unused dependencies
* lodash
* moment
* axios

Missing dependencies
* react-icons

Unused devDependencies
* @testing-library/jest-dom

자동 정리 스크립트 (package.json)

{
  "scripts": {
    "deps:check": "depcheck",
    "deps:clean": "depcheck --json | jq -r '.dependencies[]' | xargs npm uninstall"
  }
}

유의 사항

  • depcheck를 정기적으로(월 1회 또는 주요 릴리즈 전) 실행합니다.
  • 설정 파일에만 사용되는 패키지는 제거 전에 반드시 확인합니다.
  • @types/*와 같은 타입 패키지는 검색에 나타나지 않을 수 있으니 필요하면 유지합니다.
  • 의존성을 제거한 뒤에는 충분히 테스트합니다.
  • devDependenciesdependencies를 명확히 구분합니다.
  • 겉보기에 사용되지 않는 패키지를 유지하는 이유를 문서화합니다.

캐싱 및 점진적 정적 재생성 (ISR)

스마트 캐싱 전략은 콘텐츠를 더 빠르게 제공하고 서버 부하를 줄이며 데이터를 최신 상태로 유지합니다. ISR은 빌드 이후에도 정적 페이지를 업데이트할 수 있게 해줍니다.

기본 ISR 구현

// app/blog/[slug]/page.js
export const revalidate = 3600; // 매시간 재검증

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({ slug: post.slug }));
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return (
    
      
## {post.title}

      
{post.content}

      {post.publishedAt}
    
  );
}

온‑디맨드 재검증

// app/api/revalidate/route.js
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST(request) {
  const secret = request.nextUrl.searchParams.get('secret');
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
  }

  const path = request.nextUrl.searchParams.get('path');
  if (path) {
    revalidatePath(path);
    return NextResponse.json({ revalidated: true, now: Date.now() });
  }

  return NextResponse.json({ message: 'Missing path' }, { status: 400 });
}

태그 기반 재검증

// Fetch with cache tags
export default async function ProductList() {
  const products = await fetch('https://api.example.com/products', {
    next: { tags: ['products'], revalidate: 3600 }
  });
  return {/* Render products */};
}

// app/api/revalidate-products/route.js
import { revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST() {
  revalidateTag('products');
  return NextResponse.json({ revalidated: true });
}

Cache‑Control 헤더

// app/api/data/route.js
export async function GET() {
  const data = await fetchData();
  return NextResponse.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
    }
  });
}

유의 사항

  • ISR은 블로그, 제품 목록 등 주기적으로 업데이트되는 콘텐츠에 이상적입니다.
  • 자주 변하는 데이터는 짧은 재검증 간격(예: 60 s)을 사용합니다.
  • 세밀한 캐시 무효화를 위해 ISR과 태그 기반 재검증을 결합합니다.

폰트 최적화

Next.js는 내장 next/font API를 사용할 때 폰트를 자동으로 최적화합니다.

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  weight: ['400', '700'],
  variable: '--font-inter',
});

export default function RootLayout({ children }) {
  return (
    
      {children}
    
  );
}

  • 필요한 문자 서브셋만 로드합니다.
  • 요청 수를 줄이기 위해 가변 폰트를 선호합니다.
  • CSS에서 @import를 피하고 next/font API를 사용합니다.

레이지 로딩 및 코드 스플리팅

동적 임포트

import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
  loading: () => 
Loading…
,
  ssr: false, // 클라이언트 사이드에서만 로드
});
  • 차트, 지도 등 큰 라이브러리는 별도 청크로 분리합니다.
  • 브라우저 API에 의존하는 컴포넌트는 ssr: false를 사용합니다.

리스트 가상화

긴 리스트는 react-window 또는 react-virtualized 같은 라이브러리를 사용해 화면에 보이는 항목만 렌더링합니다.

import { FixedSizeList as List } from 'react-window';

export default function VirtualizedList({ items }) {
  return (
    
      {({ index, style }) => (
        {items[index].title}
      )}
    
  );
}

데이터 패칭 최적화

클라이언트‑사이드 패칭 with SWR

import useSWR from 'swr';

const fetcher = url => fetch(url).then(res => res.json());

export default function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher, {
    revalidateOnFocus: false,
    dedupingInterval: 60000,
  });

  if (isLoading) return 
Loading…
;
  if (error) return 
Error loading profile.
;

  return {data.name};
}
  • 자주 업데이트될 필요가 없는 데이터는 revalidateOnFocusfalse로 설정합니다.
  • 요청 중복을 제어하려면 dedupingInterval을 조정합니다.

JavaScript 번들 최적화

  • Tree‑shaking: 필요 없는 전체 모듈 대신 사용되는 함수만 임포트합니다(import { foo } from 'lodash' vs import _ from 'lodash').
  • 번들 분석: next build && next analyze 혹은 webpack-bundle-analyzer 같은 도구를 활용합니다.
  • 자산 압축: CDN이나 Vercel Edge에서 gzip/Brotli 압축을 활성화합니다.

CSS 최적화

  • CSS 모듈이나 styled‑components를 사용해 스타일을 스코프하고 전역 팽창을 방지합니다.
  • purgecss(Next.js에서는 next-purgecss 통합)와 같은 도구로 사용되지 않는 CSS를 제거합니다.
  • 반응형 디자인을 위해 별도 파일보다 @media 쿼리를 선호합니다.

API 라우트 캐싱

비싼 API 응답을 엣지에서 캐시합니다.

// app/api/products/route.js
import { NextResponse } from 'next/server';
import { fetchProducts } from '@/lib/db';

export async function GET() {
  const products = await fetchProducts();
  return NextResponse.json(products, {
    headers: {
      'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=300',
    },
  });
}

Edge Runtime

연산 집약적인 로직을 Edge Runtime에 배포해 지연 시간을 최소화합니다.

// app/api/geo/route.js
export const runtime = 'edge';

export async function GET(request) {
  const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
  // 빠른 지리 위치 조회 수행
  return new Response(`Your IP: ${ip}`);
}

Core Web Vitals

  • Largest Contentful Paint (LCP): 화면 위쪽 콘텐츠를 최적화해 LCP를 낮게 유지합니다.
export default function BlogPost({ post }) {
  return (
    
      {post.title} – My Blog
      
      {/* Post content */}
    
  );
}

보안 최적화

  • API 라우트나 미들웨어에서 Content‑Security‑Policy, X‑Content‑Type‑Options, Referrer‑Policy 헤더를 설정합니다.
  • next-safe 등과 같은 라이브러리를 사용해 사용자 생성 HTML을 정제합니다.
  • 의존성을 최신 상태로 유지하고 npm audit를 정기적으로 실행합니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; img-src https: data:; script-src 'self' 'unsafe-inline';"
  );
  return response;
}
Back to Blog

관련 글

더 보기 »