본문 바로가기

Web.d

[React][react-router] react-router-dom v6 (Upgrading from v5)

반응형

React v16.8

React Router v6은 React Hook을 많이 사용하므로 React Router v6으로 업그레이드를 시도하기 전에 React 16.8 이상에 있어야 합니다.
좋은 소식은 React Router v5가 React >= 15와 호환된다는 것입니다.
따라서 v5(또는 v4)를 사용 중이라면 라우터 코드를 건드리지 않고도 React를 업그레이드할 수 있어야 합니다.
React 16.8로 업그레이드했으면 앱을 배포해야 합니다.
그런 다음 나중에 다시 돌아와서 중단한 부분부터 다시 시작할 수 있습니다.

React Router v6 makes heavy use of React hooks, so you'll need to be on React 16.8 or greater before attempting the upgrade to React Router v6. The good news is that React Router v5 is compatible with React >= 15, so if you're on v5 (or v4) you should be able to upgrade React without touching any of your router code.
Once you've upgraded to React 16.8, you should deploy your app. Then you can come back later and pick up where you left off.

 

1. 설치방법

yarn add history@5 react-router-dom@6

로 기존의 react-router-dom v5 를 v6로 업데이트 !

package.json 에서 기존 버전을 확인해볼 것 !

 

2. Switch 대신 Routes 를 쓸 때의 변경사항

  • Switch 의 네이밍이 Routes로 변경되었다
  • exact 옵션 삭제
  • component / render 방식 변경 --> element 속성으로 컴포넌트를 불러온다
  • path 를 기존의 path="/Web/:id" 에서 path=":id" 로, 상대경로로 지정
    • 이 외에도, path="." / path=".." 등으로 상대경로를 표현한다
  • Switch 는 알다시피, 경로가 적합한 처음 한 컴포넌트를 찾아주었는데, 여기서 발생하는 (적합한 url의 순서를 뒤로 지정해주어 발생하는) 버그 방지

 

예시

기존 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;

 

  • Switch를 사용
  • exact로 복수의 라우팅을 막음
  • component={} 내에 arrow function을 사용하여 component를 전달

 

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;

 

  • exact 는 더이상 사용하지 않고 여러 라우팅을 매칭하고 싶은 경우 URL 뒤에 * 을 사용
  • component 대신 elemet로 바로 component를 전달

 

3. 중첩 라우팅

방법 1) Router.js 에서 중첩 라우터를 사용하고, 중첩 라우터에서 Outlet 컴포넌트 사용

// Router.js

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Web from "../Pages/Web";
import WebPost from "../Pages/WebPost";

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="web/*" element={<Web />}>
          <Route path=":id" element={<WebPost />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
};

export default Router;
// Web.js

import React from "react";
import { Link, Routes, Route, Outlet } from "react-router-dom";
import WebPost from "./WebPost";

const Web = () => {
  return (
    <div>
      <h1>This is Web</h1>
      <ul>
        <li>
          <Link to="1">Post #1</Link>
        </li>
        <li>
          <Link to="2">Post #2</Link>
        </li>
        <li>
          <Link to="3">Post #3</Link>
        </li>
        <li>
          <Link to="4">Post #4</Link>
        </li>
      </ul>

      <Outlet />
    </div>
  );
};

export default Web;

export default Web;

// --------------------------------------------------------------------------------
// WebPost.js

import React from "react";

const WebPost = () => {
  return <div>This is 포스트</div>;
};

export default WebPost;

 

  • 위 코드와 같이 Router.js에서 자식 태그로 중첩하는 라우터를 기재하고, Web.js에서 Outlet 라이브러리를 통해 가져온다
  • exact 안 쓰는 대신 /* 필수

 

방법 2) 곧바로 기재

// Router.js

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Web from "../Pages/Web";

const Router = () => {
  return (
    <BrowserRouter>
      <Header />
      <Routes>
        <Route path="web/*" element={<Web />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;
// Web.js

import React from "react";
import { Link, Routes, Route} from "react-router-dom";
import WebPost from "./WebPost";

const Web = () => {
  return (
    <div>
      <h1>This is Web</h1>
      <ul>
        <li>
          <Link to="1">Post #1</Link>
        </li>
        <li>
          <Link to="2">Post #2</Link>
        </li>
        <li>
          <Link to="3">Post #3</Link>
        </li>
        <li>
          <Link to="4">Post #4</Link>
        </li>
      </ul>
      <Routes>
        <Route path=":id" element={<WebPost />} />
      </Routes>
    </div>
  );
};

export default Web;

// --------------------------------------------------------------------------------
// WebPost.js

import React from "react";

const WebPost = () => {
  return <div>This is 포스트</div>;
};

export default WebPost;

 

  • 위 코드와 같이 Outlet 없이 곧바로 Routes , Route로 기재할 수 있다

 

4. props 사용은 ?

1) pathname 가져와 styled-components와 결합 - useLocation

const ArticlesCard = ({ article }) => {
  const { id, title, summary, tags, thumbnail, date } = article;
  return (
    <Article>
      <Link to={`article/${id}`} state={article}>
        {thumbnail && (
          <ImageWrapper ratio="56%">
            <Img src={thumbnail} alt="이미지" />
          </ImageWrapper>
        )}
      </Link>
    </Article>
  );
};

// --> `article/${id}` 페이지에서 다음과 같이 state를 받아올 수 있음

const location = useLocation();
const article = location.state;

 

  • Link 태그에서 state props를 넘겨 useLocation()으로 받아올 수 있다

 

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;

 

  • 위 코드와 같이 useLocation Hook을 사용하여 pathname을 받아올 수 있다

 

2) :id path 이용하기 - useParams

// WebPost.js

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

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

export default WebPost;

 

  • 위 코드와 같이 useParams Hook을 이용하여 :id 값을 받아온다

 

5. useRoutes

기존의 react-router-config가 useRoutes라는 Hook으로 변경되었습니다.

패키지를 추가로 설치해야했던 것과는 달리 useRoutes라는 훅으로 routes를 구성할 수 있게 되었습니다.

 

  • react-router-config의 기본 사용child의 child도 정의할 수 있다. (아래 예시에서 GrandChild가 해당) 
    • route.routes는 렌더링될 자식 컴포넌트들을 전달한다.
    • extraProps는 default로 빈객체를 가지며, 추가할 props가 있을 때 객체로 전달한다.
    • switchProps는 default로 빈 객체를 가지며, 변경할 props가 있을 때 객체로 전달한다.
  • {renderRoutes(route.routes, extraProps={}, switchProps={})}
  • 자식 컴포넌트들을 더 렌더링해야하는 경우에 renderRoutes를 사용하는데, 이 때 전달되는 파라미터는 아래와 같이 3가지이다.
  • routes라는 배열에 사용할 컴포넌트를 할당하여 사용한다.

 

// react-router-config
// yarn add react-router-config로 설치 후 사용
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")
);

 

v6부터는 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;
}

 

위에서 언급한 Routes와 요사한 것으로 보이는데, 실제로 <Routes>는 useRoutes를 감싼 wrapper라고 공식문서에서 언급하고 있습니다.

공식문서에서는 <Routes> 와 useRoutes 모두를 권장하며, 둘 중 자신이 더 선호하는 것을 사용하면 됩니다.

혹, useRoutes는 데이터를 불러오거나 서버사이드 렌더링을 위한 로직이 필요하다면 낮은 수준의 matchRoutes 기능을 사용할 수 있다

 

React Router says, "Honestly, we like and use them both."

 

6. useHistory대신 useNavigate

useHistory는 Link태그와 유사한 작업을 할 수 있게 도와주는 훅이(었)다

아래와 같이 useHistory를 사용하면, URL의 끝에 /home을 추가함으로써 페이지 이동이 가능했습니다.

 

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부터는 navigate라는 명칭을 사용한다

history.push와 history.replace 모두 navigate라는 명칭으로 사용한다

이상의 코드는 다음과 같이 변경된다

 

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

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

 

만약 replace 기능이 필요하다면, navigate(to, { replace: true })의 형태로 사용할 수 있다

state를 사용한다면 navigate(to, { state }) 의 형태로 사용할 수 있다

여기서 to는 <Link>에서 사용한 to=''와 동일한 내용을 넣으면 된다.

 

useHistory의 기능 중 { go, goBack, goForward }는 각각 해당 위치로, 이전으로, 다음으로 의 역할을 수행해왔는데, 이 부분도 navigate로 통일하고 index를 넣음으로써 해결합니다.

 

// v5
import { useHistory } from "react-router-dom";

function App() {
  const { go, goBack, goForward } = useHistory();

  return (
    <>
      <button onClick={() => go(-2)}>
        Go 2 pages back
      </button>
      <button onClick={goBack}>Go back</button>
      <button onClick={goForward}>Go forward</button>
      <button onClick={() => go(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}

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

function App() {
  const navigate = useNavigate();

  return (
    <>
      <button onClick={() => navigate(-2)}>
        Go 2 pages back
      </button>
      <button onClick={() => navigate(-1)}>Go back</button>
      <button onClick={() => navigate(1)}>
        Go forward
      </button>
      <button onClick={() => navigate(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}

 

이러한 변화의 가장 주된 이유는 React suspense와의 호환성을 더 높이기 위함입니다.

아직 이전의 클릭이 로딩 중인 상태에서 다른 라우트로의 링크를 클릭한 경우와 같이 pending이 충돌되는 경우에 더 부드러운 경험(smoother experience)를 제공할 수 있습니다.

navigate API는 이전의 pending 작업을 알아차리고 해당 내용을 history stack에 PUSH하는 것이 아니라 REPLACE함으로써 로드되지 않은 기록으로 끝나지 않도록 합니다.

 

7. 이외의 변경사항

  • <NavLink exact>가 아닌 <NavLink end>로!
    • 다른 라이브러리의 일반적인 관행과 더 일치하도록 prop의 이름이 renaming 되었다
  • activeClassName, activeStyle props 제거
    • 대신 style과 className에 함수를 전달할 수 있게 되었다
    <NavLink
      to="/messages"
    ~~- style={{ color: 'blue' }}~~
    ~~- activeStyle={{ color: 'green' }}~~
    + style={({ isActive }) => ({ color: isActive ? 'green' : 'blue' })}
    >
      Messages
    </NavLink>
    
    <NavLink
      to="/messages"
    ~~- className="nav-link"
    - activeClassName="activated"~~
    + className={({ isActive }) => "nav-link" + (isActive ? " activated" : "")}
    >
      Messages
    </NavLink>
    
  • import { StaticRouter } from "react-router-dom/server"
    • StaticRouter가 react-router-dom에서 react-router-dom/server로 번들이 이동되었습니다.
  • <Link>의 component prop 제거
    • 이제 더이상 <Link> 에서 component prop을 지원하지 않습니다.

 

자세한 내용은 공식문서로!!

반응형