Next.js로 간단한 블로그 만들기
Next.js를 사용해 정적 블로그를 빠르게 구축하는 방법을 소개합니다. 마크다운 파일을 활용한 글 작성, 동적 라우팅, 스타일링까지 쉽게 따라할 수 있습니다.

이런 것을 만들 수 있습니다
이 글을 끝까지 따라 하면, 마크다운으로 글을 작성할 수 있는 완전한 개인 블로그가 완성됩니다. 복잡한 CMS나 데이터베이스 없이도 빠르고 효율적인 블로그를 직접 구축할 수 있습니다.
GitHub에서 완성된 블로그 코드 확인하기 →
주요 특징
- Next.js 15 기반 정적 사이트 생성
- 마크다운(.md) 파일로 간편한 포스트 작성
- 동적 라우팅으로 SEO 최적화
- 반응형 레이아웃과 모던한 디자인
- GitHub Pages나 Vercel로 무료 배포 가능
프로젝트 생성 및 기본 구조 설정
이 섹션에서는 Next.js 기반 블로그 프로젝트의 시작을 위한 두 가지 핵심 단계를 진행합니다.
- Next.js 프로젝트 생성
- 기본 프로젝트 구조 및 레이아웃 추가
아래 명령어를 통해 현재 섹션 완료 코드를 볼 수 있습니다.
git clone git@github.com:sijunnoh/blog-nextjs-blog.git
git checkout section01
1. Next.js 프로젝트 생성
먼저 터미널을 열고 프로젝트를 생성할 디렉토리로 이동합니다. 그 다음 아래 명령어를 실행하여 Next.js 프로젝트를 생성합니다.
npx create-next-app@latest
프로젝트 생성 과정에서 여러 옵션을 선택하게 됩니다. 이 튜토리얼에서는 다음과 같이 설정하겠습니다.
프로젝트 이름
: blog-nextjs-blog (원하는 이름으로 변경 가능)TypeScript
: Yes (타입 안정성을 위해 권장)ESLint
: Yes (코드 품질 관리)Tailwind CSS
: Yes (빠른 스타일링)src/ 디렉토리
: Yes (코드 구조화)App Router
: Yes (Next.js 13+ 권장 방식)Turbopack
: Yes (빠른 개발 서버)Import alias
: No (기본값 사용)
npm run dev
명령어를 통해 dev server를 실행합니다.
➜ npx create-next-app@latest
Need to install the following packages:
create-next-app@15.4.6
Ok to proceed? (y)
✔ What is your project named? … blog-nextjs-blog
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias [`@/*` by default?] … No / Yes
Creating a new Next.js app in /Users/mech2cs/work/mech2cs/blog/blog-nextjs-blog.
2. UI 컴포넌트 추가 - shadcn/ui
이번 섹션에 필요한 UI 컴포넌트를 먼저 추가하겠습니다.
shadcn-ui는 다양한 UI 컴포넌트를 빠르게 적용할 수 있도록 도와주는 툴킷입니다.
Next.js 프로젝트에 최적화되어 있으며, 커스터마이징도 유연하게 할 수 있습니다.
아래 명령어 실행하여 button
, dropdown-menu
, 그리고 sheet
컴포넌트를 추가합니다.
npx shadcn@latest add
명령어를 입력하면 프로젝트 설정에 따라 몇 가지 질문을 받게 되며, 기본값으로 진행해도 괜찮습니다. 설정이 완료되면 사용할 컴포넌트를 선택할 수 있습니다.
3. 페이지 구조 및 헤더 컴포넌트 생성
블로그의 기본 구조를 만들기 위해 필요한 페이지들과 헤더 컴포넌트를 생성하겠습니다. Next.js의 App Router는 폴더 구조를 기반으로 라우팅을 자동으로 처리하므로, 적절한 위치에 파일을 생성하는 것이 중요합니다.
touch src/app/posts/page.tsx
touch src/app/about/page.tsx
touch src/components/header.tsx
또는 VSCode 등의 에디터에서 직접 폴더와 파일을 생성하셔도 됩니다. 생성된 파일 구조는 다음과 같습니다.
src/
├── app/
│ ├── posts/
│ │ └── page.tsx # /posts 경로
│ ├── about/
│ │ └── page.tsx # /about 경로
│ └── page.tsx # / (홈) 경로
└── components/
└── header.tsx # 공통 헤더 컴포넌트
4. Header component 생성
블로그 상단에 고정될 헤더 컴포넌트를 만들어보겠습니다. 헤더는 화면 최상단에 고정(fixed)되며, 스크롤을 해도 항상 보이도록 구현합니다. 헤더의 구조는 다음과 같고 테스트를 위해 임시 배경색을 넣었습니다.
- 높이: 64px (Tailwind CSS의 h-16)
- 레이아웃: 좌측 로고 영역과 우측 네비게이션 영역으로 구분
- 스타일: 반투명 배경(backdrop-blur)과 하단 테두리
- 좌우 폭 제한 (
.container
)
"use client"
export default function Header() {
return (
<header className="fixed top-0 z-50 h-16 w-screen border-b px-6 backdrop-blur-md">
<div className="container mx-auto flex h-full items-center justify-between">
{/* left logo div */}
<div className="flex h-full flex-1 items-center bg-yellow-500/10"></div>
{/* right button group div */}
<div className="flex h-full flex-1 items-center bg-blue-500/10"></div>
</div>
</header>
)
}
5. layout 파일 수정
새로운 레이아웃 적용을 위해 /src/app/layout.tsx
을 수정합니다.
children을 감싸는 영역은 pt-16
클래스를 추가해 Header 컴포넌트 영역만큼 상단 공간 확보하였습니다.
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Header from "@/components/header";
...
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Header />
<div className="pt-16">{children}</div>
</body>
</html>
);
}
6. 페이지 파일들 수정
각 페이지의 기본 구조를 설정하겠습니다. 현재는 테스트를 위한 임시 레이아웃이며, 이후 실제 콘텐츠로 채워질 예정입니다.
페이지 공통 스타일
- 최소 높이: 스크롤 테스트 가능한 화면 전체 높이 (
min-h-screen
) - 콘텐츠 정렬: 가로 중앙 정렬 (
flex justify-center
) - 임시 텍스트: 각 페이지 확인용 제목
const MainPage = () => {
return <div className="flex min-h-screen justify-center">Main Page</div>
}
export default MainPage
const PostsPage = () => {
return <div className="flex min-h-screen justify-center">PostsPage</div>
}
export default PostsPage
const AboutPage = () => {
return <div className="flex min-h-screen justify-center">AboutPage</div>
}
export default AboutPage
7. 헤더에 로고 추가
헤더의 좌측 영역에 블로그 로고(타이틀)를 추가합니다. 로고는 클릭 시 홈페이지로 이동하도록 Link 컴포넌트로 감싸줍니다.
임시로 추가했던 배경색(bg-yellow-100)을 제거하고, 실제 로고 텍스트를 넣어 헤더의 기본 형태를 완성합니다.
"use client"
import Link from "next/link"
export default function Header() {
return (
<header className="fixed top-0 z-50 h-16 w-screen border-b px-6 backdrop-blur-md">
<div className="container mx-auto flex h-full items-center justify-between">
{/* left logo div */}
<div className="flex items-center">
<Link href="/" className="text-2xl font-bold">
My Blog
</Link>
</div>
{/* right button group div */}
<div className="flex h-full flex-1 items-center bg-blue-500/10"></div>
</div>
</header>
)
}
8. 헤더에 페이지 링크 추가
헤더의 우측 영역에 네비게이션 링크들을 추가합니다. 데스크톱에서는 가로로 나열되는 링크들을 보여주고, 모바일에서는 햄버거 메뉴로 대체됩니다.
네비게이션 구조는 다음과 같이 구성됩니다.
- 데스크톱 네비게이션: 768px 이상에서 표시되는 가로 메뉴
- 테마 변경 버튼: 다크모드/라이트모드 전환 (추후 구현)
- 모바일 메뉴: 작은 화면에서 표시되는 햄버거 메뉴 (추후 구현)
각 링크에는 hover:text-blue-500
효과를 적용하여 마우스 호버 시 파란색으로 변경되도록 했습니다.
"use client"
import Link from "next/link"
export default function Header() {
return (
<header className="fixed top-0 z-50 h-16 w-screen border-b px-6 backdrop-blur-md">
<div className="container mx-auto flex h-full items-center justify-between">
...
{/* right button group div */}
<div className="flex items-center justify-between gap-3 py-6 md:gap-9">
{/* 데스크탑 링크 */}
<nav className="hidden items-center space-x-8 md:flex">
<Link href="/" className="transition-colors hover:text-blue-500">Home</Link>
<Link href="/posts" className="transition-colors hover:text-blue-500">Posts</Link>
<Link href="/about" className="transition-colors hover:text-blue-500">About</Link>
</nav>
{/* 테마 변경 버튼 */}
{/* 모바일 메뉴 */}
</div>
</div>
</header>
)
}
9. 테마 토글 버튼 구현 - 01
블로그에 다크모드/라이트모드를 전환할 수 있는 테마 토글 버튼을 추가하겠습니다. Next.js와 잘 통합되는 next-themes
라이브러리를 사용하여 간단하게 구현할 수 있습니다.
npm i next-themes
"use client";
import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ThemeToggleButton() {
...
}
10. 테마 토글 버튼 구현 - 02
테마 전환을 위한 토글 버튼 컴포넌트를 생성합니다. 이 버튼은 클릭 시 드롭다운 메뉴를 표시하여 라이트/다크/시스템 테마를 선택할 수 있도록 합니다.버튼의 아이콘은 현재 테마에 따라 자동으로 변경됩니다.
- 라이트 모드: 태양 아이콘 표시
- 다크 모드: 달 아이콘 표시
- 애니메이션 효과로 부드러운 전환
...
export function ThemeToggleButton() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<SunIcon className="size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<MoonIcon className="absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
11. next-theme provider 생성
next-themes
라이브러리를 사용하기 위해 ThemeProvider를 설정합니다. 이 Provider는 앱 전체를 감싸서 모든 컴포넌트에서 테마 기능을 사용할 수 있도록 합니다.
Provider를 별도 파일로 분리하는 이유.
- 재사용 가능한 Provider 패턴 구현
- 추후 Provider 설정 변경이 용이
먼저 providers 폴더를 생성하고 ThemeProvider 컴포넌트를 작성합니다.
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
12. layout에 provider 적용
루트 레이아웃에 ThemeProvider를 적용하여 앱 전체에서 테마 기능을 사용할 수 있도록 설정합니다. 또한 Header 컴포넌트를 추가하여 모든 페이지에서 공통으로 표시되도록 합니다. 중요한 설정 사항.
suppressHydrationWarning
: next-themes가 클라이언트에서 테마를 적용할 때 발생할 수 있는 hydration 경고를 방지
lang="kr"
: 한국어 블로그임을 명시 (SEO 및 접근성 향상)
ThemeProvider 설정 옵션
attribute="class"
: HTML 요소에 클래스로 테마 적용 (Tailwind CSS와 호환)defaultTheme="system"
: 시스템 설정을 기본값으로 사용enableSystem
: 시스템의 다크모드 설정 자동 감지disable Transition OnChange
: 테마 변경 시 깜빡임 방지
...
import { ThemeProvider } from "@/providers/theme-provider";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="kr" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Header />
<div className="pt-16">{children}</div>
</ThemeProvider>
</body>
</html>
);
}
13. header에 토글 버튼 추가
header에도 완성된 버튼을 추가합니다.
dev 서버를 통해 정상 동작 테스트를 진행합니다.
"use client";
import { ThemeToggleButton } from "@/components/theme-toggle-button";
import Link from "next/link";
export default function Header() {
return (
<header className="fixed top-0 z-50 h-16 w-screen border-b px-6 backdrop-blur-md">
...
{/* 테마 변경 버튼 */}
<ThemeToggleButton />
{/* 모바일 메뉴 */}
</div>
</div>
</header>
);
}
14. 모바일 메뉴 적용
모바일 환경에서 네비게이션을 표시할 햄버거 메뉴를 구현합니다. shadcn/ui의 Sheet 컴포넌트를 활용하여 사이드에서 슬라이드되는 메뉴를 만들겠습니다.
모바일 메뉴의 특징
- 768px 미만의 화면에서만 표시
- 햄버거 아이콘 클릭 시 사이드 패널 열림
- 메뉴 항목 클릭 시 자동으로 닫힘
- 부드러운 슬라이드 애니메이션
이 메뉴는 데스크톱 네비게이션이 숨겨질 때 대체 UI로 작동하여 모든 화면 크기에서 원활한 네비게이션을 제공합니다.
15. 모바일 메뉴 - header 수정
헤더 컴포넌트에 모바일 메뉴 기능을 추가합니다. Sheet 컴포넌트를 사용하여 햄버거 메뉴 클릭 시 사이드 패널이 열리도록 구현합니다.
구현 내용
- useState로 메뉴 열림/닫힘 상태 관리
- 햄버거 아이콘은 모바일에서만 표시 (
md:hidden
) - Sheet 컴포넌트로 슬라이드 패널 구현
- 접근성을 위한 sr-only 클래스로 스크린 리더 지원
메뉴 항목 클릭 시 자동으로 패널이 닫히도록 상태 관리도 함께 구현해야 합니다.
...
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { MenuIcon } from "lucide-react";
export default function Header() {
return (
<header className="fixed top-0 z-50 h-16 w-screen border-b px-6 backdrop-blur-md">
...
{/* 모바일 메뉴 */}
<Sheet open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<SheetTrigger asChild>
<button className="md:hidden p-2" aria-label="Toggle menu"><MenuIcon className="size-6" /></button>
</SheetTrigger>
<SheetContent className="p-6 [&>button]:size-6 [&>button>svg]:size-6">
<SheetHeader>
<SheetTitle className="sr-only">모바일 내비게이션 메뉴</SheetTitle>
<SheetDescription className="sr-only">이 다이얼로그는 모바일 환경에서 사용할 수 있는 내비게이션 메뉴입니다. 아래 링크를 선택하여 사이트의 주요 페이지로 이동할 수 있습니다.</SheetDescription>
</SheetHeader>
<nav className="flex flex-col space-y-4 py-4">
<Link href="/" className="transition-colors hover:text-blue-500">Home</Link>
<Link href="/posts" className="transition-colors hover:text-blue-500">Posts</Link>
<Link href="/about" className="transition-colors hover:text-blue-500">About</Link>
</nav>
</SheetContent>
</Sheet>
...
</header>
);
}
1. Next.js 프로젝트 생성
먼저 터미널을 열고 프로젝트를 생성할 디렉토리로 이동합니다. 그 다음 아래 명령어를 실행하여 Next.js 프로젝트를 생성합니다.
npx create-next-app@latest
프로젝트 생성 과정에서 여러 옵션을 선택하게 됩니다. 이 튜토리얼에서는 다음과 같이 설정하겠습니다.
프로젝트 이름
: blog-nextjs-blog (원하는 이름으로 변경 가능)TypeScript
: Yes (타입 안정성을 위해 권장)ESLint
: Yes (코드 품질 관리)Tailwind CSS
: Yes (빠른 스타일링)src/ 디렉토리
: Yes (코드 구조화)App Router
: Yes (Next.js 13+ 권장 방식)Turbopack
: Yes (빠른 개발 서버)Import alias
: No (기본값 사용)
npm run dev
명령어를 통해 dev server를 실행합니다.
2. UI 컴포넌트 추가 - shadcn/ui
이번 섹션에 필요한 UI 컴포넌트를 먼저 추가하겠습니다.
shadcn-ui는 다양한 UI 컴포넌트를 빠르게 적용할 수 있도록 도와주는 툴킷입니다.
Next.js 프로젝트에 최적화되어 있으며, 커스터마이징도 유연하게 할 수 있습니다.
아래 명령어 실행하여 button
, dropdown-menu
, 그리고 sheet
컴포넌트를 추가합니다.
npx shadcn@latest add
명령어를 입력하면 프로젝트 설정에 따라 몇 가지 질문을 받게 되며, 기본값으로 진행해도 괜찮습니다. 설정이 완료되면 사용할 컴포넌트를 선택할 수 있습니다.
3. 페이지 구조 및 헤더 컴포넌트 생성
블로그의 기본 구조를 만들기 위해 필요한 페이지들과 헤더 컴포넌트를 생성하겠습니다. Next.js의 App Router는 폴더 구조를 기반으로 라우팅을 자동으로 처리하므로, 적절한 위치에 파일을 생성하는 것이 중요합니다.
touch src/app/posts/page.tsx
touch src/app/about/page.tsx
touch src/components/header.tsx
또는 VSCode 등의 에디터에서 직접 폴더와 파일을 생성하셔도 됩니다. 생성된 파일 구조는 다음과 같습니다.
src/
├── app/
│ ├── posts/
│ │ └── page.tsx # /posts 경로
│ ├── about/
│ │ └── page.tsx # /about 경로
│ └── page.tsx # / (홈) 경로
└── components/
└── header.tsx # 공통 헤더 컴포넌트
4. Header component 생성
블로그 상단에 고정될 헤더 컴포넌트를 만들어보겠습니다. 헤더는 화면 최상단에 고정(fixed)되며, 스크롤을 해도 항상 보이도록 구현합니다. 헤더의 구조는 다음과 같고 테스트를 위해 임시 배경색을 넣었습니다.
- 높이: 64px (Tailwind CSS의 h-16)
- 레이아웃: 좌측 로고 영역과 우측 네비게이션 영역으로 구분
- 스타일: 반투명 배경(backdrop-blur)과 하단 테두리
- 좌우 폭 제한 (
.container
)
5. layout 파일 수정
새로운 레이아웃 적용을 위해 /src/app/layout.tsx
을 수정합니다.
children을 감싸는 영역은 pt-16
클래스를 추가해 Header 컴포넌트 영역만큼 상단 공간 확보하였습니다.
6. 페이지 파일들 수정
각 페이지의 기본 구조를 설정하겠습니다. 현재는 테스트를 위한 임시 레이아웃이며, 이후 실제 콘텐츠로 채워질 예정입니다.
페이지 공통 스타일
- 최소 높이: 스크롤 테스트 가능한 화면 전체 높이 (
min-h-screen
) - 콘텐츠 정렬: 가로 중앙 정렬 (
flex justify-center
) - 임시 텍스트: 각 페이지 확인용 제목
7. 헤더에 로고 추가
헤더의 좌측 영역에 블로그 로고(타이틀)를 추가합니다. 로고는 클릭 시 홈페이지로 이동하도록 Link 컴포넌트로 감싸줍니다.
임시로 추가했던 배경색(bg-yellow-100)을 제거하고, 실제 로고 텍스트를 넣어 헤더의 기본 형태를 완성합니다.
8. 헤더에 페이지 링크 추가
헤더의 우측 영역에 네비게이션 링크들을 추가합니다. 데스크톱에서는 가로로 나열되는 링크들을 보여주고, 모바일에서는 햄버거 메뉴로 대체됩니다.
네비게이션 구조는 다음과 같이 구성됩니다.
- 데스크톱 네비게이션: 768px 이상에서 표시되는 가로 메뉴
- 테마 변경 버튼: 다크모드/라이트모드 전환 (추후 구현)
- 모바일 메뉴: 작은 화면에서 표시되는 햄버거 메뉴 (추후 구현)
각 링크에는 hover:text-blue-500
효과를 적용하여 마우스 호버 시 파란색으로 변경되도록 했습니다.
9. 테마 토글 버튼 구현 - 01
블로그에 다크모드/라이트모드를 전환할 수 있는 테마 토글 버튼을 추가하겠습니다. Next.js와 잘 통합되는 next-themes
라이브러리를 사용하여 간단하게 구현할 수 있습니다.
npm i next-themes
10. 테마 토글 버튼 구현 - 02
테마 전환을 위한 토글 버튼 컴포넌트를 생성합니다. 이 버튼은 클릭 시 드롭다운 메뉴를 표시하여 라이트/다크/시스템 테마를 선택할 수 있도록 합니다.버튼의 아이콘은 현재 테마에 따라 자동으로 변경됩니다.
- 라이트 모드: 태양 아이콘 표시
- 다크 모드: 달 아이콘 표시
- 애니메이션 효과로 부드러운 전환
11. next-theme provider 생성
next-themes
라이브러리를 사용하기 위해 ThemeProvider를 설정합니다. 이 Provider는 앱 전체를 감싸서 모든 컴포넌트에서 테마 기능을 사용할 수 있도록 합니다.
Provider를 별도 파일로 분리하는 이유.
- 재사용 가능한 Provider 패턴 구현
- 추후 Provider 설정 변경이 용이
먼저 providers 폴더를 생성하고 ThemeProvider 컴포넌트를 작성합니다.
12. layout에 provider 적용
루트 레이아웃에 ThemeProvider를 적용하여 앱 전체에서 테마 기능을 사용할 수 있도록 설정합니다. 또한 Header 컴포넌트를 추가하여 모든 페이지에서 공통으로 표시되도록 합니다. 중요한 설정 사항.
suppressHydrationWarning
: next-themes가 클라이언트에서 테마를 적용할 때 발생할 수 있는 hydration 경고를 방지
lang="kr"
: 한국어 블로그임을 명시 (SEO 및 접근성 향상)
ThemeProvider 설정 옵션
attribute="class"
: HTML 요소에 클래스로 테마 적용 (Tailwind CSS와 호환)defaultTheme="system"
: 시스템 설정을 기본값으로 사용enableSystem
: 시스템의 다크모드 설정 자동 감지disable Transition OnChange
: 테마 변경 시 깜빡임 방지
13. header에 토글 버튼 추가
header에도 완성된 버튼을 추가합니다.
dev 서버를 통해 정상 동작 테스트를 진행합니다.
14. 모바일 메뉴 적용
모바일 환경에서 네비게이션을 표시할 햄버거 메뉴를 구현합니다. shadcn/ui의 Sheet 컴포넌트를 활용하여 사이드에서 슬라이드되는 메뉴를 만들겠습니다.
모바일 메뉴의 특징
- 768px 미만의 화면에서만 표시
- 햄버거 아이콘 클릭 시 사이드 패널 열림
- 메뉴 항목 클릭 시 자동으로 닫힘
- 부드러운 슬라이드 애니메이션
이 메뉴는 데스크톱 네비게이션이 숨겨질 때 대체 UI로 작동하여 모든 화면 크기에서 원활한 네비게이션을 제공합니다.
15. 모바일 메뉴 - header 수정
헤더 컴포넌트에 모바일 메뉴 기능을 추가합니다. Sheet 컴포넌트를 사용하여 햄버거 메뉴 클릭 시 사이드 패널이 열리도록 구현합니다.
구현 내용
- useState로 메뉴 열림/닫힘 상태 관리
- 햄버거 아이콘은 모바일에서만 표시 (
md:hidden
) - Sheet 컴포넌트로 슬라이드 패널 구현
- 접근성을 위한 sr-only 클래스로 스크린 리더 지원
메뉴 항목 클릭 시 자동으로 패널이 닫히도록 상태 관리도 함께 구현해야 합니다.
➜ npx create-next-app@latest
Need to install the following packages:
create-next-app@15.4.6
Ok to proceed? (y)
✔ What is your project named? … blog-nextjs-blog
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias [`@/*` by default?] … No / Yes
Creating a new Next.js app in /Users/mech2cs/work/mech2cs/blog/blog-nextjs-blog.
포스트 본문 페이지 구현
이제 블로그의 핵심 기능인 포스트 본문 페이지를 구현해보겠습니다. Next.js의 동적 라우팅을 활용하여 각 포스트를 개별 페이지로 표시하고, 마크다운 파일을 HTML로 렌더링하는 방법을 알아봅니다.
이 섹션에서 다룰 내용
- 동적 라우팅 설정 (
[slug]
파라미터) - 마크다운 파일 읽기 및 파싱
- 메타데이터 처리 (제목, 날짜, 태그 등)
- 마크다운을 HTML로 변환 및 스타일 적용
git clone git@github.com:sijunnoh/blog-nextjs-blog.git
git checkout section02
1. 테스트용 MDX 파일 준비하기
포스트 페이지를 테스트하기 위해 샘플 MDX 파일이 필요합니다. MDX는 마크다운에 JSX를 결합한 포맷으로, 메타데이터와 콘텐츠를 함께 관리할 수 있습니다.
src/data/posts
디렉토리를 생성한 후 mdx 파일을 추가합니다.
TIP: 직접 작성해도 되지만, ChatGPT, Claude 등 AI 툴을 활용하면 더 빠르게 다양한 예시를 생성할 수 있습니다.
---
title: "Next.js 14 App Router 완벽 가이드"
date: "2024-12-15"
description: "Next.js 14의 App Router를 활용한 모던 웹 애플리케이션 개발 방법을 상세히 알아봅니다."
tags: ["Next.js", "React", "TypeScript", "Web Development"]
category: "Frontend"
thumbnail: "/images/nextjs-14-thumbnail.jpg"
readingTime: "15분"
draft: false
---
## 1. App Router란?
App Router는 Next.js 13에서 도입된 새로운 라우팅 시스템입니다. 기존의 Pages Router와 비교하여 다음과 같은 장점이 있습니다:
- **서버 컴포넌트 기본 지원**: 클라이언트 번들 크기 감소
- **중첩 레이아웃**: 효율적인 레이아웃 관리
- **스트리밍과 Suspense**: 향상된 로딩 경험
- **병렬 라우트**: 동시에 여러 페이지 렌더링
...
2. MDX 렌더링 라이브러리 설치 및 포스트 페이지 구현
마크다운 콘텐츠를 React 컴포넌트로 변환하기 위해 next-mdx-remote-client
라이브러리를 사용합니다. 이 라이브러리는 Next.js App Router와 완벽하게 호환되며, 서버 컴포넌트에서 MDX를 처리할 수 있습니다.
npm install next-mdx-remote-client
동적 라우트 페이지를 생성하고 파일 내용도 추가해줍니다.
src/posts/[slug]/page.tsx
import { promises as fs } from "fs";
import { evaluate, EvaluateOptions } from "next-mdx-remote-client/rsc";
import path from "path";
const PostPage = async ({ params }: { params: Promise<{ slug: string }> }) => {
const { slug } = await params;
const source = await fs.readFile(path.join(process.cwd(), "src/data/posts", `${slug}.mdx`), "utf-8");
const options: EvaluateOptions = {
mdxOptions: {
remarkPlugins: [],
rehypePlugins: [],
},
parseFrontmatter: true,
};
const { content } = await evaluate({
source,
options,
});
return (
<main className="flex min-h-full w-full">
<article className="container mx-auto p-6 border-x">{content}</article>
</main>
);
};
export default PostPage;
3. 포스트 본문 페이지 테스트
포스트 페이지가 정상적으로 작동하는지 확인해봅시다. 브라우저에서 다음 URL로 접속합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 마크다운 콘텐츠가 HTML로 변환되어 표시되는지
- 레이아웃이 중앙 정렬되어 있는지
4. 포스트 본문 기본 스타일 주입
Tailwind CSS는 기본 HTML 스타일을 초기화하기 때문에 마크다운으로 작성된 콘텐츠가 스타일 없이 표시됩니다. 이를 해결하기 위해 @tailwindcss/typography
플러그인을 사용합니다.
npm install @tailwindcss/typography
globals.css 파일에 @tailwindcss/typography를 추가하고, 포스트 본문 컴포넌트에 prose 클래스를 적용합니다.
prose 클래스의 주요 기능
- 제목, 문단, 리스트 등에 적절한 여백과 크기 자동 적용
- 코드 블록과 인라인 코드 스타일링
- 링크, 인용구, 테이블 등 마크다운 요소 스타일
- 다크모드 자동 대응 (
dark:prose-invert
)
...
const PostPage = async ({ params }: { params: Promise<{ slug: string }> }) => {
...
return (
<main className="flex min-h-full w-full">
<article className="container mx-auto p-6 border-x">
<div className="prose dark:prose-invert max-w-none overflow-x-auto">{content}</div>
</article>
</main>
);
};
export default PostPage;
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@tailwindcss/typography";
...
5. 포스트 본문 테스트 - 기본 스타일
Typography 플러그인이 정상적으로 적용되었는지 확인해봅시다. 스타일이 적용되면 제목, 문단, 코드 블록 등이 보기 좋게 표시됩니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 스타일 적용 여부
- 코드 블록 스타일
- 모바일 overflow 방지 여부
- 다크모드/라이트모드
6. 코드 하이라이트 플러그인 적용
코드 블록을 더 읽기 쉽고 아름답게 만들기 위해 syntax highlighting을 추가합니다. rehype-pretty-code는 VS Code와 동일한 테마를 사용할 수 있는 강력한 코드 하이라이트 플러그인입니다.
npm install remark-gfm rehype-pretty-code
포스트 페이지에 플러그인을 적용하고, lign highlighting을 위해 css를 추가합니다.
...
import rehypePrettyCode from "rehype-pretty-code";
const PostPage = async ({ params }: { params: Promise<{ slug: string }> }) => {
...
const options: EvaluateOptions = {
mdxOptions: {
remarkPlugins: [],
rehypePlugins: [rehypePrettyCode],
},
parseFrontmatter: true,
};
...
};
export default PostPage;
code [data-highlighted-line] {
background: rgba(255, 255, 255, 0.1);
}
7. 포스트 본문 테스트 - 코드 하이라이트
코드 하이라이트 플러그인이 정상적으로 작동하는지 확인합니다. 코드 블록이 문법에 따라 색상이 구분되고, 특정 줄이 강조되어야 합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 코드 블럭 다크모드 배경색 확인
- 코드 블럭 라인 하이라이트 적용 여부 확인
8. 코드 블럭에 복사 버튼 추가하기
next-mdx-remote-client/rsc
는 MDX 컴포넌트를(h1, h2, h3, ... pre) custom된 컴포넌트로 변경하는 기능을 제공합니다.
이를 활용하여 코드 블럭(pre
)에 복사 버튼을 추가해보겠습니다.
구현 내용
- 코드 블록 우측 상단에 복사 버튼 추가
- 클릭 시 코드 내용을 클립보드에 복사
- 복사 완료 시 시각적 피드백 제공
9. Pre 커스텀 컴포넌트 생성
Pre 커스텀 컴포넌트를 구현해봅니다.
아래 경로에 파일을 생성해 줍니다.
src/features/posts/components/pre.tsx
코드 길이가 길어 코드 내용을 생략하였습니다. 전체 코드 복사 버튼을 사용해주세요.
"use client" import { DetailedHTMLProps, HTMLAttributes, useRef, useState } from "react" import { CheckIcon, ClipboardIcon } from "lucide-react/" import { Button } from "@/components/ui/button" export default function Pre({ children, ...props }: DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement>) { const [isCopied, setIsCopied] = useState(false) const preRef = useRef<HTMLPreElement>(null) const handleClickCopy = async () => { const code = preRef.current?.textContent if (!code) return if (navigator.clipboard) { await navigator.clipboard.writeText(code) } else { const textArea = document.createElement("textarea") textArea.value = code document.body.appendChild(textArea) textArea.select() document.execCommand("copy") document.body.removeChild(textArea) } setIsCopied(true) setTimeout(() => { setIsCopied(false) }, 3000) } return ( <div className="group relative"> <pre ref={preRef} {...props} style={{ ...props.style, paddingRight: "3.5rem", }} className="overflow-x-auto" > {children} </pre> <Button size="icon" disabled={isCopied} onClick={handleClickCopy} className="absolute top-2 right-2 h-8 w-8 border border-neutral-700 bg-neutral-900 opacity-60 transition-all hover:bg-neutral-800 hover:opacity-100" variant="ghost" > {isCopied ? ( <CheckIcon className="h-4 w-4 text-green-400" /> ) : ( <ClipboardIcon className="h-4 w-4" /> )} </Button> </div> ) }
"use client"
import { DetailedHTMLProps, HTMLAttributes, useRef, useState } from "react"
import { CheckIcon, ClipboardIcon } from "lucide-react/"
import { Button } from "@/components/ui/button"
export default function Pre({
children,
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement>) {
...
return ...
}
10. MDX Components 관리 파일 생성
MDX에서 사용할 커스텀 컴포넌트들을 중앙에서 관리하기 위한 파일을 생성합니다. 이 파일은 기본 HTML 요소를 커스텀 React 컴포넌트로 대체하는 매핑을 정의합니다.
역할
- MDX 렌더링 시 사용할 커스텀 컴포넌트 정의
- 여러 컴포넌트를 한 곳에서 관리
- 추후
h1
,h2
,img
등 다른 요소도 커스터마이징 가능
src/features/posts/components/mdx-components.tsx
import Pre from "@/features/posts/components/pre";
import type { MDXComponents } from "next-mdx-remote-client/rsc";
export const mdxComponents: MDXComponents = {
pre: (props) => <Pre {...props} />,
};
11. 포스트 본문에 MDX Components 적용
생성한 커스텀 컴포넌트들을 실제 MDX 렌더링에 적용합니다. evaluate 함수에 components 옵션을 추가하여 기본 HTML 요소를 커스텀 컴포넌트로 대체합니다.
적용 결과
pre
태그가 복사 버튼이 있는 Pre 컴포넌트로 렌더링- 모든 코드 블록에 자동으로 복사 기능 추가
- 추후 다른 컴포넌트도 동일한 방식으로 확장 가능
import { mdxComponents } from "@/features/posts/components/mdx-components";
const PostPage = async ({ params }: { params: Promise<{ slug: string }> }) => {
...
const { content } = await evaluate({
source,
options,
components: mdxComponents,
});
return (
<main className="flex min-h-full w-full">
<article className="container mx-auto p-6 border-x">
<div className="prose dark:prose-invert max-w-none overflow-x-auto">{content}</div>
</article>
</main>
);
};
export default PostPage;
12. 포스트 본문 테스트 - 복사 버튼
코드 블럭 내 복사 버튼 정상 동작 여부를 확인합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 코드 블럭 복사 버튼 시 css 효과 확인
- 붙여넣기하여 복사 정상 동작 화인
13. 포스트 헤더 컴포넌트 추가
포스트 상단에 제목, 작성일, 카테고리, 태그 등의 메타데이터를 보기 좋게 표시하는 헤더 컴포넌트를 생성합니다. MDX의 frontmatter 데이터를 활용하여 포스트 정보를 구조화된 형태로 보여줍니다. 아래 경로에 파일을 생성해 줍니다.
src/features/posts/components/post-header.tsx
코드 길이가 길어 코드 내용을 생략하였습니다. 전체 코드 복사 버튼을 사용해주세요.
import { PostFrontmatter } from '../types/post' interface PostHeaderProps { frontmatter: PostFrontmatter } export function PostHeader({ frontmatter }: PostHeaderProps) { return ( <header className="mb-8"> <div className="space-y-4"> <div className="flex flex-wrap gap-2"> {frontmatter.tags.map((tag) => ( <span key={tag} className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-md dark:bg-gray-800 dark:text-gray-200" > {tag} </span> ))} </div> <h1 className="text-4xl font-bold text-gray-900 dark:text-gray-100"> {frontmatter.title} </h1> <p className="text-lg text-gray-600 dark:text-gray-400"> {frontmatter.description} </p> <div className="flex flex-col sm:flex-row sm:items-center gap-4 text-sm text-gray-500 dark:text-gray-400"> <div className="flex items-center gap-4"> <time dateTime={frontmatter.date}> {new Date(frontmatter.date).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })} </time> {frontmatter.updated && ( <span> · 수정됨: {new Date(frontmatter.updated).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })} </span> )} </div> <div className="flex items-center gap-2"> <span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-md dark:bg-blue-900 dark:text-blue-200"> {frontmatter.category} </span> </div> </div> </div> </header> ) }
import { PostFrontmatter } from '../types/post'
interface PostHeaderProps {
frontmatter: PostFrontmatter
}
export function PostHeader({ frontmatter }: PostHeaderProps) {
return (
<header className="mb-8">
...
</header>
)
}
14. frontmatter 타입 파일 생성
TypeScript를 사용하여 MDX 파일의 frontmatter 구조를 명확하게 정의합니다. 이를 통해 타입 안정성을 확보하고 자동완성 기능을 활용할 수 있습니다.
src/features/posts/types/post.ts
export type PostFrontmatter = {
title: string
description: string
date: string
updated?: string
slug: string
thumbnail?: string
thumbnailAlt?: string
tags: string[]
category: string
draft: boolean
tutorialData?: string
}
15. 포스트 본문에 PostHeader 컴포넌트 추가
포스트 페이지에 PostHeader 컴포넌트를 추가하여 제목, 날짜, 카테고리, 태그 등의 메타데이터를 상단에 표시합니다.
주요 변경사항
- PostHeader 컴포넌트를 import하여 사용
- frontmatter 데이터를 PostHeader에 전달
- 레이아웃을 flex 구조로 변경하여 추후 사이드바 추가 준비
- 콘텐츠 영역에 좌우 테두리 추가로 시각적 구분
...
import { PostFrontmatter } from "@/features/posts/types/post";
import { PostHeader } from "@/features/posts/components/post-header";
...
const PostPage = async ({ params }: { params: Promise<{ slug: string }> }) => {
...
const { content, frontmatter } = await evaluate<PostFrontmatter>({
source,
options,
components: mdxComponents,
});
return (
<main className="flex min-h-full w-full">
<article className="container mx-auto flex px-3">
<div className="prose dark:prose-invert max-w-none overflow-x-auto flex-1 border-x p-6">
<PostHeader frontmatter={frontmatter} />
{content}
</div>
</article>
</main>
);
};
export default PostPage;
16. 포스트 본문 테스트 - PostHeader
포스트 본문 내 UI를 확인합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 헤더 UI
17. 헤딩 링크 & 포스트 본문에 목차 기능 추가
라이브러리를 추가합니다.
npm i rehype-slug remark-flexible-toc rehype-autolink-headings
각 플러그인의 역할
rehype-slug
: 제목(h1~h6)에 자동으로 ID를 생성합니다rehype-autolink-headings
: 제목에 앵커 링크를 추가합니다remark-flexible-toc
: 마크다운 문서에서 목차를 자동 생성합니다
18. 포스트 본문에 플러그인 적용
링크를 생성하며, 목차 데이터를 자동으로 생성합니다.
적용되는 플러그인
- remarkFlexibleToc: 마크다운에서 목차 데이터를 추출
- rehypeSlug: 각 제목에 고유 ID 부여
- rehypeAutolinkHeadings: 제목을 클릭 가능한 링크로 변환
...
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import remarkFlexibleToc, { type TocItem } from "remark-flexible-toc";
type Scope = {
toc?: TocItem[];
};
const PostPage = async ({ params }: { params: Promise<{ slug: string }> }) => {
...
const options: EvaluateOptions = {
mdxOptions: {
remarkPlugins: [remarkFlexibleToc],
rehypePlugins: [
rehypePrettyCode,
rehypeSlug,
[
rehypeAutolinkHeadings,
{
behavior: "wrap",
properties: {
className: ["heading-link"],
ariaLabel: "Link to this heading",
},
},
],
...
19. 헤딩 링크 CSS 추가
제목에 마우스를 올렸을 때 나타나는 앵커 링크(#) 스타일을 추가합니다. 이 스타일은 사용자가 특정 섹션의 링크를 쉽게 복사하거나 공유할 수 있도록 시각적 피드백을 제공합니다.
스타일 특징
- 제목 호버 시 왼쪽에 # 기호 표시
- 부드러운 페이드 인/아웃 애니메이션
- 다크모드 대응 색상
- 클릭 가능한 영역 확대
이 스타일은 GitHub나 다른 문서 사이트에서 흔히 볼 수 있는 UX 패턴입니다.
...
.heading-link {
text-decoration: none;
position: relative;
transition: color 0.2s ease;
}
.heading-link:hover {
color: oklch(0.696 0.17 162.48);
}
.heading-link::before {
content: "#";
position: absolute;
left: -1.5rem;
opacity: 0;
transition: opacity 0.2s ease;
color: oklch(0.556 0 0);
}
.dark .heading-link::before {
color: oklch(0.708 0 0);
}
.heading-link:hover::before {
opacity: 0.5;
}
20. Heading Link 테스트
Heading에 추가된 Link를 확인합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- hover 시 디자인(이미지 참조)
- 클릭 시 페이지 내 해당 헤딩 이동 동작
21. TableOfContent 컴포넌트 생성
포스트 사이드바에 표시될 목차(Table of Contents) 컴포넌트를 생성합니다. 이 컴포넌트는 현재 스크롤 위치를 추적하여 읽고 있는 섹션을 하이라이트하고, 클릭 시 해당 섹션으로 부드럽게 스크롤합니다.
주요 기능
- 스크롤 추적: IntersectionObserver API로 현재 보이는 섹션 감지
- 액티브 표시: 현재 읽고 있는 섹션 하이라이트
- 부드러운 스크롤: 목차 클릭 시 해당 섹션으로 이동
- 계층 구조: 제목 깊이에 따른 들여쓰기
- 스티키 포지션: 스크롤해도 화면에 고정
"use client"; import { useEffect, useState } from "react"; import { TocItem } from "remark-flexible-toc"; interface TableOfContentsProps { toc: TocItem[]; } export function TableOfContents({ toc }: TableOfContentsProps) { const [activeId, setActiveId] = useState<string>(""); useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setActiveId(entry.target.id); } }); }, { rootMargin: "0px 0px -80% 0px", threshold: 0.1, } ); // toc 항목들의 ID를 기반으로 heading 요소들을 관찰 const headingElements = toc .map((item) => { const id = item.href.substring(1); // # 제거 return document.getElementById(id); }) .filter((el): el is HTMLElement => el !== null); headingElements.forEach((element) => { observer.observe(element); }); return () => { headingElements.forEach((element) => { observer.unobserve(element); }); }; }, [toc]); if (!toc || toc.length === 0) { return null; } const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => { e.preventDefault(); const targetId = href.substring(1); // # 제거 const element = document.getElementById(targetId); if (element) { element.scrollIntoView({ behavior: "smooth", block: "start" }); } }; return ( <nav className="sticky top-24 pr-4"> <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3"> 목차 </h3> <ul className="space-y-1 text-sm"> {toc.map((item, index) => { const targetId = item.href.substring(1); // # 제거 const isActive = activeId === targetId; return ( <li key={index} style={{ paddingLeft: `${(item.depth - 1) * 0.75}rem`, }} > <a href={item.href} onClick={(e) => handleClick(e, item.href)} className={` block py-1.5 px-2 transition-colors duration-200 ${ isActive ? "text-blue-700 dark:text-blue-300 font-medium" : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200" } `} > {item.value} </a> </li> ); })} </ul> </nav> ); }
...
"use client";
import { useEffect, useState } from "react";
import { TocItem } from "remark-flexible-toc";
interface TableOfContentsProps {
toc: TocItem[];
}
export function TableOfContents({ toc }: TableOfContentsProps) {
...
return (
<nav className="sticky top-24 pr-4">
...
</nav>
);
}
22. 포스트 본문에 TOC 추가
포스트 페이지에 TableOfContents 컴포넌트를 추가하여 사이드바에 목차를 표시합니다. 목차는 데스크톱(lg 이상)에서만 표시되며, 스크롤해도 고정된 위치에 유지됩니다.
주요 변경사항
- TableOfContents 컴포넌트 import
- vfileDataIntoScope: "toc" 옵션으로 목차 데이터를 scope에 포함
- 우측 사이드바(aside) 영역 추가
- 반응형 디자인 (lg 브레이크포인트에서만 표시)
레이아웃 구조
- 메인 콘텐츠: flex-1로 가능한 공간 차지
- 사이드바: 고정 너비(w-56) 및 위치(fixed)
- 목차: 헤더 아래(top-16)에 고정
...
import { TableOfContents } from "@/features/posts/components/table-of-contents";
const PostPage = async ({ params }: { params: Promise<{ slug: string }> }) => {
...
const options: EvaluateOptions = {
...
parseFrontmatter: true,
vfileDataIntoScope: "toc",
};
const { content, frontmatter, scope } = await evaluate<PostFrontmatter, {toc?: TocItem[]}>({
source,
options,
components: mdxComponents,
});
return (
<main className="flex min-h-full w-full">
<article className="container mx-auto flex px-3">
...
<aside className="z-40 hidden w-56 flex-shrink-0 lg:block">
<div className="fixed top-16 w-56 h-full overflow-auto p-3">{scope?.toc && <TableOfContents toc={scope.toc} />}</div>
</aside>
</article>
</main>
);
};
export default PostPage;
23. TableOfContent 테스트
TableOfContent 기능을 테스트합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 클릭시 부드러운 스크롤로 해당 헤딩으로 이동되느지 확인
- 스크롤 시 목차 내 헤딩 활성화 CSS 동작 확인
1. 테스트용 MDX 파일 준비하기
포스트 페이지를 테스트하기 위해 샘플 MDX 파일이 필요합니다. MDX는 마크다운에 JSX를 결합한 포맷으로, 메타데이터와 콘텐츠를 함께 관리할 수 있습니다.
src/data/posts
디렉토리를 생성한 후 mdx 파일을 추가합니다.
TIP: 직접 작성해도 되지만, ChatGPT, Claude 등 AI 툴을 활용하면 더 빠르게 다양한 예시를 생성할 수 있습니다.
2. MDX 렌더링 라이브러리 설치 및 포스트 페이지 구현
마크다운 콘텐츠를 React 컴포넌트로 변환하기 위해 next-mdx-remote-client
라이브러리를 사용합니다. 이 라이브러리는 Next.js App Router와 완벽하게 호환되며, 서버 컴포넌트에서 MDX를 처리할 수 있습니다.
npm install next-mdx-remote-client
동적 라우트 페이지를 생성하고 파일 내용도 추가해줍니다.
src/posts/[slug]/page.tsx
3. 포스트 본문 페이지 테스트
포스트 페이지가 정상적으로 작동하는지 확인해봅시다. 브라우저에서 다음 URL로 접속합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 마크다운 콘텐츠가 HTML로 변환되어 표시되는지
- 레이아웃이 중앙 정렬되어 있는지
4. 포스트 본문 기본 스타일 주입
Tailwind CSS는 기본 HTML 스타일을 초기화하기 때문에 마크다운으로 작성된 콘텐츠가 스타일 없이 표시됩니다. 이를 해결하기 위해 @tailwindcss/typography
플러그인을 사용합니다.
npm install @tailwindcss/typography
globals.css 파일에 @tailwindcss/typography를 추가하고, 포스트 본문 컴포넌트에 prose 클래스를 적용합니다.
prose 클래스의 주요 기능
- 제목, 문단, 리스트 등에 적절한 여백과 크기 자동 적용
- 코드 블록과 인라인 코드 스타일링
- 링크, 인용구, 테이블 등 마크다운 요소 스타일
- 다크모드 자동 대응 (
dark:prose-invert
)
5. 포스트 본문 테스트 - 기본 스타일
Typography 플러그인이 정상적으로 적용되었는지 확인해봅시다. 스타일이 적용되면 제목, 문단, 코드 블록 등이 보기 좋게 표시됩니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 스타일 적용 여부
- 코드 블록 스타일
- 모바일 overflow 방지 여부
- 다크모드/라이트모드
6. 코드 하이라이트 플러그인 적용
코드 블록을 더 읽기 쉽고 아름답게 만들기 위해 syntax highlighting을 추가합니다. rehype-pretty-code는 VS Code와 동일한 테마를 사용할 수 있는 강력한 코드 하이라이트 플러그인입니다.
npm install remark-gfm rehype-pretty-code
포스트 페이지에 플러그인을 적용하고, lign highlighting을 위해 css를 추가합니다.
7. 포스트 본문 테스트 - 코드 하이라이트
코드 하이라이트 플러그인이 정상적으로 작동하는지 확인합니다. 코드 블록이 문법에 따라 색상이 구분되고, 특정 줄이 강조되어야 합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 코드 블럭 다크모드 배경색 확인
- 코드 블럭 라인 하이라이트 적용 여부 확인
8. 코드 블럭에 복사 버튼 추가하기
next-mdx-remote-client/rsc
는 MDX 컴포넌트를(h1, h2, h3, ... pre) custom된 컴포넌트로 변경하는 기능을 제공합니다.
이를 활용하여 코드 블럭(pre
)에 복사 버튼을 추가해보겠습니다.
구현 내용
- 코드 블록 우측 상단에 복사 버튼 추가
- 클릭 시 코드 내용을 클립보드에 복사
- 복사 완료 시 시각적 피드백 제공
9. Pre 커스텀 컴포넌트 생성
Pre 커스텀 컴포넌트를 구현해봅니다.
아래 경로에 파일을 생성해 줍니다.
src/features/posts/components/pre.tsx
코드 길이가 길어 코드 내용을 생략하였습니다. 전체 코드 복사 버튼을 사용해주세요.
10. MDX Components 관리 파일 생성
MDX에서 사용할 커스텀 컴포넌트들을 중앙에서 관리하기 위한 파일을 생성합니다. 이 파일은 기본 HTML 요소를 커스텀 React 컴포넌트로 대체하는 매핑을 정의합니다.
역할
- MDX 렌더링 시 사용할 커스텀 컴포넌트 정의
- 여러 컴포넌트를 한 곳에서 관리
- 추후
h1
,h2
,img
등 다른 요소도 커스터마이징 가능
src/features/posts/components/mdx-components.tsx
11. 포스트 본문에 MDX Components 적용
생성한 커스텀 컴포넌트들을 실제 MDX 렌더링에 적용합니다. evaluate 함수에 components 옵션을 추가하여 기본 HTML 요소를 커스텀 컴포넌트로 대체합니다.
적용 결과
pre
태그가 복사 버튼이 있는 Pre 컴포넌트로 렌더링- 모든 코드 블록에 자동으로 복사 기능 추가
- 추후 다른 컴포넌트도 동일한 방식으로 확장 가능
12. 포스트 본문 테스트 - 복사 버튼
코드 블럭 내 복사 버튼 정상 동작 여부를 확인합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 코드 블럭 복사 버튼 시 css 효과 확인
- 붙여넣기하여 복사 정상 동작 화인
13. 포스트 헤더 컴포넌트 추가
포스트 상단에 제목, 작성일, 카테고리, 태그 등의 메타데이터를 보기 좋게 표시하는 헤더 컴포넌트를 생성합니다. MDX의 frontmatter 데이터를 활용하여 포스트 정보를 구조화된 형태로 보여줍니다. 아래 경로에 파일을 생성해 줍니다.
src/features/posts/components/post-header.tsx
코드 길이가 길어 코드 내용을 생략하였습니다. 전체 코드 복사 버튼을 사용해주세요.
14. frontmatter 타입 파일 생성
TypeScript를 사용하여 MDX 파일의 frontmatter 구조를 명확하게 정의합니다. 이를 통해 타입 안정성을 확보하고 자동완성 기능을 활용할 수 있습니다.
src/features/posts/types/post.ts
15. 포스트 본문에 PostHeader 컴포넌트 추가
포스트 페이지에 PostHeader 컴포넌트를 추가하여 제목, 날짜, 카테고리, 태그 등의 메타데이터를 상단에 표시합니다.
주요 변경사항
- PostHeader 컴포넌트를 import하여 사용
- frontmatter 데이터를 PostHeader에 전달
- 레이아웃을 flex 구조로 변경하여 추후 사이드바 추가 준비
- 콘텐츠 영역에 좌우 테두리 추가로 시각적 구분
16. 포스트 본문 테스트 - PostHeader
포스트 본문 내 UI를 확인합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 헤더 UI
17. 헤딩 링크 & 포스트 본문에 목차 기능 추가
라이브러리를 추가합니다.
npm i rehype-slug remark-flexible-toc rehype-autolink-headings
각 플러그인의 역할
rehype-slug
: 제목(h1~h6)에 자동으로 ID를 생성합니다rehype-autolink-headings
: 제목에 앵커 링크를 추가합니다remark-flexible-toc
: 마크다운 문서에서 목차를 자동 생성합니다
18. 포스트 본문에 플러그인 적용
링크를 생성하며, 목차 데이터를 자동으로 생성합니다.
적용되는 플러그인
- remarkFlexibleToc: 마크다운에서 목차 데이터를 추출
- rehypeSlug: 각 제목에 고유 ID 부여
- rehypeAutolinkHeadings: 제목을 클릭 가능한 링크로 변환
19. 헤딩 링크 CSS 추가
제목에 마우스를 올렸을 때 나타나는 앵커 링크(#) 스타일을 추가합니다. 이 스타일은 사용자가 특정 섹션의 링크를 쉽게 복사하거나 공유할 수 있도록 시각적 피드백을 제공합니다.
스타일 특징
- 제목 호버 시 왼쪽에 # 기호 표시
- 부드러운 페이드 인/아웃 애니메이션
- 다크모드 대응 색상
- 클릭 가능한 영역 확대
이 스타일은 GitHub나 다른 문서 사이트에서 흔히 볼 수 있는 UX 패턴입니다.
20. Heading Link 테스트
Heading에 추가된 Link를 확인합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- hover 시 디자인(이미지 참조)
- 클릭 시 페이지 내 해당 헤딩 이동 동작
21. TableOfContent 컴포넌트 생성
포스트 사이드바에 표시될 목차(Table of Contents) 컴포넌트를 생성합니다. 이 컴포넌트는 현재 스크롤 위치를 추적하여 읽고 있는 섹션을 하이라이트하고, 클릭 시 해당 섹션으로 부드럽게 스크롤합니다.
주요 기능
- 스크롤 추적: IntersectionObserver API로 현재 보이는 섹션 감지
- 액티브 표시: 현재 읽고 있는 섹션 하이라이트
- 부드러운 스크롤: 목차 클릭 시 해당 섹션으로 이동
- 계층 구조: 제목 깊이에 따른 들여쓰기
- 스티키 포지션: 스크롤해도 화면에 고정
22. 포스트 본문에 TOC 추가
포스트 페이지에 TableOfContents 컴포넌트를 추가하여 사이드바에 목차를 표시합니다. 목차는 데스크톱(lg 이상)에서만 표시되며, 스크롤해도 고정된 위치에 유지됩니다.
주요 변경사항
- TableOfContents 컴포넌트 import
- vfileDataIntoScope: "toc" 옵션으로 목차 데이터를 scope에 포함
- 우측 사이드바(aside) 영역 추가
- 반응형 디자인 (lg 브레이크포인트에서만 표시)
레이아웃 구조
- 메인 콘텐츠: flex-1로 가능한 공간 차지
- 사이드바: 고정 너비(w-56) 및 위치(fixed)
- 목차: 헤더 아래(top-16)에 고정
23. TableOfContent 테스트
TableOfContent 기능을 테스트합니다.
npm run dev
# port 및 테스트 파일명 확인
# localhost:{port}/posts/{test_file_name}
localhost:3000/posts/test1
확인 사항
- 클릭시 부드러운 스크롤로 해당 헤딩으로 이동되느지 확인
- 스크롤 시 목차 내 헤딩 활성화 CSS 동작 확인
---
title: "Next.js 14 App Router 완벽 가이드"
date: "2024-12-15"
description: "Next.js 14의 App Router를 활용한 모던 웹 애플리케이션 개발 방법을 상세히 알아봅니다."
tags: ["Next.js", "React", "TypeScript", "Web Development"]
category: "Frontend"
thumbnail: "/images/nextjs-14-thumbnail.jpg"
readingTime: "15분"
draft: false
---
## 1. App Router란?
App Router는 Next.js 13에서 도입된 새로운 라우팅 시스템입니다. 기존의 Pages Router와 비교하여 다음과 같은 장점이 있습니다:
- **서버 컴포넌트 기본 지원**: 클라이언트 번들 크기 감소
- **중첩 레이아웃**: 효율적인 레이아웃 관리
- **스트리밍과 Suspense**: 향상된 로딩 경험
- **병렬 라우트**: 동시에 여러 페이지 렌더링
...
포스트 리스트, 메인, About 페이지 구현
블로그의 핵심 페이지들을 구현해보겠습니다. 메인 페이지는 방문자를 환영하고 최신 콘텐츠를 보여주며, 포스트 목록 페이지는 모든 글을 한눈에 볼 수 있게 하고, About 페이지는 블로그와 운영자를 소개합니다.
git clone git@github.com:sijunnoh/blog-nextjs-blog.git
git checkout section03
1. Card 컴포넌트 생성
포스트를 카드 형태로 표시하기 위해 shadcn/ui의 Card 컴포넌트를 설치합니다. 이 컴포넌트는 제목, 설명, 이미지 등을 깔끔하게 표시할 수 있는 컨테이너를 제공합니다.
npx shadcn@latest add card
이 컴포넌트는 src/components/ui/card.tsx
에 자동으로 생성되며, 포스트 카드를 만들 때 활용할 예정입니다.
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>포스트 제목</CardTitle>
<CardDescription>포스트 설명</CardDescription>
</CardHeader>
<CardContent>
콘텐츠 내용
</CardContent>
</Card>
2. 블로그 mdx 파일 읽고 frontmatter 추출하기
포스트 목록 페이지에서 모든 MDX 파일을 읽어와 각 파일의 메타데이터(frontmatter)를 추출합니다. Node.js의 파일 시스템 API를 사용하여 폴더 내 모든 MDX 파일을 찾고, 각 파일의 제목, 날짜, 설명 등을 가져옵니다.
구현 순서
content/posts
폴더의 모든 파일 목록 가져오기.mdx
확장자 파일만 필터링- 각 파일의 내용을 읽고 frontmatter 파싱
- 포스트 목록 데이터 구성
import { promises as fs } from "fs";
import path from "path";
const PostsPage = async () => {
const postFileNames = await fs.readdir(path.join(process.cwd(), "src/data/posts"));
const posts = await Promise.all(
postFileNames
.filter((filename) => filename.endsWith(".mdx"))
.map(async (filename, index) => {
const content = await fs.readFile(path.join(process.cwd(), "src/data/posts", filename), "utf-8");
const { frontmatter } = await evaluate<PostFrontmatter>({
source: content,
options: {
parseFrontmatter: true,
},
});
return {
id: index,
path: "/posts/" + filename.replace(".mdx", ""),
...frontmatter,
};
}),
);
...
};
export default PostsPage;
3. 포스트 리스트 UI 추가
추출한 포스트 데이터를 Card 컴포넌트를 활용하여 그리드 레이아웃으로 표시합니다. 각 카드는 클릭 가능한 링크로 구성되며, 호버 효과와 반응형 디자인이 적용됩니다.
구성 요소
- 그리드 레이아웃: 반응형으로 열 개수 자동 조정
- 모바일: 1열
- 태블릿(md): 2열
- 데스크톱(lg): 3열
- 카드 디자인: 제목, 설명, 카테고리, 날짜 표시
- 호버 효과: 마우스 오버 시 테두리 색상 변경
- 링크 연결: 카드 전체가 클릭 가능한 영역
import Link from "next/link";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const PostsPage = async () => {
...
return (
<div className="container mx-auto py-8 px-3">
<h1 className="text-3xl font-bold mb-8">블로그 포스트</h1>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Link key={post.id} href={post.path}>
<Card className="hover:border-amber-500 h-full flex justify-between flex-col">
<CardHeader>
<CardTitle>{post.title}</CardTitle>
<CardDescription>{post.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex justify-between text-sm text-muted-foreground">
<span>{post.category}</span>
<span>{post.date}</span>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
);
};
export default PostsPage;
4. 메인 페이지 구현
블로그의 첫 화면인 메인 페이지를 구현합니다. 방문자를 환영하는 히어로 섹션과 블로그 소개를 담아 깔끔하고 매력적인 랜딩 페이지를 만들어봅니다.
페이지 구성
- 히어로 섹션: 블로그 제목과 설명, CTA 버튼
- 소개 섹션: 블로그의 목적과 콘텐츠 소개
- 반응형 디자인: 모바일부터 데스크톱까지 최적화
사용 컴포넌트
- shadcn/ui의 Button 컴포넌트
- Tailwind CSS 유틸리티 클래스
- Next.js Link 컴포넌트
import Link from "next/link";
import { Button } from "@/components/ui/button";
const MainPage = () => {
return (
<div>
{/* 히어로 섹션 */}
<section className="container mx-auto px-4 py-20 text-center">
<h1 className="text-5xl font-bold mb-4">개발 블로그</h1>
<p className="text-xl text-muted-foreground mb-8">프로그래밍과 웹 개발에 대한 이야기를 나눕니다</p>
<Button asChild>
<Link href="/posts" className="inline-block bg-primary text-primary-foreground px-6 py-3 rounded-lg hover:opacity-90 transition">
포스트 보러가기
</Link>
</Button>
</section>
{/* 소개 섹션 */}
<section className="container mx-auto px-4 py-12">
<div className="bg-secondary rounded-lg p-8 text-center">
<h2 className="text-2xl font-bold mb-4">블로그 소개</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">이 블로그는 웹 개발, 프로그래밍, 그리고 최신 기술 트렌드에 대한 지식과 경험을 공유하는 공간입니다. 함께 성장하는 개발자 커뮤니티를 만들어가고자 합니다.</p>
</div>
</section>
</div>
);
};
export default MainPage;
5. about 페이지 구현
블로그와 운영자를 소개하는 About 페이지를 구현합니다. Card 컴포넌트를 활용하여 정보를 구조화하고, 깔끔한 레이아웃으로 방문자에게 신뢰감을 줄 수 있는 페이지를 만들어봅니다.
페이지 구성
- 블로그 소개: 블로그의 목적과 방향성
- 기술 스택: 사용된 기술들을 시각적으로 표시
- 연락처: 이메일, GitHub, 소셜 미디어 링크
- 프로필: 간단한 자기소개 (선택사항)
디자인 특징
- Card 컴포넌트로 섹션 구분
- 태그 형태의 기술 스택 표시
- 아이콘을 활용한 연락처 정보
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; const AboutPage = () => { return ( <div className="container mx-auto px-4 py-12 max-w-4xl"> <h1 className="text-4xl font-bold mb-8 text-center">About</h1> <Card className="mb-8"> <CardHeader> <CardTitle>블로그 소개</CardTitle> </CardHeader> <CardContent> <p className="text-muted-foreground">이 블로그는 Next.js 14와 TypeScript를 활용하여 만든 개인 기술 블로그입니다. 웹 개발과 프로그래밍에 대한 경험과 지식을 공유하며, 함께 성장하는 개발자 커뮤니티를 만들어가고자 합니다.</p> </CardContent> </Card> <Card className="mb-8"> <CardHeader> <CardTitle>기술 스택</CardTitle> </CardHeader> <CardContent> <div className="flex flex-wrap gap-2"> <span className="px-3 py-1 bg-secondary rounded-md">Next.js 14</span> <span className="px-3 py-1 bg-secondary rounded-md">TypeScript</span> <span className="px-3 py-1 bg-secondary rounded-md">Tailwind CSS</span> <span className="px-3 py-1 bg-secondary rounded-md">MDX</span> <span className="px-3 py-1 bg-secondary rounded-md">React</span> </div> </CardContent> </Card> <Card> <CardHeader> <CardTitle>연락처</CardTitle> </CardHeader> <CardContent> <p className="text-muted-foreground mb-2">문의사항이나 제안사항이 있으시면 언제든 연락 주세요.</p> <div className="space-y-1"> <p>📧 Email: example@email.com</p> <p>💻 GitHub: github.com/username</p> <p>🔗 LinkedIn: linkedin.com/in/username</p> </div> </CardContent> </Card> </div> ); }; export default AboutPage;
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const AboutPage = () => {
return (
<div className="container mx-auto px-4 py-12 max-w-4xl">
<h1 className="text-4xl font-bold mb-8 text-center">About</h1>
{/* 블로그 소개 */}
<Card className="mb-8">
...
</Card>
{/* 기술스택 */}
<Card className="mb-8">
...
</Card>
{/* 연락처 */}
<Card>
...
</Card>
</div>
);
};
export default AboutPage;
6. 포스트 리스트, 메인, About 페이지 테스트
구현한 3개의 페이지(메인, 포스트 목록, About)가 모두 정상적으로 작동하는지 확인합니다. 각 페이지의 디자인과 기능, 그리고 페이지 간 네비게이션을 테스트합니다.
npm run dev
# 메인 페이지
http://localhost:3000/
# 포스트 목록 페이지
http://localhost:3000/posts
# About 페이지
http://localhost:3000/about
1. Card 컴포넌트 생성
포스트를 카드 형태로 표시하기 위해 shadcn/ui의 Card 컴포넌트를 설치합니다. 이 컴포넌트는 제목, 설명, 이미지 등을 깔끔하게 표시할 수 있는 컨테이너를 제공합니다.
npx shadcn@latest add card
이 컴포넌트는 src/components/ui/card.tsx
에 자동으로 생성되며, 포스트 카드를 만들 때 활용할 예정입니다.
2. 블로그 mdx 파일 읽고 frontmatter 추출하기
포스트 목록 페이지에서 모든 MDX 파일을 읽어와 각 파일의 메타데이터(frontmatter)를 추출합니다. Node.js의 파일 시스템 API를 사용하여 폴더 내 모든 MDX 파일을 찾고, 각 파일의 제목, 날짜, 설명 등을 가져옵니다.
구현 순서
content/posts
폴더의 모든 파일 목록 가져오기.mdx
확장자 파일만 필터링- 각 파일의 내용을 읽고 frontmatter 파싱
- 포스트 목록 데이터 구성
3. 포스트 리스트 UI 추가
추출한 포스트 데이터를 Card 컴포넌트를 활용하여 그리드 레이아웃으로 표시합니다. 각 카드는 클릭 가능한 링크로 구성되며, 호버 효과와 반응형 디자인이 적용됩니다.
구성 요소
- 그리드 레이아웃: 반응형으로 열 개수 자동 조정
- 모바일: 1열
- 태블릿(md): 2열
- 데스크톱(lg): 3열
- 카드 디자인: 제목, 설명, 카테고리, 날짜 표시
- 호버 효과: 마우스 오버 시 테두리 색상 변경
- 링크 연결: 카드 전체가 클릭 가능한 영역
4. 메인 페이지 구현
블로그의 첫 화면인 메인 페이지를 구현합니다. 방문자를 환영하는 히어로 섹션과 블로그 소개를 담아 깔끔하고 매력적인 랜딩 페이지를 만들어봅니다.
페이지 구성
- 히어로 섹션: 블로그 제목과 설명, CTA 버튼
- 소개 섹션: 블로그의 목적과 콘텐츠 소개
- 반응형 디자인: 모바일부터 데스크톱까지 최적화
사용 컴포넌트
- shadcn/ui의 Button 컴포넌트
- Tailwind CSS 유틸리티 클래스
- Next.js Link 컴포넌트
5. about 페이지 구현
블로그와 운영자를 소개하는 About 페이지를 구현합니다. Card 컴포넌트를 활용하여 정보를 구조화하고, 깔끔한 레이아웃으로 방문자에게 신뢰감을 줄 수 있는 페이지를 만들어봅니다.
페이지 구성
- 블로그 소개: 블로그의 목적과 방향성
- 기술 스택: 사용된 기술들을 시각적으로 표시
- 연락처: 이메일, GitHub, 소셜 미디어 링크
- 프로필: 간단한 자기소개 (선택사항)
디자인 특징
- Card 컴포넌트로 섹션 구분
- 태그 형태의 기술 스택 표시
- 아이콘을 활용한 연락처 정보
6. 포스트 리스트, 메인, About 페이지 테스트
구현한 3개의 페이지(메인, 포스트 목록, About)가 모두 정상적으로 작동하는지 확인합니다. 각 페이지의 디자인과 기능, 그리고 페이지 간 네비게이션을 테스트합니다.
npm run dev
# 메인 페이지
http://localhost:3000/
# 포스트 목록 페이지
http://localhost:3000/posts
# About 페이지
http://localhost:3000/about
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>포스트 제목</CardTitle>
<CardDescription>포스트 설명</CardDescription>
</CardHeader>
<CardContent>
콘텐츠 내용
</CardContent>
</Card>
마치며
이번 튜토리얼을 통해 Next.js 블로그를 처음부터 끝까지 완성해 보았습니다.
구현한 기능들
- Next.js 14 App Router 기반 프로젝트 초기 설정 반응형 헤더 : 다크모드 토글 & 모바일 메뉴 지원 MDX 기반 포스트 시스템 : 코드 하이라이팅 적용 목차(TOC) 자동 생성 및 스크롤 위치 추적 포스트 목록 페이지 : 카드형 레이아웃 메인 페이지 & About 페이지 구현
이제 기본적인 블로그의 뼈대는 갖추었습니다. 앞으로는 버그를 수정하고, 세부 UI/UX를 개선하며, 배포 환경을 구성해보면 한층 완성도 높은 블로그를 만들 수 있습니다. 배포 후에는 SEO 최적화, 댓글 기능, 검색 기능 등 확장 기능도 시도해 보길 추천드립니다.
참조자료
- Next.js 공식 문서 - App Router, 라우팅, 최적화
- Tailwind CSS - 유틸리티 클래스 레퍼런스
- shadcn/ui - 컴포넌트 라이브러리
- next-mdx-remote - MDX 렌더링
- rehype-pretty-code - 코드 하이라이팅
- next-themes - 다크모드 구현
- GitHub 저장소 - 완성된 코드