본문 바로가기

중첩 라우팅과 레이아웃

1. 중첩 라우팅

중첩 라우팅은 URL 구조를 계층적으로 구성할 수 있게 해주는 React Router의 기능입니다. 복잡한 UI 구조를 URL 구조와 일치시켜 표현할 수 있으며, 부모 라우트 내에서 자식 라우트의 컴포넌트를 유연하게 렌더링할 수 있습니다.

1.1 기본 구조

import {
  createBrowserRouter,
  RouterProvider,
  Link,
  Outlet,
} from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: <Dashboard />,
    children: [
      {
        path: 'main',
        element: <MainContent />,
      },
      {
        path: 'settings',
        element: <Settings />,
      },
      {
        path: 'profile',
        element: <Profile />,
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

function Dashboard() {
  return (
    <div>
      <h1>대시보드</h1>
      <nav>
        <Link to="/dashboard/main">홈</Link>
        <Link to="/dashboard/settings">설정</Link>
        <Link to="/dashboard/profile">프로필</Link>
      </nav>
      {/* 자식 라우트가 이 위치에 렌더링됩니다 */}
      <Outlet />
    </div>
  );
}

function MainContent() {
  return <p>메인 내용</p>;
}

function Settings() {
  return <p>설정창</p>;
}

function Profile() {
  return <p>유저프로필</p>;
}

export default App;

1.2 Outlet 컴포넌트

Outlet은 부모 라우트에서 자식 라우트의 컴포넌트가 렌더링될 위치를 지정하는 React Router의 컴포넌트입니다. 중첩 라우팅에서 레이아웃을 구성할 때 핵심적인 역할을 합니다.

function Dashboard() {
  return (
    <div>
      <h1>대시보드</h1>
      <nav>
        <Link to="/dashboard/main">홈</Link>
        <Link to="/dashboard/settings">설정</Link>
        <Link to="/dashboard/profile">프로필</Link>
      </nav>
      {/* 자식 라우트가 이 위치에 렌더링됩니다 */}
      <Outlet />
    </div>
  );
}

1.3 URL 구조와 결과 화면

아래는 위 코드로 접근 가능한 URL 구조입니다. 각 URL에서는 Dashboard 컴포넌트가 레이아웃 역할을 하고, 그 안에 각각 다른 자식 컴포넌트가 렌더링됩니다.

localhost:5173/dashboard/main     -> Dashboard + MainContent 컴포넌트
localhost:5173/dashboard/settings -> Dashboard + Settings 컴포넌트
localhost:5173/dashboard/profile  -> Dashboard + Profile 컴포넌트
  1. /dashboard/main 접속 시: Dashboard 컴포넌트의 공통 레이아웃이 유지되고, Outlet 위치에 MainContent 컴포넌트가 렌더링됩니다.
  2. /dashboard/settings 접속 시: Dashboard 컴포넌트의 공통 레이아웃이 유지되고, Outlet 위치에 Settings 컴포넌트가 렌더링됩니다.
  3. /dashboard/profile 접속 시: Dashboard 컴포넌트의 공통 레이아웃이 유지되고, Outlet 위치에 Profile 컴포넌트가 렌더링됩니다.

2. 공통 레이아웃 패턴

중첩 라우팅을 활용하면 헤더, 푸터, 사이드바 등 공통 레이아웃을 쉽게 구현할 수 있습니다.

import {
  createBrowserRouter,
  RouterProvider,
  Link,
  Outlet,
} from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        index: true, // path: '/'와 동일
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'products',
        element: <Products />,
      },
      {
        path: 'contact',
        element: <Contact />,
      },
    ],
  },
]);

function RootLayout() {
  return (
    <div className="app">
      <header>
        <nav>
          <Link to="/">홈</Link>
          <Link to="/about">소개</Link>
          <Link to="/products">상품</Link>
          <Link to="/contact">문의</Link>
        </nav>
      </header>

      <main>
        <Outlet />
      </main>

      <footer>
        <p>© 2025 My Website</p>
      </footer>
    </div>
  );
}

function Home() {
  return <h1>홈 페이지</h1>;
}

function About() {
  return <h1>소개 페이지</h1>;
}

function Products() {
  return <h1>상품 페이지</h1>;
}

function Contact() {
  return <h1>문의 페이지</h1>;
}

function App() {
  return <RouterProvider router={router} />;
}

export default App;

index 라우트

index: true 속성을 사용하면 부모 경로와 정확히 일치할 때 렌더링되는 기본 자식 라우트를 정의할 수 있습니다. 위 예제에서 / 경로로 접속하면 Home 컴포넌트가 렌더링됩니다.

{
  path: '/',
  element: <RootLayout />,
  children: [
    {
      index: true,  // '/'에서 렌더링
      element: <Home />,
    },
    // ...
  ],
}

3. 다중 레이아웃

서로 다른 레이아웃이 필요한 페이지들을 구분하여 구성할 수 있습니다.

import {
  createBrowserRouter,
  RouterProvider,
  Link,
  Outlet,
} from 'react-router-dom';

const router = createBrowserRouter([
  // 메인 레이아웃 (헤더, 푸터 있음)
  {
    path: '/',
    element: <MainLayout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
      { path: 'products', element: <Products /> },
    ],
  },
  // 관리자 레이아웃 (사이드바 있음)
  {
    path: '/admin',
    element: <AdminLayout />,
    children: [
      { index: true, element: <AdminDashboard /> },
      { path: 'users', element: <AdminUsers /> },
      { path: 'settings', element: <AdminSettings /> },
    ],
  },
  // 인증 레이아웃 (최소한의 UI)
  {
    path: '/auth',
    element: <AuthLayout />,
    children: [
      { path: 'login', element: <Login /> },
      { path: 'register', element: <Register /> },
    ],
  },
]);

function MainLayout() {
  return (
    <div className="main-layout">
      <header>
        <nav>
          <Link to="/">홈</Link>
          <Link to="/about">소개</Link>
          <Link to="/products">상품</Link>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>© 2025 My Website</footer>
    </div>
  );
}

function AdminLayout() {
  return (
    <div className="admin-layout">
      <aside>
        <nav>
          <Link to="/admin">대시보드</Link>
          <Link to="/admin/users">사용자 관리</Link>
          <Link to="/admin/settings">설정</Link>
        </nav>
      </aside>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

function AuthLayout() {
  return (
    <div className="auth-layout">
      <Outlet />
    </div>
  );
}

// 각 페이지 컴포넌트들...
function Home() {
  return <h1>홈</h1>;
}
function About() {
  return <h1>소개</h1>;
}
function Products() {
  return <h1>상품</h1>;
}
function AdminDashboard() {
  return <h1>관리자 대시보드</h1>;
}
function AdminUsers() {
  return <h1>사용자 관리</h1>;
}
function AdminSettings() {
  return <h1>관리자 설정</h1>;
}
function Login() {
  return <h1>로그인</h1>;
}
function Register() {
  return <h1>회원가입</h1>;
}

function App() {
  return <RouterProvider router={router} />;
}

export default App;

4. Outlet context

Outlet 컴포넌트는 자식 라우트로 context를 전달할 수도 있습니다. 이를 통해 부모 컴포넌트의 상태나 함수를 자식 라우트에서 쉽게 사용할 수 있습니다.

context는 props를 일일이 전달하지 않고도 컴포넌트 간에 데이터를 공유할 수 있는 방식입니다.

import {
  createBrowserRouter,
  RouterProvider,
  Outlet,
  useOutletContext,
} from 'react-router-dom';
import { useState } from 'react';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'settings', element: <Settings /> },
    ],
  },
]);

function Layout() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: '홍길동' });

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    <div className={`app ${theme}`}>
      <header>
        <button onClick={toggleTheme}>테마 변경</button>
        <span>현재 테마: {theme}</span>
      </header>

      {/* context를 통해 자식 컴포넌트로 데이터 전달 */}
      <Outlet context={{ theme, toggleTheme, user, setUser }} />
    </div>
  );
}

// 자식 컴포넌트에서 context 사용
function Home() {
  const { theme, user } = useOutletContext();

  return (
    <div>
      <h1>안녕하세요, {user.name}님!</h1>
      <p>현재 테마: {theme}</p>
    </div>
  );
}

function Settings() {
  const { theme, toggleTheme, user, setUser } = useOutletContext();

  const handleNameChange = (e) => {
    setUser({ ...user, name: e.target.value });
  };

  return (
    <div>
      <h1>설정</h1>
      <div>
        <label>이름: </label>
        <input value={user.name} onChange={handleNameChange} />
      </div>
      <div>
        <label>테마: </label>
        <button onClick={toggleTheme}>
          {theme === 'light' ? '다크 모드로' : '라이트 모드로'}
        </button>
      </div>
    </div>
  );
}

function App() {
  return <RouterProvider router={router} />;
}

export default App;

Outlet의 이러한 특성을 활용하면 공통 레이아웃을 유지하면서도 내부 콘텐츠만 동적으로 변경되는 UI를 쉽게 구현할 수 있습니다. 특히 대시보드처럼 복잡한 UI 구조를 가진 애플리케이션에서 매우 유용합니다.

useOutletContext vs Context API

  • useOutletContext: 라우트 계층 구조 내에서만 데이터를 공유할 때 사용합니다. 간단하고 라우터와 밀접하게 연동됩니다.
  • Context API: 애플리케이션 전역에서 데이터를 공유해야 할 때 사용합니다. (예: 인증 상태, 전역 설정 등)

라우트 내에서만 필요한 데이터는 useOutletContext를, 전역적으로 필요한 데이터는 Context API를 사용하는 것이 좋습니다. Context API에 대한 내용은 추후 자세히 알아볼 예정입니다.

5. 깊은 중첩 라우팅

여러 단계로 중첩된 라우팅도 구현할 수 있습니다.

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <Home /> },
      {
        path: 'products',
        element: <ProductsLayout />,
        children: [
          { index: true, element: <ProductList /> },
          {
            path: ':productId',
            element: <ProductDetailLayout />,
            children: [
              { index: true, element: <ProductInfo /> },
              { path: 'reviews', element: <ProductReviews /> },
              { path: 'specs', element: <ProductSpecs /> },
            ],
          },
        ],
      },
    ],
  },
]);

위 구조에서 /products/123/reviews 경로로 접속하면 다음과 같이 렌더링됩니다.

RootLayout
└── ProductsLayout
    └── ProductDetailLayout
        └── ProductReviews

각 레이아웃 컴포넌트는 자신의 Outlet에 자식 컴포넌트를 렌더링합니다.

중첩 라우팅과 레이아웃 - React 에센셜 | 위니버시티