Next.js 이론 5강

라우팅과 레이아웃 시스템

파일 기반 라우팅, 동적 라우팅, Route Groups, Parallel Routes 등 Next.js의 강력한 라우팅 시스템을 학습합니다.

1. 파일 기반 라우팅

Next.js App Router는 파일 시스템을 기반으로 라우팅을 자동 생성합니다. 폴더 구조가 곧 URL 구조가 됩니다. 이커머스에서 상품 목록, 상세, 카테고리 페이지를 쉽게 구성할 수 있습니다. 별도의 라우터 설정 없이 폴더만 만들면 됩니다.

특수 파일들

파일역할이커머스 예시
page.tsx라우트의 UI (필수)상품 목록
layout.tsx공유 레이아웃헤더/푸터
loading.tsx로딩 UI스켈레톤
error.tsx에러 UI에러 페이지
not-found.tsx404 페이지상품 없음
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

레이아웃 특징

  • • 레이아웃은 페이지 이동 시 리렌더링되지 않음
  • • 상태가 유지됨 (스크롤 위치 등)
  • • 하위 라우트와 공유됨
  • • 이커머스: 헤더/푸터/사이드바 공유에 활용

2. 동적 라우팅

대괄호를 사용하여 동적 세그먼트를 정의합니다. URL의 일부를 변수로 받아 처리할 수 있습니다. 이커머스에서 상품 ID, 카테고리 슬러그 등을 처리하는 핵심 기능입니다.

동적 세그먼트 종류

문법예시 URLparams
[id]/products/123{ id: '123' }
[...slug]/docs/a/b/c{ slug: ['a','b','c'] }
[[...slug]]/docs 또는 /docs/a{ slug: [] } 또는 { slug: ['a'] }

기본 동적 라우트 [id]

// 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>
  );
}

Catch-all [...slug]

// 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>
  );
}

// 빵 부스러기 네비게이션에 활용

Optional Catch-all [[...slug]]

// 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]} />;
}

이커머스 활용

  • [id]: 상품 상세 페이지 (/products/123)
  • [...slug]: 카테고리 계층 (/category/electronics/phones)
  • [[...slug]]: 필터링 가능한 상품 목록
  • • generateStaticParams로 정적 생성 가능

3. Route Groups

괄호로 감싼 폴더명 (group)은 URL에 영향을 주지 않으면서 라우트를 논리적으로 그룹화합니다. 이커머스에서 마케팅 페이지와 쇼핑 페이지를 분리할 때 유용합니다.

Route Group 기본

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 활용

  • • 섹션별 다른 레이아웃 적용
  • • 코드 구조 정리 (URL 변경 없이)
  • • 관리자/사용자 영역 분리
  • • 이커머스: (shop), (marketing), (admin) 분리
  • • 각 그룹별 독립적인 loading.tsx, error.tsx 가능

4. Parallel Routes와 Intercepting Routes

고급 라우팅 패턴으로 복잡한 UI를 구현할 수 있습니다. 이커머스에서 모달, 대시보드 등에 활용됩니다.

Parallel Routes (@folder)

같은 레이아웃에서 여러 페이지를 동시에 렌더링합니다. 대시보드에서 여러 섹션을 독립적으로 로딩할 때 유용합니다.

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>
  );
}

// 대시보드에서 여러 섹션을 독립적으로 로딩

Intercepting Routes ((..)folder)

현재 레이아웃 내에서 다른 라우트를 가로채서 표시합니다. 모달 패턴에 유용합니다.

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. 새로고침 → 전체 페이지로 표시

이커머스 활용

  • • 상품 목록에서 빠른 미리보기 모달
  • • 장바구니 사이드 패널
  • • 로그인 모달
  • • URL 공유 가능한 상품 상세 모달

5. 미들웨어 활용

미들웨어는 요청이 완료되기 전에 코드를 실행합니다. 인증, 리다이렉트, 국제화 등에 활용됩니다. 이커머스에서 로그인 체크, 관리자 권한 확인 등에 필수입니다.

미들웨어 기본

// 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*'],
};

국제화 (i18n)

// 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();
}

미들웨어 주의사항

  • • Edge Runtime에서 실행 (Node.js API 제한)
  • • 가볍게 유지 (모든 요청에서 실행)
  • • matcher로 필요한 경로만 지정
  • • 무거운 로직은 서버 컴포넌트에서 처리

6. 이커머스 예제: Route Group 설계

실제 이커머스 사이트의 라우트 구조를 설계합니다. 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

설계 포인트

Route Group으로 레이아웃 분리
[...slug]로 카테고리 계층 처리
@modal로 빠른 상품 미리보기
loading.tsx로 스켈레톤 UI
error.tsx로 에러 처리

핵심 포인트

  • • 사용자 경험에 따라 레이아웃 분리
  • • URL은 깔끔하게 유지
  • • 각 섹션별 독립적인 로딩/에러 처리
  • • 미들웨어로 인증/권한 체크
  • • 동적 라우팅으로 상품/카테고리 처리