React 프레임워크 가이드

Next.js 완전 가이드

👥 방문자 수

React 기반 풀스택 프레임워크. App Router, 서버/클라이언트 컴포넌트, ISR, 서버 액션, 미들웨어까지 — 현대 웹 개발의 표준을 학습합니다.

Next.js 15 App Router Server Components Server Actions Vercel / Cloudflare

1. Next.js란?

Next.js는 Vercel이 개발한 React 기반 풀스택 프레임워크입니다. 파일 기반 라우팅, 서버 컴포넌트, ISR(증분 정적 재생성) 등으로 성능과 개발 생산성을 극대화합니다.

렌더링 방식설명사용 시점
SSG빌드 시 정적 HTML 생성변경이 적은 마케팅 페이지
ISR정적 + 주기적 재검증블로그, 상품 목록
SSR요청마다 서버에서 렌더링사용자별 맞춤 페이지
CSR클라이언트에서 렌더링대화형 대시보드

2. 프로젝트 생성

npx create-next-app@latest my-app \
  --typescript \
  --tailwind \
  --eslint \
  --app              # App Router 사용
  --src-dir          # src/ 디렉토리 구조

cd my-app
npm run dev          # http://localhost:3000bash
# 프로젝트 구조 (App Router)
my-app/
├── app/
│   ├── layout.tsx        # 루트 레이아웃
│   ├── page.tsx          # / 경로
│   ├── globals.css
│   ├── blog/
│   │   ├── page.tsx      # /blog
│   │   └── [slug]/
│   │       └── page.tsx  # /blog/:slug
│   └── api/
│       └── users/
│           └── route.ts  # /api/users
├── components/
├── lib/
└── public/text

3. App Router & 파일 기반 라우팅

// app/blog/[slug]/page.tsx
interface Props {
    params: Promise<{ slug: string }>;
    searchParams: Promise<{ lang?: string }>;
}

// generateStaticParams: 정적 경로 사전 생성
export async function generateStaticParams() {
    const posts = await fetchPosts();
    return posts.map(p => ({ slug: p.slug }));
}

export default async function BlogPost({ params }: Props) {
    const { slug } = await params;
    const post = await fetchPost(slug);
    if (!post) notFound();  // 404 페이지

    return (
        <article>
            <h1>{post.title}</h1>
            <div dangerouslySetInnerHTML={{ __html: post.content }} />
        </article>
    );
}

// 라우팅 파일 규칙
// page.tsx        — 라우트 UI
// layout.tsx      — 공유 레이아웃 (중첩 가능)
// loading.tsx     — Suspense 폴백
// error.tsx       — 에러 바운더리 ('use client' 필수)
// not-found.tsx   — 404 페이지
// route.ts        — API 엔드포인트
// middleware.ts   — 요청 인터셉터 (루트)tsx

4. 서버 vs 클라이언트 컴포넌트

ℹ️
기본값은 서버 컴포넌트입니다. 클라이언트 API(useState, useEffect, 이벤트 핸들러)가 필요할 때만 'use client'를 선언하세요. 컴포넌트 트리의 최대한 잎 노드에 배치하면 번들 크기를 최소화할 수 있습니다.
// ── 서버 컴포넌트 (기본) ─────────────────────────
// app/products/page.tsx
async function ProductList() {
    // 서버에서 직접 DB/API 접근 (비밀키 노출 없음)
    const products = await db.product.findMany({
        orderBy: { createdAt: 'desc' },
        take: 20,
    });

    return (
        <ul>
            {products.map(p => (
                <li key={p.id}>
                    <h3>{p.name}</h3>
                    <AddToCart productId={p.id} /> {/* 클라이언트 컴포넌트 */}
                </li>
            ))}
        </ul>
    );
}

// ── 클라이언트 컴포넌트 ───────────────────────────
// components/AddToCart.tsx
'use client';
import { useState } from 'react';

export function AddToCart({ productId }: { productId: string }) {
    const [loading, setLoading] = useState(false);

    const handleClick = async () => {
        setLoading(true);
        await addToCart(productId);
        setLoading(false);
    };

    return (
        <button onClick={handleClick} disabled={loading}>
            {loading ? '추가 중...' : '장바구니 담기'}
        </button>
    );
}tsx

5. 레이아웃 & 템플릿

// app/layout.tsx — 루트 레이아웃
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';

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

export const metadata: Metadata = {
    title: { default: 'My App', template: '%s | My App' },
    description: '...',
    metadataBase: new URL('https://example.com'),
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="ko">
            <body className={inter.className}>
                <header><nav>{/* 네비게이션 */}</nav></header>
                <main>{children}</main>
                <footer>{/* 푸터 */}</footer>
            </body>
        </html>
    );
}

// app/blog/layout.tsx — 중첩 레이아웃
export default function BlogLayout({ children }: { children: React.ReactNode }) {
    return (
        <div className="flex gap-8">
            <aside><BlogSidebar /></aside>
            <div className="flex-1">{children}</div>
        </div>
    );
}tsx

6. 데이터 페칭 & 캐싱

// ── fetch 캐싱 제어 ──────────────────────────────
// ISR: 300초마다 재검증
const data = await fetch('/api/posts', {
    next: { revalidate: 300 }
});

// 동적 렌더링 (캐시 없음)
const data = await fetch('/api/user', {
    cache: 'no-store'
});

// 빌드 시 한 번만 (SSG)
const data = await fetch('/api/config', {
    cache: 'force-cache'
});

// ── React cache()로 요청 중복 제거 ────────────────
import { cache } from 'react';

export const getUser = cache(async (id: string) => {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
});
// 같은 렌더 트리에서 getUser('1') 여러 번 호출해도 1번만 실행

// ── Parallel data fetching ────────────────────────
async function Page() {
    // 직렬 (비효율): await 후 다음 await
    // const user = await getUser(id);
    // const posts = await getPosts(id);

    // 병렬 (효율적): 동시 실행
    const [user, posts] = await Promise.all([
        getUser(id),
        getPosts(id),
    ]);
    return <div>{user.name}</div>;
}

// ── Loading UI (Suspense) ─────────────────────────
// app/blog/loading.tsx
export default function Loading() {
    return <div className="skeleton">로딩 중...</div>;
}

// app/blog/page.tsx
import { Suspense } from 'react';
export default function Blog() {
    return (
        <Suspense fallback={<PostListSkeleton />}>
            <PostList />
        </Suspense>
    );
}tsx

7. 서버 액션 (Server Actions)

서버에서 실행되는 비동기 함수입니다. 폼 제출, 데이터 변경에 사용합니다. API Route 없이 클라이언트에서 직접 호출 가능합니다.

// app/actions/user.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';

const CreateUserSchema = z.object({
    name:  z.string().min(2),
    email: z.string().email(),
});

export async function createUser(formData: FormData) {
    const raw = Object.fromEntries(formData);
    const parsed = CreateUserSchema.safeParse(raw);

    if (!parsed.success) {
        return { error: parsed.error.flatten().fieldErrors };
    }

    await db.user.create({ data: parsed.data });

    revalidatePath('/users');  // 캐시 무효화
    redirect('/users');         // 리디렉션
}

// app/users/new/page.tsx — 폼에서 직접 사용
'use client';
import { useActionState } from 'react';
import { createUser } from '@/app/actions/user';

export default function NewUserForm() {
    const [state, action, isPending] = useActionState(createUser, null);

    return (
        <form action={action}>
            {state?.error?.name && <p className="error">{state.error.name}</p>}
            <input name="name" placeholder="이름" />
            <input name="email" type="email" placeholder="이메일" />
            <button disabled={isPending}>
                {isPending ? '생성 중...' : '사용자 생성'}
            </button>
        </form>
    );
}tsx

8. 이미지 & 폰트 최적화

// next/image — 자동 WebP 변환, 지연 로딩, 크기 최적화
import Image from 'next/image';

// 로컬 이미지 (크기 자동 감지)
import heroImg from '@/public/hero.png';
<Image src={heroImg} alt="히어로 이미지" priority />

// 원격 이미지 (next.config.ts에 도메인 허용 필요)
<Image
    src="https://cdn.example.com/photo.jpg"
    alt="사진"
    width={800}
    height={600}
    sizes="(max-width: 768px) 100vw, 50vw"
    quality={80}
/>

// next.config.ts
const nextConfig = {
    images: {
        remotePatterns: [{ hostname: 'cdn.example.com' }],
    },
};

// next/font — 서브셋, 자가 호스팅 자동 처리
import { Inter, Noto_Sans_KR } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
const notoKR = Noto_Sans_KR({ weight: ['400','700'], subsets: ['latin'] });tsx

9. 미들웨어

// middleware.ts (루트에 위치)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
    const { pathname } = request.nextUrl;

    // 인증 체크
    const token = request.cookies.get('auth-token');
    if (pathname.startsWith('/dashboard') && !token) {
        return NextResponse.redirect(new URL('/login', request.url));
    }

    // 헤더 추가
    const response = NextResponse.next();
    response.headers.set('x-pathname', pathname);
    return response;
}

// 미들웨어가 실행될 경로 지정
export const config = {
    matcher: [
        '/dashboard/:path*',
        '/api/:path*',
        '/((?!_next/static|_next/image|favicon.ico).*)',
    ],
};tsx

10. SEO & 메타데이터

// 정적 메타데이터
export const metadata = {
    title: '블로그 — My App',
    description: '...',
    openGraph: {
        title: '블로그',
        images: [{ url: '/og-blog.png' }],
    },
};

// 동적 메타데이터 (generateMetadata)
export async function generateMetadata({ params }: Props) {
    const post = await fetchPost((await params).slug);
    return {
        title: post.title,
        description: post.excerpt,
        openGraph: {
            title: post.title,
            images: [{ url: post.coverImage }],
        },
        alternates: {
            canonical: `https://example.com/blog/${post.slug}`,
        },
    };
}tsx

11. 배포

# ── Vercel (기본) ────────────────────────────────
npm i -g vercel
vercel      # 최초 배포 (대화형)
vercel --prod # 프로덕션 배포

# ── Cloudflare Workers (@opennextjs/cloudflare) ───
npm install -D @opennextjs/cloudflare esbuild wrangler
npx opennextjs-cloudflare build
npx wrangler deploybash
# wrangler.jsonc (Cloudflare Workers)
{
  "name": "my-nextjs-app",
  "compatibility_date": "2025-05-18",
  "compatibility_flags": ["nodejs_compat"],
  "main": ".open-next/worker.js",
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  }
}json
# Docker 배포
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]dockerfile
ℹ️
Docker standalone 출력을 사용하려면 next.config.tsoutput: 'standalone'을 추가하세요.

12. 다음 단계

🚀
Next.js 심화 로드맵

Prisma / Drizzle ORM: 타입 안전 데이터베이스 쿼리
NextAuth.js (Auth.js): OAuth, JWT, 세션 인증
tRPC: 타입 안전 API (서버·클라이언트 타입 공유)
Zustand / Jotai: 경량 클라이언트 상태 관리
React Query (TanStack): 서버 상태 동기화
Storybook: 컴포넌트 문서화 및 테스트

연계 가이드: React 가이드 · TypeScript 가이드 · Docker 가이드