테마 전환

Next.js App Router 실전: 라우트 그룹과 중첩 레이아웃으로 대형 프로젝트의 디렉터리 혼란 해결하기

Easton editorial illustration: build pipeline conveyor

VS Code 왼쪽 파일 트리를 보면 app 디렉터리 아래에 120개가 넘는 폴더가 빽빽하게 들어차 있습니다. 백오피스의 사용자 관리 페이지를 찾으려고 dashboard-user-list, admin-users, backend-user-management 세 폴더를 5분 동안 오갔습니다. 모두 그럴듯해 보였기 때문입니다.

아침 코드 리뷰에서는 이씨가 새로 /about 라우트를 추가했는데, 지난주 마케팅 팀이 올린 /about과 충돌했습니다. 두 사람은 서로를 바라보며 말했습니다. 네 about은 회사 소개고, 내 about은 제품 소개인데 왜 내가 바꿔야 하지?

솔직히 이런 일이 처음은 아닙니다. 프로젝트 초반에는 페이지가 십여 개뿐이라 평면적인 디렉터리 구조도 꽤 산뜻해 보였습니다. 하지만 반년이 지나 기능이 열 배로 늘자 app 디렉터리는 정리되지 않은 옷장처럼 변했습니다. 물건이 안에 있다는 건 알지만, 찾을 때마다 전부 뒤져야 합니다.

Next.js 프로젝트를 만들고 있고, 팀원이 세 명을 넘고, 페이지 수가 50개를 넘는다면 비슷한 문제를 만날 가능성이 큽니다. 다행히 Next.js App Router에는 이 문제를 해결하기 위한 네 가지 기능이 있습니다. 라우트 그룹, 중첩 레이아웃, 병렬 라우트, 인터셉팅 라우트입니다. 다만 온라인 튜토리얼은 대개 Hello World 수준의 데모에 머물러 실제 프로젝트에 적용하려고 하면 여전히 막막합니다.

이 글에서는 실제 전자상거래 프로젝트를 예로 들어 이 네 가지 기능을 어떻게 쓰는지, 그리고 뒤죽박죽인 디렉터리를 유지보수 가능하고 확장 가능한 구조로 어떻게 다시 정리하는지 살펴보겠습니다.

문제 재현 - 전통적인 디렉터리 구조의 세 가지 문제

평면 디렉터리의 한계

먼저 예전 디렉터리 구조를 보겠습니다.

app/
├── page.tsx              # 홈
├── about/page.tsx        # 회사 소개
├── products/page.tsx     # 상품 목록
├── product-detail/[id]/page.tsx
├── cart/page.tsx
├── checkout/page.tsx
├── dashboard/page.tsx    # 백오피스 홈
├── dashboard-users/page.tsx
├── dashboard-users-active/page.tsx
├── dashboard-users-blocked/page.tsx
├── dashboard-orders/page.tsx
├── dashboard-orders-pending/page.tsx
├── dashboard-settings/page.tsx
├── auth-login/page.tsx   # 로그인
├── auth-register/page.tsx
└── ... (그 밖에 80개 이상의 폴더)

보기만 해도 머리가 아픕니다. 더 치명적인 문제는 URL 경로도 이상해진다는 점입니다. /dashboard/users/active가 아니라 /dashboard-users-active가 됩니다. 충돌을 피하려고 폴더 이름에 온갖 접두사를 붙이지만, 이는 문제를 가리는 것에 불과합니다.

어떤 페이지가 프런트 영역인지, 어떤 페이지가 백오피스인지, 어떤 페이지가 인증 관련인지 한눈에 알 수 없습니다. 새 팀원이 합류하면 디렉터리 구조를 익히는 데만 며칠이 걸립니다.

레이아웃 반복과 유지보수 난이도

프런트와 백오피스의 레이아웃은 완전히 다릅니다. 프런트에는 상단 내비게이션과 하단 저작권 정보가 있고, 백오피스에는 사이드바와 권한 제어가 있습니다. 전통적인 방식에서는 각 페이지 컴포넌트에서 레이아웃을 직접 불러옵니다.

// app/dashboard-users/page.tsx
import DashboardLayout from '@/components/DashboardLayout'

export default function UsersPage() {
  return (
    <DashboardLayout>
      <div>사용자 관리 내용</div>
    </DashboardLayout>
  )
}

이 방식에는 몇 가지 문제가 있습니다. 먼저 빠뜨리기 쉽습니다. 백오피스 페이지를 새로 만들었는데 레이아웃을 추가하지 않으면 페이지가 맨몸으로 노출됩니다. 다음으로 통일성이 떨어집니다. 어떤 사람은 DashboardLayout을 쓰고, 다른 사람은 AdminLayout을 쓰면서 나중에는 유지보수가 엉망이 됩니다.

더 심각한 것은 백오피스 레이아웃을 수정해야 할 때입니다. 예를 들어 사이드바 스타일을 바꾸려면 20개 파일을 열어 모든 페이지가 올바른 레이아웃 컴포넌트를 쓰는지 확인해야 할 수 있습니다. 솔직히 레이아웃을 고칠 때마다 불안했습니다.

모달과 팝업의 라우팅 문제

제품 매니저가 이런 요구사항을 냈습니다. 사용자가 상품 목록 페이지에서 특정 상품을 클릭하면 상세 정보를 모달로 띄우고, 동시에 URL은 /product/123으로 바뀌어 사용자가 이 링크를 공유할 수 있어야 한다는 것입니다. 요구 자체는 합리적이지만 구현은 골치 아픕니다.

전통적인 방식은 클라이언트 상태 관리로 모달 표시 여부를 직접 제어하고, URL도 직접 조작하는 것입니다. 코드는 지저분해지고 치명적인 문제가 하나 생깁니다. 사용자가 페이지를 새로고침하면 모달이 사라져 경험이 나빠집니다.

그렇다면 두 버전을 만들면 되지 않느냐고 생각할 수 있습니다. 하나는 모달 버전, 하나는 전체 페이지 버전입니다. 실제로 가능은 합니다. 하지만 같은 내용을 두 벌의 코드로 유지해야 한다는 뜻입니다. 제품 로직이 바뀌면 양쪽을 모두 고쳐야 하고 버그가 생기기 쉽습니다.

Instagram처럼 목록에서 이미지를 클릭하면 모달이 뜨고, 새로고침하면 전체 큰 이미지 페이지가 보이는 경험은 보기에는 단순하지만 전통적인 라우팅 방식으로 구현하기는 꽤 번거롭습니다.

라우트 그룹(Route Groups) - 논리 그룹은 URL에 영향을 주지 않는다

라우트 그룹이란

쉽게 말해 폴더 이름을 괄호로 감싸는 방식입니다. 예를 들어 (marketing)처럼 씁니다. 괄호 안의 이름은 URL에 나타나지 않습니다. 조금 추상적으로 들릴 수 있으니 바로 코드를 보겠습니다.

app/
├── (marketing)/           # 프런트 마케팅 페이지 그룹
│   ├── layout.tsx         # 프런트 전용 레이아웃
│   ├── page.tsx           # URL: /
│   ├── about/page.tsx     # URL: /about
│   └── products/page.tsx  # URL: /products
├── (shop)/                # 전자상거래 기능 그룹
│   ├── layout.tsx
│   ├── cart/page.tsx      # URL: /cart
│   └── checkout/page.tsx  # URL: /checkout
└── (dashboard)/           # 백오피스 관리 그룹
    ├── layout.tsx
    ├── dashboard/page.tsx # URL: /dashboard
    ├── users/page.tsx     # URL: /users (/dashboard/users가 아님!)
    └── orders/page.tsx    # URL: /orders

보면 (marketing), (shop), (dashboard) 같은 괄호 안 이름은 URL에 나타나지 않습니다. (marketing)/about/page.tsx의 라우트 경로는 /marketing/about이 아니라 여전히 /about입니다.

이쯤에서 굳이 왜 쓰는지 의문이 들 수 있습니다. 괄호 안 이름이 URL에 영향을 주지 않는다면 무슨 의미가 있을까요?

라우트 그룹의 핵심 가치는 라우트에 영향을 주는 것이 아니라 코드를 조직하는 것입니다. 비즈니스 로직, 팀 역할, 기능 모듈에 따라 라우트를 묶을 수 있고, 디렉터리 구조는 한눈에 들어오지만 URL은 길어지지 않습니다.

실전 사례: 팀 기준 그룹화

우리 팀에는 세 개의 하위 팀이 있었습니다. 마케팅 팀은 공식 사이트와 홍보 페이지를 담당하고, 제품 팀은 전자상거래 기능을 담당하며, 백오피스 팀은 관리 시스템을 담당합니다. 예전에는 모두 같은 app 디렉터리에서 작업했기 때문에 파일 충돌이 일상이었습니다. 라우트 그룹을 적용한 뒤 구조는 이렇게 바뀌었습니다.

app/
├── (team-marketing)/      # 마케팅 팀 담당
│   ├── layout.tsx
│   ├── page.tsx           # 홈
│   ├── about/page.tsx
│   └── pricing/page.tsx
├── (team-product)/        # 제품 팀 담당
│   ├── layout.tsx
│   ├── products/page.tsx
│   └── product/[id]/page.tsx
└── (team-backend)/        # 백오피스 팀 담당
    ├── layout.tsx
    ├── dashboard/page.tsx
    └── admin/page.tsx

장점은 분명합니다.

  1. 파일 충돌이 줄어듭니다. 마케팅 팀은 (team-marketing)에서, 제품 팀은 (team-product)에서 작업합니다. 서로 작업 영역이 분리되므로 Git 병합 충돌이 훨씬 줄어듭니다.

  2. 코드 리뷰가 더 명확해집니다. Pull Request를 볼 때 이 변경이 어느 팀의 코드에 영향을 주는지 바로 알 수 있습니다.

  3. 독립 레이아웃을 둘 수 있습니다. 각 라우트 그룹은 자체 layout.tsx를 가질 수 있습니다. 마케팅 페이지는 마케팅 스타일의 레이아웃을, 백오피스 페이지는 백오피스 스타일의 레이아웃을 씁니다. 각 페이지에서 수동으로 불러올 필요가 없습니다.

또 다른 사례: 레이아웃 유형 기준 그룹화

어떤 프로젝트는 팀이 아니라 레이아웃 유형으로 나누는 편이 더 자연스럽습니다.

app/
├── (with-nav)/            # 상단 내비게이션이 있는 페이지
│   ├── layout.tsx
│   ├── page.tsx
│   ├── about/page.tsx
│   └── products/page.tsx
├── (fullscreen)/          # 전체 화면 페이지(내비게이션 없음)
│   ├── layout.tsx
│   └── video/[id]/page.tsx
└── (auth)/                # 인증 페이지(간결한 레이아웃)
    ├── layout.tsx
    ├── login/page.tsx
    └── register/page.tsx

로그인과 회원가입 페이지는 보통 상단 내비게이션과 하단 정보가 필요하지 않으므로 (auth) 라우트 그룹으로 따로 관리하고 간결한 레이아웃을 줍니다. 영상 재생 페이지처럼 전체 화면이 필요한 페이지도 별도로 묶을 수 있습니다.

주의할 점

라우트 그룹은 편리하지만 함정이 하나 있습니다. 서로 다른 라우트 그룹 안에 같은 라우트 경로를 둘 수 없습니다.

예를 들어 (marketing)/about/page.tsx와 (shop)/about/page.tsx를 동시에 둘 수 없습니다. 둘 다 /about으로 해석되기 때문에 Next.js는 어느 쪽을 써야 할지 알 수 없고 바로 오류를 냅니다.

해결책은 라우트를 미리 계획해 각 경로가 유일하도록 만드는 것입니다. 정말 피할 수 없다면 한쪽에 접두사를 붙여 (shop)/about-us/page.tsx처럼 바꿀 수 있습니다.

또 하나 중요한 점은 라우트 그룹 이름을 의미 있게 지어야 한다는 것입니다. (group1), (group2)처럼 의미 없는 이름보다 (marketing), (dashboard), (auth)처럼 보자마자 이해되는 이름이 팀 협업에 훨씬 좋습니다.

중첩 레이아웃(Nested Layouts) - 자동 레이아웃 상속

중첩 레이아웃의 작동 방식

라우트 그룹은 디렉터리 조직 문제를 해결하지만, 여전히 레이아웃 계층 문제가 남습니다. 예를 들어 백오피스 관리 시스템에는 보통 다음 구조가 있습니다.

  • 1단계: 상단 제목 표시줄 + 사이드바(모든 백오피스 페이지 공통)
  • 2단계: 사용자 관리 모듈의 자체 탭(활성 사용자, 차단 사용자)
  • 3단계: 실제 페이지 내용

Next.js의 중첩 레이아웃은 바로 이런 장면을 위해 설계되었습니다. 서로 다른 계층의 폴더에 layout.tsx를 넣으면 자동으로 중첩됩니다.

app/(dashboard)/
├── layout.tsx              # 1단계 레이아웃: 상단 내비게이션 + 사이드바
├── users/
│   ├── layout.tsx          # 2단계 레이아웃: 사용자 관리 탭
│   ├── active/page.tsx     # /users/active
│   └── blocked/page.tsx    # /users/blocked
└── orders/
    ├── layout.tsx          # 2단계 레이아웃: 주문 관리 탭
    ├── pending/page.tsx
    └── completed/page.tsx

사용자가 /users/active에 접근하면 렌더링 계층은 이렇게 됩니다.

DashboardLayout (1단계)
  └─ UsersLayout (2단계)
      └─ ActiveUsersPage (페이지)

코드는 다음과 같습니다.

// app/(dashboard)/layout.tsx - 1단계 레이아웃
export default function DashboardLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard-container">
      <TopBar />
      <div className="content-area">
        <Sidebar />
        <main>{children}</main>
      </div>
    </div>
  )
}

// app/(dashboard)/users/layout.tsx - 2단계 레이아웃
export default function UsersLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div className="users-section">
      <div className="tabs">
        <Link href="/users/active">활성 사용자</Link>
        <Link href="/users/blocked">차단 사용자</Link>
      </div>
      {children}
    </div>
  )
}

// app/(dashboard)/users/active/page.tsx - 페이지
export default function ActiveUsersPage() {
  return <div>활성 사용자 목록...</div>
}

보이시나요? 페이지 컴포넌트에서 레이아웃을 직접 불러올 필요가 전혀 없습니다. Next.js가 자동으로 감싸 줍니다.

부분 렌더링의 성능 이점

중첩 레이아웃의 가장 강력한 부분은 부분 렌더링(Partial Rendering)입니다. 활성 사용자에서 차단 사용자로 전환할 때 다음처럼 동작합니다.

  • 1단계 레이아웃(상단 내비게이션, 사이드바)은 다시 렌더링되지 않습니다
  • 2단계 레이아웃(탭)도 다시 렌더링되지 않습니다
  • 페이지 내용만 다시 렌더링됩니다

이는 두 가지 의미를 갖습니다.

  1. 성능이 더 좋아집니다. 같은 레이아웃 컴포넌트를 반복 렌더링하지 않으므로 페이지 전환이 더 빠릅니다.

  2. 클라이언트 상태가 유지됩니다. 사이드바 접기/펼치기 상태가 있다면 페이지를 전환해도 그 상태가 사라지지 않습니다.

예전에 만든 한 프로젝트에서는 백오피스 사이드바에 검색창이 있었습니다. 사용자가 검색어를 입력한 뒤 페이지를 전환하면 검색창 내용이 지워져 경험이 좋지 않았습니다. 중첩 레이아웃을 쓰자 이 문제가 자연스럽게 해결됐습니다. 사이드바 컴포넌트가 다시 렌더링되지 않으니 상태도 당연히 유지됩니다.

실전 사례: 다단계 내비게이션

실제 프로젝트에서는 3단계, 때로는 4단계 내비게이션도 흔합니다. 예를 들면 다음과 같습니다.

  • 백오피스 관리(1단계 레이아웃: 상단바 + 사이드바)
    • 사용자 관리(2단계 레이아웃: 사용자 관리 탭)
      • 활성 사용자(3단계: 페이지 내용)
      • 차단 사용자(3단계: 페이지 내용)
    • 주문 관리(2단계 레이아웃: 주문 관리 탭)
      • 처리 대기 주문(3단계: 페이지 내용)
      • 완료된 주문(3단계: 페이지 내용)

디렉터리 구조가 이 계층 관계와 정확히 대응하므로 코드 논리가 명확해집니다.

app/(dashboard)/
├── layout.tsx              # 1단계: 상단바 + 사이드바
├── users/
│   ├── layout.tsx          # 2단계: 사용자 관리 영역
│   ├── active/page.tsx     # 3단계: 활성 사용자
│   └── blocked/page.tsx    # 3단계: 차단 사용자
└── orders/
    ├── layout.tsx          # 2단계: 주문 관리 영역
    ├── pending/page.tsx    # 3단계: 처리 대기
    └── completed/page.tsx  # 3단계: 완료

성능 최적화 팁

중첩 레이아웃은 기본적으로 서버 컴포넌트(Server Component)입니다. 이는 좋은 기본값입니다. 레이아웃 로직이 서버에서 렌더링되므로 클라이언트 JavaScript 용량을 늘리지 않습니다.

하지만 레이아웃 안에 검색창이나 드롭다운 메뉴 같은 상호작용이 있다면, 상호작용 부분만 클라이언트 컴포넌트로 분리해야 합니다.

// app/(dashboard)/layout.tsx - 서버 컴포넌트 유지
import SearchBar from '@/components/SearchBar' // 클라이언트 컴포넌트

export default function DashboardLayout({ children }) {
  return (
    <div>
      <SearchBar /> {/* 클라이언트 컴포넌트 */}
      <main>{children}</main>
    </div>
  )
}

// components/SearchBar.tsx - 클라이언트 컴포넌트
'use client'
import { useState } from 'react'

export default function SearchBar() {
  const [query, setQuery] = useState('')
  // ...상호작용 로직
}

이렇게 하면 레이아웃의 서버 렌더링 이점을 유지하면서도 상호작용 기능을 잃지 않습니다.

또 하나의 팁은 각 레이아웃 계층에 loading.tsx를 두어 로딩 상태를 보여 주는 것입니다. 사용자 경험이 훨씬 좋아집니다.

app/(dashboard)/
├── layout.tsx
├── loading.tsx             # 1단계 로딩 상태
└── users/
    ├── layout.tsx
    ├── loading.tsx         # 2단계 로딩 상태
    └── active/
        ├── page.tsx
        └── loading.tsx     # 3단계 로딩 상태

각 계층은 자체 로딩 애니메이션을 가질 수 있으며 서로 간섭하지 않습니다.

병렬 라우트(Parallel Routes) - 여러 페이지를 동시에 렌더링하기

병렬 라우트가 해결하는 문제

대시보드 페이지는 보통 여러 독립 모듈을 동시에 보여 줍니다. 예를 들면 다음과 같습니다.

  • 왼쪽 위: 데이터 분석 패널
  • 오른쪽 위: 팀 멤버 패널
  • 아래쪽: 최신 알림 패널

각 패널은 데이터를 독립적으로 가져오며 로딩 속도도 다릅니다. 전통적인 방식에서는 이 내용을 모두 하나의 페이지 컴포넌트에 넣지만, 그러면 한 패널의 데이터가 느릴 때 전체 페이지가 로딩 상태에 묶입니다.

병렬 라우트는 이런 모듈을 독립된 슬롯(slot)으로 분리하게 해 줍니다. 각 슬롯은 자체 로딩 상태와 오류 처리를 가질 수 있고, 조건에 따라 선택적으로 렌더링할 수도 있습니다.

기본 문법

병렬 라우트를 만드는 방법은 간단합니다. @로 시작하는 폴더를 만들면 됩니다.

app/dashboard/
├── layout.tsx
├── @analytics/page.tsx  # 분석 슬롯
├── @team/page.tsx       # 팀 슬롯
├── @notifications/page.tsx  # 알림 슬롯
└── page.tsx             # 기본 페이지

@analytics, @team, @notifications처럼 @로 시작하는 폴더가 슬롯입니다.

그다음 layout.tsx에서 이 슬롯들을 props로 받을 수 있습니다.

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
  notifications,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
  notifications: React.ReactNode
}) {
  return (
    <div className="dashboard-grid">
      <div className="main-content">{children}</div>
      <div className="top-panels">
        <div className="panel">{analytics}</div>
        <div className="panel">{team}</div>
      </div>
      <div className="bottom-panel">{notifications}</div>
    </div>
  )
}

각 슬롯은 독립된 페이지 컴포넌트에 대응하며 자체 loading과 error 상태를 가질 수 있습니다.

app/dashboard/
├── @analytics/
│   ├── page.tsx
│   ├── loading.tsx      # 분석 패널 로딩 상태
│   └── error.tsx        # 분석 패널 오류 처리
├── @team/
│   ├── page.tsx
│   ├── loading.tsx
│   └── error.tsx
└── @notifications/
    ├── page.tsx
    ├── loading.tsx
    └── error.tsx

이렇게 하면 분석 패널 데이터가 느려도 해당 패널에만 로딩 애니메이션이 보이고, 다른 패널은 정상적으로 표시됩니다. 특정 패널에서 오류가 나도 전체 페이지에는 영향을 주지 않습니다.

실전 사례: 조건부 렌더링

병렬 라우트의 또 다른 강점은 조건부 렌더링입니다. 예를 들어 팀 패널은 관리자에게만 보여야 할 수 있습니다.

// app/dashboard/layout.tsx
import { auth } from '@/lib/auth'

export default async function DashboardLayout({
  analytics,
  team,
  notifications,
}) {
  const user = await auth()
  const isAdmin = user?.role === 'admin'

  return (
    <div className="dashboard-grid">
      <div>{analytics}</div>
      {isAdmin && <div>{team}</div>}  {/* 관리자에게만 표시 */}
      <div>{notifications}</div>
    </div>
  )
}

default.tsx의 역할

병렬 라우트에는 자주 밟는 함정이 있습니다. 사용자가 /dashboard에서 /dashboard/settings로 이동할 때 슬롯에 대응하는 페이지가 없을 수 있습니다. 그러면 Next.js는 무엇을 렌더링해야 할지 몰라 오류를 냅니다.

해결 방법은 default.tsx를 만들어 대체 콘텐츠로 쓰는 것입니다.

// app/dashboard/@team/default.tsx
export default function Default() {
  return null  // 또는 자리 표시자 컴포넌트를 반환
}

default.tsx가 있으면 슬롯에 대응 페이지가 없을 때 이 대체 콘텐츠를 렌더링해 오류를 피할 수 있습니다.

언제 병렬 라우트를 써야 하나

솔직히 병렬 라우트의 사용 범위는 라우트 그룹이나 중첩 레이아웃만큼 넓지 않습니다. 적합한 장면은 다음과 같습니다.

  • 대시보드의 여러 모듈 표시: 여러 독립 데이터 패널이 있고 각각 독립적으로 로드되어야 할 때
  • A/B 테스트: 사용자 그룹에 따라 서로 다른 슬롯 콘텐츠를 보여 줄 때
  • 권한 제어: 사용자 권한에 따라 특정 슬롯을 선택적으로 렌더링할 때

단순히 위아래로 나열하는 콘텐츠이고 독립 로딩 상태가 필요 없다면 페이지 컴포넌트에 바로 작성하면 됩니다. 굳이 병렬 라우트를 도입할 필요는 없습니다.

인터셉팅 라우트(Intercepting Routes) - 모달 라우트 구현하기

Instagram식 경험

솔직히 말해 인터셉팅 라우트는 네 가지 기능 중 가장 이해하기 어렵습니다. 처음 문서를 봤을 때는 무엇을 해결하려는 기능인지 전혀 감이 오지 않았습니다. Instagram 예시를 보고 나서야 이해했습니다.

Instagram에서 피드(feed)를 보다가 사진을 클릭하면 사진이 모달에서 크게 표시되고 URL도 /photo/abc123으로 바뀝니다. 이때 다음처럼 동작합니다.

  • 페이지를 새로고침하면 모달이 사라지고 전체 사진 페이지가 표시됩니다
  • URL을 다른 사람에게 공유하면 상대방은 모달이 아니라 전체 사진 페이지를 엽니다
  • 닫기 버튼을 누르면 모달이 사라지고 다시 피드로 돌아갑니다

이 경험의 장점은 분명합니다. URL을 공유할 수 있고, 새로고침해도 컨텍스트를 잃지 않으며, 클라이언트 내비게이션은 부드럽게 유지됩니다. 전통적인 방식으로는 구현하기 까다로운 패턴입니다.

인터셉팅 라우트는 바로 이 문제를 해결합니다.

기본 문법

인터셉팅 라우트는 특별한 폴더 이름 규칙을 사용합니다.

  • (.) 같은 단계 라우트와 매칭
  • (..) 한 단계 위 라우트와 매칭
  • (..)(..) 두 단계 위 라우트와 매칭
  • (...) 루트 디렉터리 라우트와 매칭

조금 추상적이니 바로 코드를 보겠습니다.

app/
├── products/
│   ├── page.tsx                    # 상품 목록 페이지
│   └── (..)product/[id]/page.tsx   # /product/123을 인터셉트해 모달로 표시
└── product/
    └── [id]/page.tsx               # 전체 상품 상세 페이지

사용자가 /products 페이지에서 특정 상품 링크(<Link href="/product/123">)를 클릭하면 다음처럼 동작합니다.

  • 클라이언트 내비게이션: 인터셉트가 발생해 (..)product/[id]/page.tsx(모달 버전)를 렌더링합니다
  • 직접 접근 /product/123 또는 새로고침: 인터셉트가 발생하지 않고 일반 product/[id]/page.tsx(전체 페이지)를 렌더링합니다

실전 사례: 상품 상세 모달

예전에 만든 전자상거래 프로젝트에는 상품 목록에서 상품을 클릭하면 상세 정보를 모달로 보여 달라는 요구가 있었습니다.

디렉터리 구조는 다음과 같습니다.

app/
├── (shop)/
│   └── products/
│       ├── page.tsx                     # 상품 목록
│       └── (..)product/[id]/page.tsx    # 모달 버전
└── product/
    └── [id]/page.tsx                    # 전체 페이지 버전

모달 버전의 코드는 다음과 같습니다.

// app/(shop)/products/(..)product/[id]/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import Modal from '@/components/Modal'
import ProductDetail from '@/components/ProductDetail'

export default function ProductModal({
  params
}: {
  params: { id: string }
}) {
  const router = useRouter()

  return (
    <Modal onClose={() => router.back()}>
      <ProductDetail id={params.id} />
    </Modal>
  )
}

전체 페이지 버전입니다.

// app/product/[id]/page.tsx
import ProductDetail from '@/components/ProductDetail'

export default function ProductPage({
  params
}: {
  params: { id: string }
}) {
  return (
    <div className="product-page">
      <ProductDetail id={params.id} />
    </div>
  )
}

여기서 상품 상세 컴포넌트(ProductDetail)는 재사용합니다. 바깥을 감싸는 컨테이너만 다릅니다. 하나는 모달이고, 하나는 전체 페이지입니다.

병렬 라우트와 함께 사용하기

인터셉팅 라우트만 단독으로 쓰면 상태 관리 문제가 생길 수 있습니다. 더 좋은 방식은 병렬 라우트와 함께 쓰는 것입니다.

app/(shop)/products/
├── layout.tsx
├── page.tsx
├── @modal/
│   ├── (..)product/[id]/page.tsx  # 모달 슬롯
│   └── default.tsx                # 기본값은 비어 있음

레이아웃 컴포넌트입니다.

// app/(shop)/products/layout.tsx
export default function ProductsLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <>
      {children}
      {modal}
    </>
  )
}

// app/(shop)/products/@modal/default.tsx
export default function Default() {
  return null  // 모달이 없을 때 null 반환
}

이렇게 하면 모달과 주 콘텐츠가 완전히 분리되어 상태 관리가 더 명확하고 이해하기도 쉽습니다.

주의할 점

인터셉팅 라우트에는 몇 가지 주의할 점이 있습니다.

  1. 클라이언트 내비게이션에서만 인터셉트합니다. 사용자가 브라우저 주소창에 URL을 직접 입력하거나 페이지를 새로고침하면 인터셉트가 발생하지 않습니다.

  2. 두 버전을 유지해야 합니다. 모달 버전과 전체 페이지 버전을 모두 구현해야 합니다. 컴포넌트를 재사용할 수는 있지만 약간의 중복 코드는 남습니다.

  3. 경로 매칭 규칙에 주의해야 합니다. (..)는 파일 시스템 경로가 아니라 라우트 경로를 기준으로 합니다. 라우트 그룹을 쓰면 라우트 그룹이 URL에 영향을 주지 않으므로, 경로 매칭이 예상과 다를 수 있습니다.

예를 들어 다음 구조를 보겠습니다.

app/
├── (shop)/products/
│   └── (..)product/[id]/page.tsx  # /product/[id] 인터셉트

여기서 (..)는 /products에서 한 단계 위인 루트로 올라가므로 /(shop)/product/[id]가 아니라 /product/[id]와 매칭됩니다.

언제 인터셉팅 라우트를 써야 하나

인터셉팅 라우트에 잘 맞는 장면은 다음과 같습니다.

  • 이미지 갤러리: 목록에서 이미지를 클릭하면 큰 이미지를 모달로 표시
  • 상품 상세: 목록에서 상품을 클릭하면 상세 정보를 모달로 표시
  • 로그인 팝업: 내비게이션의 로그인 버튼은 모달 로그인을 띄우지만 /login 라우트도 직접 접근 가능

적합하지 않은 장면은 다음과 같습니다.

  • URL 변화가 필요 없는 단순 팝업
  • 딥링크가 필요 없는 장면

정리하면 인터셉팅 라우트는 매우 강력하지만 가장 복잡한 기능이기도 합니다. 프로젝트에 Instagram식 요구사항이 없다면 당장 쓰지 않아도 괜찮습니다.

종합 실전 - 전자상거래 프로젝트 전체 디렉터리 구조

앞에서 네 가지 기능을 살펴봤습니다. 이제 이들을 조합해 실제 전자상거래 프로젝트의 디렉터리를 어떻게 구성하면 좋을지 보겠습니다.

프로젝트 요구사항

전형적인 전자상거래 프로젝트에는 보통 다음 모듈이 있습니다.

프런트(사용자 대상):

  • 홈, 회사 소개(마케팅 페이지)
  • 상품 목록, 상품 상세(모달 지원)
  • 장바구니, 결제

백오피스(관리자 대상):

  • 대시보드(여러 모듈 표시: 데이터 분석, 팀, 알림)
  • 사용자 관리(활성 사용자, 차단 사용자)
  • 주문 관리(처리 대기, 완료)

인증:

  • 로그인, 회원가입(독립 레이아웃, 내비게이션 없음)

최종 디렉터리 구조

app/
├── layout.tsx                          # 루트 레이아웃
│
├── (marketing)/                        # 프런트 라우트 그룹
│   ├── layout.tsx                      # 프런트 레이아웃(헤더 내비게이션 + 푸터)
│   ├── page.tsx                        # / (홈)
│   ├── about/page.tsx                  # /about
│   └── pricing/page.tsx                # /pricing
│
├── (shop)/                             # 전자상거래 기능 그룹
│   ├── layout.tsx                      # 쇼핑몰 레이아웃
│   ├── products/
│   │   ├── layout.tsx                  # 상품 목록 레이아웃(모달 슬롯 포함)
│   │   ├── page.tsx                    # /products
│   │   └── @modal/
│   │       ├── (..)product/[id]/page.tsx  # 상품 상세 모달
│   │       └── default.tsx
│   ├── cart/page.tsx                   # /cart
│   └── checkout/page.tsx               # /checkout
│
├── product/
│   └── [id]/page.tsx                   # /product/123 (전체 페이지)
│
├── (dashboard)/                        # 백오피스 라우트 그룹
│   ├── layout.tsx                      # 백오피스 레이아웃(사이드바 + 상단바)
│   ├── dashboard/
│   │   ├── layout.tsx                  # 대시보드 레이아웃(병렬 라우트)
│   │   ├── page.tsx                    # /dashboard (기본 콘텐츠)
│   │   ├── @analytics/
│   │   │   ├── page.tsx                # 데이터 분석 모듈
│   │   │   ├── loading.tsx
│   │   │   └── default.tsx
│   │   ├── @team/
│   │   │   ├── page.tsx                # 팀 모듈
│   │   │   ├── loading.tsx
│   │   │   └── default.tsx
│   │   └── @notifications/
│   │       ├── page.tsx                # 알림 모듈
│   │       ├── loading.tsx
│   │       └── default.tsx
│   ├── users/
│   │   ├── layout.tsx                  # 사용자 관리 2단계 레이아웃
│   │   ├── active/page.tsx             # /users/active
│   │   └── blocked/page.tsx            # /users/blocked
│   └── orders/
│       ├── layout.tsx                  # 주문 관리 2단계 레이아웃
│       ├── pending/page.tsx            # /orders/pending
│       └── completed/page.tsx          # /orders/completed
│
└── (auth)/                             # 인증 라우트 그룹
    ├── layout.tsx                      # 간결한 레이아웃(내비게이션 없음)
    ├── login/page.tsx                  # /login
    └── register/page.tsx               # /register

전통 구조와의 비교

전통적인 평면 구조와 새 구조를 비교해 보겠습니다.

기준전통적인 평면 구조라우트 그룹 + 중첩 레이아웃 사용
파일 찾기100개 넘는 파일 속에서 접두사로 구분해야 함비즈니스 모듈별로 묶여 한눈에 파악 가능
레이아웃 관리각 페이지에서 레이아웃 컴포넌트를 직접 불러옴자동 상속되며 한 곳 수정이 전체에 적용
팀 협업모두 같은 디렉터리에서 작업해 충돌이 쉬움팀/모듈별로 다른 폴더에서 작업
URL 명확성충돌 방지를 위해 접두사 필요(dashboard-users-active)URL은 간결(/users/active)하고 디렉터리 구조는 명확
모달 경험클라이언트 상태 관리, 새로고침 시 상태 손실라우트 기반, 새로고침 시 전체 페이지 표시
성능페이지 전환 때 레이아웃 반복 렌더링부분 렌더링으로 변경된 부분만 렌더링

구체적인 효과

새 구조를 적용한 뒤 우리 팀이 실제로 얻은 효과는 다음과 같습니다.

  1. 파일 찾는 속도가 50% 향상됐습니다. 예전에는 페이지 하나를 찾으려고 여러 화면을 넘겨야 했지만, 이제는 해당 라우트 그룹으로 바로 가면 됩니다.

  2. 레이아웃 수정 효율이 좋아졌습니다. 예전에는 백오피스 사이드바를 바꾸려면 20개 파일을 확인해야 했습니다. 이제는 (dashboard)/layout.tsx 하나만 수정하면 모든 백오피스 페이지에 자동 적용됩니다.

  3. 코드 충돌이 60% 줄었습니다. 마케팅 팀은 (marketing), 제품 팀은 (shop), 백오피스 팀은 (dashboard)에서 각자 작업합니다.

  4. 새 팀원이 더 빨리 적응합니다. 새로 온 인턴도 디렉터리 구조를 보고 5분 만에 전체 프로젝트 아키텍처를 이해했습니다. 긴 설명이 필요 없었습니다.

실용적인 조언

  1. 한 번에 전부 리팩터링하지 마세요. 먼저 백오피스 관리 같은 한 모듈을 골라 시험 적용하고, 가능성을 확인한 뒤 전체 프로젝트로 확장하세요.

  2. 라우트 그룹 이름은 의미 있게 짓습니다. 우리는 (marketing), (shop), (dashboard), (auth)를 사용했습니다. 팀원이 보자마자 이해할 수 있습니다.

  3. 문서가 중요합니다. 프로젝트 README에 디렉터리 구조 설명 섹션을 추가해 각 라우트 그룹의 책임을 설명하면 새 팀원이 이해하기 쉽습니다.

  4. TypeScript와 함께 쓰세요. 라우트 그룹과 중첩 레이아웃에 TypeScript 경로 매핑(@/*)을 함께 쓰면 코드 조직이 더 명확해집니다.

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/app/*": ["./src/app/*"]
    }
  }
}

모범 사례와 주의사항

라우트 그룹 이름 규칙

라우트 그룹 이름은 코드 유지보수성에 직접 영향을 줍니다. 우리 팀은 몇 가지 규칙을 정리했습니다.

추천 이름:

  • (marketing) - 마케팅 관련 페이지
  • (dashboard) 또는 (admin) - 백오피스 관리
  • (auth) - 인증 관련
  • (team-xxx) - 팀 기준으로 나눌 때 사용
  • (feature-xxx) - 기능 기준으로 나눌 때 사용

피해야 할 이름:

  • (group1), (group2) - 의미 없는 이름
  • (temp), (test) - 임시성 이름
  • (marketing-and-sales-pages)처럼 지나치게 긴 이름 - 간결하지 않음

라우트 그룹 이름은 개발자만 보는 것이고 URL에는 나타나지 않습니다. 그러므로 팀원이 보자마자 이해할 수 있게 지어야 합니다.

라우트 충돌 피하기

서로 다른 라우트 그룹에는 같은 라우트 경로를 둘 수 없습니다. 가장 흔한 함정입니다.

❌ 잘못된 예:
app/
├── (marketing)/about/page.tsx  # URL: /about
└── (shop)/about/page.tsx       # URL: /about - 충돌!

해결 방법은 다음과 같습니다.

  1. 라우트 계획: 시작 전에 라우트 맵을 그려 모든 경로가 유일한지 확인합니다
  2. 접두사 추가: 정말 두 개의 about이 필요하다면 /about-us와 /about-product처럼 한쪽에 접두사를 붙입니다
  3. 디렉터리 조정: 충돌하는 라우트를 서로 다른 계층에 둡니다

언제 병렬 라우트를 써야 하나

병렬 라우트는 필수 기능이 아닙니다. 다음 장면에서만 쓰면 됩니다.

적합한 장면:

  • 대시보드의 여러 독립 데이터 패널
  • 독립 로딩 상태가 필요한 병렬 콘텐츠
  • 사용자 권한에 따라 서로 다른 모듈을 조건부 렌더링
  • A/B 테스트용 콘텐츠 변형

적합하지 않은 장면:

  • 단순한 위아래 배치(페이지 컴포넌트에 직접 쓰면 됨)
  • 독립 로딩 상태가 필요 없는 콘텐츠
  • 정적인 레이아웃 영역

병렬 라우트를 써야 할지 확신이 없다면 대체로 필요하지 않은 경우가 많습니다. 고급 기능이므로 대부분의 프로젝트는 라우트 그룹과 중첩 레이아웃만으로 충분합니다.

인터셉팅 라우트의 한계

인터셉팅 라우트는 강력하지만 몇 가지 주의할 점이 있습니다.

  1. 클라이언트 내비게이션에서만 인터셉트됩니다. 사용자가 URL을 직접 입력하거나 새로고침하면 인터셉트가 발생하지 않습니다.

  2. 두 버전을 유지해야 합니다. 모달 버전과 전체 페이지 버전을 모두 구현해야 합니다. 컴포넌트를 재사용할 수는 있지만 유지보수 비용은 남습니다.

  3. 경로 매칭은 파일 시스템이 아니라 라우트 기준입니다. 라우트 그룹은 URL에 영향을 주지 않기 때문에 (..)가 폴더 경로가 아니라 URL 경로를 기준으로 매칭된다는 점이 헷갈리기 쉽습니다.

URL 변화가 필요 없거나 딥링크가 필요 없는 요구사항이라면 일반 클라이언트 모달로 충분합니다. 굳이 인터셉팅 라우트를 도입하지 않아도 됩니다.

성능 최적화 팁

  1. 레이아웃 컴포넌트는 서버 컴포넌트로 유지하세요. 레이아웃은 기본적으로 서버 컴포넌트입니다. 가능한 한 이 특성을 유지하고, 상호작용이 필요한 부분만 클라이언트 컴포넌트로 분리하세요.

  2. loading.tsx를 사용하세요. 각 라우트 계층에 loading.tsx를 추가하면 친절한 로딩 상태를 제공할 수 있어 사용자 경험이 좋아집니다.

  3. Suspense를 적절히 사용하세요. loading.tsx와 Suspense를 함께 쓰면 더 세밀한 스트리밍 렌더링을 구현할 수 있습니다.

  4. 과도한 중첩을 피하세요. 레이아웃 계층은 4단계를 넘기지 않는 편이 좋습니다. 너무 깊으면 복잡도가 올라가 오히려 유지보수에 불리합니다.

마이그레이션 전략

Pages Router에서 App Router로 이전한다면 다음 순서를 추천합니다.

  1. 증분 마이그레이션: app 디렉터리와 pages 디렉터리는 공존할 수 있습니다. 한 번에 전부 바꾸지 말고 모듈 단위로 이전하세요.

  2. 레이아웃부터 이전: 라우트 그룹과 중첩 레이아웃으로 레이아웃 로직을 리팩터링합니다. 가장 효과가 뚜렷한 부분입니다.

  3. 데이터 가져오기를 이전: getServerSideProps는 fetch로, getStaticProps는 Server Component로 바꿉니다.

  4. 라우트를 마지막에 이전: getStaticPaths를 generateStaticParams로 바꿉니다.

  5. 점진 배포: feature flag로 새 라우트와 기존 라우트 전환을 제어하면 문제가 생겼을 때 빠르게 롤백할 수 있습니다.

팀 협업 조언

  1. 디렉터리 구조 규칙을 정하세요. README나 Wiki에 각 라우트 그룹의 책임, 이름 규칙, 새 페이지 추가 절차를 명확히 적어 둡니다.

  2. 코드 리뷰 포인트를 정하세요. PR 리뷰에서는 라우트 규칙을 지켰는지, 라우트 충돌이 없는지, 레이아웃이 올바르게 중첩됐는지를 중점적으로 확인합니다.

  3. ESLint 규칙을 활용하세요. 라우트 그룹 이름 규칙을 검사하거나 특정 경로 사용을 금지하는 ESLint 설정을 둘 수 있습니다.

  4. 정기적으로 리팩터링하세요. 매 이터레이션이 끝난 뒤 시간을 내어 디렉터리 구조를 정리하고, 폐기된 페이지를 삭제하며, 부적절한 라우트 그룹 이름을 바꾸세요.

디버깅 팁

라우트 그룹과 중첩 레이아웃 디버깅은 가끔 까다롭습니다. 몇 가지 팁을 공유합니다.

  1. React DevTools를 확인하세요. 브라우저 개발자 도구의 React tab에서 전체 컴포넌트 트리를 볼 수 있고, 레이아웃이 올바르게 중첩됐는지 확인할 수 있습니다.

  2. console.log를 사용하세요. 레이아웃 컴포넌트에 console.log를 넣어 반복 렌더링되는지, 내비게이션마다 트리거되는지 확인합니다.

  3. Network tab을 확인하세요. 어떤 요청이 서버에서 발생하고 어떤 요청이 클라이언트에서 발생하는지 확인해 데이터 가져오기 로직이 올바른지 검증합니다.

  4. Next.js 출력을 읽으세요. 개발 모드에서 Next.js는 라우트 충돌, 레이아웃 누락 같은 유용한 정보를 많이 출력합니다. 터미널의 경고와 오류를 잘 살펴보세요.

결론

글 앞부분의 장면으로 돌아가 보겠습니다. 새벽 한 시에 120개 폴더가 빽빽한 화면을 바라보며 페이지 하나를 찾는 데 5분을 쓰는 상황입니다.

Next.js 프로젝트에서 같은 문제를 겪고 있다면 이 글에서 다룬 네 가지 기능이 도움이 될 것입니다.

라우트 그룹은 비즈니스 로직과 팀 역할에 따라 라우트를 묶어 디렉터리 구조를 명확하게 만들지만 URL을 길게 만들지는 않습니다.

중첩 레이아웃은 레이아웃 상속을 자동으로 처리합니다. 각 페이지에서 레이아웃 컴포넌트를 직접 불러올 필요가 없고, 레이아웃 수정도 한 파일만 고치면 됩니다.

병렬 라우트는 여러 독립 모듈을 동시에 렌더링하게 해 줍니다. 각 모듈은 자체 로딩 상태와 오류 처리를 가질 수 있어 대시보드 같은 다중 패널 장면에 잘 맞습니다.

인터셉팅 라우트는 Instagram과 비슷한 모달 경험을 구현합니다. URL은 공유 가능하고, 새로고침해도 컨텍스트를 잃지 않으면서 클라이언트 내비게이션은 부드럽게 유지됩니다.

제 조언은 작게 시작하라는 것입니다. 백오피스 관리 같은 모듈 하나를 골라 라우트 그룹과 중첩 레이아웃으로 리팩터링해 보고 효과를 확인하세요. 가능성이 검증되면 전체 프로젝트로 확장하면 됩니다.

한 번에 모든 코드를 리팩터링하지 마세요. 위험이 너무 큽니다. 증분 마이그레이션으로 한 걸음씩 진행하면 문제가 생겨도 되돌리기 쉽습니다.

마지막으로 전체 전자상거래 프로젝트 디렉터리 구조 템플릿을 위에 정리해 두었습니다. 그대로 참고하거나 복사해 사용해도 됩니다. 궁금한 점이 있으면 댓글로 함께 이야기해 주세요.

여러분의 Next.js 프로젝트 디렉터리가 이제 혼란에서 벗어나 더 편하게 유지보수되기를 바랍니다.

Next.js 대형 프로젝트 디렉터리 구조 리팩터링 전체 흐름

라우트 그룹, 중첩 레이아웃, 병렬 라우트, 인터셉팅 라우트로 혼란스러운 디렉터리 구조를 리팩터링합니다

⏱️ Estimated time: 8 hr

  1. 1

    Step 1: 기존 디렉터리 구조 분석

    현재 문제를 평가합니다:
    • 폴더 수를 집계합니다(50개를 넘으면 리팩터링 권장)
    • 라우트 충돌 지점을 식별합니다
    • 반복되는 레이아웃 코드를 찾습니다
    • 팀 협업에서 발생하는 불편 지점을 기록합니다

    기능 영역을 식별합니다:
    • 마케팅 페이지(홈, 소개, 가격)
    • 쇼핑몰 페이지(상품, 장바구니, 주문)
    • 백오피스 관리(사용자, 주문, 설정)
  2. 2

    Step 2: 라우트 그룹으로 기능 영역 나누기

    괄호로 라우트 그룹을 만듭니다:
    • (marketing): 마케팅 관련 페이지
    • (shop): 쇼핑몰 관련 페이지
    • (dashboard): 백오피스 관리 페이지

    주의 사항:
    • 라우트 그룹 이름은 URL에 영향을 주지 않습니다
    • 같은 URL은 여러 그룹에 동시에 둘 수 없습니다
    • 각 라우트 그룹은 자체 layout.js를 가질 수 있습니다
  3. 3

    Step 3: 중첩 레이아웃 계층 설계

    UI 계층에 맞춰 설계합니다:
    • 1단계: app/layout.js(사이트 전체 공통)
    • 2단계: 기능 영역 layout.js(예: shop/layout.js)
    • 3단계: 상세 페이지 layout.js(예: shop/products/[id]/layout.js)

    구현 핵심:
    • 각 단계에는 해당 단계만의 UI 요소만 추가합니다
    • 자식 레이아웃은 부모 레이아웃을 자동 상속합니다
    • 페이지 전환 시 layout은 다시 렌더링되지 않습니다
  4. 4

    Step 4: 병렬 라우트 구현(필요한 경우)

    @ 기호로 슬롯을 만듭니다:
    • @modal: 모달 슬롯
    • @sales, @orders: 대시보드 독립 모듈

    layout.js에서 받습니다:
    • export default function Layout({ children, modal })
    • JSX에서 렌더링합니다: {modal}

    각 슬롯은 자체 loading.js와 error.js를 가질 수 있습니다
  5. 5

    Step 5: 인터셉팅 라우트 구현(모달이 필요한 경우)

    인터셉팅 라우트를 만듭니다:
    • @modal 아래에 (.)photos/[id]/page.js를 만듭니다
    • photos/[id]/page.js(전체 페이지)를 만듭니다

    문법:
    • (.): 같은 단계 라우트 인터셉트
    • (..): 한 단계 위 라우트 인터셉트
    • (...): 루트 디렉터리 기준 인터셉트

    반드시 null을 반환하는 default.js를 만들어야 합니다
  6. 6

    Step 6: 테스트와 검증

    검증 포인트:
    • 모든 라우트가 정상 동작하는지
    • 레이아웃이 올바르게 중첩되는지
    • 페이지 전환이 부드러운지
    • 모달이 정상 작동하는지

    디버깅 도구:
    • React DevTools로 컴포넌트 트리 확인
    • console.log로 렌더링 횟수 확인
    • Network tab에서 요청 확인
    • Next.js 터미널 출력에서 경고 확인

FAQ

언제 라우트 그룹을 써야 하나요?
프로젝트 폴더가 50개를 넘거나 기능 영역마다 다른 루트 레이아웃이 필요하다면 라우트 그룹을 쓰는 것이 좋습니다. 라우트 그룹은 여러 팀이 함께 작업하는 프로젝트에 특히 잘 맞으며, Git 충돌률을 크게 낮추고 디렉터리 구조를 더 명확하게 만듭니다.
라우트 그룹은 URL에 영향을 주나요?
아니요. 라우트 그룹은 괄호로 이름을 붙이며(예: (marketing)), Next.js는 괄호 이름을 무시하므로 URL은 그대로 유지됩니다. 예를 들어 (marketing)/about/page.js의 URL은 /marketing/about이 아니라 /about입니다.
중첩 레이아웃은 성능에 영향을 주나요?
나쁜 영향을 주지 않으며 오히려 성능을 높입니다. Next.js의 중첩 레이아웃은 하위 라우트 전환 시 다시 렌더링되지 않고 가장 안쪽 page.js만 다시 로드합니다. 따라서 사이드바 스크롤 위치 같은 상태를 layout에 보관하면 페이지 전환 중에도 그대로 유지됩니다.
병렬 라우트와 일반 컴포넌트는 무엇이 다른가요?
병렬 라우트의 각 슬롯은 독립된 페이지 조각입니다. 자체 loading.js와 error.js를 가질 수 있고, 서로 영향을 주지 않으며 병렬로 로드됩니다. 반면 일반 컴포넌트는 모든 데이터 로드가 끝나야 렌더링할 수 있고, 한 컴포넌트 오류가 전체 페이지에 영향을 줄 수 있습니다.
인터셉팅 라우트 문법은 어떻게 선택하나요?
(.)는 같은 단계 라우트를 인터셉트할 때 사용합니다(예: @modal과 photos가 모두 app 아래에 있을 때). (..)는 한 단계 위 라우트를, (...)는 루트 디렉터리 기준으로 인터셉트합니다. 확신이 없다면 먼저 (...)로 루트 기준 인터셉트를 구현해 동작을 확인한 뒤 디렉터리 구조에 맞춰 조정하세요.
라우트 충돌은 어떻게 피하나요?
라우트 그룹을 기능 기준으로 나누고 같은 URL이 여러 그룹에 동시에 나타나지 않도록 해야 합니다. 정말 같은 이름의 라우트가 필요하다면 괄호가 없는 실제 디렉터리 계층을 URL에 추가하거나, 둘 중 하나의 라우트 이름을 바꾸세요.
대형 프로젝트 리팩터링에는 시간이 얼마나 걸리나요?
프로젝트 규모에 따라 다릅니다. 중간 규모 프로젝트(50~100개 페이지)는 1~2주, 대형 프로젝트는 한 달 정도 걸릴 수 있습니다. 먼저 한 모듈로 시험 적용해 가능성을 확인한 뒤 전체 프로젝트로 확산하는 편이 증분 마이그레이션 위험을 줄입니다.

4분 읽기 · 게시일: 2025년 12월 18일 · 수정일: 2026년 10월 21일

관련 글

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog