이번 프로젝트를 진행하면서 또 하나의 도전, react-router 최신 버전 v6.8.2 사용해보기! 팀원 중 한 분이 이전의 프리 프로젝트를 진행하면서 최신 버전의 react-router를 사용해보셨는데 사용 후 긍정적인 반응을 보여주셔서 각자 공부해보기로 했다.
가장 먼저 해야 할 일은 브라우저 루트 라우터를 생성하고 루트 경로를 연결하는 것으로 그 방법은 아래 내용과 같다.
createBrowserRouter
를 사용해 router 를 생성하고 path에 경로를 지정하고 element로 해당 경로가 URL과 일치할 때 렌더링할 요소를 지정한다.
main.jsx
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
loader: rootLoader,
},
]);
커스텀한 에러 페이지를 생성했다면, errorElement
에 해당 페이지를 전달해준다.
"/" 루트 경로에서의 에러 페이지를 의미한다.
main.jsx
import ErrorPage from "./error-page";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
},
]);
더 친절한 에러페이지를 제작하고 싶다면 useRouteError
를 사용하여 발생한 오류의 응답을 가져와 보여줄 수 있다.
단, 이 기능은 데이터 라우터를 사용하는 경우에만 동작한다.
error-page.jsx
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
<div id="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}
children
: routes와 같은 라우트 설정 객체의 또 다른 배열
아래 코드와 같이 /
루트 경로에서 children 배열 안에 있는 events/:id
경로로 이동했을 시 element에 적은 컴포넌트가 Root 컴포넌트 안에 있는 Outlet 부분에 렌더링 된다.
즉, 중첩 된 URL을 사용하지 않고도 컴포넌트 중첩을 사용할 수 있게 된다.
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
loader: rootLoader,
children: [
{
path: "events/:id",
element: <Event />,
loader: eventLoader,
},
{
path: "posts/:id",
element: <Post />,
loader: eventLoader,
},
],
},
]);
import { Outlet } from "react-router-dom";
export default function Root() {
return (
<>
{/* all the other elements */}
<div id="detail">
<Outlet />
</div>
</>
);
}
이번 6.4버전 이상에서 가장 주목할 특징은 loaders, actions, fetchers 라는 data api를 제공한다는 점이다. 이 api는 서버 측 렌더링에 대한 지원을 제공하고 라우팅 기능을 사용하여 데이터를 불러오는 방법을 간편하게 만들어준다.
로더는 컴포넌트가 렌더링되기 전에 호출되며 로더 함수가 값을 리턴하면 useLoaderData()
로 컴포넌트에서 데이터를 받아 사용할 수 있다.
아래 코드로 예를 들면, Root 컴포넌트를 표시하려는 시점에는 useLoaderData로 리턴된 데이터를 이미 사용할 수 있는 것이다. 이는 사용자 경험을 향상시킬 뿐만 아니라. 동시에(co-located) 데이터 가져오기 및 렌더링과 관련된 개발자 경험을 개선할 수 있다.
아래와 같이 각 route파일에 loader
라는 함수를 만든뒤 이를 export하여 사용하는것이 일반적이다.
src/routes/root.jsx
import { Outlet, Link } from "react-router-dom";
import { getContacts } from "../contacts";
export async function loader() {
const contacts = await getContacts();
return { contacts };
}
작성한 로더함수를 loader에 불러온다.
src/routes/main.jsx
import Root, { loader as rootLoader } from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
},
]);
로더에서 리턴된 값을 컴포넌트에서 useLoaderData()
로 받아와 매핑한다.
src/routes/root.jsx
import { Outlet, Link } from "react-router-dom";
import { getContacts } from "../contacts";
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
{/* other code */}
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
{contact.favorite && <span>★</span>}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
{/* other code */}
</div>
</>
);
}
우리가 action을 배울때 주목해야할 부분은 HTML form이다. HTML form은 특정 url에 데이터를 전송해서 처리하는 요청과정이다. 그리고 그 요청을 처리할 주소값은 보통 action에다가 정의한다.
그럼 리액트 라우터는 이를 처리하기 위해 Form이라는 것을 사용하고 이는 html form을 모방하여 리퀘스트를 날린다.
<from>
과 <Form>
을 헷갈릴 수 있지만, <form>
을 사용하면 서버에다가 리퀘스트를 날리는 것이고 <Form>
을 사용하면 클라이언트 측에다가 리퀘스트를 날리는 것이다.
그리고 클라이언트측에서 리퀘스트를 받았다면, 이는 action에서 처리하고 사용방법은 아래와 같다.
<Form>
을 사용하고 action 함수를 정의한다.
src/routes/root.jsx
import {
Outlet,
Link,
useLoaderData,
Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
await createContact();
}
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
{/* other code */}
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
{/* other code */}
</div>
</>
);
}
라우터에 action을 연결한다.
src/main.jsx
import {
Outlet,
Link,
useLoaderData,
Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
await createContact();
}
/* other code */
import Root, {
loader as rootLoader,
action as rootAction,
} from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
주의할 점! action은 post로 보내야 호출된다. get으로 보내면 loader가 불린다.
인터페이스 URLSearchParams
는 URL의 쿼리 문자열과 함께 작동하는 유틸리티 메서드를 정의할 수 있다.
이름을 검색한다고 하면 쿼리스트링의 값은 name의 속성값에 따라 달라지겠지만, 아래 예시와 같이 name 속성값을 q라고 하고 name이 jungho인 사람을 찾는다고 가정하면 ?search=jungho
으로 표시될 것이다.
src/routes/root.jsx
<Form id="search-form" role="search">
<input
id="bn"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div id="search-spinner" aria-hidden hidden={true} />
<div className="sr-only" aria-live="polite"></div>
</Form>
이때 쿼리스트링 값을 따로 파싱하고 싶다면 URLSearchParams
을 사용하여 더욱 쉽게 다를 수 있다.
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts };
}
와 진짜 설명 잘하시는 것 같습니다