웹 애플리케이션에서 라우팅이라는 개념은 사용자가 요청한 URL에 따라 알맞은 페이지를 보여주는 것을 의미합니다.
블로그 애플리케이션을 구현한다고 했을 때 어떤 페이지가 필요한지 생각해봅시다.
이렇게 여러 페이지로 구성된 웹 애플리케이션을 만들 때 페이지 별로 컴포넌트들을 분리해가면서 프로젝트를 관리하기 위해 필요한 것이 바로 라우팅 시스템입니다.
리액트에서 라우트 시스템을 구축하기 위해 사용할 수 있는 선택지는 크게 두가지가 있습니다.
싱글 페이지 애플리케이션이란 하나의 페이지로 이루어진 애플리케이션이라는 의미입니다.
해당 개념을 이해하기 위해 멀티 페이지 애플리케이션 작동을 이해해야합니다.
멀티 페이지 애플리케이션에서는 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아오고 페이지를 로딩할 때마다 서버에서 CSS, JS, 이미지 파일 등의 리소스를 전달받아 브라우저 화면에 보여 주었습니다. 각 페이지마다 다른 html 파일을 만들어서 제공을 하거나, 데이터에 따라 유동적인 html을 생성해 주는 템플릿 엔진을 사용하기도 했습니다.
사용자 인터렉션이 별로 없는 정적인 페이지들은 적합하지만 사용자 인터렉션이 많고 다양한 정보를 제공하는 모던 웹 애플리케이션은 이 방식이 서버의 자원을 많이 사용하고 트래픽도 더 많이 발생하기에 적합하지 않습니다.그래서 리액트 같은 라이브러리를 사용해 뷰 렌더링을 사용자가 담당하도록 하고, 우선 웹 애프리케이션을 브라우저에 불러와 실행시킨 후 사용자 인터렉션이 발생하면 필요한 부분만 자바스크립트를 사용하여 업데이트 하는 방식을 사용하게 되었습니다.
HTML은 한번만 받아와서 웹 애플리케이션을 실행시킨 후, 이후에는 필요한 데이터만 받아와서 화면에 업데이트 하는 것이 싱글 페이지 애플리케이션입니다.
싱글 페이지 애플리케이션은 기술적으로는 한 페이지만 존재하지만, 사용자가 경험하기에는 여러 페이지가 존재하는 것처럼 느낄 수 있습니다.
리액트 라우터를 다음과 같은 순서로 사용해보겠습니다.
1. 프로젝트 생성 및 라이브러리 설치
2. 페이지를 만들고 이동해보기
3. URL 파라미터와 쿼리스트링 사용해보기
4. 중첩된 라우트 구현하기
5. 리액트 라우터의 부가기능 사용해보기
$yarn create react-app router-tutorial
$cd router-tutorial
$yarn add react-router-dom
프로젝트에 리액트 라우터를 적용할 떄는 src/index.js파일에서 react-router-dom에 내장되어 있는 BrowserRouter라는 컴포넌트를 사용하여 감싸면 됩니다. 이 컴포넌트는 웹 애플리케이션에 HTMl5의 History API를 사용하여 페이지를 새로 불러오지 않고도 주소를 변경하고 현재 주소의 경로에 관련된 정보를 리액트 컴포넌트에서 사용할 수 있도록 해줍니다.
src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Home,About 페이지 컴포넌트 생성
src/pages/Home.js
const Home = () => {
return (
<div>
<h1>홈</h1>
<p>가장 먼저 보여지는 페이지입니다.</p>
</div>
);
};
export default Home;
src/pages/About.js
const About = () => {
return (
<div>
<h1>소개</h1>
<p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
</div>
);
};
export default About;
사용자의 브라우저 주소 경로에 따라 우리가 원하는 컴포넌트를 보여주려면 Route라는 컴포넌트를 통해 라우트 설정을 해주어야 합니다.
<Route path="주소규칙" element={보여 줄 컴포넌트 JSX} />
Route 컴포넌트는 Routes 컴포넌트 내부에서 사용되어야 합니다.
src/App.js
import { Route, Routes } from "react-router-dom";
import About from "./pages/About";
import Home from "./pages/Home";
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
);
};
웹 페이지에서는 원래 링크를 보여줄 때 a 태그를 사용하는데 리액트 라우터를 사용하는 프로젝트에서는 a 태그를 바로 사용하면 안됩니다. 왜냐하면 a 태그를 클릭하여 페이지를 이동할 때 브라우저에서는 페이지를 새로 불러오게 되기 때문입니다.
Link 컴포넌트 역시 a 태그를 사용하긴 하지만, 페이지를 새로 불러오는 것을 막고 History API를 통해 브라우저 주소의 경로만 바꾸는 기능이 내장되어 있습니다.
<Link to="경로">링크 이름</Link>
src/pages/Home.js
import { Link } from "react-router-dom";
const Home = () => {
return (
<div>
<h1>홈</h1>
<p>가장 먼저 보여지는 페이지입니다.</p>
<Link to="/about">소개</Link>
</div>
);
};
export default Home;
Home Page
About Page
페이지 주소를 정의할 때 가끔은 유동적인 값을 사용해야 할 때도 있습니다.
URL 파라미터는 주소의 경로에 유동적인 값을 넣는 형태고, 쿼리스트링은 주소의 뒷부분 ? 문자열 이후에 key=value 값을 정의하여 &로 구분하는 형태입니다.
URL 파라미터는 주로 ID 또는 이름을 사용하여 특정 데이터를 조회할 때 사용하고,
쿼리스트링은 키워드 검색, 페이지네이션, 정렬 방식 등 데이터 조회에 필요한 옵션을 전달할 때 사용합니다.
src/pages/Profile.js
import { useParams } from "react-router-dom";
const data = {
velopert: {
name: "김민준",
description: "리액트를 좋아하는 개발자",
},
gildong: {
name: "홍길동",
description: "고전 소설 홍길동전의 주인공",
},
};
const Profile = () => {
const params = useParams();
const profile = data[params.username];
return (
<div>
<h1>사용자 프로필</h1>
{protile ? (
<div>
<h2>{profile.name}</h2>
<p>{profile.description}</p>
</div>
) : (
<p>존재하지 않는 프로필 입니다.</p>
)}
</div>
);
};
export default Profile;
URL 파라미터는 useParams라는 Hook을 사용하여 객체 형태로 조회할 수 있습니다. URL 파라미터의 이름은 라우트 설정을 할 때 Route 컴포넌트의 path props를 통해 설정합니다.
src/App.js
import { Route, Routes } from "react-router-dom";
import About from "./pages/About";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/profiles/:username" element={<Profile />} />
</Routes>
);
};
export default App;
URL 파라미터는 /profiles/:username 과 같이 경로에 :를 사용하여 설정합니다. 마약 파라미터가 여러 개인 경우에는 /profiles/:username/:field와 같은 형태로 설정할 수 있습니다.
src/pages/Home.js
import { Link } from "react-router-dom";
const Home = () => {
return (
<div>
<h1>홈</h1>
<p>가장 먼저 보여지는 페이지입니다.</p>
<ul>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profiles/velopert">velopert의 프로필</Link>
</li>
<li>
<Link to="/profiles/gildong">gildong의 프로필</Link>
</li>
<li>
<Link to="/profiles/void">존재하지 않는 프로필</Link>
</li>
</ul>
</div>
);
};
export default Home;
쿼리스트링을 사용할 때는 URL파라미터와 달리 Route 컴포넌트를 사용할 때 별도로 설정해야 하는 것이 없습니다.
src/pages/About.js
import { useLocation } from "react-router-dom";
const About = () => {
const location = useLocation();
return (
<div>
<h1>소개</h1>
<p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
<p>쿼리스트링: {location.search}</p>
</div>
);
};
export default About;
위 컴포넌트에서는 useLocation이라는 Hook을 사용했습니다.
이 Hook은 location 객체를 반환합니다.
src/pages/About.js
import { useSearchParams } from "react-router-dom";
const About = () => {
const [searchParams, setSearchParams] = useSearchParams();
const detail = searchParams.get("detail");
const mode = searchParams.get("mode");
const onToggleDetail = () => {
setSearchParams({ mode, detail: detail === "true" ? false : true });
};
const onIncreaseMode = () => {
const nextmode = mode === null ? 1 : parseInt(mode) + 1;
setSearchParams({ mode: nextmode, detail });
};
return (
<div>
<h1>소개</h1>
<p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
<p>detail: {detail}</p>
<p>mode: {mode}</p>
<button onClick={onToggleDetail}>ToggleDetail</button>
<button onClick={onIncreaseMode}>mode+1</button>
</div>
);
};
export default About;
useSerachPrarms는 배열 타입의 값을 반환합니다.
첫번째 요소는 쿼리파라미터를 조회하거나 수정하는 메서드들이 담긴 객체를 반환합니다.
두번째 요소는 쿼리 파라미터를 객체 형태로 업데이트 할 수 있는 함수를 반환합니다.
만약 조회 시, 쿼리 파라미터가 존재하지 않는다면 null로 조회됩니다.
주의할 점은 쿼리파라미터를 조회할 때 값은 무조건 문자열 타입이라는 점입니다.
src/pages/Articles.js
import { Link } from "react-router-dom";
const Articles = () => {
return (
<ul>
<li>
<Link to="/articles/1">게시글 1</Link>
</li>
<li>
<Link to="/articles/1">게시글 2</Link>
</li>
<li>
<Link to="/articles/1">게시글 3</Link>
</li>
</ul>
);
};
export default Articles;
src/pages/Article.js
import { useParams } from "react-router-dom";
const Article = () => {
const { id } = useParams();
return (
<div>
<h2>게시글 {id}</h2>
</div>
);
};
export default Article;
게시글 목록 페이지에서 게시글을 열었을 때, 게시글 하단에 목록을 보여주는 방법을 중첩된 라우트를 사용해 구현해 보겠습니다.
App.js
<Route path="/articles" element={<Articles />}>
<Route path=":id" element={<Article />} />
</Route>
Articles.js
import { Link, Outlet } from "react-router-dom";
const Articles = () => {
return (
<div>
<Outlet />
<ul>
<li>
<Link to="/articles/1">게시글 1</Link>
</li>
<li>
<Link to="/articles/2">게시글 2</Link>
</li>
<li>
<Link to="/articles/3">게시글 3</Link>
</li>
</ul>
</div>
);
};
export default Articles;
중첩된 라우트와 Outlet은 페이지끼리 공통적으로 보여줘야 하는 레이아웃이 있을때도 유용하게 사용할 수 있습니다.
Layout.js
import { Outlet } from "react-router-dom";
const Layout = () => {
return (
<div>
<header style={{ background: "lightgray", padding: 16, fontSize: 24 }}>
Header
</header>
<main>
<Outlet />
</main>
</div>
);
};
export default Layout;
App.js
import { Route, Routes } from "react-router-dom";
import About from "./pages/About";
import Layout from "./Layout";
import Article from "./pages/Article";
import Articles from "./pages/Articles";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
const App = () => {
return (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/profiles/:username" element={<Profile />} />
</Route>
<Route path="/articles" element={<Articles />}>
<Route path=":id" element={<Article />} />
</Route>
</Routes>
);
};
export default App;
Route 컴포넌트에는 index라는 props가 있습니다.
이 props는 path="/"와 동일한 의미를 가집니다.
<Route index element={<Home/>}/>
index props 를 사용하면 상위 라우트의 경로와 일치하지만 그 이후에 경로가 주어지지 않았을 때 보여지는 라우트를 설정할 수 있습니다.
useNavigate는 Link 컴포넌트를 사용하지 않고 다른 페이지로 이동해야 하는 상황에 사용하는 Hook 입니다.
Layout.js
import { Outlet, useNavigate } from "react-router-dom";
const Layout = () => {
const navigate = useNavigate();
const goBack = () => {
navigate(-1);
};
const goArticles = () => {
navigate("./articles", { replace: true });
};
return (
<div>
<header style={{ background: "lightgray", padding: 16, fontSize: 24 }}>
<button onClick={goBack}>뒤로가기</button>
<button onClick={goArticles}>게시글 목록</button>
Header
</header>
<main>
<Outlet />
</main>
</div>
);
};
export default Layout;
{replace:true} 옵션을 주면 페이지를 이동할 때 현재 페이지를 페이지 기록에 남기지 않습니다.
NavLink 컴포넌트는 링크에서 사용하는 경로가 현재 라우트의 경로와 일치하는 경우 특정 스타일 또는 CSS 클래스를 적용하는 컴포넌트 입니다.
이 컴포넌트의 style과 className은 {isActive : boolean}을 파라미터로 전달받는 함수 타입의 값을 전달합니다.
<NavLink
style={({isActive}) => isActive ? activeStyle : undefined}
/>
<NavLink
style={({isActive}) => isActive ? 'active' : undefined}
/>
이 페이지는 사전에 정의되지 않는 경로에 사용자가 진입했을 때 보여주는 페이지 입니다.
<Route path="*" element={<NotFound />}
*는 wildcard문자인데요 아무텍스트나 매칭한다는 뜻입니다.
이 라우트 엘리먼트의 상단에 위치하는 라우트들의 규칙을 모두 확인하고 일치하는 라우트가 없다면 이 라우트가 화면에 나타나게 됩니다.
Navigate 컴포넌트는 컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동을 하고 싶을 때 사용하는 컴포넌트 입니다. 즉 페이지를 리다이렉트하고 싶을 때 사용합니다.