본문 바로가기

온라인 강의(유데미, 인프런 등)/React 완벽 가이드(유데미)

[react] React Router v6.4 loader/action/fetchers

반응형

React Router 부분 강의를 듣다가 이전에 배운 적 없었던 개념인 loader, action, fetchers에 대해 배우게 되었다. 구글링 해보니 작년 후반기쯤 업데이트가 되어 자료가 별로 없는 것 같지만 다행히 아주 잘 정리되어 있는 블로그가 있어 해당 블로그들과 인강을 통해 개념을 이해해 보자.

나중에 꼭 리액트 쿼리에 대해 배우고 싶었는데(멘토님이 실시간 데이터 업데이트 부분 말씀하시면서 한 번 언급하셨던 걸 기억하여), 리액트 라우터와 리액트 쿼리가 환상의 짝이라고 하니!! 더욱 정리해 놓아야겠다는 생각이 들었다.

리액트 라우터와 리액트 쿼리는 환상의 짝입니다.

 

React Router

메인 프로젝트에서 데이터 실시간 업데이트 면에서 아주 애를 먹었기 때문에(useAxios 훅 만들어서~ 종속성 배열에 값 추가하고~ 어떤 건 안 돼서 setTimeout 넣어주고~ㅠㅠ), 밑에서 살펴볼loader 함수의 개념은 꽤 흥미롭게 다가왔다.
"페이지가 렌더링 되기 전에 데이터가 가져와진다고?! 그런 게 있었어!?"

6.4 버전에서 가장 주목할 특징은 client side browse를 제공한다는 점이라고 한다. 즉 서버에 요청하지 않고 클라이언트 사이드에서 페이지 이동 처리할 걸 다 해서 속도 면에서 엄청난 이점이 있다고 한다! 

데이터를 "가능한 한 빨리-when" 가져오는 것은 최상의 사용자 경험을 제공하기 위한 중요한 개념입니다.

nextjs 또는 Remix와 같은 풀 스택 프레임워크는 이 단계를 서버로 이동합니다. 왜냐하면 이것이 가장 빠른 진입점이기 때문입니다.
클라이언트 렌더링 애플리케이션에서는 이러한 사치를 부릴 수 없습니다.
 
중요 개념은 총 세 가지 이다 => loaders, actions, fetchers  
 
이 세가지 API는 서버에서 해줘야 할 작업들을 클라이언트측으로 옮겨왔다고 생각하면 된다. data api를 요약하면 다음과 같다.
  • loader : 컴포넌트가 생성되기 전에 컴포넌트에 데이터를 전달한다.
  • actions : url에 form과 같은 리퀘스트를 보낼 때 데이터를 처리하는 부분이다.
  • fetchers : url을 변경하지 않고, 요청한 url에 데이터를 요청한다.

 

loader()

loader 함수를 사용하면 컴포넌트가 렌더링 되며 데이터를 가져오는 것을 기다리는 과정에 필요한 로딩 화면을 만들어주지 않아도 되나 보다.

loader의 핵심 개념은 다음과 같다!

  • loader의 호출 시점은 컴포넌트가 렌더링 되기 전이다.
  • 각 route 파일에 loader라는 함수를 만든 뒤, 이를 export 하여 사용하는 것이 일반적이다.
  • loader 함수가 값을 리턴하면 useLoaderData()로 컴포넌트에서 데이터를 받을 수 있다.
  • GET 요청을 하면 loader가 호출된다.

 

tutorial에서 나온 예시를 살펴보자. 연락처 목록이 있다고 가정하고 그중 하나를 클릭하면 연락처 세부 정보가 표시된다고 하자. 

src/routes/contact.jsx

import { 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()
  // render some jsx
}

src/routes/main.jsx-> 라우터 설정

import Contact, { loader as contactLoader } from './routes/contact'

const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    children: [
      {
        path: 'contacts',
        element: <Contacts />,
        children: [
          {
            path: 'contacts/:contactId',
            element: <Contact />,
            // ⬇️ 세부 경로에 로더 사용을 설정합니다.
            loader: contactLoader,
          },
        ],
      },
    ],
  },
])

contact/1로 이동하면 컴포넌트가 렌더링 되기 전에 해당 연락처에 대한 데이터를 가져와져 useLoaderData를 사용해 해당 데이터를 이용하여 지지고 볶아 줄 수 있다. 이것은 사용자 경험을 향상할 뿐만 아니라 동시에(co-located) 데이터 가져오기 및 렌더링 관련 개발자 경험을 개선해준다고 한다.

 

Action

우리가 action을 배울 때 주목해야 할 부분은 HTML form이다. HTML form은 특정 url에 데이터를 전송해서 처리하는 요청 과정이다. 그리고 그 요청을 처리할 주소 값은 보통 action에다가 정의한다.

클라이언트 사이드에서 form을 처리하기 위해 리액트 라우터는'Form'이라는 것을 사용한다. 그리고 이는 html form을 모방하여 클라인트 측에다 리퀘스트를 보낸다.

<form> // html 폼
<Form> // react router의 폼. 클라이언트 사이드에서 처리한다

즉 <form>을 사용하면 서버에다가 리퀘스트를 보내는 것이고, <Form>을 사용하면 클라이언트 측에다가 리퀘스트를 보내는 것이다. 그리고 클라이언트 측에서 리퀘스트를 받았다면, 이는 action에서 처리한다.

action의 핵심 개념을 정리해 보자.

  • 클라이언트 측에서 form을 처리하기 위해 Form을 사용하고 이는 클라이언트에 리퀘스트를 보내는 것이다.
  • 클라이언트 측에서 리퀘스트를 받으면 action이 이를 처리하는데, POST 요청 시 호출된다.

 

새로운 연락처를 등록하는 기능을 만든다고 가정해 보자.

routes/root.jsx -> <form>을 <Form>으로 바꾸기(method는 post로!)

이렇게 해두면 요청이 백엔드로 가지 않고 우리가 만든 action으로 전송된다.

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

src/routes/edit.jsx

edit 컴포넌트를 만들었다고 가정하고, action을 추가해 준 뒤 데이터 업데이트가 완료되면 리다이렉트 해준다.

formData()를 사용하면 action 함수에 전달된(요청에 사용된) 데이터를 받을 수 있다.

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

export async function action({ request, params }) {
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`);
}

src/main.jsx -> 액션을 라우터에 연결하기

/* existing code */
import EditContact, {
  action as editAction,
} from "./routes/edit";

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

 

fetcher

지금까지 우리는 form에 데이터를 날려서 navigate 되는 형태로 코드를 작성해 왔다. 때때로는 navigate 하지 않고 데이터를 변화시키고 싶을 때가 있다. 이때 사용하는 것이 fetcher이다.

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

fetcher의 핵심 기능을 정리해 보자

  •  navigate 하지 않고 Form을 이용해 loader와 action과 커뮤니케이션할 수 있다. =>  Form을 사용했을 때와 가장 큰 차이점이다.

 

fetcher를 사용하여 Favorite 기능을 구현한다고 해보자.

src/routes/contact.js

fetcher.Form을 사용하면 액션을 트리거 하지만 라우팅은 하지 않는다. 그래서 fetcher는 액션을 트리거하거나 loader 함수의 도움으로 loader를 트리거하지만 실제로 그 loader가 속한 페이지 또는 그 액션이 속한 페이지로 이동하지 않을 때 사용해야 한다.

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

위 코드를 보면 contact에서 favorite 값을 받아온다. 그리고 이를 통해 css를 분기한다.

src/routes/contact.jsx-> action을 정의하기

마지막으로 fetcher를 이용하여 favorite 값을 바꾸도록 action에 데이터를 보내는 형태로 되어 있다.

// 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
}

src/main.jsx -> route 연결하기

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

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

그러면 아래처럼 favorite 버튼기능이 완성된다.

 

이렇게하여 React Router 업데이트 내용과 관련 튜토리얼이 잘 설명된 블로그와 인강을 참고하여 간단하게 세 가지 개념에 대해 살펴보았다. 확실히 내가 직접 사용해보지 않아 개념만 어렴풋이 이해되고 피부로 확 와닿은 상태는 아니지만, 언젠가 이 개념들을 사용하여 프로젝트를 진행하게 될 경우를 대비하여 관련 개념을 미리 학습해 두면 더 좋지 않을까 하는 기대감을 가져본다.

 

이 포스트는 loader, action, fetchers의 핵심개념을 한눈에 볼 수 있게 매우 축약하여 정리하는 것에 포커스가 맞춰져 있으므로, 예시 코드도 필요한 부분만 가져와 한눈에 이해가 되지 않을 수도 있다.

튜토리얼을 보다 자세히 공부하고 싶다면 튜토리얼 과정을 아주 잘 설명해 둔 블로그를 참고해 보자.

https://lucky516.tistory.com/217

 

React Router v6.4 튜토리얼 배우기

https://reactrouter.com/en/main/start/tutorial Tutorial Tutorial Welcome to the tutorial! We'll be building a small, but feature-rich app that let's you keep track of your contacts. We expect it to take between 30-60m if you're following along. 👉 Every

lucky516.tistory.com

 

 

아래의 블로그는 리액트 라우터와 리액트 쿼리를 연계 사용하고 싶을 때 참고하자.

https://itchallenger.tistory.com/719#recentEntries

 

리액트 쿼리 : 리액트 라우터와 연계하기

리액트 쿼리와 리액트 라우터를 함께 효과적으로 사용하는 방법을 배워봅니다. 원문 링크입니다. https://tkdodo.eu/blog/react-query-meets-react-router React Query meets React Router React Query and React Router are a match

itchallenger.tistory.com

 

반응형
SMALL