react-router v6 변경점 정리

PeaceSong·2022년 1월 23일
3
post-thumbnail
post-custom-banner

0. Intro

2021년 11월 3일에 react-router 6.0.0이 릴리즈되었다. 메이저 버전이 바뀐 것임을 감안하더라도 굉장히 많은 것들이 바뀌었는데, 그 중에 우리 팀의 가장 큰 주목을 끈 것은 번들링 사이즈가 70% 가량 줄어들었다는 것이다. 빌드 사이즈를 줄이려던 우리 팀에게 이 부분은 매력적으로 보였고, v5에서 v6으로 업그레이드하기 위해 사전 조사를 하는 업무를 내가 맡게 되었다. 아직 작업을 시작하지는 않았지만 변경점을 정리한 것을 이슈로만 두기는 아까워 여기에도 올린다.

1. Routes replaces Switch

  • 라우트 매치 알고리즘이 개선되었다. 기존의 Switch 컴포넌트는 경로에 매치되는 첫 번째 라우트를 렌더한다.
// react-router v5
import { Switch, Route } from 'react-router-dom'

const App = () => {
  return (
    <Switch>
      <Route path="/products" exact component={Products} />
      <Route path="/products/edit" component={ProductEdit} />
      <Route path="/products/:productId" component={ProductDetail} />
    </Switch>
  )
}

위 코드에서 /products/edit 주소를 매치한다면, 라우터가 마주치는 첫 번째 라우트인 ProductEdit 라우트를 매치하여 렌더할 것이다. 만약 이 순서가 뒤바뀌어

<Route path="/products/:productId" component={ProductDetail} />
<Route path="/products/edit" component={ProductEdit} />

위와 같이 라우트가 배치되어있다면 /products/edit 주소는 { productId: "edit" } 패러미터를 들고 ProductDetail 라우트에 매치될 것이다. 따라서 Switch 컴포넌트 안의 Route 컴포넌트 간의 순서가 중요하다.

하지만 v6에서 Switch 컴포넌트를 대체하는 Routes 컴포넌트는 자체적으로 주소에 가장 알맞은 라우트를 매치하는 알고리즘을 탑재하고 있다. 따라서 라우트의 순서는 중요하지 않다.

// react-router v6
import { Routes, Route } from 'react-router-dom'

const App = () => {
  return (
    <Routes>
      <Route path="/products" element={<Products />} />
      <Route path="/products/:productId" element={<ProductDetail />} />
      <Route path="/products/edit" element={<ProductEdit />} />
    </Routes>
  )
}

v6에서는 위 코드처럼 ProductEditProductDetail 라우트가 배치되어 있어도, /products/edit 주소를 가장 먼저 배치되어 있는 ProductDetail 라우트에 매치하는 것이 아니라 가장 알맞은 ProductEdit 라우트에 매치한다. ProductEdit 라우트는 더 구체적으로 /products/edit 주소에 매치되는 라우트이기 때문이다. 이 때문에 잘못된 라우트 순서에서 기인하는 오동작을 예방할 수 있다.

  • 기본 경로 매치 동작이 exact match로 변경되었다. 기존 react-router는 경로를 start with 방식으로 매치하였지만, v6부터는 full match 방식으로 매치하게 된다.
// react-router v5
<Route path="/products" component={Products} />
<Route path="/products/edit" component={ProductEdit} />

v5 버전의 코드에서 /products/edit 주소가 들어온다면 ProductEdit 라우트가 아닌 Products 주소가 매치되었다. 이를 방지하기 위해서는 exact props를 넘겨주었어야 했다.

// react-router v5
<Route path="/products" exact component={Products} />

하지만 v6에서는 exact match가 기본이므로 아래와 같이 작성하게 된다.

// react-router v6
<Route path="/products" element={<Products />} />
<Route path="/products/edit" element={<ProductEdit />} />

만약 v5의 버전처럼 start with 방식으로 매치하고 싶다면 /*를 path의 뒤에 붙여주면 된다.

<Route path="/products/*" element={<Products />} />
<Route path="/products/edit" element={<ProductEdit />} />
  • 이미 위의 코드들에도 반영이 되었지만, v6에서는 component props 대신, suspense fallback처럼 element props를 사용한다.
// react-router v5
<Route path="/products" component={Products} />

// react-router v6
<Route path="/products" component={<Products />} />

따라서 props를 정의해서 내려주기 위해 renderchildren props로 내려줄 필요가 없어졌다. 또한 Route 컴포넌트에서 children props는 후술할 nested route를 위해 예약되어있어 사용할 수 없기도 하다.

2. New navigation API

  • useHistory hook과 Redirect 컴포넌트는 이제 새로운 useNavigation hook과 Navigate 컴포넌트로 대체된다.

2.1. useNavigation hook

  • v5에서 imperative하게 이동하기 위해서는 useHistory hook으로 가져온 history 객체의 메소드를 호출하였다.
// react-router v5
import { useHistory } from 'react-router-dom'

const App = () => {
  const history = useHistory()
  const handleClick = () => history.push('/home')
  
  return (
    <div>
      <button onClick={handleClick}>
        Go to Home
      </button>
    </div>
  )
}

v6에서는 useNavigate hook으로 가져온 navigate 함수를 직접적으로 호출하게 된다.

// react-router v6
import { useNavigate } from 'react-router-dom'

const App = () => {
  const navigate = useNavigate()
  const handleClick = () => navigate('/home')
  
  return (
    <div>
      <button onClick={handleClick}>
        Go to Home
      </button>
    </div>
  )
}

history 객체의 .go, .goForward, .goBack 메소드의 경우 아래와 같이 navigate 함수에 정수 인자를 전달하는 방식으로 대체된다.

navigate(N) // history.go(N)
navigate(1) // history.goForward()
navigate(-1) // history.goBack()
navigate(2) // go 2 pages forward
navigate(-2) // go 2 pages backward
  • navigate 함수에 path 값을 넣을 때, 주소 이동 간 보존해야 할 state가 있다면 두 번째 인자(optional)에 state 필드로 state 객체를 전달한다.
navigate('/home', { state })
  • navigate 함수는 기본적으로 history push 방식으로 동작한다. 만약 history replace 방식으로 사용하고 싶다면 두 번째 인자를 넣을 때 replace: true 필드를 같이 전달한다.
navigate('/home', { state, replace: true })

2.2. Navigate component

  • v5에서는 Redirect 컴포넌트로 declarative하게 이동하였다.
// react-router v5
import { Redirect } from 'react-router-dom'

const LoginPage = () => {
  const user = useLoginContext()
  
  return (
    <>
      { user && <Redirect to="/home" /> }
      <div>
        ...
      </div>
    </>
  )
}

v6에서는 Navigate 컴포넌트를 사용한다.

// react-router v6
import { Navigate } from 'react-router-dom'

const LoginPage = () => {
  const user = useLoginContext()
  
  return (
    <>
      { user && <Navigate to="/home" replace /> }
      <div>
        ...
      </div>
    </>
  )
}

v6의 Navigate 컴포넌트는 useNavigate hook을 래핑한 컴포넌트이기 때문에 받아오는 props가 useNavigate의 인자와 동일하다. 이 때문에 Navigate 컴포넌트도 기본값으로 history push를 사용하여 이동하므로, history replace로 이동하기 위해서는 replace props를 넘겨줘야 한다. state 또한 state props로 넘겨줄 수 있다.

<Navigate to="/home" state={state} replace />

3. Relative routes and nested routes

  • v5에서 nested route를 구현한다면 아래처럼 구현할 수 있을 것이다.
// react-router v5
const App = () => {
  return (
    <Switch>
      <Route path={'/welcome'}>
        <WelcomePage />
      </Route>
      {/* OR */}
      {/* <Route path={'/welcome'} component={WelcomePage} /> */}
    </Switch>
  )
}

const Welcome = () => {
  const match = useRouteMatch()

  return (
    <>
      <p>Welcome Page</p>
      <Link to={`${match.path}/new-user`}>New User</Link>
      <Switch>
        <Route path={`${match.path}/new-user`} component={() => <p>Hi New User!</p>} />
      </Switch>
    <>
  )
}
  • v5에서는 위와 같이 useRouteMatch를 통해 현재 경로를 가져온 뒤 path props에 라우트할 경로를 덧붙여 넣어주는 방식으로 경로를 지정해주어야 했다. 하지만 v6부터는 Route 컴포넌트와 Link 컴포넌트가 부모의 라우트 경로를 보고 이로부터 자신의 경로를 빌드하므로, 더 이상 처음부터 경로를 만들 필요가 없다.
// react-router v6
const App = () => {
  return (
    <Routes>
      <Route path={'/welcome/*'}> element={<WelcomePage />} />
      {/* 
        path={'/welcome'} won't work here, as the new router uses "exact" route matching.
        concatenate "/*" to indicate there are more paths to be matched in "WelcomePage".
      */}
    </Routes>
  )
}

const Welcome = () => {
  return (
    <>
      <p>Welcome Page</p>
      <Link to="/new-user">New User</Link>
      {/* automatically builds path "/welcome/new-user" */}
      <Routes>
        <Route path="/new-user" element={<p>Hi New User!</p>} />
      </Routes>
    <>
  )
}
  • v5에서는 neseted route를 사용하기 위해 예제 코드에서처럼 컴포넌트로 감쌌지만, v6에서는 그럴 필요 없이 Route 컴포넌트의 자식 컴포넌트로 바로 Route 컴포넌트를 둘 수 있게 되었다. 이 경우 들여쓰기를 통해 경로를 시각적으로 파악할 수 있으므로 가독성이 향상된다.
// react-router v6
const App = () => {
  return (
    <Routes>
      <Route path={'/welcome'}> element={<WelcomePage />}>
      {/* notice the absence of trailing "/*": the router can explicitly see there is a nested route */}
        <Route path={'/new-user'} element={<p>Hi New User!</p>} />
      </Route>
    </Routes>
  )
}

const Welcome = () => {
  return (
    <>
      <p>Welcome Page</p>
      <Link to={'/new-user'}>New User</Link>
      <Outlet />
      {/* indicates where the element props of the nested route should render */}
    <>
  )
}

이 때 상위 라우트는 /*를 경로에 붙이지 않아도 되는데, 이 경우에는 라우터가 명시적으로 하위 라우트가 존재함을 알 수 있기 때문이다.

또한 하위 라우트가 존재하는 컴포넌트의 경우, 하위 라우트의 컴포넌트를 그려줄 곳을 명시해줘야 한다. 이를 위해 Outlet 컴포넌트를 붙여 주면, 하위 라우트의 컴포넌트(이 경우, <p>Hi New User!</p>Outlet 컴포넌트가 위치한 곳에서 렌더된다.

  • 상술했듯 Route 컴포넌트의 children props는 nested route 기능을 위해 예약되어있기 때문에 element props로 컴포넌트를 내려주는 것이다.

  • 또한 이제 단독으로 Route 컴포넌트를 사용할 수 없으며, Route 컴포넌트는 무조건 Routes 컴포넌트로 감싸져있어야 한다.

4. useMatch replaces useRouteMatch

  • useMatch 또한 새로 도입된 매치 알고리즘을 사용한다.
  • v5에서 useRouteMatch는 인자 없이 호출하면 현재 주소에 대한 매치를 제공했다.
import { useRouteMatch } from 'react-router-dom' 

const match = useRouteMatch()

switch (match?.path) {
  case '/home':
  ...
}

하지만 v6의 useMatch는 무조건 패턴 경로를 인자로 넣어줘야 한다.

import { useMatch } from 'react-router-dom'

const match = useMatch('/home')

if (match) {
  // current path is "/home"
  ...
} else {
  // current path is NOT "/home"
  ...
}
  • 리턴하는 Match 타입의 객체가 변경되어 PathMatch 타입을 리턴한다.
// react-router v5
interface match<ParamKey> {
  isExact: boolean
  params: Params<ParamKey>
  path: string
  url: string
}
// react-router v6
interface PathMatch<ParamKey> {
  params: Params<ParamKey>;
  pathname: string;
  pattern: PathPattern;
}

interface PathPattern {
  path: string;
  caseSensitive?: boolean;
  end?: boolean;
}

5. No optional params

  • 더 이상 경로 패러미터를 optional하게 선택하지 못하게 되었다.
// react-router v5
<Route path="/products/:productId?" component={ProductDetail} />
  • 거추장스럽기는 하지만 새로 도입된 nested route로 이 문제를 해결할 수 있다.
// react-router v6
<Route path="/products">
  <Route path=":productId" element={<ProductDetail />} />
  <Route path="" element={<ProductDetail />} />
</Route>

이 경우 /products/12345 경로는 상위 ProductDetail 라우트에 매치되어 productId: "12345" 패러미터를 들고 ProductDetail 컴포넌트를 렌더할 것이고, /products 경로는 하위 ProductDetail 라우트에 매치되어 productId: null 패러미터를 들고 ProductDetail 컴포넌트를 렌더할 것이다.

6. Deprecation of useHistory

  • v6부터는 useHistory hook을 제공하지 않는다. 따라서 history 객체를 사용하기 위해서는 history 패키지에서 createBrowserHistory 함수를 가져와서 custom history를 만드는 등의 workaround가 필요하다.

7. Outro

react-router가 v6으로 버전업되면서 바뀐 변경점 중 큰 변경사항이나 우리 팀의 코드에 큰 영향을 미치는 내용을 정리해보았다. 이 내용 말고도 다른 변경점들이 많으니(Prompt의 삭제 등) 다른 변경점은 React Router v6 API DocsUpgrading From React Router v5 문서를 참조하면 좋을 것이다.

profile
127.0.0.1
post-custom-banner

0개의 댓글