233 lines
7.7 KiB
Markdown
233 lines
7.7 KiB
Markdown
# 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`)
|
|
|
|
```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`)
|
|
|
|
```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`)
|
|
|
|
```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`)
|
|
|
|
```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`)
|
|
|
|
관리자 인터페이스는 일관된 레이아웃과 네비게이션이 필요합니다:
|
|
|
|
```tsx
|
|
// (with-base-layout)/layout.tsx
|
|
return (
|
|
<BaseLayout
|
|
queryClient={queryClient}
|
|
currentPath={pathname ?? ""}
|
|
handleMenuClick={handleMenuClick}
|
|
>
|
|
<PageLayout>{children}</PageLayout>
|
|
</BaseLayout>
|
|
);
|
|
```
|
|
|
|
이 레이아웃은 다음과 같은 기능을 제공합니다:
|
|
- 메뉴 시스템을 통한 페이지 간 이동
|
|
- 현재 페이지 경로 표시
|
|
- 일관된 페이지 타이틀 및 구조
|
|
|
|
### 2. 독립적인 페이지 (`without-base-layout`)
|
|
|
|
설문 기사나 결과 페이지와 같은 독립적인 페이지는 기본 레이아웃 없이 콘텐츠에 집중합니다:
|
|
|
|
```tsx
|
|
// (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)` 그룹을 통해 관리자 인터페이스와 독립적인 페이지를 명확하게 분리하면서도 코드 재사용성과 유지보수성을 높일 수 있습니다.
|
|
|
|
이 구조는 확장성이 뛰어나 새로운 페이지나 기능을 추가할 때 기존 패턴을 따르기 쉽고, 애플리케이션이 성장함에 따라 필요한 경우 추가 라우트 그룹을 도입할 수 있는 유연성을 제공합니다.
|