React Router[React]

SnowCat·2023년 2월 1일
1

React Router

목록 보기
1/3
post-thumbnail

React Router?

  • 라우팅: 사용자가 요청한 URL에 맞게 알맞은 페이지를 보여주는 것
  • React Router: 컴포넌트 단위로 라우팅을 할 수 있게 해주는 라이브러리
  • html을 한번만 받아오고 필요할때마다 데이터를 받아와 화면을 업데이트 하는 SPA(Single Page Application) 구현 가능

Setup for tutorial

  1. 커맨드 입력
npm create vite@latest name-of-your-project -- --template react
# follow prompts
cd <your new project directory>
npm install react-router-dom localforage match-sorter sort-by
npm run dev

result:

 VITE v3.0.7  ready in 175 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  1. src폴더에 index.css, contacts.js 복붙
  2. src 폴더에 index.css, contacts.js, main.jsx 이외의 모든 파일 삭제 및 main.jsx의 App import 삭제

Adding a Router

  • 우선 라우팅을 위해 Browser Router의 생성 필요
  • createBrowserRouter를 통해 router 생성 가능

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";
import './index.css'

const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world!</div>,
  },
]);

ReactDOM.createRoot(document.getElementById('root')).render(

  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

The Root Route

  • root에 사용되는 route를 root route로 부름
  • root route 생성을 위해 src 폴더 내에 routes 폴더를 만들고 root.jsx파일 생성 및 main.jsx 변경

root.jsx

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.jsx

/* import 추가 */
import Root from "./routes/root";

/* .... */
/* element 대상을 Root로 변경 */
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
]);

/* .... */

Handling Not Found Errors

  • 사이드바에 있는 버튼 클릭시 404에러 출력

  • 에러 확인시 리액트 라우터는 이를 확인해 오류화면을 출력함

  • 에러 페이지를 출력하는 컴포넌트를 만들고 이를 router 객체의 errorElement 속성으로 추가해 에러 화면을 커스텀할 수 있음

src/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>
  );
}
  • 잘못된 경로로 이동시 useRouteError에서 error를 받고, statusText를 받아 출력됨

main.jsx

/* ... */
import ErrorPage from "./error-page";

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

Create Contact Route UI

  • src/routes/contact.jsx 추가 및 router에 새로운 경로 추가

contact.jsx

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

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

  return (
    <div id="contact">
      <div>
        <img
          key={contact.avatar}
          src={contact.avatar || null}
        />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{" "}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter && (
          <p>
            <a
              target="_blank"
              href={`https://twitter.com/${contact.twitter}`}
            >
              {contact.twitter}
            </a>
          </p>
        )}

        {contact.notes && <p>{contact.notes}</p>}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          <Form
            method="post"
            action="destroy"
            onSubmit={(event) => {
              if (
                !confirm(
                  "Please confirm you want to delete this record."
                )
              ) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

function Favorite({ contact }) {
  // yes, this is a `let` for later
  let 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>
  );
}

main.jsx

/* existing imports */
import Contact from "./routes/contact";

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

/* existing code */

Nested Routes

  • 위의 출력을 아래와 같이 바꾸고 싶음

  • 우선 contact 부분을 Root의 child로 두어야 함
    main.jsx
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);
  • 결과로 화면이 넘어가지는 않지만 Contact 컴포넌트가 출력되지 않음
  • root.jsx에 <Outlet> 컴포넌트를 추가해 라우팅에 따른 다른 child component가 올 수 있음을 알려야함

root.jsx

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

export default function Root() {
  return (
    <>
      {/* all the other elements */}
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}

Client Side Routing

  • a 태그를 사용하면 링크를 눌렀을 때 서버에 새로운 요청을 보내며 새로고침됨
  • 이를 막고 클라이언트에서 직접 라우팅을 시도하려면 Link 태그를 사용해야함

root.jsx

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

export default function Root() {
  return (
    <>
      <div id="sidebar">
        {/* other elements */}

        <nav>
          <ul>
            <li>
              <Link to={`contacts/1`}>Your Name</Link>
            </li>
            <li>
              <Link to={`contacts/2`}>Your Friend</Link>
            </li>
          </ul>
        </nav>

        {/* other elements */}
      </div>
    </>
  );
}

Loading Data

  • URL Segment는 이에 대응되는 컴포넌트와 데이터와 함께 결합되는 경우가 많음
  • 데이터를 받아오기 위해서는 loader와 useLoadData를 호출함
    loader는 컴포넌트 생성 전에 호출되어 useLoadData를 사용하는 부분에 데이터를 넘겨주게 됨

root.jsx

import {
  Outlet,
  Link,
  useLoaderData,
} from "react-router-dom";
import { getContacts } from "../contacts";

export async function loader() {
  const contacts = await getContacts();
  return { contacts };
}

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>
    </>
  );
}
/* other code */

main.jsx

/* other imports */
import Root, { loader as rootLoader } from "./routes/root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

Data Writes + HTML Forms

  • 웹에서 HTML Form을 통해 서버를 데이터 제출 가능
  • a 태그를 사용할때처럼 navigation을 발생시키지만, 추가로 데이터 전송을 위해 request method를 POST로 바꾸고, 전송할 request body를 생성한다는 차이가 있음
  • React Router도 마찬가지로 데이터 전송과 리다이렉션을 수행하지만, 서버에 request하는 대신 routet action에 데이터를 전달하고, 리다이렉션 대신 CSR을 하게됨

Creating Contacts

  • React Router의 Form을 사용하기 위해서는 form 태그를 Form으로 바꾸고, submit시 사용할 action 함수를 작성해야함
  • Form 함수를 통해 action이 실행된 이후 페이지에 변경된 데이터들을 갱신하게 됨

root.jsx

import {
  Outlet,
  Link,
  useLoaderData,
  Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";

export async function action() {
  const contact = await createContact();
  return { contact };
}

/* 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>
    </>
  );
}

main.jsx

/* other imports */

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 />,
      },
    ],
  },
]);

URL Params in Loaders

[
  {
    path: "contacts/:contactId",
    element: <Contact />,
  },
];
  • :을 가지고 있는 url segment는 dynamic segment라 하며, 들어오는 값에 따라 URL 주소는 바뀌게 됨
  • 들어오는 값에 따라 URL을 바꾸는것 이외에도 dynamic segment값을 loader에 전달할 수도 있음

contact.jsx

import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts";

export async function loader({ params }) {
  return getContact(params.contactId);
}

export default function Contact() {
  const contact = useLoaderData();
  // existing code
}

main.jsx

/* existing code */
import Contact, {
  loader as contactLoader,
} from "./routes/contact";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,
      },
    ],
  },
]);

/* existing code */

출처:
https://reactrouter.com/en/6.8.0/start/tutorial

profile
냐아아아아아아아아앙

0개의 댓글