들어가기 전
21년 말, react-router-dom을 사용할 땐 v6.3이었다. 현재는 메이저 버전은 동일하지만 v6.4를 기점으로 기존에 없던 새로운 기능이 추가되어 이를 소개하고자 한다. 아래의 사진에서 보는 것과 같이 React Router 공식 문서에서도 v6.4에 대해 무엇이 새로운지 소개하고 있다. React Router 공홈 바로가기
현재 react-router-dom의 최신 버전은 v6.11.1이다. 얼마나 자주 업데이트 되는지 궁금해서 Version History를 찾아봤는데, v6.11.1은 현재기준(23년 5월 16일) 12일 전에 공개가 되었다. 그리고 v6.4.0은 8개월 전인 22년 9월에 공개되었다. 꾸준히 기능이 업데이트되고 있다고 생각한다.
더 재밌는 점은 이전의 v5의 react-router-dom의 다운로드 수가 더 많다는 것이다.
아무튼..! 많은 기능이 추가되었지만 이번장에서는 Picking a Router, RouterProvider, createBrowerRouter, Route에 대해 정리하고자 한다.
1. Router 선택하기
v6.4에는 다음과 같은 새로운 router가 추가되었다.
- createBrowserRouter
- createMemoryRouter
- createHashRouter
- createStaticRouter
이러한 router는 v6.4에서 추가된 새롭게 추가된 data API를 제공한다. data API에는 route.action, route.errorElement, route.lazy, route.loader, 등이 있다. 모두 유용하게 사용될 수 있는 API이기 때문에 공식문서를 한 번 읽어보는 것을 추천한다.
🚨 v6.4의 새로운 router로 업데이트 하기
공식문서에는 v6.4에서 새롭게 추가된 router들 중 하나로 업데이트를 하는 것을 권장하고 있다. 하지만 오늘 기준 React Native에서는 아직 지원하지 않는다고 한다.
그렇다면 어떻게 업데이트를 할 수 있을까? 이미 잘 사용하고 있는 Route를 삭제하고 새로운 route objects 만드는 것은 까다로운 작업일 수 있다. 때문에 createRoutesFormElements를 사용하여 업데이트를 하는 것을 공식문서에서 소개하고 있다.
const router = createBrowserRouter(
createRoutesFromElements(
// 기존의 Route를 createRoutesFormElements에 전달하기
<Route path="/" element={<Root />}>
<Route path="dashboard" element={<Dashboard />} />
{/* ... etc. */}
</Route>
)
);
// 다음과 같이 RouterProvider에 router를 넘겨줘야 한다.
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
다음은 언제 어떤 router를 사용할지에 대한 내용이다.
🛜 Web Project
웹 프로젝트를 진행할 때 createBrowserRouter를 사용하는 것을 권장한다. 다른 대안으로는 createHashRouter가 있는데, 이는 프로젝트 규모 및 특징에 따라 정하면 된다. 이 둘의 특징을 간단히 정리하자면 다음과 같다.
createBrowserRouter
- 동적인 페이지에 적합
- 검색엔진 최적화(SEO)
- github-pages 배포가 까다로움
createHashRouter
- 정적인 페이지에 적합(개인 포트폴리오)
- 검색 엔진으로 읽지 못함(#값 때문에 서버가 읽지 못하고 서버가 페이지의 유무를 모름)
- github-pagers 배포가 간편함
하지만 만약 data API를 사용하지 않는다면 이전 router인 BrowserRouter, HashRouter를 그대로 사용해도 된다.
🛠️ Test
React Router API를 사용한 컴포넌트를 테스트를 하기 위해선 createMemoryRouter가 유용하다. Storybook과 같은 데스트 및 개발 도구에 주로 사용하면 된다.
2. RouterProvider
어떤 Router 객체이든 Router 객체 RouterProvider에 등록되어야 한다.
가장 최상위 루트(보통 index.ts 또는 main.ts)에 다음과 같이 RouterProvider를 추가하면 된다.
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} /> // RouterProvider 추가
</React.StrictMode>
);
router 객체는 다음 파트에서 다루게 된다.
RouterProvider에는 router 속성뿐 아니라 fallbackElement 속성도 있는데 이는 loader가 모두 완료되기 전 실행된다. 조금 더 설명하자면 Route 속성 중 loader는 특정 주소가 랜더 되기 전 실행되는데, loader가 완료되기 전가지 fallbackElement가 실행된다. 즉, 아직 데이터가 불러와지지 않았기 때문에 로딩 화면을 보여준다는 것이다.
예를 들어 다음은 1초 동안 `Loading...` 이 보이고 App 컴포넌트가 랜더링 된다.
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import App from "./App";
const router = createBrowserRouter([
{
path: "",
// 3초 뒤에 finish가 반환된다.
loader: async () => {
return new Promise((res) => {
setTimeout(() => {
return res("finish!");
}, 3000);
});
},
element: <App />,
},
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider
router={router}
fallbackElement={<div>Loading...</div>} //
/>
</React.StrictMode>
);
3. createBrowserRouter
createBrowserRouter는 위에서 설명했다시피 웹 프로젝트를 진행할 때 보통 사용하는 Router이다. createBrowserRouter는 DOM History API를 사용하여 URL를 업데이트하고 history stack를 관리한다.
Type Declaration
function createBrowserRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: FutureConfig;
hydrationData?: HydrationState;
window?: Window;
}
): RemixRouter;
routes
routes은 바로 아래에서 다룰 Route 객체를 요소로 가지는 배열이다. 여러 Route 객체를 가질 수 있는데, 이는 children 속성으로 연결되어 있다.
const router = createBrowserRouter([
{
// route1
path: "/"
element: (
<div>
가장 기본 페이지 입니다.
<Outlet />
</div>
),
children: [
{
// route2
path: "home",
element: <div>홈입니다.</div>,
},
{
// route3
path: "user/:userId",
element: <div>사용자 상세 페이지입니다.</div>,
},
],
},
]);
위의 예시에서 route1, route2, route3 객체가 createBrowserRouter의 routes이다. route2, route3이 route1의 children인 것을 확인할 수 있다.
basename
basename은 root URL를 의미한다. 이는 github-pages로 배포할 때 유용하다.
const router = createBrowserRouter([
{
// route1
path: "/home"
element: (
<div>
가장 기본 페이지 입니다.
<Outlet />
</div>
),
},
{
basename: "/something"
},
]);
route1로 접근하기 위해서 "/something/home"으로 이동해야 한다.
4. Route
Route은 react-router-dom에서 가장 중요하다고 할 수 있다. 다음은 6.4 버전 이후에서 Route를 다루는 방법이다.
Type declaration
interface RouteObject {
path?: string;
index?: boolean;
children?: React.ReactNode;
caseSensitive?: boolean;
id?: string;
loader?: LoaderFunction;
action?: ActionFunction;
element?: React.ReactNode | null;
Component?: React.ComponentType | null;
errorElement?: React.ReactNode | null;
ErrorBoundary?: React.ComponentType | null;
handle?: RouteObject["handle"];
shouldRevalidate?: ShouldRevalidateFunction;
lazy?: LazyRouteFunction<RouteObject>;
}
path
말 그대로 주소이다. URL 패턴에 해당하는 path가 있는 컴포넌트가 랜더링 된다. 여기서 페턴이라고 하는 이유는 URL 주소가 정확히 일치할 수도 있지만 그렇지 않은 경우도 있기 때문이다. 어떤 패턴이 있는지 알아보자.
Dynamic Segments
dynamic segments는 path의 어떤 단락이 `:`으로 시작하는 것을 말한다. dynamic segments은 useParams() API를 통해 불러와 사용할 수 있다.
다음의 예시를 보자.
const router = createBrowserRouter([
{
path: "/user/:userId",
element: <Profill />,
},
])
function Home () {
const { userId } = useParams();
// ...
}
만약, /user/12 주소로 이동했다면 userId는 12가 된다. 즉, :이후의 값은 주소에 따라 바뀌게 된다. 또한 다수의 dynamic segments를 사용할 수 있다.
const router = createBrowserRouter([
{
path: "/user/:userId/:region",
element: <Profill />,
},
])
function Home () {
const { userId, region } = useParams();
// ...
}
/user/23/korea 주소로 이동했다면 userId는 23, region은 korea가 된다.
Optional Segments
optional segments는 path의 어떤 단락이 `?`로 끝나는 것을 말한다. optional라는 단어에서 알 수 있듯이 있을 수도 있고 없을 수도 있다는 것을 의미한다. 이 또한 useParams() API를 통해 불러와 사용할 수 있다.
다음의 예시를 보자.
const router = createBrowserRouter([
{
path: "/user/:userId?",
element: <Profill />,
},
])
function Home () {
const { userId } = useParams() as { userId: string };
// ...
}
위의 예시에서 optional segments를 적용하지 않고 /user로 이동을 했다면 에러가 나타났을 것이다. 하지만 `:userId` 뒤에 `?`를 추가하여 optional segments가 되었기 때문에 `userId`를 생략해도 오류가 나타나지 않고 Profill 컴포넌트가 불러와진다.
dynamic segments, optional segments는 함께, 여러 번 사용할 수 있다.
Splats
catchall 또는 star segments으로도 불리는 splats는 패턴이 `/*`으로 끝나게 되는 경우인데 이때에는 어떠한 주소도 모두 허용한다는 뜻을 가진다. 즉, 앞부분만 만족하면 된다.
다음의 예시를 보자.
const router = createBrowserRouter([
{
path: "/user/:userId/*",
element: <Profill />,
},
])
function Home () {
const { userId, "*": splat } = useParams();
// ...
}
위의 예시에서는 URL 주소가 /user/:userId 패턴만 지켜진다면 뒤에 어떤 주소가 오든 모두 허용한다. 만약 주소가 /user/12/one/two 라면 userId는 12, splat는 one/two이다.
Layout Routes
만약 path를 생략하게 된다면 이는 단지 UI layout를 위한 route가 된다. 다음의 예시를 보자.
const router = createBrowserRouter([
{
// route1
element: (
<div>
가장 기본 페이지 입니다.
<Outlet />
</div>
),
children: [
{
// route2
path: "home",
element: <div>홈입니다.</div>,
},
{
// route3
path: "about",
element: <div>about입니다.</div>,
},
{
// route4
element: (
<div>
사용자 페이지입니다.
<Outlet />
</div>
),
children: [
{
// route5
path: "user/:userId",
element: <div>사용자 상세 페이지입니다.</div>,
},
],
},
],
},
]);
route1의 element를 보자 해당 위치에는 path가 정의되어 있지 않다. 때문에 route1의 element는 단지 UI layout를 위한 route가 된다. 즉, children에 해당하는 주소로 이동하더라도 `가장 기본 페이지입니다.`를 페이지에서 볼 수 있다. 이때 중요한 점은 Outlet를 통해 children이 랜더링 될 위치를 정해야 한다는 것이다.
이번에는 `user/12`으로 이동을 했다고 생각해 보자. 어떤 문장이 보일까? 다음과 같은 문장이 보인다.
- 가장 기본 페이지입니다.
- 사용자 페이지입니다.
- 사용자 상세 페이지입니다.
route4에도 path가 없으므로 UI layout를 위한 route이다. 또 route4도 `가장 기본 페이지입니다.`의 자식이므로 route5에서 정의된 path로 이동할 경우 상위의 Layout Route(route1, route4)를 모두 렌더링 하게 된다.
이를 어떻게 사용할 수 있을까? 특정 페이지마다 공통으로 가지는 Layout이 있을 것이다. 이때 유용하게 사용할 수 있다. 예를 들어 모든 페이지에 Header 컴포넌트가 있다고 하면 Layout Route에 Header 컴포넌트를 랜더링 하고 Outlet를 사용하면 된다. 그리고 만약 로그인, 회원가입 페이지에는 Header 컴포넌트가 없다면 그곳엔 다른 Layout Route를 만들어주면 된다.
또한 path를 생략하지 않고 싶다면 생략하지 않아도 된다. 단, 이렇게 된다면 path를 꼭 포함한 URL 주소를 입력해야 한다.
Index
기본값은 false이다. 하지만 index를 true가 된다면 부모의 route path와 동일한 URL를 가진다. 또한 index router는 부모의 Outlet으로 랜더링 된다. 이는 예제를 살펴보면 쉽게 이해할 수 있다.
다음의 예시를 살펴보자.
const router = createBrowserRouter([
{
// route1
path: "home",
element: (
<div>
홈입니다.
<Outlet />
</div>
),
children: [
{
// route2
index: true,
element: <div>홈에서도 가장 기본입니다.</div>,
},
{
// route3
path: "weather",
element: <div>날씨를 보여주는 홈입니다.</div>,
},
],
},
]);
위의 예시에서 route2의 index는 true이다. 때문에 `/home`으로 이동한다면 route1, route2가 랜더링 된다. 이때 route2의 element는 route1의 Outlet에 위치한다. 즉 index router인 route2가 부모 route인 route1의 Outlet으로 랜더링된다.
- /home으로 이동할 경우: 홈입니다. 홈에서도 가장 기본입니다.
- /weather으로 이동할 경우: 오류
- /home/weather으로 이동할 경우: 홈입니다. 날씨를 보여주는 홈입니다.
children
지금까지 살펴보았다면 children이 무엇인지 감이 왔을 것이다. 중첩된 URL를 사용할 경우 children를 사용하면 된다.
- user
- user/:userId
- user/:userId/:region
user의 자식은 :userId이고 :userId의 자식은 :region이다.
caseSensitive
caseSensitive 속성은 주소가 정확히 일치해야 하는지의 여부를 나타낸다. 즉, caseSensitive가 true라면 대소문자까지 완벽하게 일치한 주소만 허용하고 false라면 대소문자 정도는 달라도 주소를 허용해 준다.
기본값은 false이다. 때문에 caseSensitive를 true로 설정하지 않는다면 path의 값이 "home"은 '/homE'을 허용한다.
하지만 caseSensitive을 true로 설정한다면 오류가 나타난다.
const router = createBrowserRouter([
{
path: "home",
element: (
<div>
홈입니다.
<Outlet />
</div>
),
caseSensitive: true, // 대소문자를 포함하여 home과 완벽히 일치해야 한다.
},
]);
당연히 철자가 틀리다면 caseSensitive와 상관없이 오류가 나타난다.
loader
route의 loader 속성은 route element/component가 렌더링 되기 전 실행된다. 실행된 값은 useLoaderData()을 통해 element에 데이테로 제공한다.
loader 속성은 createBrowserRouter와 같은 data router와 함께 사용해야 한다. 6.4 이전의 router를 사용한다면 loader은 동작하지 않을 것이다.
loader은 위에서 설명한 RouterProvider의 fallbackElement 속성과 연관이 있다. 위에서 예시를 참고 바람.
loader 속성은 6.4 버전에 새롭게 등장한 data API 중 하나이다. 때문에 '장바구니' 미션에 이를 적용하고 싶었으나, 생각처럼 잘 되지 않아 결국엔 다른 방법으로 api 통신을 하였다. 나중엔 성공하리라...
loader은 아직 공부가 더 필요하고 예제도 많이 있어야 하므로 따로 정리를 해보자.
action
loader 속성은 주소에 해당하는 페이지가 렌더 되기 전이라면 action은 된 후, 특정 동작에 의해 실행되는 속성이다. 당연히 함수이다. loader와 똑같이 createBrowerRouter와 같은 data router와 함께 사용해야 한다.
이런 action은 Form, fetcher 또는 submission과 함께 사용된다.
loader와 action은 데이터 통신을 위한 하나의 방법일 뿐, 효율적인 다른 방법이 있다면 다른 것을 쓰도록 하자. 아직까진 뭔가 다른 방법들(useForm, react Query 등등)이 더 매끄럽다고 생각한다.
element / Component
React element를 사용하여 페이지를 렌더링을 하고 싶으면 element를 그렇지 않고 Component만 사용하여 페이지를 렌더링을 하고 싶으면 Component를 사용하면 된다. React Router가 알아서 React element를 만들어 준다.
뭐... 어떤 방법으로 페이지를 렌더링을 하는지에 대한 여부이다. 크게 어려운 것은 없다.
// element를 사용한 방법
const router = createBrowserRouter([
{
path: "",
element: <Home />
},
]);
// Component을 사용한 방법
const router = createBrowserRouter([
{
path: "",
Component: Home
},
]);
errorElement / ErrorBoundary
우선 용어에만 차이가 있을 뿐 둘 다 하는 역할은 같다. error에서 알 수 있듯이 무언가가 잘못되었을 때, 렌더링 되는 것을 정해주는 역할을 한다. ErrorBoundary는 element / component에서 component라고 생각하면 된다.
그러면 무엇이 잘못되었을 때, errorElement 또는 ErrorBoundary가 렌더링이 될까? 다음과 같은 경우이다.
1. 원래 렌더링이 되어야 할 페이지에서 오류가 나타났을 때
2. loader에서 오류가 나타났을 때
3. action에서 오류가 나타났을 때
4. 이상한 주소로 이동했을 때
지금까지 가장 유용하게 사용하고 있는 상황은 4. 이상한 주소로 이동했을 때이다. 최상위 route에 errorElement를 만들어 사용자가 이상한 주소로 이동했을 때, error 페이지를 보여주자.
const router = createBrowserRouter([
{
path: "",
element: <App />,
errorElement: <div>이상한 주소로 이동했네요.</div>,
children: [
// ...
]
},
]);
추가로 route에서 발생한 에러는 useRouteError()에서 확인할 수 있다.;
'⚛️ 리액트' 카테고리의 다른 글
타이머를 구현하면서 마주한 문제와 해결 (1) | 2024.03.06 |
---|---|
State를 구조화하는 다섯 가지 원칙 (0) | 2023.06.05 |
🧭 Paths Alias 사용하기(CRA, Storybook, Jest 설정도 함께) (0) | 2023.04.25 |