Next.js
넥스트JS
React 기반 풀스택 프레임워크. SSR, SSG, API Routes 지원.
넥스트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*']
};
"새 프로젝트 프레임워크를 선정해야 하는데, 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 옵션을 사용하세요. 캐싱 동작을 이해하지 못하면 오래된 데이터가 표시될 수 있습니다.