THE DEVLOG

scribbly.

etc

2024.03.26 17:07:19

React Router Dom v6

  1. 설치

yarn create vite . --template react-ts
yarn install

  1. CSS 리셋
    먼저 CSS를 리셋해준다.
    index.css에 Eric Meyer’s “Reset CSS” 2.0를 추가해준다.

  2. react-router-dom 적용
    SPA는 그 이름답게 페이지를 이동하지 않는다. 하지만 History 관리(뒤로가기, 앞으로가기 등)와 쿼리 스트링을 위해 Routing을 사용할 필요성이 있다.

벨로퍼트 - React Router v6 튜토리얼

설치

yarn add react-router-dom
src/main.tsx

BrowserRouter

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { BrowserRouter } from "react-router-dom"; // 추가

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter> {/* //추가 */}
      <App />
    </BrowserRouter> {/* //추가 */}
  </React.StrictMode>
);

src\pages\About\index.tsx

const About = () => {
  return <div>index</div>;
};

export default About;

src\pages\Home\index.tsx

const Home = () => {
  return <div>index</div>;
};

export default Home;

Route, Routes

src\App.tsx

import { Route, Routes } from "react-router-dom";
import About from "./pages/About";
import Home from "./pages/Home";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/profiles/:username" element={<Profile />} />
    </Routes>
  );
}

export default App;

profile 페이지를 만들어 진행해보자.
'/profiles/e'로 접속한다.
버튼을 누르면 주소가 '/profiles/e?detail=true&mode=1'로 바뀐다.

useLocation, useParams, useSearchParams

src\pages\Profile\index.tsx

import { useLocation, useParams, useSearchParams } from "react-router-dom";

const Profile = () => {
  const location = useLocation(); // {"pathname":"/profiles/e","search":"?detail=true&mode=1","hash":"","state":null,"key":"xmwls75l"}
  const params = useParams(); // {"username":"e"}
  const [searchParams, setSearchParams] = useSearchParams(); // detail=true&mode=1 => { detail:"true", mode:"1" }

  return (
    <div>
      <div>{JSON.stringify(location)}</div>
      <div>{JSON.stringify(params)}</div>
      <div>{searchParams.get("detail")}</div>
      <div>{searchParams}</div>
      <button
        type="button"
        onClick={() => setSearchParams({ detail: "true", mode: "1" })}
      >
        버튼
      </button>
    </div>
  );
};

export default Profile;

useLocation() : 현재 주소와 쿼리스트링, 그리고 State를 반환한다. State는 페이지 이동에서 사이드 이펙트가 필요한 경우에 주로 사용한다.
useParams() : Route의 Path Variable으로 넘겨주는 인자들을 객체에 담아 반환한다.
useSearchParams() : 첫번째 인자로 쿼리스트링을 Parsing하여 객체 형태로 반환한다. 두번째 인자는 setSearchParams으로, 문자열을 넘겨주거나("detail=true&mode=1"), 객체를 넘겨주면 된다.

그럼 Path Variable과 Query Parameter를 각각 언제 사용해야 하는가?Integerous DevLog

만약 어떤 resource를 식별하고 싶으면 Path Variable을 사용하고,
정렬이나 필터링을 한다면 Query Parameter를 사용하는 것이 Best Practice이다.

Link

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

const user = {
  name: "김복남",
  age: 32,
};

function Home() {
  return (
    <div>
      <Link to={`/profiles/${user.name}`} state={{ age: user.age }} />
    </div>
  );
}

export default Home;

Link 태그는 라우팅을 변경시킨다.
state를 넘겨주고 싶을 때에는 state 프롭을 사용한다.
useLocation을 통해 반환받는 객체의 state에 해당 값이 저장된다. #

{"pathname":"/profiles/김복남","search":"","hash":"","state":{"age":32},"key":"guz9ncgo"}

상대경로를 사용할 수도 있다. #

레이아웃과 중첩된 Routing

    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/profiles/:username" element={<Profile />} />
        <Route path="/profiles" element={<Profiles />}>
          <Route path=":id" element={<Profile />} />
        </Route>
      </Route>
  	  <Route path="*" element={<NotFound />} />
    </Routes>

위와 같이 중첩된 라우팅을 만들 수 있다. path 프롭스 대신 'index'는 라우트의 상위 경로와 일치할 때 사용한다.

한편 path="*"과 같이 와일드카드 *을 사용하면 모든 문자를 인식한다. 위에서는 404 페이지를 넣었다.

레이아웃을 만들어보자.

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

const Layout = () => {
  return (
    <div>
      <header>헤더입니다.</header>
      <main>
        <Outlet />
      </main>
    </div>
  );
};

export default Layout;

Outlet은 Route 요소에서 하위 Route 요소를 렌더링하고자 할 때 사용한다.

useNavigate

  const navigate = useNavigate();

  const goBack = () => {
    navigate(-1);
  };
  const goHome = () => {
    navigate("/", { replace: true });
  };
  const goProfile = () => {
    navigate("/profiles/e", { state: { age: "32" } });
  };

navigate는 페이지를 이동할 때 사용하는 function이다.
첫번째 인자로 -1을 넘기면 뒤로 이동한다.
두번째 인자는 navigate의 options인데,
replace가 true인 경우에는 브라우저 히스토리에 현재의 페이지를 남기지 않는다.
또한 state도 options로 넘길 수 있다.

Navigate

Navigate는 해당 컴포넌트가 마운트되는 순간 이동할 때에 사용한다.
주로 리다이렉트에 사용된다.

const MyPage = () => {
  const isLoggedIn = false;

  if (!isLoggedIn) {
    return <Navigate to="/login" replace={true} />;
  }

  return <div>마이 페이지</div>;
};

NavLink

네비게이션을 만들 때에,
아래와 같이 현재의 주소와 Link 태그가 이동하고자 하는 주소가 같은지를 확인해야 하는 경우가 있다.

function Home() {
  const location = useLocation();
  const isActive = location.pathname === "/";

  return (
    <div>
      <Link to={"/"} style={isActive ? { color: "gray" } : {}}>
        홈으로 이동하기
      </Link>
    </div>
  );
}

이와 같이 현재의 주소와 이동하고자 하는 주소가 같은지를 판별하여, style 또는 className의 콜백 콜백함수의 인자로 넘겨주는 것이 NavLink이다.

function Home() {
  return (
    <div>
      <NavLink
        to={"/"}
        className={({ isActive }) => (isActive ? "activated-link" : "")}
        style={({ isActive }) => (isActive ? { color: "gray" } : {})}
      >
        홈으로 이동하기
      </NavLink>
    </div>
  );
}

RouterProvider와 CreateBrowserRouter

React Router v6.4에 추가된 방식이다.
참고 DH의 개발 공부로그

main.tsx에 BrowserRouter는 추가하지 않는다.

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

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

대신 App.tsx에서 RouterProvider를 추가한다.

import { RouterProvider } from "react-router-dom";
import { router } from "./router";

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

export default App;

해당 엘리먼트는 router 객체를 필수적으로 넘겨줘야 한다.
지금까지 작업한 Routes를 router 객체로 만들면 아래와 같다.

import { createBrowserRouter } from "react-router-dom";
import Layout from "./pages/Layout";
import Home from "./pages/Home";
import About from "./pages/About";
import Profile from "./pages/Profile";
import Profiles from "./pages/Profiles";
import NotFound from "./pages/NotFound";

export const router = createBrowserRouter([
  {
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: "/about",
        element: <About />,
      },
      {
        path: "/profiles",
        element: <Profile />,
        children: [
          {
            path: ":id",
            element: <Profiles />,
          },
        ],
      },
    ],
  },
  {
    path: "*",
    element: <NotFound />,
  },
]);

router 객체는 createBrowserRouter 함수에 RouteObject의 배열을 인자로 넘겨 생성한다.

RouteObject는 다음의 프로퍼티들을 가진다.

index : 인덱스 여부. 기본값 false
path : 주소
element : 렌더링할 요소
children : 자식요소

Recoil

Redux는 '하나의 source of truth'를 매우 강조한 방식이다. 반면 Recoil은 Atoms라는 여러 개의 source of truth를 활용한다.

MobX는 클래스를 선언하여 상태 관리를 진행한다. 반면, Recoil은 자바스크립트 Object를 인자로 받아 Atom을 생성하며, 생성된 Atom은 별도의 Observer 함수 없이 리액트 컴포넌트에서 호출하여 사용하게 된다. 이를 통해 러닝커브가 낮고, 기존 리액트 Hook과 잘 어우러진다.

보일러 플레이트가 적고, 활용이 쉽다는 점에서 Recoil은 장점이 있다.
하지만 비동기 처리가 까다롭다는 단점이 있어, 주로 React-Query와 함께 사용된다.

설치

yarn add recoil

RecoilRoot

리코일의 기능을 제공해주는 상위 컴포넌트이다.
App.tsx에 추가해준다.

import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import { RecoilRoot } from "recoil";

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

export default App;

폴더 구조

Wesley Rast - Recoil Project Structure Best Practices
parkjju - 리코일 Best-Practices
엉썬님 번역글

리코일은 크게 Atoms - 상태의 원천, Selectors - 상태에서 파생된 데이터로 나뉜다. 이에 따라 폴더구조를 atoms와 selectors로 나누는 경우 아래와 같은 문제가 발생하는데,

  1. import 구문이 지나치게 많아진다. -> index 파일을 이용해 해결할 수 있다.
  2. 관심사별로 atoms를 나누기 힘들며, selector가 어떤 값을 반환하는지 알기 어렵다.

이에 따라 아래와 같이 폴더 구조를 작성하는 것이 좋은 방법이 될 수 있다.

recoil
├── category
│   ├── atom.ts
│   └── index.ts
└── todo
    ├── index.ts
    ├── atom.ts
    └── withCompleted.ts

todo/atom을 통해 해당 atom이 todo와 관련된 아톰임을 알 수 있다.
todo/withCompleted는 with라는 접두사를 통해 selector임을 나타내고, completed인 상태를 반환함을 명시적으로 알 수 있게 된다.
index.ts는 selector가 있는 경우에 추가합니다. atom을 export default 하고, 셀렉터는 인스턴스로 반환한다.

시작하기 전에 src\types\todo.ts에 아래와 같이 todo 타입을 선언한다.

export interface Todo {
  id: string;
  title: string;
  isDone: boolean;
}

export type TodoList = Todo[];

Atoms

Atom은 상태의 일부를 나타낸다.
Atom의 값을 읽는 컴포넌트들은 Atom을 구독하며, Atom의 변화에 따라 리렌더링이 일어난다.
atom<T>(options)함수는 쓰기 가능한 RecoilState를 반환한다.

options
key: 내부적으로 atom을 식별하는데 사용되는 고유한 문자열이다. (주로 해당 아톰의 이름을 key로 하면 중복되지 않는다.)
default: atom의 초기값이다. 기본값을 atom, selector의 반환값이나 Promise로 줄 수도 잇다.

src\recoil\todo\atom.ts

import { atom } from "recoil";
import { TodoList } from "../../types/todo";

const todoAtom = atom<TodoList>({
  key: "todo",
  default: [],
});

export default todoAtom;

Selectors

Selector는 '파생된 상태(derived state)'의 일부를 나타낸다.
selector<T>(options)
options
key : Selectors의 고유값이다.
get : 값을 평가하는 함수이다. 인자로 get 함수를 넘긴다.
get(RecoilValue<T>) : atom이나 selector로부터 값을 찾는데 사용되는 함수이다. 이 함수에 전달된 atom 혹은 selector는 해당 selector의 의존성 목록에 추가된다.
set? : selector에 set 속성이 없는 경우엔 RecoilValueReadOnly, set 속성이 있는 경우엔 RecoilState을 반환한다.
get(RecoilValue<T>) : atom이나 selector로부터 값을 찾는데 사용되는 함수이다. 이 함수에 전달된 값이 의존성 목록에 추가되진 않는다.
set(RecoilState<T>, ValueOrUpdater<T>) : Recoil 상태를 받아 새로운 값으로 업데이트 한다.
reset(RecoilState<T>) : 상태를 초기화한다.

아래는 isDone이 true인 todo만 Array로 반환하는 예시이다.

import { selector } from "recoil";
import todoAtom from "./atom";
import { TodoList } from "../../types/todo";

const todoWithCompleted = selector<TodoList>({
  key: "todoWithCompleted",
  get: ({ get }) => {
    const todoList = get(todoAtom);
    return todoList.filter((todo) => !!todo.isDone);
  },
});

export default todoWithCompleted;

이렇게 추가된 Atom과 Selectors에 대해 index.ts 파일을 만든다.
src\recoil\todo\index.ts

import todoAtom from "./atom";
import withCompleted from "./withCompleted";

export { withCompleted };
export default todoAtom;

export default로 todoAtom을 선언함으로써 selector 없이 atom만을 사용하는 경우와 동일하게 import 된다.

위의 atom과 selector를 컴포넌트에서 import하는 경우 아래와 같다.

import todoAtom, { withCategory } from 'recoil/todo';

useRecoilState(), useRecoilValue(), useSetRecoilState(), useResetRecoilState()

useRecoilState(atom)은 atom을 읽고 쓰려고 할 때에 이 훅을 사용한다. useRecoilState(atom)는 [RecoilState, SetterOrUpdater]를 반환한다.
읽거나, 쓰기만 하는 경우에는 useRecoilValue, useSetRecoilState를 반환한다. 각각 RecoilState와 SetterOrUpdater를 반환한다.

아래는 간단한 Todo 앱의 예시이다.
src\pages\Todo\index.tsx

import { useRecoilState } from "recoil";
import todoAtom from "../../recoil/todo/atom";
import { Todo } from "../../types/todo";

const TodoPage = () => {
  const [todoList, setTodoList] = useRecoilState(todoAtom);

  const onAdd = () => {
    const newData: Todo = {
      id: String(todoList.length + 1),
      isDone: false,
      title: "새 할일",
    };
    setTodoList([...todoList, newData]);
  };

  return (
    <div>
      {todoList.map((todo) => {
        return (
          <div key={todo.id} style={{ display: "flex" }}>
            <div>{todo.id}</div>
            <div>{todo.isDone}</div>
            <div>{todo.title}</div>
          </div>
        );
      })}
      <button type="button" onClick={onAdd}>
        새 할일 추가하기
      </button>
    </div>
  );
};

export default TodoPage;

atomFamily

kelly woo - Recoil — 새로운 리액트 상태 관리 라이브러리?
atomFamily는 atom과 동일한 역할을 수행하지만, params를 넘길 수 있다는 점에서 다르다. atomFamily에서는 하나의 key 값을 갖지만,

아래와 같은 상황이 있다고 해보자.

  1. 이미지를 호출하는 url이 있다.
  2. 해당 url은 todo의 id를 param으로 받는다.

아래와 같이 get 요청을 통해 해당하는 이미지 객체를 생성할 수 있다.

src\types\image.ts

export interface IImage {
  id: string;
  name: string;
  url: string;
  metadata: {
    width: string;
    height: string;
  };
}

src\services\getImage.ts

import { IImage } from "../types/image";

const getImage = async (id: string) => {
  return new Promise<IImage>((resolve) => {
    const url = `http://someplace.com/${id}.png`;
    let image = new Image();
    image.onload = () =>
      resolve({
        id,
        name: `Image ${id}`,
        url,
        metadata: {
          width: `${image.width}px`,
          height: `${image.height}px`,
        },
      });
    image.src = url;
  });
};

export default getImage;

이를 이용하여 id를 param으로 넘기고 이미지를 반환받는 atom 패밀리를 만들어 보자.

import { useRecoilValue } from "recoil";
import imageAtom from "../../recoil/image/atom";

const Image = ({ id }: { id: string }) => {
  const { name, url } = useRecoilValue(imageAtom(id));

  return (
    <div className="image">
      <div className="name">{name}</div>
      <img src={url} alt={name} />
    </div>
  );
};

export default Image;

selectorFamily

Selectors Family in Recoil for Statement Management — Nextjs

selectorFamily 역시 selector와 동일한 역할을 수행하지만, get과 set 함수에서 parameter를 받을 수 있다는 점에서 다르다.

앞서 만든 예시에서 todo앱의 title을 수정하는 input 태그를 만들어보자.

src\components\todo\TitleInput.tsx

const TitleInput = ({ id }: { id: string }) => {
  return <div>Input</div>;
};

export default TitleInput;

이제 selectorFamily를 만들 차례이다.

src\recoil\todo\withId.ts
먼저 get 함수를 정의하자.

import { selectorFamily } from "recoil";
import todoAtom from ".";

const withId = selectorFamily({
  key: "todoWithId",
  get:
    (id: string) =>
    ({ get }) => {
      const todoList = get(todoAtom);
      const todo = todoList.find((todo) => todo.id === id);
      return todo;
    },
});

export default withId;

id 값을 받아 일치하는 id 하나를 반환한다.

set함수를 이용하여 특정한 data를 변경하도록 작성한다. 이때 set함수의 newValue가 todo가 되도록 한다.

import { DefaultValue, selectorFamily } from "recoil";
import todoAtom from ".";
import { Todo } from "../../types/todo";

const withId = selectorFamily<Todo | undefined, string>({
  key: "todoWithId",
  get:
    (id) =>
    ({ get }) => {
      const todoList = get(todoAtom);
      const todo = todoList.find((todo) => todo.id === id);
      return todo;
    },
  set:
    (id) =>
    ({ get, set }, newValue) => {
      const todoList = get(todoAtom);
      const todoIndex = todoList.findIndex((todo) => todo.id === id);
      const newTodoList = [...todoList];

      // 상태가 설정되어 있지 않은 경우 newValue는 recoil DefaultValue의 instance이므로 예외처리 한다.
      if (newValue instanceof DefaultValue) {
        return newTodoList[todoIndex];
      }
      // newValue를 인자로 넘기지 않은 경우 예외처리 한다.
      if (newValue === undefined) {
        return newTodoList[todoIndex];
      }

      newTodoList[todoIndex] = newValue;
      set(todoAtom, newTodoList);
    },
});

export default withId;

마지막으로 index.ts에 추가해준다.

import todoAtom from "./atom";
import withCompleted from "./withCompleted";
import withId from "./withId";

export { withCompleted, withId };
export default todoAtom;

이제 TitleInput에서 해당 atom을 불러와 사용하자

import { useRecoilState } from "recoil";
import { withId } from "../../recoil/todo";
import { ChangeEventHandler } from "react";

const TitleInput = ({ id }: { id: string }) => {
  const [todo, setTodo] = useRecoilState(withId(id));
  if (!todo) return <></>;

  const onChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
    const title = e.target.value;
    setTodo({
      ...todo,
      title,
    });
  };

  return <input type="text" value={todo.title} onChange={onChangeHandler} />;
};

export default TitleInput;

Setter 함수의 공통화 (Reducer 패턴)

Redux나 Mobx에 비해, Recoil은 Setter함수를 한 곳에 모아 관리하기가 까다롭다.

다음의 해결책들을 구상해볼 수 있다.

Custom Hook

참고 : Showmaker(Medium)

import { useSetRecoilState } from "recoil";
import counterAtom from "../recoil/todo";

const useRecoilCounter = () => {
  const setCounter = useSetRecoilState(counterAtom);

  const upCount = () => setCounter((prev) => prev + 1);
  const downCount = () => setCounter((prev) => prev - 1);

  return { upCount, downCount };
};

export default useRecoilCounter;

useRecoilCounter라는 커스텀 훅을 통해 setter 함수들을 반환받는 방식이다.
코드가 간략하다는 장점이 있으나,
커스텀 훅을 호출할 때마다 함수가 다시 선언된다는 점에서 비효율적이다.

Selectors

export const incrementSelector = selector({
  key: "incrementSelector",
  get: ({ get }) => get(counterState),
  set: ({ set, get }) => set(counterState, get(counterState) + 1)
});

export const decrementSelector = selector({
  key: "decrementSelector",
  get: ({ get }) => get(counterState),
  set: ({ set, get }) => set(counterState, get(counterState) - 1)
});

위와 같이 selector를 이용하여 패턴을 만들 수 있다.

useRecoilTransaction_UNSTABLE

공식문서

import { useRecoilTransaction_UNSTABLE } from "recoil";
import { countAtom } from "../recoil/counter/atom";

export const useCounterReducer = () => {
  return useRecoilTransaction_UNSTABLE(
    ({ get, set }) =>
      (action: { type: "sum" | "dim"; value: number }) => {
        const count = get(countAtom); // 특정 state의 값을 불러오고 싶은 경우에 사용한다.
        switch (action.type) {
          case "sum":
            set(countAtom, (value) => {
              return value + action.value; //value는 첫번째 인자의 현재 값이다.
            });
            break;
          case "dim":
            set(countAtom, () => count - action.value);
            break;
        }
      }
  );
};

React.Suspense와 에러 경계(Error Boundaries)

React.Suspense

React Suspense란? (Jun Song - Medium)
React.Suspense는 Suspense 이하의 컴포넌트가 로딩될 때까지 특정 컴포넌트로 대체한다.

<Suspense fallback={<Spinner id="comments-spinner" />}>
  <Comments />
</Suspense>

fallback에는 로딩이 끝날 때 까지 대신 보여줄 컴포넌트를 삽입한다.
주로 로딩 스피너 등을 삽입한다.

클라이언트에서 최초로 HTML을 받을 때, fallback의 컴포넌트를 렌더링 한다.

<main>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>

Suspense 이하의 컴포넌트가 렌더링 할 준비가 되면,
스트림으로 추가적인 HTML을 전송한다. (인라인 스크립트의 형식을 따른다.)

<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

이때 특이한 점은 해당 <Comments /> 컴포넌트는 Hydration이 이루어지지 않는다.

Hydration : "이벤트 핸들러 함수들" 을 정적인 DOM 노드들에 붙여서 동적으로 상호작용이 가능하도록 바꾸어주는 기능
React-hydration이란-root란 (@chltjdrhd777)

Suspense 태그를 사용하면, 사용자와의 인터랙션이 일어날 때에 '동기적으로' Hydration을 진행한다. 이러한 특성을 성능 최적화에 사용할 수 있다.

Error Boundary

에러경계 (공식문서)
에러 바운더리 코드 (yiyb-blog)
카카오 기술 블로그
react-error-boundary 라이브러리
에러 바운더리는 클래스 컴포넌트의 "getDerivedStateFromError" 메서드를 이용하여 구현이 가능하다.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

this.state에 hasError라는 플래그를 세운다.
getDerivedStateFromError 메서드를 통해 에러를 catch하는 경우, hasError를 true로 한다.

render 함수에서 this.state.hasError가 truthy한 값일 때에 특정 컴포넌트를 렌더링하도록 한다.

React-Query와 Error Boundary

React-Query의 Query Client 생성자에는 useErrorBoundary 옵션이 주어진다.
해당 옵션을 true로 하면, React-Query의 onError 메서드가 비활성화되고, 에러 바운더리로 error를 throw 한다.

만일, 에러가 발생했을 때에 retry를 하고 싶다면, useQueryErrorResetBoundary 훅을 이용하여 reset 함수를 반환받을 수 있다.
코드 참고 : React Error Boundary를 사용하여 에러 핸들링하기(react-query) - datoybi

function Page() {
  const { reset } = useQueryErrorResetBoundary();
  return (
    <Article>
      <ErrorBoundary onReset={reset}>
        <AccountMain />
      </ErrorBoundary>
    </Article>
  );
}

에러 바운더리 코드 (yiyb-blog)

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
      return <button type="button" onClick={this.props.onReset()}>다시 시도하기</button>;
    }

    return this.props.children;
  }
}

React-Query

시작하기

npm i @tanstack/react-query를 통해 설치한다.
QueryClientProvider를 최상단으로 두어 App을 감싼다.
이때 new QueryClient()라는 생성자를 이용해 queryClient를 생성하고, 이를 인자로 넘긴다.

import { RouterProvider } from 'react-router-dom';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { router } from './router';

const App = () => {
  const queryClient = new QueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      <ErrorBoundary onReset={reset}>
        <RouterProvider router={router} />
      </ErrorBoundary>
    </QueryClientProvider>
  );
};

export default App;

QueryClient

QueryClientProvider를 통해 어플리케이션에 캐시 저장소를 제공할 때에,
QueryClient 생성자에 default option 객체를 넣어 queries와 mutations에 기본 옵션을 줄 수 있다.

export const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: error => {
      console.log(error);
    },
    onSuccess: data => {
      console.log(data);
    },
    onSettled: (data, error) => {
      console.log(data, error);
    },
  }),
  mutationCache: new MutationCache({
    onError: error => {
      console.log(error);
    },
    onSuccess: data => {
      console.log(data);
    },
  }),
  defaultOptions: {
    queries: {
      staleTime: Infinity,
      refetchOnWindowFocus: false,
      throwOnError: true,
    },
    mutations: {
      throwOnError: true,
    },
  },
});

queryCache : query 저장소가 업데이트 될 때에 자동으로 수행할 콜백 함수들이다. onError, onSuccess, onSettled

mutationCache : mutation이 될 때에 자동으로 수행할 콜백 함수들이다. onError, onSuccess
defaultOptions : queries와 mutations에 줄 기본 옵션을 담은 객체이다.
queries : 쿼리 저장소에 적용될 옵션들이다.
mutations : 뮤테이션 시에 사용될 옵션들이다.
staleTime : query를 저장할 기본 시간(milliseconds)이다. 0인 경우에는 사실상 캐싱이 되지 않는다.
refetchOnWindowFocus : 화면을 전환할 때마다 새로 캐싱을 할지 여부이다.
throwOnError : 에러가 발생했을 때에 리액트 쿼리에서 에러를 핸들링할지, throw할 지 여부이다. 에러 바운더리를 사용하는 경우 이 옵션을 true로 둔다. true로 하면 status가 error로 바뀌지 않으며, onError도 사용되지 않는다.
suspense : 해당 옵션을 사용하면 status가 loading으로 바뀌지 않고 pending 상태로 남는다. suspense 컴포넌트를 사용하는 경우 이 옵션을 true로 둔다.