https://www.youtube.com/playlist?list=PLC3y8-rFHvwjkxt8TOteFdT_YmzwpBlrG
해당 유튜브 강의를 간단하게만 정리한 내용입니다.
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
${도메인} => Home 컴포넌트 렌더
${도메인}/about => About 컴포넌틑 렌더
import { Link } from 'react-router-dom'
<Link to="/">Home</Link>
<Link to="/about">About</Link>
import { NavLink } from 'react-router-dom'
<NavLink to="/">Home</NavLink>
<NavLink to="/about">About</NavLink>
NavLink가 Link와 다른 점은 클릭된 링크에 active라는 className이 추가된다는 차이가 있다.
import { useNavigate } from 'react-router-dom'
const navigate = useNavigate();
<button onClick={() => navigate(-1)}>뒤로가기</button>
<button onClick={() => navigate('about')}>Push를 통해 About으로 가기</button>
<button onClick={() => navigate('about', { replace: true })}>Replace를 통해 About으로 가기</button>
두번째 옵션을 추가하지 않으면 history.push와 같이 동작하고, 추가하면 history.replace와 같이 동작한다.
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
path="*"를 통해 path가 지정되지 않은 경로로 사용자가 진입한 경우 NotFound 페이지를 보여줄 수 있다.
<Routes>
<Route path="products" element={<Product />}>
<Route path="featured" element={<FeaturedProducts />} />
<Route path="new" element={<NewProducts />} />
</Route>
</Routes>
${도메인}/products => Product 컴포넌트 렌더
${도메인}/products/featured => FeaturedProducts 컴포넌트 렌더
${도메인}/products/new => NewProducts 컴포넌트 렌더
하지만! Product 컴포넌트에서는 routes tree에서 자식 컴포넌트들이 어떻게 설정되어 있는지 알지 못한다. 이를 위해 Product 컴포넌트 마지막에 react-router-dom의 Outlet 컴포넌트를 호출해주어야 한다.
<Routes>
<Route path="products" element={<Products />}>
<Route index element={<FeaturedProducts />} />
</Route>
</Routes>
다음과 같이 index로 작성한 Route가 존재하는 경우
${도메인}/products => FeaturedProducts 컴포넌트를 렌더
<Routes>
<Route path="users" element={<Users />}>
<Route path=":userId" element={<UserDetails />} />
</Routes>
${도메인}/users => Users 컴포넌트 렌더
${도메인}/users/${userId} => UserDetails 컴포넌트 렌더
UserDetails 컴포넌트에서 userId 값은, useParams 훅을 통해 확인할 수 있다.
<Routes>
<Route path="users" element={<Users />} />
</Routes>
다음과 같이 Route가 설정되어 있는 상태에서 다음과 같은 코드로 query param을 설정할 수 있다.
import { Outlet, useSearchParams } from 'react-router-dom'
const Users = () => {
const [searchParams, setSearchParams] = useSearchParams()
const showActiveUsers = searchParams.get('filter') === 'active'
return (
<div>
<button onClick={() => setSearchParams({ filter: 'active' })}>Active User</button>
<button onClick={() => setSearchParams({})}>Reset Filter</button>
</div>
{
showActiveUsers ? <h2>Showing Active Users</h2> : <h2>Showing All</h2>
}
)
}
Active User 버튼을 클릭하는 경우 url은 => ${도메인}/users/?filter=active 가 된다.
Reset Filter 버튼을 클릭하는 경우 url은 => ${도메인}/users 가된다.
<Link to="featured">Featured</Link>
<Link to="/new">New</Link>
/ 하나의 차이지만 완전히 다른 의미를 가진다.
현재 url이 ${도메인}/products 인 상태에서
Featured Link 클릭시 도메인은 => ${도메인}/products/featured 가 된다.
New Link 클릭시 도메인은 => ${도메인}/new 가 된다.
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom'
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<PageLayout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
</Route>
)
)
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)
이런 형태로 외부에서 router를 만들어 주입하는 형태로 많이 사용함
createRoutesFromElement 내부에서는 Routes를 사용할 수 없음. createRoutesFromElement가 Routes의 기능을 대체한다고 생각하면 좋을 듯
Nested Route를 활용하여 중첩되게 Outlet을 사용할 수 있다.
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<RootLayout />}>
<Route index element={<Home />} />
<Route path="help" element={<HelpLayout />}>
<Route path="faq" element={<Faq />} />
</Route>
</Route>
)
)
RootLayout과 HelpLayout 모두 내부에서 Outlet 컴포넌트를 렌더하는 형태라고 가정한다.
사용자가 /help/faq 로 진입하는 경우 RootLayout 안의 HelpLayout 안의 Faq 컴포넌트가 렌더된다.
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<RootLayout />}>
<Route index element={<Home />} />
<Route path="help" element={<HelpLayout />}>
<Route path="faq" element={<Faq />} />
<Route path="contact" element={<Contact />} />
</Route>
<Route path="careers" element={<CareersLayout />}>
<Route
index
element={<Careers />}
loader={careersLoader}
/>
</Route>
<Route path="*" element={<HotFound />} />
</Route>
)
)
다음과 같은 Router가 있다고 가정
// Careers.jsx
import { useLoaderData } from 'react-router-dom'
export default function Careers() {
const careers = useLoaderData()
return (<div>{careers}</div>)
}
const careersLoader = async () => {
const res = await fetch(url)
return res.json()
}
careersLoader 함수가 호출되는 시점은 router가 실행되는 시점이다. 이후 useLoaderData 훅을 통해 이전에 실행된 함수의 결과값을 불러오는 형태이다.
이러한 훅이 필요한 이유는, api 호출이 완료된 이후에 컴포넌트 렌더가 일어나야 하는 경우가 존재하기 때문이다. 특히 layout shift 현상을 해결하기 위해 해당 훅을 활용해볼 수 있다.
기존 방법인 useEffect 훅을 사용하는 방법은 컴포넌트 렌더 이후에 api call이 일어나기 때문에 layout shift 현상을 피하기 어렵다.
<Route path="careers" element={<CareersLayout />}>
<Route path=":id" element={<CareerDetails />} />
</Route>
다음과 같은 router가 있다고 가정
import { useParams, useLoaderData } from 'react-router-dom'
export default function CareerDetails() {
const { id } = useParams()
const career = useLoaderData()
return (
<div className="career-details">
<h2>{ id } { career }</h2>
</div>
)
}
export const careerDetailsLoader = async ({ params }) => {
const { id } = params
const res = await fetch('https://localhost' + id)
return res.json()
}
loader 함수의 인자로 params가 자동 전달된다는 사실을 기억해야 한다.
/careers/:id로 path를 지정한 경우 /careers/15와 같은 경로도 허용하지만, /careers/fdsgnjlgds 와 같은 경로도 허용된다. 하지만 id의 값이 정수로 기대되는 경우 fdsgnjlgds와 같은 문자열이 parameter로 넘어온다면 해당 페이지가 아닌 에러 페이지를 보여주는 것이 좋다.
이전의 로더 함수를 다음과 같이 변경한다.
export const careerDetailsLoader = async ({ params }) => {
const { id } = params
const res = await fetch('https://localhost' + id)
if (!res.ok) {
throw Error('적절하지 않은 parameter입니다.')
}
return res.json()
}
import { useRouteError } from 'react-router-dom'
export default function CareersError() {
const error = useRouteError()
return (
<div>
<p>{error.message}</p>
</div>
)
}
<Route
path=":id"
element={<CareerDetails />}
loader={careerDetailsLoader}
errorElement={<CareersError />}
/>
Route에 등록한 loader 함수 호출에서 에러가 발생하는 경우! errorElement를 렌더하는 방식이다.
<Route
path="careers"
element={<CareersLayout />}
errorElement={<CareersError />}>
>
<Route
index
element={<Careers />}
loader={careersLoader}
/>
<Route
path=":id"
element={<CareerDetails />}
loader={careerDetailsLoader}
/>
</Route>
만약 다음과 같은 router에서 /careers, /careers/:id 모두에서 errorElement를 등록해야한다면 어떻게 해야 할까?
이런 경우 상위의 Route에 등록해주면 된다. 상위의 Route에 errorElement를 등록해주면, 하위의 모든 Route들에 해당 errorElement가 등록된다. (계속해서 errorElement를 찾기 위해 bubble up 한다고 생각하면 된다.)
import { Form, useActionData } from 'react-router-dom'
export const Contact = () => {
const data = useActionData()
return (
<Form method="post" action="/help/contact">
{data && data.error && <p>{data.error}</p>}
</Form>
)
}
export const contactAction = async ({ request }) => {
const data = await request.formData()
const submission = {
email: data.get('email'),
message: data.get('message')
}
if (submission.message.length < 10) {
return { error: 'Message must be over 10 chars long' }
}
return redirect('/')
}
<Route path="help" element={<HelpLayout />}>
<Route path="contact" element={<Contact />} action={contactAction} />
</Route>