*참고로 아래 목록은 모두 Function이다. { } 로 불러오는 것 보면 알 수 있다.
0. <Router>
Router는 props로 꽤 많은 기능을 포함하고 있는 것을 알 수 있다. Router 컴포넌트는 기본적으로 리액트의 context provider 기능을 포함시키고 있다. 그래서 routing 정보를 대부분의 자식 요소와 공유하게 된다.
declare function Router(
props: RouterProps
): React.ReactElement | null;
interface RouterProps {
basename?: string;
children?: React.ReactNode;
location: Partial<Location> | string;
navigationType?: NavigationType;
navigator: Navigator;
static?: boolean;
}
출처 : 공식문서 https://reactrouter.com/kr/main/router-components/router
1. BrowserRouter
이 보다는 아래 2번 사용을 권장. React Router v6에서는 미지원
import React from "react";
import Router from "./Router";
const App = () => {
return (
<div>
<Router />
</div>
);
};
export default App;
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Header from "./components/Header";
import About from "./screens/About";
import Home from "./screens/Home";
const Router = () => {
return (
<BrowserRouter>
<Header />
<Routes> // Switch 대신 사용
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
특별히 다른 것은 없다. v6 되면서 위와 같이 변경되었다. 중요한 것은 2번이다. 계속 보자.
2. createBrowserRouter
BrowserRouter (이하 BR) 보다는 createBrowserRouter(이하 cBR)의 사용이 좀 더 권장된다. 그 이유는 좀 더 순차적인 구조이기 때문이다.
BR의 경우, 스위치에 좀 더 가깝게 작성이 된 느낌이라면, cBR은 좀 더 분명하게 Root에 children으로 소속되는 것을 명시적으로 적혀있다.
App.tsx -> Root.tsx (안바꾸어도 관계 없지만 가독성을 위해 변경)
공식문서 설명으로는 DOM History API를 사용하여 URL을 Update하고 History Stack을 관리한다고 한다.
import React from "react";
import { Outlet } from "react-router-dom";
import Header from "./components/Header";
const Root = () => {
return (
<div>
<Header /> {/* Root를 렌더하고 있는 것을 알 수 있다. */}
<Outlet /> {/*경로에 따라 Component Element를 바꾸어주는 역할을 한다. */}
</div>
);
};
export default Root;
import React from "react";
import { createBrowserRouter } from "react-router-dom";
import About from "./screens/About";
import Home from "./screens/Home";
import Root from "./Root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // to Render under Root Element
children: [
{ path: "", element: <Home /> },
{ path: "about", element: <About /> },
],
},
]);
export default router;
그러나, 공식문서를 확인해보니, 이런 방법도 있는 듯.
createRoutesFromElements 라는 Utility가 있다.
이 것을 사용하면, 기존에 작성하던 element 작성 방식으로 사용가능한 모양이다.
객체 방식이 낯설면 이렇게 하는 것도 괜찮은 것 같다. 😊
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
// You can do this:
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="about" element={<About />} />
</Route>
)
);
// Instead of this:
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "dashboard",
element: <Dashboard />,
},
{
path: "about",
element: <About />,
},
],
},
]);
*basename (BaseURL) 설정 방법 : https://bit.ly/3Fr3vt0
3. Route
3-1. errorElement
Home Component에 아래와 같이 하면, TypeScript가 에러를 뱉어낸다.
import React from "react";
const Home = () => {
const users: any = [];
return <h1>{users[0].name}</h1>;
};
export default Home;
import React from "react";
import { createBrowserRouter } from "react-router-dom";
import About from "./screens/About";
import Home from "./screens/Home";
import Root from "./Root";
import NotFound from "./screens/NotFound";
import ErrorComponent from "./components/ErrorComponent";
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // to Render under Root Element
children: [
{ path: "", element: <Home />, errorElement: <ErrorComponent /> },
{ path: "about", element: <About /> },
],
errorElement: <NotFound />,
},
]);
export default router;
각 경로의 Component(이하 Comp)에 문제가 있을 때, 각기 다르게 ErrorComponent 조작이 가능하다. 이렇게 하면 Error가 모든 parents Component까지 번지는 Bubbling을 방지할 수 있다. 일종의 Error 경계선을 만든 것이다.
(굉장히 Cool 한 기능인듯.. v5에는 없었다고 한다...사용했던 경험으로는 그랬던 것 같다. FE는 아직도 발전 중인 거구나...ㅠㅜ)
import React from "react";
const ErrorComponent = () => {
return <h1>ErrorComponent crashed</h1>;
};
export default ErrorComponent;
import React from "react";
const NotFound = () => {
return <h1>404 Not Found</h1>;
};
export default NotFound;
4. useNavigate (useHistory 대신 사용)
v5 버전의 history.push 같은 기능이다.
사용자를 코드적으로 강제적으로 다른 링로 이동시킬 때 사용한다. 굉장히 편리해졌다.
import React from "react";
import { Link, useNavigate } from "react-router-dom";
const Header = () => {
const navigate = useNavigate();
const onAboutClick = () => {
navigate("/about"); // 이렇게 가능하다.
};
return (
<header>
<ul>
<li>
<Link to={"/"}>Home</Link>
</li>
<li>
<button onClick={onAboutClick}>About</button>
</li>
<li>
<button onClick={() => navigate("/")}>Home</button> // 이렇게 익명함수로 사용도 당연히 가능
</li>
</ul>
</header>
);
};
export default Header;
5. useParams
Router 에 아래와 같이 설정하고
import React from "react";
import { createBrowserRouter } from "react-router-dom";
import About from "./screens/About";
import Home from "./screens/Home";
import Root from "./Root";
import NotFound from "./screens/NotFound";
import ErrorComponent from "./components/ErrorComponent";
import User from "./screens/users/User";
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // to Render under Root Element
children: [
{ path: "", element: <Home />, errorElement: <ErrorComponent /> },
{ path: "about", element: <About /> },
{ path: "users/:userID", element: <User /> },
// { path: "users", children: [{ path: "userId" }], element: <User /> },
// 이렇게 하면, /users 경로에 접속 시에 화면이 노출되기 때문.
//그러나 사용할 일이 없음. 그렇기에 이런방식으로 하지 않음
],
errorElement: <NotFound />,
},
]);
export default router;
아래와 같이 작성할 수 있다.
import React from "react";
import { useParams } from "react-router-dom";
import { users } from "../../db";
const User = () => {
const { userId } = useParams();
console.log("userId ", userId);
return (
<>
<h1>User ID : {userId}</h1>
<h1>User Name : {users[Number(userId) - 1].name}</h1>
</>
);
};
export default User;
// 나중에 api를 연동한다고 가정한 파일 - db.ts
export const users = [
{ id: 1, name: "park" },
{ id: 2, name: "song" },
];
6. Outlet (중첩(서브) 라우팅 처리, 공통 컴포넌트 사용)
children 컴포넌트를 렌더해주는 역할을 한다.
import React from "react";
import { Link, Outlet, useParams } from "react-router-dom";
import { users } from "../../db";
const User = () => {
const { userId } = useParams();
console.log("userId ", userId);
return (
<div>
<h1>User ID : {userId}</h1>
<h1>User Name : {users[Number(userId) - 1].name}</h1>
<hr />
<Link to={"followers"}>See Followers</Link> // useRouteMatch 대신 상대경로 사용
{/* to 경로에 "/"를 빼고 쓰면 url 뒤에 바로 추가되는 형태로 동작 */}
<Outlet />
</div>
);
};
export default User;
import React from "react";
import { Link, Outlet, useParams } from "react-router-dom";
import { users } from "../../db";
const User = () => {
const { userId } = useParams();
console.log("userId ", userId);
return (
<div>
<h1>User ID : {userId}</h1>
<h1>User Name : {users[Number(userId) - 1].name}</h1>
<hr />
<Link to={"followers"}>See Followers</Link>
{/* to 경로에 "/"를 빼고 쓰면 url 뒤에 바로 추가되는 형태로 동작 */}
<Outlet />
</div>
);
};
export default User;
// Router.tsx
import React from "react";
import { createBrowserRouter } from "react-router-dom";
import About from "./screens/About";
import Home from "./screens/Home";
import Root from "./Root";
import NotFound from "./screens/NotFound";
import ErrorComponent from "./components/ErrorComponent";
import User from "./screens/users/User";
import Followers from "./screens/users/Followers";
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // to Render under Root Element
children: [
{ path: "", element: <Home />, errorElement: <ErrorComponent /> },
{ path: "about", element: <About /> },
{
path: "users/:userId",
element: <User />,
children: [{ path: "followers", element: <Followers /> }], // Followers
},
],
errorElement: <NotFound />,
},
]);
export default router;
//Follwers.tsx
import React from "react";
const Followers = () => {
return <h1>Followers</h1>;
};
export default Followers;
위에서는 렌더만 추가하였다.
추가적으로 followers 부분에 데이터를 내려주고 싶으면 아래 useOutletContext를 활용한다.
7. useOutletContext
Outlet에 context라는 props를 이용해서 데이터를 children에 내려줄 수 있다. 우선 아래와 같이 작성한다.
Context 라는 것만 이해하면, 특별한 건 없기 때문에 코드로 설명을 마친다.
Context에 대한 이해는 공식문서를 참조하길 바란다. (React Context 공식문서)
너무 복잡도가 커지거나 전역적으로 사용해야할 것은
Recoil과 같은 전역상태관리를 사용하는 게 더 바람직한 경우도 있을 것이다. 이건 추후에 더 후술하겠다.
import React from "react";
import { Link, Outlet, useParams } from "react-router-dom";
import { users } from "../../db";
const User = () => {
const { userId } = useParams();
return (
<div>
<h1>User ID : {userId}</h1>
<h1>User Name : {users[Number(userId) - 1].name}</h1>
<hr />
<Link to={"followers"}>See Followers</Link>
<Outlet context={{ nameOfMyUser: users[Number(userId) - 1].name }} /> // context 추가
</div>
);
};
export default User;
children 컴포넌트는 아래와 같이 작성한다.
import React from "react";
import { useOutletContext } from "react-router-dom";
interface IFollowersContext {
nameOfMyUser: string;
}
const Followers = () => {
const { nameOfMyUser } = useOutletContext<IFollowersContext>();
// 구조분해할당 방식으로 작성-> {변수} = 함수명<타입>();
console.log("nameOfMyUser", nameOfMyUser, typeof nameOfMyUser);
return <h1>Here are {nameOfMyUser}'s Followers</h1>;
};
export default Followers;
추가 포스팅 : Outlet으로 props 전달하기 (feat. useOutletContext)
8. useMatch (경로가 일치하는지 확인)
위와 같이 중첩 라우팅을 구현했을 때, 특정 styling을 변경할 때 사용한다.
아래와 같이 코드를 작성해서 경로의 일치 여부를 check 한다.
interface RouteParams extends Params {
coinID: string;
}
...
const { coinID } = useParams<RouteParams>();
const matchedPrice = useMatch(`${coinID}/price`); // match 된 것이 없으면 return null;
const matchedChart = useMatch(`${coinID}/chart`);
// Render
<div
style={{
border: "1px solid white",
textAlign: "center",
padding: "20px",
color: matchedChart !== null ? theme.accentColor : theme.textColor,
}}>
<Link to="chart">Chart</Link>
</div>
9. useSearchParams (useLocation 대신 사용)
Function 들의 method는 MDN에서 확인할 수 있다. (링크)
import { userInfo } from "os";
import React from "react";
import { users } from "../db";
import { Link, useSearchParams } from "react-router-dom";
const Home = () => {
const [readSearchParams, setSearchParams] = useSearchParams();
console.log(readSearchParams.has("geo")); // true
console.log(readSearchParams.get("geo")); // 123 출력
return (
<div>
<ul>
{users.map(user => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
</div>
);
};
export default Home;
여기서 우리가 주목할 지점은 아래 구문이 아닐까 싶다.
const [readSearchParams, setSearchParams] = useSearchParams();
이 것을 보면 useState와 동일한 컨셉을 가지고 있는 것을 알 수 있다.
useState처럼 다른 값으로 업데이트하게 되면, 새로 Rendering 하게 될 것이다.
공식문서에도 functional update를 지원한다고 되어 있다. (Functional Updates 설명 - 리액트 공식문서)
useSearchParams에 대한 좀 더 자세한 내용은 공식문서를 참조하자 (링크)
10. RouterProvider
앱을 렌더링하고 나머지 API들을 가능하게 하고자 아래와 같이 작성이 필요하다.
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "dashboard",
element: <Dashboard />,
},
{
path: "about",
element: <About />,
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<RouterProvider
router={router}
fallbackElement={<BigSpinner />}
/>
);
공식문서 보니까 fallbackElement도 있던데, 왜 있는지는 정확히 잘 모르겠다.
"If you are not server rendering your app, DataBrowserRouter will initiate all matching route loaders when it mounts. During this time, you can provide a fallbackElement to give the user some indication that the app is working."
이런 구절 있는 거 보면 SSR에 관련된 게 아닐까 싶다.
Conclusion
나열한 것 외에도 업데이트 된 기능이 너무 많다.
주요 컨셉이 React Router가 fetch, form 같은 기능들을 많이 갖고 오려는 성향이 엿보인다. 노마드 코더 니코 선생 말로는 Remix 라는 라이브러리를 만든 사람이 React Router도 만든 것이라고 한다. (Remix 공식)
그래서 그런지 굉장히 많은 컨셉을 Remix에서 차용한 것을 알 수 있다. loader, action,...등 그 외에도 꽤 많다.
좀 더 알아보면 좋겠지만, 그러다보면 이제 프로젝트를 시작하는 것이 무리겠지ㅎ
이쯤하고 이제 고마 쌔리 달려야겠다ㅎㅎ
참고
1. 노마드코더
2. React Router 공식문서 https://reactrouter.com/kr/main
3.React Router v6 업데이트 정리 https://bit.ly/3Fy2aSw
4. 리액트 공식문서 - Context https://ko.reactjs.org/docs/context.html
'React & Library > Router' 카테고리의 다른 글
[React / Router] Link 에서 state로 값 넘기기 (0) | 2022.12.16 |
---|---|
[React / Router] url 마지막에 있는 slash 없애기 (0) | 2022.12.13 |
[React / Router] v6 - creatBrowserRouter basename (baseURL) 설정 방법 (0) | 2022.12.13 |
[React Router] 특정 경로 접속 시, 다른 경로로 변경 (리다이렉트) 하기 (0) | 2021.08.02 |
댓글