2021년 11월 3일에 react-router
6.0.0이 릴리즈되었다. 메이저 버전이 바뀐 것임을 감안하더라도 굉장히 많은 것들이 바뀌었는데, 그 중에 우리 팀의 가장 큰 주목을 끈 것은 번들링 사이즈가 70% 가량 줄어들었다는 것이다. 빌드 사이즈를 줄이려던 우리 팀에게 이 부분은 매력적으로 보였고, v5에서 v6으로 업그레이드하기 위해 사전 조사를 하는 업무를 내가 맡게 되었다. 아직 작업을 시작하지는 않았지만 변경점을 정리한 것을 이슈로만 두기는 아까워 여기에도 올린다.
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에서는 위 코드처럼 ProductEdit
과 ProductDetail
라우트가 배치되어 있어도, /products/edit
주소를 가장 먼저 배치되어 있는 ProductDetail
라우트에 매치하는 것이 아니라 가장 알맞은 ProductEdit
라우트에 매치한다. ProductEdit
라우트는 더 구체적으로 /products/edit
주소에 매치되는 라우트이기 때문이다. 이 때문에 잘못된 라우트 순서에서 기인하는 오동작을 예방할 수 있다.
// 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 />} />
component
props 대신, suspense fallback처럼 element
props를 사용한다.// react-router v5
<Route path="/products" component={Products} />
// react-router v6
<Route path="/products" component={<Products />} />
따라서 props를 정의해서 내려주기 위해 render
나 children
props로 내려줄 필요가 없어졌다. 또한 Route
컴포넌트에서 children
props는 후술할 nested route를 위해 예약되어있어 사용할 수 없기도 하다.
useHistory
hook과 Redirect
컴포넌트는 이제 새로운 useNavigation
hook과 Navigate
컴포넌트로 대체된다. useNavigation
hookuseHistory
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 })
Navigate
componentRedirect
컴포넌트로 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 />
// 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>
<>
)
}
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>
<>
)
}
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
컴포넌트로 감싸져있어야 한다.
useMatch
replaces useRouteMatch
useMatch
또한 새로 도입된 매치 알고리즘을 사용한다.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;
}
// react-router v5
<Route path="/products/:productId?" component={ProductDetail} />
// 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
컴포넌트를 렌더할 것이다.
useHistory
useHistory
hook을 제공하지 않는다. 따라서 history
객체를 사용하기 위해서는 history
패키지에서 createBrowserHistory
함수를 가져와서 custom history를 만드는 등의 workaround가 필요하다.react-router
가 v6으로 버전업되면서 바뀐 변경점 중 큰 변경사항이나 우리 팀의 코드에 큰 영향을 미치는 내용을 정리해보았다. 이 내용 말고도 다른 변경점들이 많으니(Prompt
의 삭제 등) 다른 변경점은 React Router v6 API Docs나 Upgrading From React Router v5 문서를 참조하면 좋을 것이다.