react-router docs tutorial with typescript-1(basic)

GI JUNG·2024년 2월 4일
0

react

목록 보기
6/8
post-thumbnail

🍀 react-router tutorial 시작하기

react-router를 사용하긴 했지만, 간단한 동작만 이해하고 있기 때문에 각 잡고 공부하고 싶어서 시작하는 블로그다.
이 블로그는 react-router docs 홈페이지에서 제공하는 tutorial을 기반으로 작성할 것이다.

❗️❗️ 공식 홈페이지와 다른점

  • 공부하면서 내가 이해하기 편한 순서로 목차의 순서를 바꿨다.
  • typescript를 migration하면서 router를 공부하는 것을 목표한다.

시작에 앞서 tutorial에서 사용하는 3가지의 package들에 대해서 간략하게 짚고 넘어가자.

  • LocalForage
  • Match Sorter
  • Sort By

1. LocalForage

LocalForage는 비동기 스토리지 API를 사용하여, IndexedDB, WebSQL 또는 localStorage를 사용하여 데이터를 저장하는 JavaScript 라이브러리로 데이터 저장소 API의 사용 및 접근을 쉽게 할 수 있다.

import localforage from 'localforage';

// 데이터 저장
localforage.setItem('key', 'value').then(function () {
    return localforage.getItem('key');
}).then(function (value) {
    console.log(value); // 'value'
}).catch(function (err) {
    console.error(err);
});

// 데이터 검색
localforage.getItem('key').then(function (value) {
    console.log(value); // 'value'
}).catch(function (err) {
    console.error(err);
});

2. Match Sorter

Match Sorter는사용자가 입력한 내용과 가장 부합하는 내용들을 정렬된 배열로 제공해주는 라이브러리이다. 자동완성 또는 검색에 유용하게 사용할 수 있다.

import matchSorter from 'match-sorter';

const items = ['Apple', 'Banana', 'Orange', 'Grape'];
const result = matchSorter(items, 'ap'); // 사용자 입력이 'ap'일 때

console.log(result); // ['Apple', 'Grape'] 'ap'와 가장 잘 매치되는 항목 순서대로

3. Sort By

Sort By는 객체 또는 배열의 정렬을 쉽게 도와주는 라이브러리이다.

import sortBy from 'sort-by';

const users = [
    { name: 'Fred', age: 48 },
    { name: 'John', age: 30 },
    { name: 'Emma', age: 36 }
];

users.sort(sortBy('name'));
// 이름으로 정렬된 사용자 배열

users.sort(sortBy('-age'));
// 나이로 내림차순 정렬된 사용자 배열

router를 배우기에 앞서 관련된 pacakge들을 설치해주자. csscontacts.ts는 tutorial 페이지에서 찾을 수 있다.

$) yarn create vite learn-react-router-dom --template react-ts

# 관련 package들 설치
$) yarn add react-router-dom localforage match-sorter sort-by 

➕ router 추가하기

router를 이용하기 위해서는 provider를 통해서 application에 제공해주어야 하며 router에 관한 명세서를 createBrowserRouter객체를 통해서 제공할 수 있다.

💡 createBrowserRouter말고도 Route component를 이용할 수도 있지만, 추후 나오는 loader를 v6버전과 createBrowserRouter로 진행한다.

먼저 root router에 대한 component를 만들어서 router와 matching 하자.

// src/routes/root.tsx

export default function Root() {
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <form id="search-form" role="search">
            <input
              id="q"
              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>
          <form method="post">
            <button type="submit">New</button>
          </form>
        </div>
        <nav>
          <ul>
            <li>
              <a href={`/contacts/1`}>Your Name</a>
            </li>
            <li>
              <a href={`/contacts/2`}>Your Friend</a>
            </li>
          </ul>
        </nav>
      </div>
      <div id="detail"></div>
    </>
  );
}
// main.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import Root from "./routes/root.tsx";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
]);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

path: ‘/’를 가지는 router 하위에 나머지 router에 관련된 component들이 rendering되기 때문에 root router라고 부른다.

⚠️ 에러 페이지 다루기

navigation에 있는 아무 tab을 누르면 관련된 페이지가 없어 오류를 표출한다.

react-router는 오류를 잡아서 아래와 같은 기본 error page를 제공한다.

하지만, error page도 custom할 수 있는데 react-router가 제공하는 useRouteError훅을 사용하여 에러에 대한 정보를 노출시킬 수 있다.

💡 useRouteError는 에러를 잡아 던져주는 훅 이다.

// src/error-page.tsx

import { isRouteErrorResponse, useRouteError } from "react-router-dom";

export default function ErrorPage() {
  const error = useRouteError();
  const errorResponse: { message?: string; data?: any } = {};

  function errorMessage(error: unknown): string {
    if (isRouteErrorResponse(error))
      return (
        (errorResponse.message = `${error.status} ${error.statusText}`),
        (errorResponse.data = error.data)
      );
    if (error instanceof Error) return (errorResponse.message = error.message);
    if (typeof error === "string") return (errorResponse.message = error);

    console.error(error);
    return (errorResponse.message = "Unknown error");
  }

  errorMessage(error);

  return (
    <div id="error-page">
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occurred.</p>
      <p>
        <i>{errorResponse.message}</i>
      </p>
    </div>
  );
}

// main.tsx

...//

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />, // 👉🏻 error page추가
  },
]);

...//

내가 만든 error-page를 errorElement에 errorPage를 할당함으로써 react-router가 error를 잡아 custom error page를 보여줄 수 있게 된다.

근데, useRouteError()가 반환하는 타입이 unknown이기 때문에 type narrowing을 통해서 error handling해야 한다. error handling코드는 아래 참고에서 조금 변형한 코드이다.

⚠️ handle router error with typescript

없는 route에 접근을 하면 오류 메세지가 나오는 것을 볼 수 있다. docs와 다른 점은 status code인 404를 추가했다

📝 페이지 등록하기

위에서 / route에 해당하는 root component를 만들었다. 다른 페이지도 필요하므로 /contacts/:contactIdcontact page에 해당하는 component도 만들어주자.

// src/routes/contact.tsx

type Contact = {
  first: string;
  last: string;
  avatar: string;
  twitter: string;
  notes: string;
  favorite: boolean;
};

type FavoriteProps = {
  contact: Contact;
};

export default function Contact() {
  const contact: Contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placekitten.com/g/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };

  return (
    ...//
  );
}

function Favorite({ contact }: FavoriteProps) {
  // yes, this is a `let` for later
  const favorite = contact.favorite;
  return (
    <Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={favorite ? "Remove from favorites" : "Add to favorites"}
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
}

contacts/:contactId path에 해당하는 contact component도 router에 추가하자

// main.tsx

const router = createBrowserRouter([
  ...//
  // 👇 contacts/:contactId 경로에 Contact component rendering을 명시
  {
    path: "contacts/:contactId",
    element: <Contact />,
  },
]);

이제 contacts/아무값으로 url을 변경하면 contact page를 확인할 수 있다.

이상한 점이 있는데 navigation bar가 사라졌다. 우리가 원하는 것은 navigation bar와 동시에 contact page를 보여주는 것이다.

이를 해결하기 위해서는 Nested Router(중첩 라우터) 개념을 사용하여 기존의 navigation bar는 보이게 하고 내가 원하는 하위 컴포넌트를 rendering할 수 있게한다.

🕸️ Nested Router

중첩 라우터가 없다면 아마 모든 page에 navigation bar를 일일이 import하여 rendering하는 방법 말고는 없을 것이다. 하지만, 이렇게 되면 page의 이동이 있을 때 navigation component도 re-rendering되게 된다.

또한, 동일한 layout을 재사용하면서 페이지 이동 시 원하는 page를 보여주기 위해선 react-router가 제공하는 Outlet 을 사용하면 된다.

현재, root에 navigation이 정의되어 있으니 이 자식 page로 contact를 정의해주자.

// main.tsx

...//

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
	  // 👇 contact page를 root path의 하위 페이지로 선언
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);


...//

결과를 확인해보면 contact page가 사라졌다...
이는 router가 하위 페이지로 어떤 것을 rendering을 해야하는지 몰라서 발생하는 문제로 Outlet을 이용하여 router에게 하위 페이지로 contact를 보여주고 싶어~라고 말해주면 된다.

💡💡 Outlet은 부모요소에게 정의해주어야 한다.

contact page의 부모 페이지는 root이므로 root.tsx에 선언해주자.

// routes/root.tsx

import { Outlet } from "react-router-dom";

export default function Root() {
  return (
    <>
      <div id="sidebar">
				...//
      </div>
      <div id="detail">
				// 👇 자식 페이지를 rendering 해주세요~
        <Outlet />
      </div>
    </>
  );
}

router는 Outlet을 보고 하위페이지로 children에 정의한 contact component를 잘 rendering하는 것을 확인할 수 있다.

♻️ Client Side Rendering(새로고침 ❌)

navigation을 현재 anchor tag로 작성하였는데 기본적으로 anchor tag는 href 속성에 지정된 URL로 HTTP 요청을 보내 새로고침을 유발하게된다. 이는 preventDefault함수를 호출하여 기본동작(서버로의 요청)을 막아 state가 날아가는 것을 방지하여 CSR의 동작을 유지할 수 있다.

이러한 이유 때문에 react-router는 페이지 이동 시 새로고침을 방지하는 기능을 수행하는 Link component를 제공한다. 즉, CSR에 최적화된 기능을 수행한다.

root에 있는 anchor tag를 Link component로 대체하기 이전에 Link를 잠시 살펴보자

  • to property: anchor tag의 href 속성과 같으며 to에 넣어준 path로 페이지를 이동시킨다.
// routes/root.tsx
import { Link, Outlet } from "react-router-dom";

export default function Root() {
  return (
    <>
      <div id="sidebar">
				...//
        <nav>
          <ul>
            <li>
			  // 👇 anchor tag to Link Component
              <Link to="/contacts/1">Your Name</Link>
            </li>
            <li>
			  // 👇 anchor tag to Link Component
              <Link to="/contacts/2">Your Friend</Link>
            </li>
          </ul>
        </nav>
      </div>
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}

이제 다시 navigation의 버튼을 클릭하면 새로고침이 되지 않고 페이지 이동이 되는 것을 확인할 수 있다.

🔥 마치며

여기까지 기본적인 router를 알아봤다. 이전에 공부할 때는 바로 블로깅을 하면서 했는데, 노션에 먼저 정리를 한 후 정제하니까 속도가 올라갔다. typescript를 적용하기 시작한지 얼마 안 됐는데 router가 좋은 기능을 갖추었지만 type지원이 좀 아쉬운 생각이 든다. type지원을 받고 싶다면 출시한지 별로 되지 않은 tanstack router를 사용하는 것이 좋을 것 같다.

📚 참고

react-router-tutorial
tanstack router support type
useRouteError typescript stackoverflow
useRouteError typescript github

profile
step by step

0개의 댓글