리액트 라우터로 라우팅 기능 사용하기

#React #router

MPA와 SPA

MPA(Multi Page Application)은 url이 바뀔 때 마다 서버에서 html을 받아와 웹 브라우저에 출력하는 방식으로 구현된 웹 어플리케이션이다. 서버에서 html을 계속 받아오므로, SSR(Server Side Rendering) 방식이 잘 맞다. 정적으로 웹 페이지를 구성하므로 검색 엔진에 노출되는 면(SEO, Search Engine Optimization)에서 비교적 유리하고 초반 렌더링 속도가 빠르지만, url이 바뀔 때 마다 페이지가 렌더링 되는 시간이 소요되며 사용자가 렌더링 되지 않은 하얀색 화면을 볼 수 있어 사용자 경험이 비교적 나쁘다. (최근에는 구글의 검색 로봇(Crawler)이 자바스크립트를 해석할 수 있어서 SPA도 어느 정도 크롤링이 가능해졌다고 한다.)

SPA(Single Page Application)은 초반에만 html을 서버에서 받아오고, url이 바뀔 때마다 변경되는 화면 출력은 동적으로 JS를 통해 렌더링하는 방식으로 구현된 웹 어플리케이션이다. 클라이언트에서 화면을 계속 구성하므로 CSR(Client Side Rendering) 방식이 잘 맞다. 동적으로 웹 페이지를 구성하므로 검색 엔진에 노출되는 면에서 비교적 불리하며 초반 렌더링 속도가 느리지만, url이 바뀔 때 화면이 렌더링 되는 속도가 빠르다.

리액트 라우터

리액트는 SPA 방식을 따르며, SPA 방식은 히스토리 API를 사용해서 동적으로 url을 처리한다. (pushstate, popstate 이벤트 발생) 그리고 히스토리 API를 리액트에서 좀 더 편하게 사용할 수 있도록 개발된 라이브러리가 바로 리액트 라우터이다.

리액트 라우터 설치 방법

$ npm install react-router

리액트 라우터 사용 방법

pages 폴더 안에 컴포넌트를 정의하고, main에서 BrowserRouter, App에서 Routes와 Route로 감싸 사용한다.

// pages/Home.tsx
export default function Home() {
  return(
    <h1>Home</h1>
  );
}
// pages/About.tsx
export default function About() {
  return(
    <h1>About</h1>
  );
}
// main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
)
// App.tsx
import { Route, Routes } from "react-router";
import Home from "./pages/Home";
import About from "./pages/About";

export default function App() {
  return(
    <Routes>
      <Route path="/" element={<Home />}></Route>
      <Route path="about" element={<About />}></Route>
    </Routes>
  );
}

리액트 라우터 - 라우팅 심화

중첩 라우트

중첩 라우트를 사용하여 계층적인 하위 페이지 구조를 구성할 수 있다. 중첩 라우트를 설정할 때는 Outlet 컴포넌트를 필수적으로 사용하여, 상위 라우트에 하위 라우트가 렌더링될 자리를 표시해주어야한다.

중첩 라우트에서 부모 라우트에 path 속성이 없으면 레이아웃 라우트로 작동하며, element 속성이 없으면 라우트 프리픽스로 사용할 수 있다.

레이아웃 라우트는 여러 페이지에서 공통으로 사용하는 레이아웃을 정의하는 라우트이며, 라우트 프리픽스는 특정 그룹의 여러 라우트 경로에 공통된 접두사를 붙일 수 있는 라우트입니다.

// pages/Dashboard.tsx
import { Outlet } from "react-router";

export default function Dashboard() {
  return(
    <>
      <h1>Dashboard</h1>
      <Outlet />
    </>
  );
}
// pages/Summary.tsx
export default function Summary() {
  return(
    <h1>Summary</h1>
  );
}
// pages/Settings.tsx
export default function Settings() {
  return(
    <h1>Settings</h1>
  );
}
// App.tsx
import { Route, Routes } from "react-router";
import Home from "./pages/Home";
import About from "./pages/About";
import Dashboard from "./pages/Dashboard";
import Settings from "./pages/Settings";
import Summary from "./pages/Summary";

export default function App() {
  return(
    <Routes>
      <Route path="/" element={<Home />}></Route>
      <Route path="about" element={<About />}></Route>
      <Route path="dashboard" element={<Dashboard />}>
        <Route index element={<Summary />}></Route>
        <Route path="settings" element={<Settings />}></Route>
      </Route>
    </Routes>
  );
}

동적 세그먼트

동적 세그먼트는 url에 동적으로 바뀌는 요소를 넣을 수 있는 기능이다. Route 컴포넌트의 path 속성에 콜론을 붙이면 동적 세그먼트로 처리된다. 동적 세그먼트에 ?를 넣어 옵셔널 세그먼트로 만들 수 있으며, 옵셔널 세그먼트로 처리된 path는 url에서 제외되어도 라우트가 작동된다.

// pages/Team.tsx
import { useParams } from "react-router";

export default function Team() {
  const params = useParams();
  return(
    <h1>Team Overview - Team ID: {params.teamId}</h1>
  );
}
// App.tsx
import { Route, Routes } from "react-router";
import Team from "./pages/Team";

export default function App() {
  return(
    <Routes>
      <Route path="team/:teamId" element={<Team />}></Route>
    </Routes>
  );
}

스플랫(와일드카드)

path에 * 기호를 사용하면 모든 하위 경로를 한꺼번에 매칭할 수 있다. 스플랫 라우터는 존재하지 않는 경로를 처리할 때 자주 사용한다.

// App.tsx
import { Route, Routes } from "react-router";
import Team from "./pages/Team";
import NotFound from "./pages/NotFound";

export default function App() {
  return(
    <Routes>
      <Route path="team/:teamId" element={<Team />}></Route>
      <Route path="*" element={<NotFound />}></Route>
    </Routes>
  );
}

문서 메타데이터 설정하기

리액트 19부터는 HTML 문서의 <head>에 작성하던 <title>, <link>, <meta>, <script async> 등의 메타데이터 태그를 JSX 안에서 직접 사용할 수 있는 기능이 추가되었습니다. 즉, 이전까지는 별도의 외부 라이브러리(react-helmet)를 사용하거나 index.html을 수정해야 했던 작업을 이제는 각 컴포넌트 내부에서 직접 JSX 형태로 작성하면 자동으로 <head>에 반영됩니다.

리액트 라우터 - 네비게이션

네비게이션은 페이지 간 이동을 말한다. HTML에서는 페이지를 이동할 때 <a> 태그를 사용하지만, 리액트는 <Link><NavLink> 태그를 사용한다. <Link> 태그는 단순히 페이지를 이동할 때 사용하며, <NavLink>는 현재 경로와 일치하는지 감지하여 스타일이나 클래스를 동적으로 변경할 때 사용한다. (HTML의 <a> 태그는 클릭 시 페이지 전체를 새로고침(서버 요청)하지만, <Link> 태그는 preventDefault()를 통해 새로고침을 막고 URL만 변경하여 SPA의 장점을 유지해준다.)

// layouts/RootLayout.tsx
import { Link, NavLink, Outlet } from "react-router"

export default function RootLayout() {
  return(
    <>
      <header>Header</header>
      <nav>
        <Link to='/'>Home</Link>
        <NavLink
          to='/about'
          className={({ isActive }) => (isActive ? 'active' : '')}>About
        </NavLink>
        <NavLink
          to='/dashboard'
          style={({ isActive }) => ({ color: isActive ? 'red' : 'black' })}>Dashboard
        </NavLink>
        <NavLink
          to='/dashboard/settings'>
          {({ isActive }) => <span>settings({isActive && 'selected'})</span>}
        </NavLink>
      </nav>
      <Outlet />
      <footer>Footer</footer>
    </>
  );
}
// App.tsx
import { Route, Routes } from "react-router";
import Home from "./pages/Home";
import About from "./pages/About";
import Dashboard from "./pages/Dashboard";
import Settings from "./pages/Settings";
import Summary from "./pages/Summary";
import Team from "./pages/Team";
import RootLayout from "./layouts/RootLayout";

export default function App() {
  return(
    <Routes>
      <Route element={<RootLayout />}>
        <Route path="/" element={<Home />}></Route>
        <Route path="about" element={<About />}></Route>
        <Route path="team/:teamId" element={<Team />}></Route>
        <Route path="dashboard" element={<Dashboard />}>
          <Route index element={<Summary />}></Route>
          <Route path="settings" element={<Settings />}></Route>
        </Route>
      </Route>
    </Routes>
  );
}

프로그래밍 방식 라우팅

useNavigate 훅을 호출하거나 <Navigate to='경로' /> 컴포넌트를 사용하여 자바스크립트 코드 안에서 직접 라우팅을 할 수 있다.

  1. useNavigate 훅 예시
import { useEffect } from "react";
import { Link, NavLink, Outlet, useNavigate } from "react-router"

export default function RootLayout() {
  const navigate = useNavigate();
  useEffect(()=>{
    setTimeout(() => {
      navigate('/about');
    }, 3000);
  }, [navigate]);
  return(
    <>
      <header>Header</header>
      <nav>
        <Link to='/'>Home</Link>
        <NavLink
          to='/about'
          className={({ isActive }) => (isActive ? 'active' : '')}>About
        </NavLink>
        <NavLink
          to='/dashboard'
          style={({ isActive }) => ({ color: isActive ? 'red' : 'black' })}>Dashboard
        </NavLink>
        <NavLink
          to='/dashboard/settings'>
          {({ isActive }) => <span>settings({isActive && 'selected'})</span>}
        </NavLink>
      </nav>
      <Outlet />
      <footer>Footer</footer>
    </>
  );
}
  1. <Navigate to='경로' /> 예시
import { Navigate, Route, Routes } from "react-router";
import Home from "./pages/Home";
import About from "./pages/About";
import Dashboard from "./pages/Dashboard";
import Settings from "./pages/Settings";
import Summary from "./pages/Summary";
import Team from "./pages/Team";
import RootLayout from "./layouts/RootLayout";
import NotFound from "./pages/NotFound";

export default function App() {
  return(
    <Routes>
      <Route element={<RootLayout />}>
        <Route path="/" element={<Home />}></Route>
        <Route path="about" element={<About />}></Route>
        <Route path="team/:teamId" element={<Team />}></Route>
        <Route path="dashboard" element={<Dashboard />}>
          <Route index element={<Summary />}></Route>
          <Route path="settings" element={<Settings />}></Route>
        </Route>
        <Route path="/not-found" element={<NotFound />} />
        <Route path="*" element={<Navigate to='/not-found' />} />
      </Route>
    </Routes>
  );
}