Files
frontend/docs/approute.md

7.7 KiB

Next.js App Router의 라우트 그룹(Route Groups) 구현 분석

개요

Next.js의 App Router에서는 괄호로 묶인 디렉토리((with-base-layout), (without-base-layout))를 라우트 그룹으로 처리하여 URL 경로에 영향을 주지 않으면서 서로 다른 레이아웃을 적용할 수 있게 합니다. 이 문서에서는 프로젝트에서 라우트 그룹이 어떻게 구현되고 사용되는지 분석합니다.

라우트 그룹 구조 분석

1. 디렉토리 구조

프로젝트에서는 두 가지 주요 라우트 그룹이 사용되고 있습니다:

/apps/cs/app/
├── (with-base-layout)/
│   ├── banner/
│   ├── bbs/
│   ├── code/
│   ├── contents/
│   ├── dashboard/
│   ├── ...
│   ├── layout.tsx
│   └── loading.tsx
├── (without-base-layout)/
│   ├── user-survey-article/
│   └── user-survey-result/
└── layout.tsx

2. 라우트 그룹별 레이아웃 구현

루트 레이아웃 (/apps/cs/app/layout.tsx)

import type { Metadata } from "next";
import { App } from "@/app";

export const metadata: Metadata = {
  title: "유역하수도 통합 정보플랫폼",
};

export default App;

App 컴포넌트 (/apps/cs/src/app/index.tsx)

import "./styles/base.css";
import "./styles/components.css";
import "@fontsource-variable/noto-sans-kr";

import { AppProvider } from "./providers";
import { cookies } from "next/headers";

const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
  const theme = (await cookies()).get("site-theme-key");

  return (
    <html lang="ko" data-theme={theme?.value}>
      <body>
        <AppProvider>{children}</AppProvider>
      </body>
    </html>
  );
};

export { App };

(with-base-layout) 레이아웃 (/apps/cs/app/(with-base-layout)/layout.tsx)

"use client";

import { usePathname, useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { BaseLayout } from "@common/components";
import type { MenuItem } from "@/entities/menu";
import { useMenuStore } from "@/shared/lib";
import { PageLayout } from "@/shared/ui/page-layout";

export default function WithBaseLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const router = useRouter();
  const pathname = usePathname();
  const queryClient = useQueryClient();

  const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);

  const handleMenuClick = (item: MenuItem) => {
    setCurrentMenuInfo(item);
    router.push(item.urlPath);
  };

  return (
    <BaseLayout
      queryClient={queryClient}
      currentPath={pathname ?? ""}
      handleMenuClick={handleMenuClick}
    >
      <PageLayout>{children}</PageLayout>
    </BaseLayout>
  );
}

PageLayout 컴포넌트 (/apps/cs/src/shared/ui/page-layout.tsx)

import { PageTitle } from "@common/components";
import { useMenuStore } from "../lib/use-menu-store";

function PageLayout({ children }: { children: React.ReactNode }) {
  const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);

  const menuPaths = currentMenuInfo?.menuPath
    ?.split(">")
    .map((path) => path.trim());

  return (
    <div className="h-auto min-h-full bg-container pb-2">
      <PageTitle
        title={currentMenuInfo?.menuKornNm ?? ""}
        menuPaths={menuPaths ?? []}
      />
      {children}
    </div>
  );
}

라우트 그룹의 작동 원리

1. URL 경로에 영향을 주지 않는 그룹화

Next.js App Router에서 괄호로 묶인 디렉토리는 URL 경로에 포함되지 않습니다. 예를 들어:

  • /bbs/register 경로는 (with-base-layout)/bbs/register 디렉토리에 매핑됩니다.
  • /user-survey-article/1 경로는 (without-base-layout)/user-survey-article/[srvySn] 디렉토리에 매핑됩니다.

2. 레이아웃 분리 및 재사용

라우트 그룹을 사용하면 서로 다른 레이아웃을 가진 페이지들을 효율적으로 구성할 수 있습니다:

  • (with-base-layout) 그룹: 메뉴, 헤더, 사이드바 등을 포함하는 BaseLayout과 PageLayout을 사용합니다.
  • (without-base-layout) 그룹: 기본 레이아웃 없이 페이지 컴포넌트만 렌더링합니다.

3. 중첩 라우트 그룹

프로젝트에서는 중첩된 라우트 그룹도 사용되고 있습니다:

(with-base-layout)/bbs/(post)/bbsnormal/

여기서 (post)는 게시물 관련 페이지들을 그룹화하는 중첩된 라우트 그룹입니다.

라우트 그룹 사용 사례

1. 관리자 인터페이스 (with-base-layout)

관리자 인터페이스는 일관된 레이아웃과 네비게이션이 필요합니다:

// (with-base-layout)/layout.tsx
return (
  <BaseLayout
    queryClient={queryClient}
    currentPath={pathname ?? ""}
    handleMenuClick={handleMenuClick}
  >
    <PageLayout>{children}</PageLayout>
  </BaseLayout>
);

이 레이아웃은 다음과 같은 기능을 제공합니다:

  • 메뉴 시스템을 통한 페이지 간 이동
  • 현재 페이지 경로 표시
  • 일관된 페이지 타이틀 및 구조

2. 독립적인 페이지 (without-base-layout)

설문 기사나 결과 페이지와 같은 독립적인 페이지는 기본 레이아웃 없이 콘텐츠에 집중합니다:

// (without-base-layout)/user-survey-article/[srvySn]/page.tsx
export default async function UserSurveyArticlePage({
  params,
}: {
  params: Promise<{ srvySn: string }>;
}) {
  const { srvySn } = await params;
  return <UserSurveyArticle srvySn={srvySn} />;
}

이러한 페이지는:

  • 외부 링크나 팝업으로 열리는 경우가 많음
  • 독립적인 기능을 제공(예: 설문 제출 후 창 닫기)
  • 메인 애플리케이션 UI와 분리된 경험 제공

두 라우트 그룹의 주요 차이점

(with-base-layout)

  • BaseLayout 컴포넌트를 포함하는 레이아웃을 사용합니다.
  • 이 레이아웃은 메뉴, 헤더, 사이드바 등 기본 UI 요소를 포함합니다.
  • PageLayout 컴포넌트로 감싸져 있어 일관된 페이지 구조를 제공합니다.
  • 주로 관리자 페이지나 로그인 후 사용하는 내부 페이지에 사용됩니다.
  • 다양한 관리 기능을 포함하는 여러 하위 디렉토리가 있습니다.

(without-base-layout)

  • 기본 레이아웃 없이 페이지 컴포넌트만 렌더링합니다.
  • 메뉴, 헤더, 사이드바 등의 UI 요소가 없는 독립적인 페이지입니다.
  • 주로 외부 사용자용 페이지나 팝업, 임베드 페이지에 사용됩니다.
  • 현재 두 가지 특수 목적 페이지만 포함하고 있습니다.

기술적 이점

  1. 코드 구성 개선: 관련 페이지와 레이아웃을 논리적으로 그룹화할 수 있습니다.
  2. 레이아웃 재사용: 공통 레이아웃을 여러 페이지에서 재사용할 수 있습니다.
  3. URL 구조 단순화: 그룹 이름이 URL에 포함되지 않아 깔끔한 URL 구조를 유지할 수 있습니다.
  4. 유지보수성 향상: 관련 기능을 함께 그룹화하여 코드 탐색과 유지보수가 용이합니다.

결론

Next.js App Router의 라우트 그룹 기능은 프로젝트에서 다양한 레이아웃 요구사항을 효율적으로 관리하는 데 중요한 역할을 합니다. (with-base-layout)과 (without-base-layout) 그룹을 통해 관리자 인터페이스와 독립적인 페이지를 명확하게 분리하면서도 코드 재사용성과 유지보수성을 높일 수 있습니다.

이 구조는 확장성이 뛰어나 새로운 페이지나 기능을 추가할 때 기존 패턴을 따르기 쉽고, 애플리케이션이 성장함에 따라 필요한 경우 추가 라우트 그룹을 도입할 수 있는 유연성을 제공합니다.