Skip to content
Back to Blog
#nextjs #react #웹개발 #튜토리얼 #typescript

Next.js 기초 완벽 가이드 - 2025년 최신 버전으로 시작하기

5 min read

Next.js 15의 최신 기능으로 현대적인 웹 애플리케이션을 구축하는 완벽한 가이드입니다. App Router, React Server Components, Turbopack을 활용한 빠른 개발부터 프로덕션 배포까지, 실전 예제와 함께 단계별로 학습하세요.

📋 핵심 내용 요약

  • 최신 버전: Next.js 15 + React 19로 최첨단 웹 개발
  • App Router: 파일 기반 라우팅과 Server Components로 최적의 성능
  • Turbopack: Rust 기반의 초고속 번들러로 개발 속도 향상
  • 완벽한 풀스택: 프론트엔드와 백엔드를 하나의 프레임워크로
  • 프로덕션 준비: Vercel, AWS, Docker 등 어디든 배포 가능

🤖 AI 요약

Next.js는 React 기반의 풀스택 웹 프레임워크로, Vercel이 개발하고 있으며 현대적인 웹 애플리케이션 개발의 표준이 되었습니다.

왜 Next.js인가? Next.js 15는 React 19 지원, Turbopack Dev의 안정화, 그리고 혁신적인 캐싱 전략을 도입했습니다. Turbopack은 Webpack보다 최대 96.3% 빠른 Hot Module Replacement를 제공하며, 대규모 프로젝트에서도 즉각적인 피드백을 받을 수 있습니다.

App Router와 Server Components: Next.js 13부터 도입된 App Router는 React Server Components(RSC)를 활용하여 서버에서 렌더링되는 컴포넌트를 기본으로 제공합니다. 이를 통해 JavaScript 번들 크기를 줄이고 초기 페이지 로딩 속도를 크게 개선할 수 있습니다. 클라이언트 컴포넌트가 필요한 경우에만 ‘use client’ 지시문을 사용하면 됩니다.

파일 기반 라우팅: app/ 디렉토리의 폴더 구조가 곧 라우트입니다. page.tsx는 페이지를, layout.tsx는 공통 레이아웃을, loading.tsx는 로딩 상태를 정의합니다. 동적 라우트는 [id] 폴더로 간단하게 만들 수 있습니다.

데이터 페칭: Server Components에서는 직접 async/await로 데이터를 가져올 수 있습니다. 데이터베이스 쿼리, API 호출을 컴포넌트 내에서 바로 수행하며, 이 코드는 클라이언트에 전송되지 않습니다. 자동 중복 제거와 캐싱으로 최적의 성능을 제공합니다.

최신 기능 (2025): Next.js 15.5는 프로덕션 Turbopack 빌드(베타), TypeScript 타입 라우트, Node.js 미들웨어 안정화를 포함합니다. 빌드 시간이 2배에서 5배까지 개선되었으며, 타입 안전성이 크게 향상되었습니다.

배포: Vercel에 원클릭 배포는 물론, AWS, Google Cloud, Docker 컨테이너, 심지어 정적 사이트로도 내보낼 수 있습니다. 엣지 런타임을 활용하면 전 세계 어디서나 빠른 응답 속도를 보장합니다.


Next.js는 React로 웹 애플리케이션을 만드는 가장 현대적이고 효율적인 방법입니다. 이 글에서는 Next.js 15의 최신 기능을 활용하여 기초부터 실전까지 완벽하게 배워보겠습니다.

Next.js란?

Next.js는 Vercel이 개발한 React 기반의 풀스택 프레임워크로, 다음과 같은 강력한 기능을 제공합니다:

  • 📁 App Router: 직관적인 파일 기반 라우팅 시스템
  • ⚡ Server Components: 서버에서 렌더링되는 React 컴포넌트
  • 🚀 Turbopack: Rust 기반의 초고속 번들러
  • 🎯 자동 최적화: 이미지, 폰트, 코드 스플리팅 자동화
  • 🔌 API Routes: 프론트엔드와 백엔드를 하나로
  • 🌍 국제화: 다국어 지원 내장
  • 📊 분석: 실시간 성능 모니터링

왜 Next.js를 선택해야 할까?

1. 최첨단 성능

Turbopack Dev (Next.js 15):

  • 로컬 서버 시작: 76.8% 더 빠름
  • Fast Refresh: 96.3% 더 빠름
  • 초기 라우트 컴파일: 최대 45.8% 더 빠름

React Server Components:

  • 서버에서 렌더링되어 JavaScript 번들 크기 0
  • 데이터베이스 직접 접근으로 API 레이어 불필요
  • 자동 코드 분할로 필요한 것만 로드

2. 개발 경험

// 데이터 페칭이 이렇게 간단합니다!
async function BlogPost({ params }) {
  const post = await db.posts.findOne({ id: params.id });

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

컴포넌트가 곧 데이터 페칭입니다. 별도의 getServerSidePropsuseEffect 없이 직접 데이터를 가져올 수 있습니다.

3. 프로덕션 준비

  • 자동 최적화: 이미지, 폰트, 스크립트 자동 최적화
  • SEO 친화적: 서버 사이드 렌더링으로 완벽한 SEO
  • 보안: API 키와 민감한 로직을 서버에서만 실행
  • 확장성: 엣지 런타임으로 전 세계 빠른 응답

시작하기

사전 준비사항

  • Node.js 18.18 이상 (LTS 버전 권장)
  • 코드 에디터 (VS Code + ESLint, Prettier 권장)
  • 기본 지식: JavaScript, React, TypeScript

새 프로젝트 생성

Next.js 프로젝트를 만드는 가장 빠른 방법:

# 프로젝트 생성
npx create-next-app@latest my-app

# 프로젝트 디렉토리로 이동
cd my-app

# 개발 서버 시작
npm run dev

설치 중 다음을 선택하게 됩니다:

 Would you like to use TypeScript?  Yes
 Would you like to use ESLint?  Yes
 Would you like to use Tailwind CSS?  Yes
 Would you like your code inside a src/ directory?  Yes
 Would you like to use App Router?  Yes
 Would you like to use Turbopack for next dev?  Yes
 Would you like to customize the import alias?  No

권장 설정:

  • TypeScript: ✅ Yes (타입 안전성)
  • ESLint: ✅ Yes (코드 품질)
  • Tailwind CSS: ✅ Yes (빠른 스타일링)
  • App Router: ✅ Yes (최신 기능)
  • Turbopack: ✅ Yes (빠른 개발)

프로젝트 구조

생성된 프로젝트의 구조를 이해해봅시다:

my-app/
├── src/
│   └── app/
│       ├── layout.tsx       # 루트 레이아웃 (모든 페이지 공통)
│       ├── page.tsx         # 홈페이지 (/)
│       ├── globals.css      # 전역 스타일
│       └── favicon.ico      # 파비콘
├── public/                  # 정적 파일
│   ├── images/
│   └── fonts/
├── package.json             # 프로젝트 설정 및 의존성
├── tsconfig.json            # TypeScript 설정
├── next.config.ts           # Next.js 설정
└── tailwind.config.ts       # Tailwind CSS 설정

주요 디렉토리 설명

src/app/

  • 모든 페이지와 라우트가 위치
  • 폴더 구조 = URL 구조
  • 특수 파일들로 다양한 기능 구현

public/

  • 이미지, 폰트 등 정적 파일
  • / 경로로 직접 접근 가능
  • 빌드 시 그대로 복사됨

App Router 완벽 가이드

특수 파일 규칙

Next.js App Router는 특수한 파일 이름으로 다양한 기능을 제공합니다:

파일 용도 설명
layout.tsx 레이아웃 여러 페이지에서 공유되는 UI
page.tsx 페이지 라우트의 고유 UI
loading.tsx 로딩 UI Suspense 기반 로딩 상태
error.tsx 에러 UI 에러 처리 화면
not-found.tsx 404 페이지 찾을 수 없음 에러
route.ts API 엔드포인트 HTTP 메서드 핸들러
template.tsx 템플릿 재마운트되는 레이아웃

기본 라우팅

파일 시스템이 곧 라우팅입니다:

app/
├── page.tsx                  →  /
├── about/
│   └── page.tsx             →  /about
├── blog/
│   ├── page.tsx             →  /blog
│   └── [slug]/
│       └── page.tsx         →  /blog/:slug
└── dashboard/
    ├── layout.tsx           →  대시보드 공통 레이아웃
    ├── page.tsx             →  /dashboard
    ├── settings/
    │   └── page.tsx         →  /dashboard/settings
    └── analytics/
        └── page.tsx         →  /dashboard/analytics

동적 라우트

기본 동적 라우트:

// app/blog/[slug]/page.tsx
interface PageProps {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

export default async function BlogPost({ params }: PageProps) {
  const post = await getPost(params.slug);

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

// 정적 경로 생성 (SSG)
export async function generateStaticParams() {
  const posts = await getPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// 메타데이터 생성
export async function generateMetadata({ params }: PageProps) {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
  };
}

Catch-all 라우트:

// app/docs/[...slug]/page.tsx
// /docs/a → params.slug = ['a']
// /docs/a/b → params.slug = ['a', 'b']
// /docs/a/b/c → params.slug = ['a', 'b', 'c']

export default function DocsPage({ params }: { params: { slug: string[] } }) {
  return <div>문서: {params.slug.join(' / ')}</div>;
}

레이아웃 시스템

루트 레이아웃 (필수):

// app/layout.tsx
import './globals.css';
import { Inter } from 'next/font/google';

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

export const metadata = {
  title: 'My Next.js App',
  description: 'Built with Next.js 15',
};

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

중첩 레이아웃:

// app/dashboard/layout.tsx
import { Sidebar } from '@/components/Sidebar';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen">
      <Sidebar />
      <div className="flex-1 overflow-auto p-8">
        {children}
      </div>
    </div>
  );
}

로딩과 에러 처리

로딩 상태:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center h-full">
      <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
    </div>
  );
}

에러 처리:

// app/dashboard/error.tsx
'use client'; // Error components는 Client Component여야 합니다

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center h-full">
      <h2 className="text-2xl font-bold mb-4">문제가 발생했습니다!</h2>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        다시 시도
      </button>
    </div>
  );
}

Server Components vs Client Components

Next.js 15의 가장 중요한 개념입니다.

Server Components (기본)

서버에서만 실행되는 컴포넌트입니다:

// app/posts/page.tsx (Server Component)
import { db } from '@/lib/db';

export default async function PostsPage() {
  // 데이터베이스 직접 접근 (서버에서만 실행됨)
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div>
      <h1>블로그 글 목록</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={'/posts/' + post.slug}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

장점:

  • ✅ 데이터베이스 직접 접근
  • ✅ API 키 안전하게 사용
  • ✅ JavaScript 번들 크기 0
  • ✅ 초기 페이지 로딩 빠름
  • ✅ SEO 최적화

Client Components

브라우저에서 실행되는 인터랙티브 컴포넌트입니다:

// components/counter.tsx
'use client'; // 이 지시문이 Client Component를 만듭니다

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        증가
      </button>
    </div>
  );
}

언제 Client Component를 사용할까:

  • ✅ 상태 관리 (useState, useReducer)
  • ✅ 이벤트 핸들러 (onClick, onChange)
  • ✅ 브라우저 API (localStorage, window)
  • ✅ React Hooks (useEffect, useContext)
  • ✅ 인터랙티브 UI 라이브러리

베스트 프랙티스

❌ 나쁜 예:

'use client'; // 전체 페이지를 Client Component로 만들면 성능 저하

export default function Page() {
  return (
    <div>
      <Header />
      <Content />
      <Footer />
    </div>
  );
}

✅ 좋은 예:

// Server Component (기본)
import { Counter } from './counter'; // Client Component

export default function Page() {
  return (
    <div>
      <Header />
      <Content />
      {/* 필요한 부분만 Client Component */}
      <Counter />
      <Footer />
    </div>
  );
}

데이터 페칭

Next.js 15의 데이터 페칭은 혁신적으로 간단합니다.

Server Components에서

// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    // 캐싱 옵션
    next: { revalidate: 3600 } // 1시간마다 재검증
  });

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

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

  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.price}</p>
        </div>
      ))}
    </div>
  );
}

캐싱 전략

Next.js 15는 캐싱 기본값을 변경했습니다:

// 캐시하지 않음 (기본값 - Next.js 15)
fetch('https://api.example.com/data');

// 재검증 시간 설정
fetch('https://api.example.com/data', {
  next: { revalidate: 60 } // 60초
});

// 무기한 캐시
fetch('https://api.example.com/data', {
  cache: 'force-cache'
});

// 캐시하지 않음 (명시적)
fetch('https://api.example.com/data', {
  cache: 'no-store'
});

병렬 데이터 페칭

export default async function Page() {
  // 병렬로 실행 - 더 빠름!
  const [products, categories, reviews] = await Promise.all([
    getProducts(),
    getCategories(),
    getReviews(),
  ]);

  return (
    <div>
      <Products data={products} />
      <Categories data={categories} />
      <Reviews data={reviews} />
    </div>
  );
}

순차적 데이터 페칭

export default async function Page({ params }) {
  // 1. 먼저 사용자 정보 가져오기
  const user = await getUser(params.id);

  // 2. 사용자 ID로 게시물 가져오기
  const posts = await getUserPosts(user.id);

  return (
    <div>
      <UserProfile user={user} />
      <UserPosts posts={posts} />
    </div>
  );
}

API Routes

Next.js에서 백엔드 API를 만드는 것도 매우 간단합니다.

GET 요청

// app/api/posts/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';

export async function GET() {
  try {
    const posts = await db.post.findMany();
    return NextResponse.json(posts);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch posts' },
      { status: 500 }
    );
  }
}

POST 요청

// app/api/posts/route.ts
export async function POST(request: Request) {
  try {
    const body = await request.json();
    const { title, content } = body;

    const post = await db.post.create({
      data: { title, content },
    });

    return NextResponse.json(post, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create post' },
      { status: 500 }
    );
  }
}

동적 라우트 API

// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server';

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await db.post.findUnique({
    where: { id: params.id },
  });

  if (!post) {
    return NextResponse.json(
      { error: 'Post not found' },
      { status: 404 }
    );
  }

  return NextResponse.json(post);
}

export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  const body = await request.json();

  const post = await db.post.update({
    where: { id: params.id },
    data: body,
  });

  return NextResponse.json(post);
}

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await db.post.delete({
    where: { id: params.id },
  });

  return NextResponse.json({ message: 'Deleted successfully' });
}

스타일링

Tailwind CSS (권장)

Next.js는 Tailwind CSS를 완벽하게 지원합니다:

export default function Button({ children }: { children: React.ReactNode }) {
  return (
    <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors">
      {children}
    </button>
  );
}

CSS Modules

컴포넌트 스코프 CSS:

// Button.module.css
.button {
  background-color: blue;
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
}

.button:hover {
  background-color: darkblue;
}
// Button.tsx
import styles from './Button.module.css';

export default function Button({ children }) {
  return <button className={styles.button}>{children}</button>;
}

Global Styles

/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  h1 {
    @apply text-4xl font-bold;
  }

  h2 {
    @apply text-3xl font-semibold;
  }
}

@layer components {
  .btn-primary {
    @apply bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600;
  }
}

이미지 최적화

Next.js의 Image 컴포넌트는 자동으로 이미지를 최적화합니다:

import Image from 'next/image';

export default function Profile() {
  return (
    <div>
      {/* 로컬 이미지 */}
      <Image
        src="/profile.jpg"
        alt="프로필 사진"
        width={500}
        height={500}
        priority // LCP 이미지는 priority 설정
      />

      {/* 외부 이미지 */}
      <Image
        src="https://example.com/photo.jpg"
        alt="사진"
        width={800}
        height={600}
        placeholder="blur" // 블러 플레이스홀더
        blurDataURL="data:image/jpeg..." // 또는 자동 생성
      />
    </div>
  );
}

자동 최적화:

  • ✅ WebP/AVIF 자동 변환
  • ✅ 반응형 이미지 생성
  • ✅ Lazy loading 기본 적용
  • ✅ CLS 방지 (누적 레이아웃 이동)

폰트 최적화

// app/layout.tsx
import { Inter, Noto_Sans_KR } from 'next/font/google';

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

const notoSansKr = Noto_Sans_KR({
  subsets: ['korean'],
  weight: ['400', '700'],
  display: 'swap',
});

export default function RootLayout({ children }) {
  return (
    <html lang="ko" className={inter.className + ' ' + notoSansKr.className}>
      <body>{children}</body>
    </html>
  );
}

메타데이터와 SEO

정적 메타데이터

// app/about/page.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: '회사 소개',
  description: '우리 회사를 소개합니다',
  openGraph: {
    title: '회사 소개',
    description: '우리 회사를 소개합니다',
    images: ['/og-image.jpg'],
  },
  twitter: {
    card: 'summary_large_image',
    title: '회사 소개',
    description: '우리 회사를 소개합니다',
    images: ['/twitter-image.jpg'],
  },
};

export default function AboutPage() {
  return <div>회사 소개 페이지</div>;
}

동적 메타데이터

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.date,
      authors: [post.author],
    },
  };
}

환경 변수

# .env.local
DATABASE_URL="postgresql://..."
API_KEY="your-secret-key"

# 클라이언트에서 접근 가능 (NEXT_PUBLIC_ 접두사)
NEXT_PUBLIC_API_URL="https://api.example.com"
// Server Component (안전)
export default async function Page() {
  const apiKey = process.env.API_KEY; // 서버에서만 접근 가능
  // ...
}

// Client Component
'use client';

export default function ClientPage() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL; // 클라이언트에서 접근 가능
  // ...
}

배포

Vercel (가장 쉬운 방법)

  1. GitHub에 코드 푸시
  2. Vercel에 로그인
  3. 프로젝트 임포트
  4. 배포 완료!

자동으로 제공되는 것들:

  • ✅ HTTPS
  • ✅ 글로벌 CDN
  • ✅ 자동 스케일링
  • ✅ 프리뷰 배포
  • ✅ 분석 및 모니터링

Docker

# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
// next.config.ts
module.exports = {
  output: 'standalone',
};
# 빌드 및 실행
docker build -t my-nextjs-app .
docker run -p 3000:3000 my-nextjs-app

정적 사이트 내보내기

// next.config.ts
module.exports = {
  output: 'export',
};
npm run build
# out/ 폴더가 생성됩니다

성능 최적화 팁

1. Code Splitting

자동으로 적용되지만, 필요시 수동 제어:

import dynamic from 'next/dynamic';

// 클라이언트에서만 로드
const DynamicComponent = dynamic(() => import('@/components/Heavy'), {
  ssr: false,
  loading: () => <p>로딩 중...</p>,
});

2. Streaming과 Suspense

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <h1>대시보드</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
      <FastComponent />
    </div>
  );
}

3. 부분 프리렌더링 (Partial Prerendering)

Next.js 15의 실험적 기능:

// next.config.ts
module.exports = {
  experimental: {
    ppr: true,
  },
};

4. 데이터 캐싱

import { unstable_cache } from 'next/cache';

const getCachedData = unstable_cache(
  async () => {
    return await db.data.findMany();
  },
  ['data-cache'],
  { revalidate: 3600 }
);

디버깅

React DevTools

브라우저 확장 프로그램 설치:

Next.js DevTools

개발 모드에서 자동으로 활성화됩니다.

환경별 코드

if (process.env.NODE_ENV === 'development') {
  console.log('개발 환경에서만 실행');
}

if (process.env.NODE_ENV === 'production') {
  console.log('프로덕션 환경에서만 실행');
}

실전 예제: 블로그 만들기

완전한 블로그를 만들어봅시다:

1. 프로젝트 구조

src/
└── app/
    ├── layout.tsx
    ├── page.tsx
    ├── blog/
    │   ├── page.tsx
    │   └── [slug]/
    │       └── page.tsx
    └── api/
        └── posts/
            └── route.ts

2. 블로그 목록 페이지

// app/blog/page.tsx
import Link from 'next/link';
import { getAllPosts } from '@/lib/posts';

export default async function BlogPage() {
  const posts = await getAllPosts();

  return (
    <div className="max-w-4xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">블로그</h1>
      <div className="grid gap-8">
        {posts.map((post) => (
          <article key={post.slug} className="border-b pb-8">
            <Link href={'/blog/' + post.slug}>
              <h2 className="text-2xl font-bold mb-2 hover:text-blue-600">
                {post.title}
              </h2>
            </Link>
            <time className="text-gray-600">{post.date}</time>
            <p className="mt-4 text-gray-700">{post.excerpt}</p>
            <Link
              href={'/blog/' + post.slug}
              className="text-blue-600 hover:underline mt-2 inline-block"
            >
              더 읽기 →
            </Link>
          </article>
        ))}
      </div>
    </div>
  );
}

3. 블로그 상세 페이지

// app/blog/[slug]/page.tsx
import { getPost, getAllPosts } from '@/lib/posts';
import { notFound } from 'next/navigation';
import { Metadata } from 'next';

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

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug);
  if (!post) return {};

  return {
    title: post.title,
    description: post.excerpt,
  };
}

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

  if (!post) {
    notFound();
  }

  return (
    <article className="max-w-4xl mx-auto px-4 py-12">
      <header className="mb-8">
        <h1 className="text-5xl font-bold mb-4">{post.title}</h1>
        <time className="text-gray-600">{post.date}</time>
      </header>
      <div
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: post.content }}
      />
    </article>
  );
}

4. 데이터 레이어

// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';

const postsDirectory = path.join(process.cwd(), 'content/posts');

export async function getAllPosts() {
  const fileNames = fs.readdirSync(postsDirectory);

  const posts = fileNames.map((fileName) => {
    const slug = fileName.replace(/.md$/, '');
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    const { data } = matter(fileContents);

    return {
      slug,
      title: data.title,
      date: data.date,
      excerpt: data.excerpt,
    };
  });

  return posts.sort((a, b) => (a.date < b.date ? 1 : -1));
}

export async function getPost(slug: string) {
  try {
    const fullPath = path.join(postsDirectory, slug + '.md');
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    const { data, content } = matter(fileContents);

    const htmlContent = marked(content);

    return {
      slug,
      title: data.title,
      date: data.date,
      excerpt: data.excerpt,
      content: htmlContent,
    };
  } catch {
    return null;
  }
}

마무리

Next.js 15는 현대적인 웹 개발의 모든 것을 제공하는 강력한 프레임워크입니다.

핵심 요점:

  • Turbopack: 초고속 개발 경험
  • 🎯 App Router: 직관적인 파일 기반 라우팅
  • 🚀 Server Components: 최적의 성능
  • 🔧 풀스택: 프론트엔드 + 백엔드
  • 🌍 배포 자유: 어디든 배포 가능

다음 단계:

  1. 작은 프로젝트로 시작하세요
  2. 공식 문서를 참고하세요
  3. 커뮤니티에 참여하세요
  4. 실전 프로젝트를 만들어보세요

더 알아보기

추천 라이브러리:

Next.js로 멋진 웹 애플리케이션을 만들어보세요!

Happy coding! 🚀


질문이나 피드백이 있으신가요?

이 튜토리얼에 대한 질문이나 제안사항이 있다면 언제든 연락주세요!

📧 이메일: [email protected]

이 글 공유하기

💡 LifeTech Hub

삶을 업그레이드하는 기술과 지혜 - 재테크, 개발, AI, IT, 일상생활

Quick Links

Connect

© 2025 LifeTech Hub. Built with 💜 using SvelteKit

Privacy Terms RSS