파일 기반 라우팅, 동적 라우팅, Route Groups, Parallel Routes 등 Next.js의 강력한 라우팅 시스템을 학습합니다.
Next.js App Router는 파일 시스템을 기반으로 라우팅을 자동 생성합니다. 폴더 구조가 곧 URL 구조가 됩니다. 이커머스에서 상품 목록, 상세, 카테고리 페이지를 쉽게 구성할 수 있습니다. 별도의 라우터 설정 없이 폴더만 만들면 됩니다.
| 파일 | 역할 | 이커머스 예시 |
|---|---|---|
| page.tsx | 라우트의 UI (필수) | 상품 목록 |
| layout.tsx | 공유 레이아웃 | 헤더/푸터 |
| loading.tsx | 로딩 UI | 스켈레톤 |
| error.tsx | 에러 UI | 에러 페이지 |
| not-found.tsx | 404 페이지 | 상품 없음 |
| template.tsx | 리렌더링 레이아웃 | 애니메이션 |
app/
├── page.tsx → /
├── layout.tsx → 전체 레이아웃
├── products/
│ ├── page.tsx → /products
│ ├── loading.tsx → 로딩 상태
│ └── [id]/
│ ├── page.tsx → /products/123
│ └── error.tsx → 에러 처리
├── cart/
│ └── page.tsx → /cart
└── about/
└── page.tsx → /about// app/layout.tsx - 루트 레이아웃
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/products/layout.tsx - 상품 섹션 레이아웃
export default function ProductsLayout({ children }) {
return (
<div className="flex">
<Sidebar /> {/* 카테고리 사이드바 */}
<main>{children}</main>
</div>
);
}
// /products/123 접속 시:
// RootLayout > ProductsLayout > ProductPage레이아웃 특징
대괄호를 사용하여 동적 세그먼트를 정의합니다. URL의 일부를 변수로 받아 처리할 수 있습니다. 이커머스에서 상품 ID, 카테고리 슬러그 등을 처리하는 핵심 기능입니다.
| 문법 | 예시 URL | params |
|---|---|---|
| [id] | /products/123 | { id: '123' } |
| [...slug] | /docs/a/b/c | { slug: ['a','b','c'] } |
| [[...slug]] | /docs 또는 /docs/a | { slug: [] } 또는 { slug: ['a'] } |
// app/products/[id]/page.tsx
// URL: /products/123, /products/456, ...
type Props = {
params: { id: string };
};
export default async function ProductPage({ params }: Props) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>상품 ID: {params.id}</p>
</div>
);
}// app/categories/[...slug]/page.tsx
// URL: /categories/electronics/phones/iphone
type Props = {
params: { slug: string[] };
};
export default function CategoryPage({ params }: Props) {
// params.slug = ['electronics', 'phones', 'iphone']
return (
<div>
<nav>
{params.slug.map((segment, i) => (
<span key={i}> / {segment}</span>
))}
</nav>
</div>
);
}
// 빵 부스러기 네비게이션에 활용// app/shop/[[...slug]]/page.tsx
// URL: /shop, /shop/men, /shop/men/shirts
type Props = {
params: { slug?: string[] };
};
export default function ShopPage({ params }: Props) {
const slug = params.slug || [];
if (slug.length === 0) {
return <AllProducts />; // /shop
}
if (slug.length === 1) {
return <CategoryProducts category={slug[0]} />; // /shop/men
}
return <SubCategoryProducts category={slug[0]} sub={slug[1]} />;
}이커머스 활용
괄호로 감싼 폴더명 (group)은 URL에 영향을 주지 않으면서 라우트를 논리적으로 그룹화합니다. 이커머스에서 마케팅 페이지와 쇼핑 페이지를 분리할 때 유용합니다.
app/
├── (marketing)/
│ ├── layout.tsx → 마케팅 레이아웃
│ ├── page.tsx → /
│ └── about/
│ └── page.tsx → /about
├── (shop)/
│ ├── layout.tsx → 쇼핑 레이아웃 (사이드바)
│ ├── products/
│ │ └── page.tsx → /products
│ └── cart/
│ └── page.tsx → /cart
└── (auth)/
├── layout.tsx → 인증 레이아웃 (심플)
├── login/
│ └── page.tsx → /login
└── register/
└── page.tsx → /register
// (marketing), (shop), (auth)는 URL에 포함되지 않음// app/(shop)/layout.tsx
// 상품 페이지용 레이아웃 (사이드바 포함)
export default function ShopLayout({ children }) {
return (
<div className="flex">
<aside className="w-64">
<CategorySidebar />
</aside>
<main className="flex-1">{children}</main>
</div>
);
}
// app/(auth)/layout.tsx
// 인증 페이지용 레이아웃 (심플, 중앙 정렬)
export default function AuthLayout({ children }) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-md">
{children}
</div>
</div>
);
}// 완전히 다른 루트 레이아웃 사용 가능
app/
├── (main)/
│ ├── layout.tsx → html, body 포함
│ └── page.tsx
└── (admin)/
├── layout.tsx → 다른 html, body
└── dashboard/
└── page.tsx
// app/(main)/layout.tsx
export default function MainLayout({ children }) {
return (
<html lang="ko">
<body className="bg-white">{children}</body>
</html>
);
}
// app/(admin)/layout.tsx
export default function AdminLayout({ children }) {
return (
<html lang="ko">
<body className="bg-gray-100">{children}</body>
</html>
);
}Route Group 활용
고급 라우팅 패턴으로 복잡한 UI를 구현할 수 있습니다. 이커머스에서 모달, 대시보드 등에 활용됩니다.
같은 레이아웃에서 여러 페이지를 동시에 렌더링합니다. 대시보드에서 여러 섹션을 독립적으로 로딩할 때 유용합니다.
app/
├── layout.tsx
├── page.tsx
├── @analytics/
│ └── page.tsx
└── @team/
└── page.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid grid-cols-2 gap-4">
{analytics}
{team}
</div>
</div>
);
}
// 대시보드에서 여러 섹션을 독립적으로 로딩현재 레이아웃 내에서 다른 라우트를 가로채서 표시합니다. 모달 패턴에 유용합니다.
app/
├── products/
│ ├── page.tsx → /products (상품 목록)
│ └── [id]/
│ └── page.tsx → /products/123 (상품 상세)
├── @modal/
│ └── (.)products/
│ └── [id]/
│ └── page.tsx → 모달로 표시
└── layout.tsx
// 인터셉트 규칙:
// (.) → 같은 레벨
// (..) → 한 레벨 위
// (..)(..) → 두 레벨 위
// (...) → 루트에서
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}// app/@modal/(.)products/[id]/page.tsx
// 상품 목록에서 클릭 시 모달로 표시
import { Modal } from '@/components/Modal';
export default async function ProductModal({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
return (
<Modal>
<h2>{product.name}</h2>
<p>{product.price.toLocaleString()}원</p>
<a href={`/products/${params.id}`}>
상세 페이지로 이동
</a>
</Modal>
);
}
// 동작:
// 1. /products에서 상품 클릭 → 모달로 표시
// 2. 직접 /products/123 접속 → 전체 페이지로 표시
// 3. 새로고침 → 전체 페이지로 표시이커머스 활용
미들웨어는 요청이 완료되기 전에 코드를 실행합니다. 인증, 리다이렉트, 국제화 등에 활용됩니다. 이커머스에서 로그인 체크, 관리자 권한 확인 등에 필수입니다.
// middleware.ts (프로젝트 루트)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 모든 요청에서 실행
console.log('요청 URL:', request.nextUrl.pathname);
return NextResponse.next();
}
// 특정 경로에만 적용
export const config = {
matcher: ['/products/:path*', '/cart/:path*'],
};// 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');
const { pathname } = request.nextUrl;
// 보호된 경로 체크
const protectedPaths = ['/cart', '/checkout', '/mypage'];
const isProtected = protectedPaths.some(path =>
pathname.startsWith(path)
);
if (isProtected && !token) {
// 로그인 페이지로 리다이렉트
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ['/cart/:path*', '/checkout/:path*', '/mypage/:path*'],
};// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['ko', 'en', 'ja'];
const defaultLocale = 'ko';
function getLocale(request: NextRequest) {
// 쿠키에서 언어 설정 확인
const cookieLocale = request.cookies.get('locale')?.value;
if (cookieLocale && locales.includes(cookieLocale)) {
return cookieLocale;
}
// Accept-Language 헤더에서 확인
const acceptLanguage = request.headers.get('accept-language');
// ... 파싱 로직
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 이미 로케일이 있는지 확인
const hasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!hasLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
);
}
return NextResponse.next();
}미들웨어 주의사항
실제 이커머스 사이트의 라우트 구조를 설계합니다. Route Group을 활용하여 마케팅, 쇼핑, 관리자 영역을 분리합니다.
app/ ├── (home)/ │ ├── layout.tsx → 홈 레이아웃 (풀 너비) │ └── page.tsx → / (홈페이지) │ ├── (shop)/ │ ├── layout.tsx → 쇼핑 레이아웃 (사이드바) │ ├── products/ │ │ ├── page.tsx → /products │ │ ├── loading.tsx │ │ └── [id]/ │ │ ├── page.tsx → /products/123 │ │ └── error.tsx │ └── categories/ │ └── [...slug]/ │ └── page.tsx → /categories/electronics/phones │ ├── (purchase)/ │ ├── layout.tsx → 구매 플로우 레이아웃 (심플) │ ├── cart/ │ │ └── page.tsx → /cart │ └── checkout/ │ ├── page.tsx → /checkout │ └── success/ │ └── page.tsx → /checkout/success │ ├── (auth)/ │ ├── layout.tsx → 인증 레이아웃 (중앙 정렬) │ ├── login/ │ │ └── page.tsx → /login │ └── register/ │ └── page.tsx → /register │ ├── (account)/ │ ├── layout.tsx → 마이페이지 레이아웃 │ └── mypage/ │ ├── page.tsx → /mypage │ ├── orders/ │ │ └── page.tsx → /mypage/orders │ └── settings/ │ └── page.tsx → /mypage/settings │ ├── @modal/ │ └── (.)products/ │ └── [id]/ │ └── page.tsx → 상품 빠른 보기 모달 │ └── layout.tsx → 루트 레이아웃
(home) - 홈 레이아웃
풀 너비 히어로 배너, 프로모션 섹션
(shop) - 쇼핑 레이아웃
카테고리 사이드바, 필터 옵션
(purchase) - 구매 플로우
심플한 헤더, 진행 단계 표시
(auth) - 인증 레이아웃
중앙 정렬, 최소한의 UI
핵심 포인트