[React 숙련] React Router DOM 2 - Dynamic Route, 중첩된 라우트

조아영·2025년 3월 8일

Dynamic Route

Dynamic Route는 동적 라우팅이라고도 부르며
path에 유동적인 값을 포함해 특정 페이지로 이동하도록 구현하는 방식임.

// ❌️
<Route path="/works/1" element={<Works />} />
<Route path="/works/2" element={<Works />} />
<Route path="/works/3" element={<Works />} />

예를 들어 works 페이지에 여러 개의 work가 있고, 각 work마다 독립적인 상세 페이지가 필요하다고 가정함.
이 경우 works/1, works/2, works/3처럼 모든 경로를 하나씩 직접 작성하는게 아니라.
react-router-dom에서 제공하는 Dynamic Routes 기능을 사용해서 동적으로 변하는 경로를 간결하게 처리 가능함.

설정

// src/shared/Router.js

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "../pages/Home";
import About from "../pages/About";
import Contact from "../pages/Contact";
import Works from "../pages/Works";
import Work from "../pages/Work";

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="contact" element={<Contact />} />
        <Route path="works" element={<Works />} />
				{/* path에 :id 사용 */}
        <Route path="works/:id" element={<Work />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

Works 페이지에 여러 개의 Work가 있고, 클릭 시 각각의 상세 페이지로 이동하도록 구현.

기존과 달리 path에 :id 사용함. :id는 동적인 값을 받겠다는 의미임.
따라서 works/1, works/2 … works/100 모두 같은 <Work /> 컴포넌트로 이동함.
이때 id 값은 useParams 훅을 통해 조회 가능.

◼ useParams

Dynamic Routes를 사용하면
같은 페이지 컴포넌트(Work.js)를 모두 동일하게 렌더링하게 됨.
하지만 useParams를 사용하면
같은 컴포넌트를 렌더링 하더라도 각각 다른 id 값 조회 가능함.

useParams는 path의 있는 id 값을 조회할 수 있게 해주는 훅(path parameter 조회 목적).
예를 들어 works/100으로 이동하면 { id: "100" } 형태의 객체 반환함.

// src/pages/Works.js

import React from 'react';
import { Link } from 'react-router-dom';

const data = [
  { id: 1, todo: '리액트 배우기' },
  { id: 2, todo: '노드 배우기' },
  { id: 3, todo: '자바스크립트 배우기' },
  { id: 4, todo: '파이어 베이스 배우기' },
  { id: 5, todo: '넥스트 배우기' },
  { id: 6, todo: 'HTTP 프로토콜 배우기' },
];

function Works() {
  return (
    <div>
      {data.map((work) => {
        return (
          <div key={work.id}>
            <div>할일: {work.id}</div>
            {/* 링크 목록추가 */}
            <Link to={`/works/${work.id}`}>
              <span style={{ cursor: 'pointer' }}>➡️ Go to: {work.todo}</span>
            </Link>
          </div>
        );
      })}
    </div>
  );
}

export default Works;
// src/pages/Work.js

import React from 'react';
import { useParams } from 'react-router-dom';

const data = [
  { id: 1, todo: '리액트 배우기' },
  { id: 2, todo: '노드 배우기' },
  { id: 3, todo: '자바스크립트 배우기' },
  { id: 4, todo: '파이어 베이스 배우기' },
  { id: 5, todo: '넥스트 배우기' },
  { id: 6, todo: 'HTTP 프로토콜 배우기' },
];

function Work() {
	// useParams 사용
  const param = useParams();
  // param 객체에서 id 값 추출 가능. 
  // 해당 id를 기준으로 데이터 필터링 가능.
  const work = data.find(
	  (work) => work.id === parseInt(param.id)
	);

  return <div>{work.todo}</div>;
}

export default Work;

◼ 중첩된 라우트

중첩 라우팅은 특정 라우트 내부에 추가로 라우트를 정의하는 방식.
여러 계층의 UI 구성 시 유용함.

예시로 대시보드 구조가 있음.
/dashboard 하위에 여러 섹션 존재하는 구조임.

// src/shared/Router.js

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import DashboardLayout from './DashboardLayout';
import Profile from './Profile';
import Settings from './Settings';
import Reports from './Reports';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route index element={<Profile />} />
          <Route path="settings" element={<Settings />} />
          <Route path="reports" element={<Reports />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

/dashboard는 공통 레이아웃(DashboardLayout)을 사용함.
하위 경로마다 서로 다른 페이지 렌더링함.
라우트 구조가 명확해지고 UX 개선에 도움 됨.

◼ Outlet

자식 라우트가 렌더링될 위치를 지정하는 컴포넌트.
복잡한 라우트 구조를 명확하게 관리 가능함.

// src/shared/Router.js

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Layout from './Layout';
import Home from "../pages/Home";
import About from "../pages/About";

const Router = () => {
  return (
    <BrowserRouter>
	    <Routes>
			  <Route path="/" element={<Layout />}>
			    <Route index element={<Home />} />
			    <Route path="about" element={<About />} />
			  </Route>
			</Routes>
    </BrowserRouter>
  );
};

export default Router;
// src/shared/Layout.js

import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <header>Header Section</header>
      <main>
        <Outlet /> {/* 이 위치에 자식 라우트의 컴포넌트 렌더링. */}
      </main>
      <footer>Footer Section</footer>
    </div>
  );
}

◼ children

props와 chilren을 활용한 공통 Layout.

children은 어떤 자식 엘리먼트가 들어올지 모를 때 사용.
Sidebar, Dialog 같은 범용 컴포넌트에서 자주 사용.

Layout 역할의 컴포넌트로 이해 가능. composition(합성) 개념 기반 구조.
Header, Footer, Page를 조합하여 공통 레이아웃 구성.

// src/shared/Layout.js

import React from 'react';

const HeaderStyles = {
  width: '100%',
  background: 'black',
  height: '50px',
  display: 'flex',
  alignItems: 'center',
  paddingLeft: '20px',
  color: 'white',
  fontWeight: '600',
};
const FooterStyles = {
  width: '100%',
  height: '50px',
  display: 'flex',
  background: 'black',
  color: 'white',
  alignItems: 'center',
  justifyContent: 'center',
  fontSize: '12px',
};

const layoutStyles = {
  display: 'flex',
	flexDirection: 'column',
  justifyContent: 'center',
  alignItems: 'center',
  minHeight: '90vh',
}

function Header() {
  return (
    <div style={{ ...HeaderStyles }}>
      <span>Sparta Coding Club - Let's learn React</span>
    </div>
  );
}

function Footer() {
  return (
    <div style={{ ...FooterStyles }}>
      <span>copyright @SCC</span>
    </div>
  );
}

function Layout({ children }) {
  return (
    <div>
      <Header />
      <div style={{...layoutStyles}}>
        {children}
      </div>
      <Footer />
    </div>
  );
}

export default Layout;
// src/shared/Router.js

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Home from '../pages/Home';
import About from '../pages/About';
import Contact from '../pages/Contact';
import Works from '../pages/Works';
import Layout from './Layout';

const Router = () => {
  return (
    <BrowserRouter>
	    {/* Layout으로 Routes 감쌈 */}
      <Layout>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
          <Route path="works" element={<Works />} />
        </Routes>
      </Layout>
    </BrowserRouter>
  );
};

export default Router;

공통 UI 유지하면서 페이지별 내용만 교체 가능.
레이아웃 재사용에 적합한 구조.

◼ 중첩라우팅 vs 공통 레이아웃

중첩 라우팅(Outlet)

<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
  </Route>
</Routes>

라우트마다 다른 레이아웃 적용 가능.
동적이고 유연한 라우트 관리를 제공.

공통 레이아웃(children)

<Layout>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="about" element={<About />} />
  </Routes>
</Layout>

모든 페이지에 동일한 레이아웃 적용 시 적합.
구조가 단순해서 관리에 용이.

권장 방식 - 중첩 라우팅 + Outlet

이유1. 라우트(URL) 구조와 UI 구조 일치

  • URL 구조와 컴포넌트 구조가 자연스럽게 매칭됨
  • /dashboard, /dashboard/settings 같은 계층 표현에 적합함
  • 라우트만 봐도 화면 구조 파악 가능함

이유2. 레이아웃 분기 처리 용이

  • 페이지마다 서로 다른 레이아웃 적용 가능함
    예시 - 로그인 페이지: 헤더 없음, 메인 페이지: 헤더 있음
  • 조건 분기 코드 없이 라우트 단위로 처리 가능함

이유 3. 유지보수 및 확장성 우수

  • 라우트 추가 시 구조 변경 최소화
  • 대규모 서비스에서 구조 안정적임
  • 팀 협업 시 역할 분리 명확함

공통 레이아웃 방식이 적합한 경우

  • 모든 페이지가 동일한 레이아웃 사용
  • 소규모 프로젝트
  • 페이지 수 적음
  • 구조 단순함

한계

  • 페이지별 레이아웃 분기 어려움
  • 조건부 렌더링 증가
  • 라우트 구조와 UI 구조 분리됨

0개의 댓글