Front-End(Web)/React - 프레임워크(React, Next)

[React Router/lib.] React Router v6

ttaeng_99 2021. 12. 14. 22:55
반응형

오랜만에 React 학습을 진행하는데, React Router가 6버전이 출시되었다고 한다.

기존에 사용하던 문법들이 일부 수정된 것을 감안하여, 이를 한번 훑고자 짧게나마 포스팅을 적는다.


💙 개요

React Router 6버전은, 기존의 5버전에 비해 React 최신문법에 걸맞도록 업데이트 되었다고 공식문서는 소개한다.

특히, React Hooks가 적용되었기에, 이를 사용하려면 React v16.8 이상을 우선 설치해야하며, 5 -> 6버전으로 마이그레이션을 진행하면 된다.

 

React Router 6버전은 번들 사이즈가 5버전에 비해 약 70%가 감소하였으며, 이는 App 빌드시 큰 이점이 될 것이다.

출처 : https://blog.woolta.com/categories/1/posts/211

 

💙 주요 변경사항

 

1. <Switch> ➡️ <Routes> 로 변경

  • <Route> 요소들을 포함하는 <Routes>로 Wrapper 명칭변경
  • <Route>는 component 및 render에 화살표 함수를 사용하지 않고, element 속성에 JSX 컴포넌트를 전달준다.
  • exact 옵션이 삭제되었다. 기본적으로 exact가 적용되며, 모든 하위 path에서 라우팅을 하고 싶다면 path 끝에 *을 붙인다.
  • path에 ":id"와 같은 하위경로, "." 및 ".." 와 같은 상대경로도 지정이 가능하다.
// 기존(v5)
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Home from "./pages/Home";
import Write from "./pages/Write";

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/" component={() => <Home />} />
        <Route exact path="/write" component={() => <Write />} />
        <Route component={() => <div>Page Not Found</div>} />
      </Switch>
    </BrowserRouter>
  );
}

export default App;
// 변경(v6)
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Main, Page1, Page2, NotFound } from "../pages";
import { Header } from ".";

const Router = () => {
  return (
    <BrowserRouter>
      <Header />
      <Routes>
        <Route path="/" element={<Main />} />
        <Route path="/page1/*" element={<Page1 />} />
        <Route path="/page2/*" element={<Page2 />} />
        <Route path="/*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

 

그래서, 기존에 App.tsx를 Routes.tsx로 명명하던 관례를 원복하면 된다.

 

 

2. 간편해진 중첩 라우팅

기존에는 중첩 라우팅을 위해서는 페이지, 컨테이너 컴포넌트 각각을 <Switch>로 랩핑하고 내부에 <Route> 컴포넌트를 돌려야 했다.

특히, 컨테이너 컴포넌트는 useRouteMatch() Hooks를 통해 path를 받고 여기에 하위path를 연결시켜야만 했다.

 

6버전에서는 페이지 단에서 모든 경로를 중첩하고, 중첩영역 렌더링 요소는 <Outlet> 컴포넌트로 그려서 이중작업을 방지할 수 있다.

(이렇게 하면, 통상 App.tsx 최상단의 <Routes> 내에서 모든 라우팅 구조를 중첩할 수 있어 직관적이게 된다.)

// 기존(v5)
import { BrowserRouter, Switch, Route, Link, useRouteMatch } from 'react-router-dom';

// App(루트) Component
function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/user" component={User} />
      </Switch>
    </BrowserRouter>
  );
}
// Page Component
function User() {
  const { path } = useRouteMatch();
  return (
    <div>
      <Switch>
        <Route path={`${path}/detail`}>
          <UserDetail />
        </Route>
      </Switch>
    </div>
  );
}
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';

// App(루트) Component : Route 집약
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='user' element={<User />} >
          <Route path='detail' element={<UserDetail />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}
// Page Component : <Outlet> 활용
function User() {
  return (
    <>
      <Outlet />
    </>
  )
}

 

3. useLocation, useParams - Router Parameter 사용

Routing을 통해 진입한 페이지의 파라미터를 참조하기 위한 HooksuseLocation과 useParams를 제공한다.

useLocation은 pathname(정적 경로), useParams는 :id(동적 경로 혹은 파라미터)를 참조하는데 사용된다.

// useLocaiton
import React from "react";
import { Link, useLocation } from "react-router-dom";
import styled from "styled-components";

const HeaderWrapper = styled("header")`
  margin-bottom: 30px;
`;
const List = styled("ul")`
  display: flex;
`;
const Item = styled("li")`
  margin-right: 20px;
  text-transform: uppercase;
  font-weight: 600;
  color: ${(props) => (props.selected ? "white" : "black")};
  background-color: ${(props) => (props.selected ? "#f1c40f" : "white")};
`;

const Header = () => {
  const { pathname } = useLocation();
  return (
    <HeaderWrapper>
      {/* header 태그 */}
      <List>
        {/* ul 태그 */}
        <Item selected={pathname.startsWith("/web")}>
          {/* li 태그 */}
          <Link to="/web">Go to Web</Link>
        </Item>
        <Item selected={pathname === "/design"}>
          <Link to="/design">Go to Design</Link>
        </Item>
        <Item selected={pathname === "/server"}>
          <Link to="/server">Go to Server</Link>
        </Item>
      </List>
    </HeaderWrapper>
  );
};

export default Header;
// useParams
import React from "react";
import { useParams } from "react-router";

const WebPost = () => {
  const { id } = useParams();
  return <div>#{id}번째 포스트</div>;
};

export default WebPost;

 

4. useRoutes

기존의 react-router-configuseRoutes Hooks 으로 대체되었다. (패키지 설치가 불필요해짐)

기존엔, routes 배열을 정의하여 여기에 컴포넌트를 배치하고, renderRoutes 메서드로 Root(App) 컴포넌트에 반영할 수 있다.

// 기존(v5)
import { renderRoutes } from "react-router-config";

const routes = [
  {
    component: Root,
    routes: [
      {
        path: "/",
        exact: true,
        component: Home
      },
      {
        path: "/child/:id",
        component: Child,
        routes: [
          {
            path: "/child/:id/grand-child",
            component: GrandChild
          }
        ]
      }
    ]
  }
];

const Root = ({ route }) => (
  <div>
    <h1>Root</h1>
    {/* 자식 라우트들이 렌더할 수 있도록  renderRoutes 실행 */}
    {renderRoutes(route.routes)}
  </div>
);

const Home = ({ route }) => (
  <div>
    <h2>Home</h2>
  </div>
);

const Child = ({ route }) => (
  <div>
    <h2>Child</h2>
    {/*  renderRoutes가 없으면 자식들은 렌더되지 않음  */}
    {renderRoutes(route.routes)}
  </div>
);

const GrandChild = ({ someProp }) => (
  <div>
    <h3>Grand Child</h3>
    <div>{someProp}</div>
  </div>
);

ReactDOM.render(
  <BrowserRouter>
    {/* renderRoutes에 가장 처음 정의했던 routes 자체를 뿌려줌으로써 차례로 렌더링될 수 있도록 함 */}
    {renderRoutes(routes)}
  </BrowserRouter>,
  document.getElementById("root")
);

 

이 패턴을, 버전6부터는 useRoutes Hooks를 통해 직접 Root(App)에 라우터를 배치할 수 있다.

<Route>의 중첩 라우팅, useRoutes 둘 다 공식문서에서 권장하는 방법으로 기호에 따라 사용하면 된다.

function App() {
  let element = useRoutes([
		// Route에서 사용하는 props의 요소들과 동일
    { path: "/", element: <Home /> },
    { path: "dashboard", element: <Dashboard /> },
    {
      path: "invoices",
      element: <Invoices />,
			// 중첩 라우트의 경우도 Route에서와 같이 children이라는 property를 사용
      children: [
        { path: ":id", element: <Invoice /> },
        { path: "sent", element: <SentInvoices /> }
      ]
    },
		// NotFound 페이지는 다음과 같이 구현할 수 있음
    { path: "*", element: <NotFound /> }
  ]);
	
	// element를 return함으로써 적절한 계층으로 구성된 element가 렌더링 될 수 있도록 함
  return element;
}

 

5. useHistory ➡️ useNavigate

버전5까지 history.push, history.replace 를 했던 useHistory HooksuseNavigate로 대체되었다.

// 기존(v5)
import { useHistory } from "react-router-dom";

function App() {
  let history = useHistory();
  function handleClick() {
	    history.push("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}
// 변경(v6)
import { useNavigate } from "react-router-dom";

function App() {
  let navigate = useNavigate();
  function handleClick() {
    navigate("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}

 

6. 이외 변경사항

  • <NavLink exact> 가 <NavLink end> 로 변경 (현재 활성화된 라우터)
  • props 명칭들의 renaming
  • activeClassNAme, activeStyle props 제거 => style, className에 함수 사용 가능
  • StaticRouter가 react-router-dom/server 번들로 이동  (SSR 라우팅)
  • <Link>의 component prop 제거

React를 오랜만에 사용하면서, Router 설정에서 오류 메세지가 많이 발생하였고 그 원인이 버전 변경에 걸맞는 문법이 아니어서였다.

기능들보다는 컴포넌트나 props 명칭들 위주로 변경되었으며, 중첩 라우팅과 useRoutes() 가 직관적으로 개선되어 Router 구조를 가시화하는데에는 큰 진전이 있었다고 느껴진다!

 

[출처]

- [React Router] 공식문서 : https://reactrouter.com/docs/en/v6/upgrading/v5  

- [v6 변경사항] soryeongk 님의 블로그 : https://velog.io/@soryeongk/ReactRouterDomV6

- [v6 변경사항] woolta 님의 블로그 : https://blog.woolta.com/categories/1/posts/211  

 

반응형