Next.js는 React를 기반으로 라우팅, 서버 렌더링, 데이터 페칭, API 개발과 배포를 하나의 프로젝트에서 처리할 수 있는 풀스택 프레임워크입니다. 이 글에서는 App Router를 중심으로 서버·클라이언트 컴포넌트, 캐시 정책, Server Actions, Route Handlers, 미들웨어, 메타데이터, 스트리밍, 이미지 최적화와 배포까지 실무에 필요한 핵심 내용을 정리합니다.
```Next.js에서는 모든 페이지에 하나의 렌더링 방식을 적용하는 것이 아니라 페이지와 컴포넌트의 특성에 따라 정적 렌더링, 서버 렌더링, 클라이언트 렌더링과 캐시 정책을 다르게 설계하는 것이 중요합니다.

1. Next.js란?
Next.js는 React 위에 라우팅, 렌더링 전략, 데이터 처리와 배포 기능을 추가한 풀스택 프레임워크입니다. 앱 전체를 하나의 방식으로 렌더링하지 않고 페이지와 컴포넌트의 요구사항에 따라 서로 다른 렌더링 방식을 조합할 수 있습니다.
| 렌더링 방식 | 설명 | 적합한 화면 |
|---|---|---|
| SSG | 빌드 시점에 정적 HTML을 생성합니다. | 회사 소개, 문서, 변경이 적은 콘텐츠 |
| ISR | 정적 페이지를 제공하면서 주기적으로 재검증합니다. | 블로그, 상품 목록, 뉴스 목록 |
| SSR | 사용자의 요청마다 서버에서 화면을 렌더링합니다. | 회원별 화면, 권한별 페이지, 최신 데이터 |
| CSR | 브라우저에서 JavaScript로 화면을 렌더링합니다. | 대화형 대시보드, 실시간 편집 화면 |
검색 노출과 빠른 초기 화면이 중요하면 SSG 또는 ISR을 우선 검토하고, 사용자마다 다른 데이터를 즉시 반영해야 한다면 SSR 또는 클라이언트 데이터 조회를 조합합니다.
2. App Router와 파일 기반 라우팅
App Router에서는 app 디렉터리의 폴더 구조가 URL 경로를 결정합니다. 폴더 안의 page.tsx가 실제 페이지가 되고, layout.tsx, loading.tsx, error.tsx 같은 특수 파일은 라우트에 자동으로 연결됩니다.
app/
├── layout.tsx # 루트 레이아웃
├── page.tsx # /
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
├── docs/
│ └── [...slug]/
│ └── page.tsx # /docs/a/b/c
├── (marketing)/
│ └── about/
│ └── page.tsx # /about
└── api/
└── users/
└── route.ts # /api/users
주요 라우팅 패턴
| 패턴 | 의미 |
|---|---|
[id] |
하나의 동적 경로를 매칭합니다. |
[...slug] |
여러 URL 세그먼트를 배열 형태로 매칭합니다. |
[[...slug]] |
상위 경로를 포함하는 선택적 catch-all 라우트입니다. |
(marketing) |
URL에는 노출되지 않고 프로젝트 폴더만 그룹화합니다. |
@modal |
같은 레이아웃에서 여러 페이지를 병렬로 렌더링합니다. |
(.)photo |
모달 화면처럼 다른 라우트를 가로채서 렌더링합니다. |
3. 서버 컴포넌트와 클라이언트 컴포넌트
App Router의 기본 컴포넌트는 서버 컴포넌트입니다. 상태, 이벤트 처리, 브라우저 API가 필요한 컴포넌트에만 'use client'를 선언하는 것이 좋습니다.
서버 컴포넌트
// 서버 컴포넌트가 기본입니다.
// 서버에서 DB나 내부 API에 직접 접근할 수 있습니다.
async function ProductList() {
const products = await db.product.findMany({
take: 20,
});
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name}
</li>
))}
</ul>
);
}
클라이언트 컴포넌트
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button
type="button"
onClick={() => setCount(count + 1)}
>
{count}
</button>
);
}
'use client'경계는 하위 컴포넌트까지 영향을 줍니다.- 클라이언트 컴포넌트는 상태나 이벤트가 필요한 작은 컴포넌트에만 적용합니다.
- 서버 컴포넌트는 비동기 함수로 작성하고 데이터를 직접
await할 수 있습니다. - 서버 전용 비밀키나 DB 연결 정보는 클라이언트 컴포넌트에서 사용하지 않습니다.
4. 데이터 페칭과 캐싱
Next.js에서는 데이터의 변경 빈도와 사용자별 처리 여부에 따라 캐시 정책을 선택해야 합니다. 정적 데이터, 주기적으로 변경되는 데이터와 요청마다 달라지는 데이터를 동일한 방식으로 처리해서는 안 됩니다.
// 정적 데이터
const response = await fetch(
'https://api.example.com/products'
);
// ISR: 60초마다 재검증
const response2 = await fetch(url, {
next: {
revalidate: 60,
},
});
// 태그 기반 캐시 재검증
const response3 = await fetch(url, {
next: {
tags: ['products'],
},
});
// 요청마다 최신 데이터 조회
const response4 = await fetch(url, {
cache: 'no-store',
});
// 페이지 전체를 동적으로 렌더링
export const dynamic = 'force-dynamic';
| 설정 | 동작 | 적용 예시 |
|---|---|---|
| 정적 캐시 | 생성된 데이터를 캐시하고 재사용합니다. | 공지, 문서, 회사 정보 |
revalidate: N |
N초 이후 백그라운드에서 데이터를 재검증합니다. | 상품 목록, 블로그 목록 |
tags |
특정 데이터 그룹만 선택적으로 무효화합니다. | 상품·게시물 등록 후 목록 갱신 |
no-store |
매 요청마다 서버에서 새 데이터를 가져옵니다. | 개인화 페이지, 실시간 데이터 |
5. Server Actions와 폼
Server Action은 클라이언트 화면에서 호출할 수 있는 서버 함수입니다. 폼 제출이나 데이터 변경 작업을 별도의 REST API 없이 처리할 수 있습니다.
서버 액션 작성
'use server';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const schema = z.object({
name: z
.string()
.min(1, '이름을 입력하세요.'),
});
type FormState = {
error?: string;
};
export async function createUser(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const parsed = schema.safeParse({
name: formData.get('name'),
});
if (!parsed.success) {
return {
error: parsed.error.issues[0].message,
};
}
await db.user.create({
data: parsed.data,
});
revalidatePath('/users');
return {
error: undefined,
};
}
클라이언트 폼 연결
'use client';
import { useActionState } from 'react';
import { createUser } from './actions';
export function UserForm() {
const [
state,
formAction,
isPending,
] = useActionState(createUser, {});
return (
<form action={formAction}>
<label htmlFor="name">
이름
</label>
<input
id="name"
name="name"
/>
{state.error && (
<p role="alert">
{state.error}
</p>
)}
<button
type="submit"
disabled={isPending}
>
{isPending ? '등록 중...' : '추가'}
</button>
</form>
);
}
클라이언트에서 이미 검증한 값이라도 Server Action 내부에서 다시 검증해야 합니다. 사용자 인증과 권한 확인도 데이터 변경 직전에 서버에서 수행해야 합니다.
6. Route Handlers
Route Handler는 외부 클라이언트가 호출하는 REST API, 모바일 앱 연동, 결제나 GitHub 등의 webhook 수신 기능을 구현할 때 사용합니다.
import {
NextRequest,
NextResponse,
} from 'next/server';
type RouteContext = {
params: Promise<{
id: string;
}>;
};
export async function GET(
_request: NextRequest,
{ params }: RouteContext
) {
const { id } = await params;
const user = await db.user.findUnique({
where: {
id,
},
});
if (!user) {
return NextResponse.json(
{
error: 'not found',
},
{
status: 404,
}
);
}
return NextResponse.json(user);
}
export async function DELETE(
_request: NextRequest,
{ params }: RouteContext
) {
const { id } = await params;
await db.user.delete({
where: {
id,
},
});
return new NextResponse(null, {
status: 204,
});
}
| 상황 | 추천 방식 |
|---|---|
| 같은 Next.js 앱의 폼 제출과 데이터 변경 | Server Action |
| 모바일 앱이나 외부 서비스가 호출하는 API | Route Handler |
| 결제·GitHub 등의 webhook 수신 | Route Handler |
| 파일이나 스트림 응답 제공 | Route Handler |
7. 미들웨어
미들웨어는 요청이 실제 페이지나 API에 도달하기 전에 실행됩니다. 인증 확인, 리다이렉트, URL rewrite, 언어 선택과 헤더 처리 등에 활용할 수 있습니다.
import {
NextRequest,
NextResponse,
} from 'next/server';
export function middleware(
request: NextRequest
) {
const token =
request.cookies.get('session')?.value;
if (!token) {
return NextResponse.redirect(
new URL('/login', request.url)
);
}
return NextResponse.next();
}
export const config = {
matcher: [
'/dashboard/:path*',
'/settings/:path*',
],
};
matcher로 미들웨어 실행 범위를 제한합니다.- 정적 이미지와 JavaScript 파일까지 불필요하게 검사하지 않도록 설정합니다.
- 무거운 DB 조회나 복잡한 비즈니스 로직은 미들웨어에서 처리하지 않습니다.
- 미들웨어는 빠른 인증 판단이나 요청 분기에 집중하는 것이 좋습니다.
8. 메타데이터와 SEO
Next.js는 정적 metadata 객체와 동적 generateMetadata 함수를 이용해 페이지별 제목, 설명, Open Graph 정보와 canonical URL을 설정할 수 있습니다.
동적 메타데이터
import type {
Metadata,
} from 'next';
type PageProps = {
params: Promise<{
slug: string;
}>;
};
export async function generateMetadata(
{ params }: PageProps
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [
post.coverImage,
],
},
};
}
사이트맵 생성
export default async function sitemap() {
const posts = await getAllPosts();
return posts.map((post) => ({
url:
'https://example.com/blog/' +
post.slug,
lastModified:
post.updatedAt,
}));
}
SEO 체크포인트
- 페이지마다 고유한 제목과 설명을 설정합니다.
- 중복 URL이 존재한다면 canonical URL을 지정합니다.
- 공유용 대표 이미지를 Open Graph 정보에 포함합니다.
sitemap.ts와robots.ts를 함께 관리합니다.- 비공개 페이지나 관리자 페이지가 검색엔진에 노출되지 않도록 확인합니다.
9. Loading·Error UI와 스트리밍
App Router에서는 특수 파일을 이용해 로딩 화면, 오류 화면과 404 화면을 라우트 단위로 구성할 수 있습니다.
app/dashboard/
├── layout.tsx
├── page.tsx
├── loading.tsx
├── error.tsx
└── not-found.tsx
오류 화면
'use client';
type ErrorProps = {
error: Error;
reset: () => void;
};
export default function Error({
error,
reset,
}: ErrorProps) {
return (
<div role="alert">
<p>
문제가 발생했습니다:
{error.message}
</p>
<button
type="button"
onClick={() => reset()}
>
다시 시도
</button>
</div>
);
}
Suspense 스트리밍
import {
Suspense,
} from 'react';
export default function Dashboard() {
return (
<>
<Header />
<Suspense
fallback={
<p>
통계 불러오는 중...
</p>
}
>
<SlowStats />
</Suspense>
</>
);
}
전체 데이터가 준비될 때까지 빈 화면을 보여주는 대신 먼저 준비된 레이아웃과 콘텐츠를 사용자에게 전달하고, 시간이 오래 걸리는 영역만 별도의 로딩 UI로 표시할 수 있습니다.
10. 이미지와 폰트 최적화
Next.js의 next/image와 next/font를 사용하면 이미지 크기, 로딩 우선순위와 폰트 제공 방식을 애플리케이션에 맞게 최적화할 수 있습니다.
import Image from 'next/image';
import {
Inter,
} from 'next/font/google';
const inter = Inter({
subsets: [
'latin',
],
display: 'swap',
});
export default function Hero() {
return (
<main className={inter.className}>
<Image
src="/hero.png"
alt="제품 소개 이미지"
width={1200}
height={600}
priority
/>
</main>
);
}
이미지 최적화 체크포인트
width와height를 지정해 화면 이동을 방지합니다.priority는 최초 화면의 핵심 이미지에만 사용합니다.- 정보를 전달하는 이미지에는 의미 있는
alt를 작성합니다. - 외부 이미지는 허용할 도메인과 경로를 설정 파일에 등록합니다.
- 작은 아이콘까지 모두 고해상도 원본으로 제공하지 않습니다.
11. Vercel·Cloudflare 배포
Next.js 애플리케이션은 Vercel, Cloudflare 또는 Docker 기반 자체 인프라에 배포할 수 있습니다. 배포 환경에 따라 지원되는 런타임과 캐시 동작을 사전에 확인해야 합니다.
# Vercel
npm install -g vercel
vercel --prod
# Cloudflare Workers
npm install -D @opennextjs/cloudflare
npx opennextjs-cloudflare build
npx wrangler deploy
| 배포 대상 | 특징 | 적합한 경우 |
|---|---|---|
| Vercel | Next.js 프로젝트를 간편하게 빌드하고 배포할 수 있습니다. | 빠른 출시와 관리형 배포가 필요한 서비스 |
| Cloudflare | OpenNext 어댑터를 이용해 Workers에 배포합니다. | 글로벌 엣지 배포와 Cloudflare 연계 |
| Docker | 컨테이너를 직접 운영하고 인프라를 제어합니다. | Kubernetes, 사내 서버, 자체 클라우드 |
로컬 개발 환경에서 정상적으로 실행되더라도 배포 플랫폼의 Node.js 런타임, Edge 런타임, 환경 변수 주입 방식과 캐시 정책이 다를 수 있습니다.
12. 다음 학습 단계
Next.js 기본기를 익혔다면 React 내부 동작, TypeScript 타입 설계, CI/CD와 다른 프론트엔드 프레임워크로 학습 범위를 확장할 수 있습니다.
13. Next.js 실무 설계
Next.js 실무 설계의 핵심은 서버와 클라이언트의 경계를 명확히 하고, 데이터 조회 위치와 캐시 정책을 페이지별로 결정하는 것입니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 서버·클라이언트 경계 | 브라우저에서 반드시 실행해야 하는 기능인가? | 상태와 이벤트가 필요한 작은 영역만 클라이언트로 분리합니다. |
| 데이터 조회 | 데이터는 서버에서 조회할 수 있는가? | 초기 데이터는 가능한 서버에서 조회하고 전달합니다. |
| 캐시 정책 | 데이터는 얼마나 자주 변경되는가? | 정적, 시간 기반, 태그 기반과 비캐시 정책을 구분합니다. |
| 데이터 변경 | 내부 폼인가, 외부 API인가? | 내부 변경은 Server Action, 외부 연동은 Route Handler를 검토합니다. |
| 장애 처리 | 실패했을 때 사용자는 어떤 화면을 보는가? | loading, error, not-found 화면과 재시도 경로를 설계합니다. |
14. Next.js 운영 기준
운영 단계에서는 페이지가 정상적으로 보이는 것뿐 아니라 캐시 갱신, hydration 오류, 번들 크기, 메타데이터와 이미지 최적화까지 함께 확인해야 합니다.
- Server·Client Boundary: 불필요한 클라이언트 번들 증가 여부를 확인합니다.
- Cache Policy: ISR 주기와 캐시 무효화 시점을 문서화합니다.
- Dynamic Route: 동적 경로의 404 처리와 접근 권한을 확인합니다.
- Metadata: title, description, canonical과 Open Graph 정보를 확인합니다.
- Hydration: 서버 HTML과 클라이언트 렌더링 결과가 다른지 확인합니다.
- Bundle Split: 대용량 라이브러리가 초기 번들에 포함되지 않는지 확인합니다.
- Image Optimization: 대표 이미지 크기와 LCP 성능을 확인합니다.
- Runtime: Node.js와 Edge 런타임의 차이를 배포 전에 검증합니다.
서버 오류, Route Handler 응답 시간, 캐시 적중 여부, 사용자 브라우저 오류와 주요 Web Vitals를 운영 지표로 관리하는 것이 좋습니다.
15. Next.js 검증 전략
Next.js 프로젝트에서는 UI 테스트 외에도 Route Handler, 메타데이터, 사이트맵, 캐시 재검증과 hydration mismatch를 함께 검증해야 합니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 페이지, Server Action과 API의 정상·실패 흐름을 자동화합니다. | 핵심 사용자 시나리오가 반복 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 상황을 재현하는 테스트를 추가합니다. | 같은 장애가 다음 배포에서 다시 발생하지 않습니다. |
| SEO | 제목, 설명, canonical, sitemap과 robots를 검증합니다. | 검색엔진이 올바른 URL과 메타데이터를 확인할 수 있습니다. |
| 렌더링 | 서버와 클라이언트 결과가 일치하는지 확인합니다. | hydration 관련 오류가 브라우저 콘솔에 발생하지 않습니다. |
| 운영성 | 로그, 지표, 알림과 장애 추적 경로를 확인합니다. | 문제가 생겼을 때 원인을 빠르게 확인할 수 있습니다. |
배포 전 최종 체크리스트
- 동적 라우트의 정상, 오류와 404 화면을 확인합니다.
- Server Action에서 인증, 권한과 입력값을 검증합니다.
- Route Handler의 HTTP 상태 코드와 오류 응답 형식을 확인합니다.
- 캐시 변경 후
revalidatePath또는 태그 갱신이 동작하는지 확인합니다. - 메타데이터와 사이트맵에 테스트·개발 도메인이 남아 있지 않은지 확인합니다.
- 모바일 화면에서 이미지 크기와 레이아웃 이동을 확인합니다.
- 배포 환경의 환경 변수와 비밀키가 클라이언트 번들에 노출되지 않는지 확인합니다.
Next.js 개발에서 가장 중요한 것은 기능을 많이 사용하는 것이 아니라 서버와 클라이언트의 경계, 데이터 조회 위치와 캐시 수명 주기를 명확하게 설계하는 것입니다.
기본적으로 서버 컴포넌트를 우선 사용하고, 사용자 인터랙션이 필요한 작은 영역만 클라이언트 컴포넌트로 분리하는 것이 좋습니다. 데이터 변경은 Server Action과 Route Handler의 목적을 구분하고, 운영 단계에서는 캐시 갱신, SEO, hydration, 이미지 성능과 오류 추적까지 함께 관리해야 합니다.
```'개발 가이드 > Frontend' 카테고리의 다른 글
| React 완전 가이드 (0) | 2026.07.29 |
|---|
댓글