리액트 라우터로 라우팅 기능 사용하기
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='경로' /> 컴포넌트를 사용하여 자바스크립트 코드 안에서 직접 라우팅을 할 수 있다.
- 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>
</>
);
}
<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>
);
}