[Next.js] Routing : Layout / Route Groups / Dynamic Routes(동적 라우팅)

 

01. Layout

Next.js 14에서 Layout은 앱 전체 구조를 구성하는 중요한 요소로, 여러 페이지에서 공통적으로 사용되는 UI 요소를 한 번에 관리할 수 있게 해준다. 특히 Next.js의 App Router에서 레이아웃 기능이 강화되었으며, 페이지 간 전환시에도 지속적으로 유지되는 구조이다.

 

#1. Navbar와 같은 공통된 Layout

일반적으로 Navbar, Footer, 전역 스타일과 같은 요소들은 모든 페이지에서 공통적으로 적용되어야 한다. 하지만 이때 공통된 요소들을 모든 Page.tsx에 import하는 것은 매우 비효율적이다. 페이지 수가 적다면 어느정도는 가능하지만 만약 100페이지 이상된다면 해당 요소들을 모든 페이지에서 불러와야 하는 문제점이 발생한다. 이를 해결하기 위해 공통된 Layout으로 어플리케이션의 모든 페이지에 동일하게 적용하는 레이아웃을 생성할 수 있다.

 

우선 components/navigation.tsx 파일에 다음과 같이 페이지마다 공통적으로 적용할 Navbar를 생성한다.

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";

export default function Navigation() {
  const path = usePathname();
  return (
    <nav>
      <ul>
        <li>
          <Link href="/">Home</Link>
          {path === "/" ? "🔥" : ""}
        </li>
        <li>
          <Link href="/about-us">About Us</Link>
          {path === "/about-us" ? "🔥" : ""}
        </li>
      </ul>
    </nav>
  );
}

 

 

다음으로 app/layout.tsx 파일은 앱 전체의 루트 레이아웃을 정의한다. Root Layout은 각 페이지가 공유하는 공통 레이아웃으로, 모든 페이지에 공통적으로 적용될 HTML 구조(<html>, <body> 태그)를 포함해야 한다. Root Layout은 페이지나 다른 하위 레이아웃으로의 페이지 전환 시에도 유지된다. 아래 코드는 전체 페이지에 적용될 공통 레이아웃(Navigation.tsx)을 import해오며, Root Layout을 정의한다.

import Navigation from "@app/components/navigation";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <Navigation />
        {children}
      </body>
    </html>
  );
}

 

여기서 children은 현재 렌더링되는 페이지의 콘텐츠를 의미한다. 예를 들어, /about-us 경로로 이동하면 /about-us 폴더에 있는 page.tsx의 콘텐츠가 이 자리에 렌더링된다. 이렇게 children을 사용함으로써 레이아웃의 일관성을 유지하면서 페이지마다 다른 콘텐츠를 표시할 수 있다.

 

 

 

#2. 중첩된 Layout

중첩된 Layout은 페이지마다 필요한 부분에만 적용되는 레이아웃을 정의할 수 있으며, 공통된 레이아웃에 추가로 특정 섹션에서만 사용할 레이아웃을 쌓아갈 수 있다. 이를 통해 페이지 전환 시 공통 레이아웃은 유지하면서 특정 페이지 혹은 페이지 그룹에서만 고유한 레이아웃을 추가로 적용할 수 있다.

 

예를 들어, /about-us/company 경로 내부의 모든 페이지에는 고유한 레이아웃을 가져야 한다고 가정한다. 

/app/about-us/company/layout.tsx 파일 내에 다음과 같이 코드를 작성한다.

export default function AboutUsLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <div>
      {children}
      &copy; Next JS is great
    </div>
  );
}

 

children은 해당 경로에 속한 페이지의 콘텐츠를 의미한다. 예를 들어, /about-us/company/team 페이지에 접속하면, 그 페이지의 콘텐츠가 children 자리에 렌더링된다. 이렇게 함으로써 /about-us/company 경로 하위모든 페이지에 공통적인 레이아웃을 적용하면서도, 페이지마다 고유한 콘텐츠를 표시할 수 있다.

 

&copy; Next JS is great 부분은 모든 하위 페이지의 하단에 동일하게 출력되는 내용이다. 예를 들어, /about-us/company 경로에서 하위 페이지를 방문할 때마다 "Next JS is great"라는 문구가 항상 표시될 것이다. 이를 통해 경로에 맞춘 특정 레이아웃을 쉽게 적용할 수 있다.

 

 

💡레이아웃은 서로 상쇄되는 것이 아닌 서로 중첩된다.

레이아웃은 서로 상쇄되는 것이 아닌 서로 중첩되며, 가장 가까이에 있는 레이아웃을 찾고 상위로 올라가는 방식으로 찾아간다.

/about-us 경로에도 레이아웃을 설정할 수 있고, 그 하위에 있는 /about-us/company는 이 상위 레이아웃을 기반으로 동작하면서도, 추가로 고유한 요소들을 포함할 수 있다. 이를 통해 재사용 가능한 구조를 만들고, 각 페이지 그룹에 맞는 레이아웃을 유연하게 관리할 수 있다.

 

 

 

 

02. Route Groups

Route Groups는 Next.js에서 폴더 구조를 URL 경로에 영향을 주지 않고 정리할 수 있는 기능이다.

즉, 폴더를 만들고 그 안에 여러 파일을 넣더라도 실제 사용자게에 보이는 URL에는 그 폴더가 나타나지 않는다.

이를 통해 프로젝트 구조를 깔끔하게 유지하면서 각 경로에 맞는 다양한 설정을 할 수 있다.

 

출처 : https://nextjs.org/docs/app/building-your-application/routing/route-groups#opting-specific-segments-into-a-layout

 

예를 들어,(marketing) 이라는 폴더를 만들고 그 안에 파일을 넣더라도, URL에는 /marketing이 나타나지 않는다.

app/(marketing)/about/page.js 파일을 만들면, 이 페이지의 실제 경로는 /about이 된다.

 

 

#1. 특정 경로에 맞는 레이아웃 적용

Route Groups를 사용하면, 특정 경로에만 적용되는 개별 레이아웃을 만들 수 있다.

예를 들어, 마케팅 관련 페이지는 마케팅 전용 레이아웃을, 쇼핑 관련 페이지는 쇼핑 전용 레이아웃을 적용할 수 있다.

(marketing) 및 (shop) 내부의 경로가 동일한 URL 계층 구조를 공유하더라도, 폴더 내에 layout.js 파일을 추가하여 각 그룹에 대해 다른 레이아웃을 아래와 같이 생성할 수 있다.

  • app/(marketing)/layout.js 파일을 만들면, 마케팅 관련 페이지들에는 해당 레이아웃이 적용된다.
  • app/(shop)/layout.tsx 파일을 만들면, 쇼핑 관련 페이지들에는 쇼핑 전용 레이아웃이 적용된다.

 

 

#2. 특정 경로에만 동일한 레이아웃 적용

특정 경로에만 동일한 레이아웃을 적용하고 싶을 때, Route Groups를 사용할 수 있다.

이를 통해 같은 레이아웃이 적용될 경로들만 그룹으로 묶고, 해당 그룹에만 공통 레이아웃을 적용하는 방식이다.

 

예를 들어, account와 cart 페이지에 동일한 레이아웃을 적용하고 싶다면, (shop)이라는 Route Group을 만들어서 이 두 경로를 해당 그룹에 넣을 수 있다. 이렇게 하면, account와 cart 페이지는 동일한 레이아웃을 사용하게 된다. 하지만, 같은 레이아웃을 사용하지 않길 원하는 페이지들은 그룹 밖에 두면 된다.

  • (shop) 그룹 안에 레이아웃(layout.js)을 공유하는 경로들:
    • /account 페이지 (shop 레이아웃 적용)
    • /cart 페이지 (shop 레이아웃 적용)
  • 그룹 밖에 있는 경로:
    • /checkout 페이지는 해당 레이아웃을 공유하지 않음(shop 그룹 밖에 있으므로 shop 레이아웃 적용 안됨)

이렇게 하면, /account와 /cart는 동일한 레이아웃을 가지지만, /checkout 페이지는 전혀 다른 레이아웃을 사용할 수 있다. 즉, 그룹 내에 있는 경로들만 레이아웃을 공유하고, 그룹 외부의 경로는 그 영향을 받지 않는 구조이다.

 

이 방식은 특정 페이지 그룹에만 고유한 UI나 레이아웃을 적용할 때 유용하다.

 

 

 

#3. 다중 루트 레이아웃 생성

다중 루트 레이아웃은 여러 개의 서로 다른 루트 레이아웃을 만들고 싶을 때 사용할 수 있는 방식이다.

기본적으로, Next.js의 루트 레이아웃은 lauout.js 파일을 통해 전역적으로 적용된다.

하지만, 특정 경로 그룹만다 완전히 다른 레이아웃을 적용하고 싶을 때는 Route Groups을 사용하여 각 그룹에 고유한 루트 레이아웃을 설정할 수 있다.

  1. 최상위 레이아웃 파일 제거: 먼저, 프로젝트의 최상위에 있는 layout.js 파일을 삭제한다. 이 파일을 제거함으로써 전체 애플리케이션에 공통된 레이아웃을 적용하지 않도록 만든다.
  2. 각 Route Group에 레이아웃 설정: 그다음, 각 Route Group에 layout.js 파일을 만들어 해당 그룹만의 루트 레이아웃을 설정한다. 예를 들어, marketing 그룹과 shop 그룹이 있다면, 각 그룹 내에 고유한 layout.js 파일을 추가하여 서로 다른 레이아웃을 적용할 수 있다.
  3. HTML 및 Body 태그 추가: 이때, 각 그룹의 layout.js 파일에는 반드시 <html> 및 <body> 태그를 포함해야 한다. 왜냐하면, 최상위 레이아웃 파일을 삭제했기 때문에 각 루트 레이아웃이 HTML 문서의 구조를 정의해야 하기 때문이다.

 

위 예시 그림의 구조는 다음과 같다.

  • app/(marketing)/layout.js → marketing 그룹의 루트 레이아웃
  • app/(shop)/layout.js → shop 그룹의 루트 레이아웃

이렇게 설정하면, /marketing 관련 페이지는 marketing 그룹의 레이아웃을 사용하고, /shop 관련 페이지는 shop 그룹의 레이아웃을 사용하게 된다.

 

 

이 방식은 각기 다른 루트 레이아웃을 사용하는 경우, 페이지 간 이동이 전체 페이지 리로드를 유발한다. 예를 들어, /cart에서 /blog로 이동할 때 각기 다른 루트 레이아웃을 사용하고 있다면, 전체 페이지가 새로 고침된다.

하지만, 애플리케이션의 특정 섹션에 대해 완전히 다른 UI나 경험을 제공하고 싶을 때 유용하다. 예를 들어, 쇼핑 섹션은 shop 레이아웃, 마케팅 섹션은 marketing 레이아웃을 적용하여 각각 독립된 스타일을 유지할 수 있다.

 

 

 

 

03. Dynamic Routes(동적 라우팅)

 동적 라우팅은 미리 알 수 없는 경로(segment) 이름을 다루고, 동적 데이터를 기반으로 라우트를 생성할 때 유용하다.

Next.js에서는 동적 세그먼트를 사용하여 요청 시점에 결정되는 경로를 설정할 수 있다.

 

 

#1. 동적 세그먼트

동적 세그먼트는 URL의 특정 부분이 미리 정해져 있지 않을 때 사용된다. 예를 들어, 블로그 포스트의 URL은 보통 각 포스트의 제목이나 ID를 포함하므로, 미리 정해진 이름이 없다.

 

동적 세그먼트를 생성하기 위해서는 파일 또는 폴더 이름을 대괄호([])로 감싸서 만들 수 있다. 예를 들어, [id] 또는 [slug]와 같은 형식으로 설정한다. 예를 들어, /blog/[slug]/page.tsx에서 [slug]는 동적 세그먼트를 나타낸다.

동적 세그먼트에 접근하기 위해서는 Next.js의 useRouter 훅을 사용할 수 있다. 이를 통해 동적 세그먼트의 값을 컴포넌트 내에서 사용할 수 있다.

// app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
  return <div>My Post: {params.slug}</div>
}

 

/blog/a, /blog/b 같은 URL을 요청하면 각각의 slug 값이 router.query.slug에 할당된다.

 

Route Example URL params
app/blog/[slug]/page.js /blog/a { slug: 'a' }
app/blog/[slug]/page.js /blog/b { slug: 'b' }
app/blog/[slug]/page.js /blog/c { slug: 'c' }

 

 

 

#2. Catch-all Segments

동적 세그먼트를 확장하여 이후 모든 세그먼트를 포함하는 catch-all segments를 만들 수 있다. 즉, 여러 개의 세그먼트를 함께 처리할 수 있는 기능이다. 이는 URL이 여러 단계로 나뉘어 있을 때 유용하다.

이때는 대괄호 안에 점 세 개(...)를 추가한다. 예를 들어 다음과 같이 라우트를 설정할 수 있다.

// pages/shop/[...slug]/page.js
import { useRouter } from 'next/router';

export default function Page() {
  const router = useRouter();
  return <p>Path: {router.query.slug.join(' / ')}</p>; // 모든 세그먼트를 출력
}

 

 

Route Example URL params
app/shop/[...slug]/page.js /shop/a { slug: ['a'] }
app/shop/[...slug]/page.js /shop/a/b { slug: ['a', 'b'] }
app/shop/[...slug]/page.js /shop/a/b/c { slug: ['a', 'b', 'c'] }

 

 

 

#3. Optional Catch-all Segments

Optional Catch-all Segments는 동적 라우팅에서 특정 경로의 뒤에 올 수 있는 여러 개의 경로 세그먼트를 모두 처리할 수 있는 방법이다. 그리고 'optional' 이라는 이름에서 알 수 있듯이, 이 세그먼트를 선택적으로 사용할 수 있도록 만들어, 특정 경로가 존재하지 않을 때도 경로를 매칭할 수 있게 한다. 이를 통해 다양한 경로를 유연하게 처리할 수 있다.

 

Catch-all Segments는 [...slug] 형태로 작성하여 지정된 경로 뒤에 올 수 있는 모든 세그먼트를 받아 처리할 수 있다. 예를 들어, /shop/[...slug]는 /shop 아래의 다양한 하위 경로를 모두 처리할 수 있다. 이 방식은 하위 경로의 개수가 고정되지 않은 경우 유용하다.

여기에 Optional의 개념을 더하면, 해당 경로가 없을 때도 매칭될 수 있도록 확장된다.

이를 구현하는 방식은 이중 대괄호 [[...segmentName]]을 사용하여, 해당 세그먼트가 있어도 되고 없어도 되게 설정한다.

 

예를 들어, app/shop/[[...slug]]/page.js는 /shop 경로뿐만 아니라, 그 하위 경로들까지도 동적으로 처리할 수 있다. 이때 중요한 점은 Optional이라는 특성 때문에 매개변수가 없더라도 /shop 경로와도 일치한다는 점이다. 즉, 세그먼트가 없는 기본 경로와 세그먼트가 여러 개 있는 경로를 모두 처리할 수 있다.

Route Example URL params
app/shop/[[...slug]]/page.js /shop {}
app/shop/[[...slug]]/page.js /shop/a { slug: ['a'] }
app/shop/[[...slug]]/page.js /shop/a/b { slug: ['a', 'b'] }
app/shop/[[...slug]]/page.js /shop/a/b/c { slug: ['a', 'b', 'c'] }

 

<작동 방식>

  • /shop 경로에 접근하면 { slug: [] }로 처리되어 세그먼트가 없는 경우도 매칭
  • /shop/a 경로에 접근하면 { slug: ['a'] }로 처리되어 a라는 단일 세그먼트를 포함
  • /shop/a/b 경로는 { slug: ['a', 'b'] }로 두 개의 세그먼트를 처리
  • /shop/a/b/c 경로는 { slug: ['a', 'b', 'c'] }로 세 개의 세그먼트를 처리

 

💡 Optional Catch-all vs. Catch-all

[[...slug]]는 경로에 세그먼트가 없어도 경로와 매칭할 수 있는 Optional Catch-all이다.

반면에, [...slug]는 세그먼트가 반드시 있어야 매칭이 되며, 세그먼트가 없는 /shop 같은 기본 경로는 매칭되지 않는다.

 

Optional Catch-all Segments는 세그먼트가 있을 때와 없을 때 모두 동작하게 하고 싶을 때 사용하면 매우 유용하다.

 

 

 

 
 

 

 

반응형