JiSoo's Devlog

[Udemy React 완벽 가이드] 리액트 라우터가 있는 SPA 다중 페이지 구축 본문

Frontend/React

[Udemy React 완벽 가이드] 리액트 라우터가 있는 SPA 다중 페이지 구축

지숭숭숭 2024. 6. 17. 21:57

웹사이트를 방문하면 도메인 이름 뒤에 경로를 첨부할 수 있다 ex) /welcome

브라우저 주소창에 다른 URL을 입력하거나 URL을 변경하는 링크를 클릭하면 다른 페이지가 로딩된다

이게 바로 라우팅이 하는 일!

 

다른 경로에 대해 다른 콘텐츠를 로딩하게 되면 항상 새 콘텐츠를 가져와야 하기 때문에 새로운 HTTP 요청을 전송하고 새로운 응답을 받는 과정에서 사용자의 흐름이 중단될 수 있다는 단점이 있다

그래서 더 복잡한 사용자 인터페이스를 구축할 때 싱글 페이지 애플리케이션을 원한다

 

라우팅URL을 감시하고 다양한 콘텐츠를 로딩하는 기능을 한다

npm install react-router-dom

 

첫 번째 단계에서 URL과 경로, 다양한 경로에 대해 어떤 컴포넌트가 로딩되어야 하는지 정의해야 한다

 

두 번째 단계에서 라우터를 활성화하고 첫 번째 단계에서 정의한 라우트 정의를 로딩한다

세 번째 단계에서 우리가 로딩하려는 모든 컴포넌트들이 있는지 확인하고 그 페이지들 간에 이동할 수단을 제공했는지 확인한다

 

라우트 정의

react-router-dom 패키지에서 createBrowserRouter 함수를 제공하고 이 함수로 라우트를 정의할 수 있다

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

여기서 라우트 객체들은 라우트를 구성하는 몇 가지 프로퍼티를 받게 된다

거의 항상 추가할 주요 프로퍼티는 path 프로퍼티인데 이걸로 라우트가 작동해야 하는 경로를 정의한다

path는 도메인 뒤에 있는 부분

element에는 설정한 경로가 활성화되면 화면에 렌더링 되었으면 하는 JSX코드

변수나 상수는 router를 화면에 렌더링해야 한다거나 router를 로딩해야 하고 적절한 페이지를 화면에 렌더링해야 한다고 리액트에게 알려주기 위해 필요하다

 

이 router를 사용해야 한다고 알려주기 위해 필요한 게 RouterProvider 컴포넌트

RouterProvider 컴포넌트에는 router 프로퍼티를 설정해줘야 한다

function App() {
  return <RouterProvider router={router} />;
}

 

배열에 담긴 객체 대신 JSX 코드를 이용해 모든 라우트를 정의할 수도 있다

 

const routeDefinitions = createRoutesFromElements(
  <Route>
    <Route path="/" element={<HomePage />} />
    <Route path="/products" element={<Products />} />
  </Route>
);

const router = createBrowserRouter(routeDefinitions);

 

Link 컴포넌트는 배후에서 앵커 요소를 렌더링하게 된다

기본적으로 그 요소에 대한 클릭을 감시하고 링크를 클릭했을 때 HTTP 요청을 전송하는 브라우저 기본 설정을 막아준다

단순히 라우트 정의를 확인해 그에 맞춰 페이지를 업데이트하고 적절한 콘텐츠를 로딩하게 된다

URL을 변경하지만 새로운 HTTP를 요청하지는 않게 된다

 

 

중첩된 라우트

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    children: [
      { path: "/", element: <HomePage /> },
      { path: "/products", element: <Products /> },
    ],
  },
]);

RootLayout에서 자녀 라우트 컴포넌트와 요소가 어디 있는지도 정의해야 한다

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

function RootLayout() {
  return (
    <>
      <h1>Root Layout</h1>
      <Outlet />
    </>
  );
}
export default RootLayout;

Outlet 컴포넌트는 자녀 라우트 요소들이 렌더링되어야 할 장소를 표시하는 역할을 한다

이 장소가 HomePage와 ProductPage가 된다

 

이렇게 레이아웃 역할을 하는 루트 라우트를 만들면 RootLayout이 실제로 이 페이지 컴포넌트들의 래퍼 역할을 한다는 장점이 있다

 

 

errorElement 프로퍼티를 라우트 정의에 추가해 오류 생성시 원하는 페이지를 로딩할 수 있다

존재하지 않고 지원되지 않는 URL을 방문하는 경우에 지원할 수 있다

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      { path: "/", element: <HomePage /> },
      { path: "/products", element: <ProductsPage /> },
    ],
  },
]);

 

 

NavLink는 Link와 똑같이 사용 가능

className 프로퍼티를 추가하면 문자열을 받는 일반적인 프로퍼티가 아니라 함수를 받는 프로퍼티가 된다

그 함수는 앵커 태그에 추가되어야 하는 CSS 클래스 이름을 리턴한다

 

<NavLink to="/" className={({ isActive }) => isActive ? classes.active : undefined }>

isActive 프로퍼티를 객체에 넣어줄 수 있고 이건 불리언이라 이 링크가 현재 활성이면 참, 아니면 거짓이 된다

end 프로퍼티에 true나 fasle를 설정해줄수있는데 true로 설정되면 현재 활성인 라우트의 URL 뒤가 이 경로로 끝나면 이 링크를 활성으로만 간주해야 한다는 것이다

NavLink를 사용할 때 인라인 스타일을 적용할 수 있고 함수 형태도 지원한다

 

 

useNavigate 함수로 네비게이션 동작을 트리거할 수 있다

예를 들어 타이머가 만료되었거나 하는 상황이 있을 때 프로그램적인 강제적 네비게이션이 필요할 때 사용한다

const navigate = useNavigate();

  function navigateHandler() {
    navigate("/products");
  }

 

 

동적 라우트

콜론 넣고 그 뒤에 id나 원하는 식별자를 넣어 경로에 파라미터를 추가한다

콜론은 경로의 변경하길 원하는 그 부분이 역동적이라는 걸 react-router-dom에게 알려주는 것

{ path: "/products/:productId", element: <ProductDetailPage /> },

productId 부분이 역동적이기 때문에 어떤 게 와도 된다

productId 대신 실제 값을 알아내기 위한 툴로 useParams 훅을 사용한다

 

useParams 훅을 호출하면 params 객체를 주고 그 객체는 라우트 정의에서 프로퍼티로 정의한 모든 역동적 경로 세그먼트가 담긴 자바스크립트 객체이다

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

function ProductDetailPage() {
  const params = useParams();

  return (
    <>
      <h1>Product Details!</h1>
      <p>{params.productId}</p>
    </>
  );
}

이런 식으로  URL에 인코딩된 데이터를 잡을 수 있다

일반적으로는 제품의 id같은 걸 URL에 인코딩한다

 

 

절대 경로

경로가 /로 시작하면 절대 경로

{
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      { path: "/", element: <HomePage /> },
      { path: "/products", element: <ProductsPage /> },
      { path: "/products/:productId", element: <ProductDetailPage /> },
    ],
  },

 

 

상대 경로

라우트를 정의할 때 단순히 정의된 경로들이 래퍼 라우트의 경로 뒤에 첨부된다

{
    path: "/root",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      { path: "", element: <HomePage /> },
      { path: "products", element: <ProductsPage /> },
      { path: "products/:productId", element: <ProductDetailPage /> },
    ],
  },

상대 경로로 된 자녀 라우트가 있다면 리액트 라우터는 부모 라우트의 경로를 보고 자녀 라우트를 부모 라우트 경로 뒤에 첨부하게 된다

<p>
   Go to <Link to="products">the list of products</Link>.
</p>

 

Link에 relative 프로퍼티를 추가할 수 있는데 이건 path나 route 중 하나로 설정할 수 있다

이걸로 세그먼트를 현재 활성인 라우트 경로에 대해 상대적으로 추가하는지 혹은 URL에서 현재 활성인 경로에 대해 추가하는지 제어한다

<p>
   <Link to=".." relative="path">
     Back
   </Link>
</p>

기본값이 route인데 이건 라우트 정의에 대해 상대적이고 path로 설정하면 라우터는 대신 현재 활성인 경로를 살펴보고 그 경로에서 한 세그먼트만 제거하게 된다

".."이 아니라 절대 경로가 있으면 항상 그 절대 경로가 도메인 뒤에 추가되기 때문에 중요하지 않다

상대 경로가 있을 때 relative 프로퍼티를 사용해 리액트 라우터의 거동을 제어할 수 있다

 

 

인덱스 라우트

index 프로퍼티를 추가해 true로 설정하면 인덱스 라우트로 변한다

부모 라우트가 현재 활성이면 표시되어야 하는 기본 라우트가 이 라우트라는 의미

{ index: true, element: <HomePage /> }, // path: ''

꼭 써야 하는 건 아니지만 빈 경로를 추가하는 대신 이렇게 해줄 수 있다

 

 


 

 

리액트 라우터가 데이터를 가져오고 그 다양한 상태를 처리하는 것을 도와준다

라우터 정의에 loader 프로퍼티를 추가할 수 있는데 값으로 함수를 취한다

{
  index: true,
  element: <EventsPage />,
  loader: async () => {
    const response = await fetch("http://localhost:8080/events");

    if (!response.ok) {
                // ...
    } else {
      const resData = await response.json();
      return resData.events;
    }
  },
},

 

라우트가 렌더링되기렌더링 되기 직전 JSX 코드가 렌더링 되기 직전에 loader 함수가 리액트 라우터에 의해 트리거 되고 실행된다

그래서 loader 함수 안에서 데이터를 로딩하고 가져올 수 있다

이렇게 loader 데이터를 라우트 컴포넌트 안에서 사용하면 컴포넌트 함수를 더 쉽고 가볍게 고안할 수 있다

 

데이터를 가져오는 수준보다 더 높은 수준에서 useLoaderData를 사용하면 안된다

import EventsList from "../components/EventsList";
import { useLoaderData } from "react-router-dom";

function EventsPage() {
  const events = useLoaderData();

  return <EventsList events={events} />;
}

export default EventsPage;

일반적으로 loader 코드는 우리가 필요로 하는 컴포넌트 파일에 넣는 것을 권장한다

import EventsList from "../components/EventsList";
import { useLoaderData } from "react-router-dom";

function EventsPage() {
  const events = useLoaderData();

  return <EventsList events={events} />;
}

export default EventsPage;

export async function loader() {
  const response = await fetch("http://localhost:8080/events");

  if (!response.ok) {
    // ...
  } else {
    const resData = await response.json();
    return resData.events;
  }
}
import EventsPage, {loader as eventsLoader} from "./pages/Events";

.
.
.

{
            index: true,
            element: <EventsPage />,
            loader: eventsLoader,
          },

Events.js에서 정의하고 export 하는 그 함수를 App.js에서 포인터처럼 지시하면 두 페이지 모두 간소해진다

 

어떤 페이지에 대한 loader는 그 페이지로 이동하기 시작할 때 호출된다

 

 

useNavigation 훅으로 현재 라우트 전환 상태를 확인할 수 있다

전환이 개시되었는지, 데이터가 도착하길 기다리고 있는지, 이미 다 왔는지를 확인할 수 있다

function RootLayout() {
  const navigation = useNavigation();

  return (
    <>
      <MainNavigation />
      <main>
        {navigation.state === "loading" && <p>Loading...</p>}
        <Outlet />
      </main>
    </>
  );
}

로딩 인디케이터는 우리가 전환할 목적지인 페이지에 추가되는 게 아니고 전환이 시작되었을 때 이미 화면에 표시되어 있는 페이지, 컴포넌트에 추가된다

 

loader에서 모든 종류의 데이터를 리턴할 수 있다

숫자, 텍스트, 객체, 응답 객체도 리턴 가능하다

Response() 생성자 함수는 브라우저에 내장된 기능인데 자신만의 응답을 구축할 수 있다

응답 생성자는 첫 번째 인자로 원하는 어떤 데이터든 받을 수 있고 두 번째 인자로 설정할 수 있는 추가 객체를 이용해 그걸 더 자세히 설명 가능하다

const res = new Response("any data", { status: 201 });

이런 식으로 응답의 상태 코드를 설정할 수 있고 loader에서 그런 응답을 리턴할 때마다 리액트 라우터 패키지는 useLoaderData를 사용할 때 우리의 응답에서 자동으로 데이터를 추출할 것이다

그렇기 때문에 useLoaderData가 리턴하는 데이터는 우리가 loader에서 리턴한 응답의 일부인 응답 데이터가 될 것이다

 

 

loader 안에서 정의된 코드는 서버가 아닌 브라우저에서 실행된다

loader 함수에서 어떤 브라우저 API도 사용할 수 있다

loader 함수는 리액트 컴포넌트가 아니기 때문에 useState 같은 리액트 훅은 사용할 수 없다

 

Response를 생성하는 기능 대신 리액트 라우터가 제공하는 헬퍼 유틸리티인 json이 있다

json()은 react-router-dom에서 import 할 수 있고 json 형식의 데이터가 포함된 Response 객체를 생성하는 함수이다

json 함수에 Response에 포함되어야 할 데이터를 넣어주면 된다

return json(
      { message: "Could not fetch events." },
      {
        status: 500,
      }
    );

json함수로 코드를 줄일 수 있고 Response 데이터를 쓰는 곳에서 수동으로 JSON 형식을 파싱 할 필요도 없게 된다

 

action 프로퍼티는 loader와 마찬가지로 함수를 받는다

action 함수 안에서 백엔드에 요청을 전송할 수 있다

loader를 쓸 때와 마찬가지로 브라우저에서 실행되는 코드이다

export async function action({ request, params }) {
  const data = request.formData();

  const eventData = {
    title: data.get("title"),
    image: data.get("image"),
    date: data.get("date"),
    description: data.get("description"),
  };

  const response = await fetch("http://localhost:8080/events", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(eventData),
  });
  if (!response.ok) {
    throw json({ message: "Could not save event." }, { status: 500 });
  }
  return redirect("/events");
}

redirect는 json과 마찬가지로 응답 객체를 생성하는데 특수 응답 객체로 사용자를 다른 페이지로 리다이렉션 하기만 한다

 

 

Form 컴포넌트를 사용하면 Form에서 하는 요청이 자동으로 백엔드로 전송되지 않고 액션으로 전송된다

<Form method='post' className={classes.form}>

Form의 action 프로퍼티 값을 우리가 action을 트리거하려는 라우트의 경로로 설정해 그 action을 지시할 수 있다

 

 

useFetcher 훅은 실행 시 객체를 주는데 유용한 프로퍼티와 메서드가 많이 포함되어 있다

 const fetcher = useFetcher();


  return (
    <fetcher.Form method="post" className={classes.newsletter}>

fetcher.Form은 실제로 액션을 트리거하는데 라우트 전환을 시작하지 않는다

fetcher는 액션을 트리거하거나 실제로 loader가 속한 페이지 또는 그 액션이 속한 페이지로 이동하지 않을 때 사용해야 한다

즉, 라우트 변경을 트리거하지 않은 채로 배후에서 요청을 전송할 때 사용한다

fetcher 객체에는 트리거한 액션이나 loader가 성공했는지 알 수 있게 도와주는 프로퍼티가 많이 포함되어 있다

const fetcher = useFetcher();
  const { data, state } = fetcher;
  
  useEffect(()=>{
    if(state==='idle' && data&& data.message){
      window.alert(data.message)
    }
  },[data,state])

공통된 컴포넌트가 있거나 같은 페이지에서 여러 번 사용되는 컴포넌트가 있을 경우에 배후에서 데이터만 업데이트하거나 받으려고 할 때 유용하다

 

 

Suspense 컴포넌트는 다른 데이터가 도착하길 기다리는 동안에 fallback을 보여주는 특정한 상황에서 사용할 수 있다

<Suspense fallback={<p style={{ textAlign: "center" }}>Loading...</p>}>
      <Await resolve={events}>
        {(loadedEvents) => <EventsList events={loadedEvents} />}
      </Await>
    </Suspense>

 

 

728x90