[react-router] 튜토리얼

김범식·2023년 8월 28일
0

React-Router-Dom

목록 보기
4/4
post-thumbnail

⭐ 검색 스피너 추가

로딩표시가 없으면 검색이 다소 느린 느낌이 듭니다. 어쩔 때는 앱이 멈춘것 처럼 보이기도 합니다. 데이터 베이스를 더 빠르게 만드는것도 중요하지만 더 나은 UX를 위해 검색에 대한 즉각적인 UI 피드백을 추가해 봅시다.

여기서는 useNavigation을 사용합니다.


📎 검색 스피너 추가

src/routes/root.jsx

// existing code

export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();
  const submit = useSubmit();

  const searching =
    navigation.location &&
    new URLSearchParams(navigation.location.search).has(
      "q"
    );

  useEffect(() => {
    document.getElementById("q").value = q;
  }, [q]);

  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              className={searching ? "loading" : ""}
              // existing code
            />
            <div
              id="search-spinner"
              aria-hidden
              hidden={!searching}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>
  );
}

이제 검색을 진행할 때 로딩스피너가 추가되었습니다.

여기서 navigation.location 은 앱이 새 URL로 이동하고 이에 대한 데이터를 로드할 때 표시됩니다. 새 URl로 이동할 때 loader가 데이터를 전부 로딩하기 전까지는 여전히 이전페이지를 보여줍니다. 그사이의 시간동안 로딩스피너를 보여주므로써 사용자경험을 향상시킬 수 있습니다.



⭐ 기록스택 관리

submit을 하게 되면 제출 기록이 남습니다. 근데 지금 onChange함수로 키보드의 input이 있을 때마다 submit을 해주기 때문에 모든 단어의 기록이 기록스택에 남게 됩니다.

히스토리 스택의 현재 항목을 미렁넣는 대신 다음페이지로 대체하면 이를 방지할 수 있습니다.


📎 submit에서 replace 사용하기

src/routes/root.jsx

// existing code

export default function Root() {
  // existing code

  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              // existing code
              onChange={(event) => {
                const isFirstSearch = q == null;
                submit(event.currentTarget.form, {
                  replace: !isFirstSearch,
                });
              }}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>
  );
}

이렇게 하면 첫번째 검색결과가 아니라면 검색 결과를 고체하도록 설계하였습니다.



⭐ 탐색 없는 변경

지금 까지는 탐색을 통해 데이터를 찾고 변경시켰지만 탐색을 하지 않고 데이터를 변경하는 방법에 대해 알아보겠습니다. useFetcher를 사용하여 탐색을 하지 않고 로더 및 작업과 통신할 수 있습니다.

탐색을 하지 않는다는것은 url변경에 의한 이동이 없다는 뜻!


📎  생성하기

src/routes/contact.jsx

import {
  useLoaderData,
  Form,
  useFetcher,
} from "react-router-dom";

// existing code

function Favorite({ contact }) {
  const fetcher = useFetcher();
  let favorite = contact.favorite;

  return (
    <fetcher.Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </fetcher.Form>
  );
}

favorite에 따라 그려지는 버튼의 모양을 다르게 합니다. post 요청이기 때문에 action을 만들어 줍니다.


📎 액션 만들기

src/routes/contact.jsx

// existing code
import { getContact, updateContact } from "../contacts";

export async function action({ request, params }) {
  let formData = await request.formData();
  return updateContact(params.contactId, {
    favorite: formData.get("favorite") === "true",
  });
}

export default function Contact() {
  // existing code
}

이제 양식을 가져와 요청을 보냅니다. post 요청이기 때문에 request.에서 formData()를 가져옵니다.

이제 favorite 표시를 사용할 수 있습니다.

html에서 Form을 제출할때 기본적으로 action경로로 페이지 이동이 발생합니다. 이것을 navigation(탐색)을 유발한다고 표현하는데 url이 변경되는 걸 말합니다. action이 없더라도 같은 페이지로 navigation(탐색)이 발생하기 때문에 navigation.state 가 loading이 되는 현상이 발생합니다. 좋아요 같은 버튼기능은 이런 기능이 필요 없습니다.

가장 기본적으로 이러한 현상을 막는것이 e.preventDefault() 이고 react-router-dom을 사용할 때는 위의 예시와 같이 useFetcher를 사용합니다.

fetcher.Form을 사용하면 페이지 이동이 발생하지 않습니다. 즉 url이 변경되지 않고 기록 스택이 영향을 받지 않습니다.

그래서 왜 사용하냐고요? 만약 페이지를 전환할 때 loader가 데이터를 불러오는데 시간이 걸린다면 개발자는 화면이 전환되는동안 끊기는 느낌을 주지 않기 위해 로딩스피너를 넣을 것입니다. 그 조건이 바로 navigation입니다.

const navigation = useNavigation()
navigation.state //loading

다음 훅을 사용하면 페이지 전환상태를 받아올 수 있는데 좋아요 버튼을 누를 때마다 페이지 이동이 발생 즉 navigation(탐색)이 발생하면 좋아요. 버튼을 클릭할 때마다 로딩스피너가 보일것입니다.

이것을 방지하기위함 그리고 페이지 이동을 막기 위해 fetcher.Form를 사용합니다.

더욱 다양한 사용법은 https://reactrouter.com/en/main/hooks/use-fetcher 공식문서에서 확인할 수 있습니다.



⭐ 낙관적UI

본 예제에서는 즐겨찾기(Favorite)버튼을 누를 때 약간의 지연이 발생되도록 설계되었습니다. 이는 실제 서비스 환경에서 네트워크 문제가 있을 수 있기 때문입니다.

우리는 navigation.state를 사용했지만 fetcher.state를 사용해서 더 나은 피드백을 제공하도록 할 수 있습니다.

이때 사용하는 전략이 낙관적UI입니다.

🪄 낙관적 UI란?
사용자 인터페이스 디자인 및 개발 패턴중 하나로, 사용자에게 빠른 피드백과 더 나은 사용자 경험을 제공하기 위해 사용됩니다. 낙관적 UI의 핵심 개념은 사용자의 동작에 대한 응답을 가능한 한 빨리 보여주는 것입니다.


📎 낙관적 전략 사용

src/routes/contact.jsx

// existing code

function Favorite({ contact }) {
  const fetcher = useFetcher();

  let favorite = contact.favorite;

  if (fetcher.formData) {
    favorite = fetcher.formData.get("favorite") === "true";
  }

  return (
    <fetcher.Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </fetcher.Form>
  );
}

이제 버튼을 클릭하면 즉각적으로 상태가 변경됩니다. 실제 제출하는데이터가 있다면 제출된 데이터를 대신 사용하고 제출하는 데이터가 없다면 실제 데이터를 사용하게 됩니다.

만일 업데이트가 실패했다고 하더라도 contact.favorite을 사용하기 때문에 원래 데이터로 돌아갑니다.

<button/>을 누르게 되면 fetcher의 상태는 다음과 같이 변경됩니다.

submitting → loading → idle

위의 코드에는 fetcher.formData를 사용하여 요청한 favorite 데이터를 가져와 사용합니다. 이 데이터는 submittingloading 상태에서 유지되다가 action함수가 완료되면서 fetcher의 상태가 idle 변경되면서 fetcher.formDatanull값으로 변경됩니다. 이때는 추가한 if문이 동작하지 않기 때문에 실제로 변환된 데이터를 가져와 사용하게 됩니다!

이로써 즉시 데이터를 변경할 수 있고, 실제로 네트워크 에러가 나더라도 idle상태에서는 정상적인 favorite 값을제공할 수 있습니다.


⭐ 찾을 수 없는 데이터

로드하려는 연락처가 존재하지 않는다면 어떻게 할까요?

📎 loader 에서 404에러 던지기

src/routes/contact.jsx

export async function loader({ params }) {
  const contact = await getContact(params.contactId);
  if(!contact){
    throw new Response("", {   //구체적인 커스텀 에러를 생성해서 반환할 수 있다. 
      status:404,
      statusText:`custom error page Not Found`
    })
  }
  return { contact };
}

Cannot read properties of null 을 피하고 오류 경로를 렌더링 하여 사용자에게 구체적인 내용을 알려줄 수 있습니다.

이경우 뒤로가기나 새로고침 밖에 해결할 방법이 없습니다.

위 화면과 같은 형태의 에러페이지를 만들어 봅시다.

📎 경로 없는 경로로 하위 경로를 래핑합니다.

src/index.jsx

createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    errorElement: <ErrorPage />,
    children: [
      {
        errorElement: <ErrorPage />,
        children: [
          { index: true, element: <Index /> },
          {
            path: "contacts/:contactId",
            element: <Contact />,
            loader: contactLoader,
            action: contactAction,
          },
          /* the rest of the routes */
        ],
      },
    ],
  },
]);

하위 경로에서 오류가 발생하면 경로가 없고 errorElement만 있는 경로에서 오류를 포작하고 렌더링하여 루트경로 의 UI 즉 sidebar는 그대로 유지됩니다.!



⭐ JSX경로

마지막으로 사람들은 jsx를 사용해 경로를 구성하는것을 선호합니다. createRoutesFromElements 로 경로를 구성할 때 JSX 나 객체 사이에는 기능적인 차이가 없으며 단순히 스타일에 따른 선호일 뿐입니다.

import {
  createRoutesFromElements,
  createBrowserRouter,
  Route,
} from "react-router-dom";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      path="/"
      element={<Root />}
      loader={rootLoader}
      action={rootAction}
      errorElement={<ErrorPage />}
    >
      <Route errorElement={<ErrorPage />}>
        <Route index element={<Index />} />
        <Route
          path="contacts/:contactId"
          element={<Contact />}
          loader={contactLoader}
          action={contactAction}
        />
        <Route
          path="contacts/:contactId/edit"
          element={<EditContact />}
          loader={contactLoader}
          action={editAction}
        />
        <Route
          path="contacts/:contactId/destroy"
          action={destroyAction}
        />
      </Route>
    </Route>
  )
);


지금 까지 공식문서에 있는 튜토리얼을 따라 작업을 계속했습니다.
Router를 사용한 적은 많았는데 이렇게 세부적인 기능까지 사용한 적은 없었던것 같습니다. 이해하는데 시간이 걸렸지만 그만큼 유용한 시간였습니다. 물론 React Router로 할 수 있는 일은 이것보다 훨씬 더 많기 때문에 공식 문서에서 API를 확인해보세요!


참고문서
https://reactrouter.com/en/main/start/tutorial#the-root-route

profile
frontend developer

0개의 댓글