리액트로 만든 앱은 대개 index.html 한 장으로 돌아간다. 그런데 우리가 아는 웹사이트는 홈, 소개, 글 목록처럼 주소가 여러 개다. 페이지가 한 장인데 주소는 여럿이어야 하는 이 모순을 풀어주는 게 라우팅(주소에 맞춰 화면을 바꾸는 일)이다. 여기서 초보가 제일 먼저 하는 실수는 그냥 a 태그를 쓰는 것이다. <a href="/about">를 누르면 브라우저가 서버에 /about을 새로 요청하고, 리액트 앱이 통째로 다시 로딩된다. 화면이 하얗게 깜빡이고, 방금까지 들고 있던 상태도 전부 날아간다. 나도 처음엔 왜 클릭할 때마다 앱이 리셋되는지 몰라서 한참을 헤맸다.


클라이언트 라우팅. 서버에 다시 안 물어보고 화면만 갈아끼운다. 단일 페이지 앱(SPA)의 핵심은 주소가 바뀌어도 페이지를 새로 안 받는 것이다. 자바스크립트가 주소만 슬쩍 바꾸고, 그 주소에 맞는 컴포넌트를 그 자리에서 그려낸다. 서버 왕복이 없으니 깜빡임도 없고 상태도 안 날아간다. 이걸 손으로 구현하려면 골치가 아프기 때문에, 리액트 진영은 react-router-dom이라는 라이브러리를 사실상 표준으로 쓴다. 아래 예제는 전부 버전 6 기준이다.


BrowserRouter. 앱 전체를 라우터로 감싼다. 라우팅을 쓰려면 먼저 앱의 바깥을 BrowserRouter로 둘러야 한다. 이게 현재 주소를 읽고, 뒤로 가기 같은 브라우저 동작을 챙겨준다. 그 안에서 링크와 경로를 정의한다.


import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">홈</Link>
<Link to="/about">소개</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}


여기 두 가지 새 조각이 있다. LinkRoutes/Route다. 하나씩 뜯어보자.


Link. a 태그 대신 이걸 쓴다. Link는 화면상으론 링크처럼 보이지만, 누를 때 서버로 새 요청을 보내지 않는다. 주소만 바꾸고 화면은 리액트가 갈아끼운다. 실제로 렌더된 결과는 a 태그라 마우스 오른쪽 클릭으로 새 탭 열기도 잘 된다. 다만 클릭 동작만 가로채는 것이다. to에 이동할 주소를 적으면 끝이다.


Routes와 Route. 주소와 컴포넌트를 짝지어준다. Routes 안에 여러 Route를 넣고, 각 Routepath(주소)와 element(그 주소에서 보여줄 컴포넌트)를 짝지운다. 현재 주소가 /Home이, /about이면 AboutRoutes 자리에 나타난다. navRoutes 바깥이라 주소가 바뀌어도 그대로 남는다. 그래서 상단 메뉴는 고정된 채 아래 본문만 갈리는, 우리가 흔히 보는 그 구조가 된다.


홈에서 소개를 누르면 주소창이 /about으로 바뀌고, 새로고침 없이 About 컴포넌트가 그 자리에 뜬다. 이게 클라이언트 라우팅의 실제 감각이다. 서버는 아무것도 모른 채 화면만 바뀐다.


동적 경로. 글 번호처럼 값이 바뀌는 주소를 받는다. /posts/1, /posts/2처럼 뒤 숫자만 바뀌는 주소를 일일이 Route로 적을 순 없다. 이럴 땐 path에 콜론을 붙여 자리표를 만든다.


<Routes>
<Route path="/posts/:id" element={<PostDetail />} />
</Routes>


:id가 그 자리표다. /posts/7로 들어오면 id7이 담긴다. 이 값은 useParams라는 훅으로 꺼낸다.


import { useParams } from "react-router-dom";

function PostDetail() {
const { id } = useParams();
return <h2>{id}번 글 상세</h2>;
}


/posts/7로 접속하면 화면에 7번 글 상세가 뜬다. 여기서 꼭 알아둘 게 있다. useParams가 주는 값은 항상 문자열이다. 7이 아니라 "7"이다. 나는 이걸 그대로 서버 API에 넘겼다가 숫자 비교가 어긋나 목록에서 엉뚱한 글이 잡힌 적이 있다. 숫자로 써야 하면 Number(id)로 바꾸는 걸 잊지 말자.


useNavigate. 클릭이 아니라 코드로 페이지를 옮긴다. 로그인 성공 후 대시보드로 보내는 것처럼, 사용자가 링크를 누르는 게 아니라 코드가 알아서 이동시켜야 할 때가 있다. 이럴 땐 useNavigate가 주는 함수를 부른다.


import { useNavigate } from "react-router-dom";

function LoginButton() {
const navigate = useNavigate();
return (
<button onClick={() => navigate("/dashboard")}>
로그인
</button>
);
}


버튼을 누르면 navigate("/dashboard")가 불리고, Link를 클릭한 것과 똑같이 주소가 바뀌며 화면이 갈린다. navigate(-1)처럼 숫자를 넣으면 뒤로 가기도 된다. 폼 제출이 끝나고 결과 페이지로 넘길 때 이걸 자주 쓴다.


NavLink. 지금 보고 있는 메뉴에 표시를 준다. 상단 메뉴에서 현재 페이지가 어디인지 굵게 칠하고 싶을 때가 있다. Link 대신 NavLink를 쓰면, 지금 주소와 맞아떨어지는 링크에 isActive(지금 이 링크가 활성인지)라는 정보를 넘겨준다.


import { NavLink } from "react-router-dom";

<NavLink
to="/about"
style={({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
})}
>
소개
</NavLink>


지금 주소가 /about이면 isActivetrue가 되어 소개 글자가 굵어지고, 다른 페이지로 넘어가면 알아서 원래 굵기로 돌아온다. 현재 위치를 표시하려고 주소를 직접 비교하는 코드를 짤 필요가 없어진다.


없는 주소는 별표로 받아낸다. 사용자가 /asdf 같은 엉뚱한 주소로 들어오면 아무 Route에도 안 걸려 화면이 텅 빈다. 이걸 막으려고 맨 아래에 path="*"를 하나 둔다. 어떤 경로에도 안 맞으면 이게 걸린다.


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


이제 등록 안 된 주소는 전부 NotFound로 흘러가 우리가 만든 404 화면을 보여준다. 정리하면 라우팅은 네 조각이면 대부분 끝난다. 앱을 BrowserRouter로 감싸고, Routes/Route로 주소와 화면을 짝짓고, 이동은 LinkuseNavigate로 하고, 주소 속 값은 useParams로 꺼낸다. 처음엔 a 태그를 쓰고 싶은 손을 참는 게 제일 어려운데, 그 습관만 넘기면 나머지는 금방 손에 붙는다.