🌐 웹개발

Next.js

넥스트JS

React 기반 풀스택 프레임워크. SSR, SSG, API Routes 지원.

📖 상세 설명

Next.js는 Vercel에서 개발한 React 기반 풀스택 프레임워크입니다. 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 증분 정적 재생성(ISR) 등 다양한 렌더링 전략을 지원하여 SEO 최적화와 초기 로딩 성능을 크게 개선합니다. 파일 기반 라우팅으로 복잡한 설정 없이 페이지를 구성할 수 있습니다.

Next.js 13부터 도입된 App Router는 React Server Components를 네이티브로 지원합니다. 서버 컴포넌트는 기본적으로 서버에서만 실행되어 클라이언트 번들 크기를 줄이고, 데이터베이스나 파일 시스템에 직접 접근할 수 있습니다. 클라이언트 상호작용이 필요한 경우에만 'use client' 지시어로 클라이언트 컴포넌트를 사용합니다.

API Routes(또는 Route Handlers)를 통해 백엔드 API를 같은 프로젝트 내에서 구현할 수 있어 풀스택 개발이 가능합니다. 데이터베이스 연결, 인증, 외부 API 호출 등을 별도 백엔드 서버 없이 처리할 수 있습니다. 또한 Edge Runtime을 지원하여 전 세계 CDN 엣지에서 API와 미들웨어를 실행할 수 있습니다.

Next.js는 이미지 최적화(next/image), 폰트 최적화(next/font), 스크립트 최적화(next/script) 등 프로덕션 최적화 기능을 내장하고 있습니다. Turbopack(Rust 기반 번들러)을 통해 개발 서버 시작 속도와 HMR(Hot Module Replacement) 성능이 크게 향상되었습니다.

💻 코드 예제

// === App Router 구조 (Next.js 13+) ===

// app/layout.tsx - 루트 레이아웃 (필수)
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
    title: { default: 'My App', template: '%s | My App' },
    description: 'Next.js 애플리케이션',
};

export default function RootLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="ko">
            <body className={inter.className}>
                <header>{/* 공통 헤더 */}</header>
                {children}
                <footer>{/* 공통 푸터 */}</footer>
            </body>
        </html>
    );
}

// app/page.tsx - 서버 컴포넌트 (기본)
import { ProductList } from './components/ProductList';

// 정적 메타데이터
export const metadata = {
    title: '홈',
    description: '메인 페이지입니다.',
};

// 서버에서 직접 데이터 페칭 (async 컴포넌트)
async function getProducts() {
    const res = await fetch('https://api.example.com/products', {
        next: { revalidate: 3600 } // ISR: 1시간마다 재생성
    });

    if (!res.ok) throw new Error('Failed to fetch');
    return res.json();
}

export default async function HomePage() {
    const products = await getProducts();

    return (
        <main>
            <h1>제품 목록</h1>
            <ProductList products={products} />
        </main>
    );
}

// app/components/ProductList.tsx - 클라이언트 컴포넌트
'use client'; // 클라이언트 컴포넌트 지시어

import { useState } from 'react';
import { Product } from '@/types';

export function ProductList({ products }: { products: Product[] }) {
    const [filter, setFilter] = useState('');

    const filtered = products.filter(p =>
        p.name.toLowerCase().includes(filter.toLowerCase())
    );

    return (
        <div>
            <input
                type="text"
                placeholder="검색..."
                value={filter}
                onChange={(e) => setFilter(e.target.value)}
            />
            <ul>
                {filtered.map(product => (
                    <li key={product.id}>{product.name}</li>
                ))}
            </ul>
        </div>
    );
}

// app/products/[id]/page.tsx - 동적 라우트
import { notFound } from 'next/navigation';

// 동적 메타데이터
export async function generateMetadata({ params }: { params: { id: string } }) {
    const product = await getProduct(params.id);
    return { title: product?.name || 'Not Found' };
}

// 정적 생성할 경로 미리 생성
export async function generateStaticParams() {
    const products = await getProducts();
    return products.map((product) => ({ id: product.id }));
}

export default async function ProductPage({ params }: { params: { id: string } }) {
    const product = await getProduct(params.id);

    if (!product) notFound();

    return <div>{product.name}</div>;
}

// app/api/products/route.ts - Route Handler (API)
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
    const searchParams = request.nextUrl.searchParams;
    const category = searchParams.get('category');

    const products = await db.products.findMany({
        where: category ? { category } : undefined
    });

    return NextResponse.json(products);
}

export async function POST(request: NextRequest) {
    const body = await request.json();

    const product = await db.products.create({
        data: body
    });

    return NextResponse.json(product, { status: 201 });
}

// middleware.ts - 전역 미들웨어
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
    // 인증 체크
    const token = request.cookies.get('auth-token');

    if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
        return NextResponse.redirect(new URL('/login', request.url));
    }

    return NextResponse.next();
}

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

🗣️ 실무 대화 예시

PM

"새 프로젝트 프레임워크를 선정해야 하는데, Next.js와 순수 React 중 어떤 걸 써야 할까요?"

개발자

"SEO가 중요하거나 초기 로딩 속도가 중요하면 Next.js가 좋아요. 서버 컴포넌트로 번들 크기도 줄이고, API Routes로 백엔드도 같이 구현할 수 있어요. 관리자 페이지처럼 SEO가 필요 없는 SPA라면 순수 React도 괜찮고요."

동료

"App Router와 Pages Router 중 어떤 걸 써야 하나요?"

개발자

"새 프로젝트라면 App Router를 추천해요. Server Components, 중첩 레이아웃, 스트리밍 같은 최신 기능을 쓸 수 있어요. 기존 프로젝트는 급하게 마이그레이션할 필요 없고, 두 라우터를 함께 사용하면서 점진적으로 전환하면 됩니다."

면접관

"Next.js에서 SSR, SSG, ISR의 차이점을 설명해주세요."

지원자

"SSR은 매 요청마다 서버에서 HTML을 생성하고, SSG는 빌드 시 미리 HTML을 생성합니다. ISR은 SSG + 재검증으로, 빌드 후에도 설정한 시간 간격으로 페이지를 백그라운드에서 다시 생성해요. fetch의 revalidate 옵션으로 쉽게 설정할 수 있습니다."

⚠️ 주의사항

서버/클라이언트 컴포넌트 구분: App Router에서 컴포넌트는 기본적으로 서버 컴포넌트입니다. useState, useEffect 등 React 훅을 사용하려면 파일 최상단에 'use client'를 추가해야 합니다. 서버 컴포넌트에서 클라이언트 훅을 사용하면 에러가 발생합니다.

데이터 페칭 위치: 서버 컴포넌트에서는 async/await로 직접 데이터를 가져올 수 있지만, 클라이언트 컴포넌트에서는 useEffect나 SWR/React Query 같은 라이브러리를 사용해야 합니다. 서버 컴포넌트에서 민감한 API 키를 사용해도 클라이언트에 노출되지 않습니다.

캐싱 동작: Next.js의 fetch는 기본적으로 결과를 캐싱합니다. 실시간 데이터가 필요하면 cache: 'no-store'를 설정하고, 주기적 갱신이 필요하면 revalidate 옵션을 사용하세요. 캐싱 동작을 이해하지 못하면 오래된 데이터가 표시될 수 있습니다.

🔗 관련 용어

📚 더 배우기