[Ant Design] Message를 Route와 상관없이 Global하게 사용하기

마리 Mari·2023년 11월 20일

문제

react-router-dom의 useNavigate과 antd message.useMessage의 messageApi가 동시에 동작하지 않는 문제가 있었다. 새 게시글 등록이 완료되면 작성된 게시글 상세 페이지로 이동 시킨 후 "등록이 완료되었습니다"라는 메세지를 띄우려고 했는데, 메세지가 나오지 않았다. 페이지를 이동하면서 기존 페이지 안에 정의해둔 useMessage의 context holder의 범위를 벗어나면서 메세지가 뜨지 못하게 된 것으로 보였다.
Global 범위에 contextHolder를 배치하면 해결되겠지만, messageApi를 어떻게 전달해주면 효율적일지 고민이 되어 방법을 찾아보았다. 결론부터 말하자면

결론

  1. 전체 route를 감싸는 layout 만들기
  2. layout에 contextHolder 붙이기, Outlet의 context props로 messageApi 내려주기.
  3. useOutletContext를 이용하여 messageApi 받아와 사용하기

코드로 보기

1. route를 감싸는 layout 만들기

// index.tsx, import 생략
const rootElement = document.getElementById("root")!;
const root = ReactDOM.createRoot(rootElement);

const router = createBrowserRouter([
  {
    element: <AppLayout />,
    children: [
      { path: "/", element: <App /> },
      { path: "/create", element: <AnotherPage /> }
    ]
  }
]);

root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
)

2. layout에 contextHolder 붙이기, context로 messageApi 내려주기.

// AppLayout.tsx
import { Outlet, useOutletContext } from "react-router-dom";
import { MessageInstance } from "antd/lib/message/interface";

type ContextType = { messageApi: MessageInstance };

export const AppLayout = () => {
  const [messageApi, contextHolder] = message.useMessage();
  return (
    <>
      {contextHolder}
      <Outlet context={{ messageApi } satisfies ContextType} />
    </>
  );
};

// for typescript
export const useMessageApi = () => useOutletContext<ContextType>();

3. OutletContext에서 messageApi 받아와 사용하기

// AnotherPage.tsx
import { useNavigate } from "react-router-dom";
import { Button } from "antd";
import { useMessageApi } from "./AppLayout.tsx";

export const AnotherPage = () => {
  const { messageApi } = useMessageApi();
  const navigate = useNavigate();

  return (
    <>
      <h3>Create Page</h3>
      <Button
        onClick={() => {
          messageApi.success("게시글이 등록되었습니다!");
          navigate(-1);
        }}
      >
        등록하기
      </Button>
    </>
  );
};


messageApi를 outletContext로 내려보내기

useMessage가 route의 위치에 영향을 받지 않고 messageApi를 전송할 수 있도록, contextHolder를 모든 route를 감싸는 최상위 Layout route에 배치하였다. Layout route에서는 <Outlet/> 컴포넌트를 통해 자식 route를 Nested UI로 렌더하도록 한다.
이때 Outlet에 context props를 이용하여 원하는 값을 내려보내줄 수 있다. Outlet의 context로 내려준 값은 useOutletContext()를 이용하여 받아올 수 있다.

코드 보기

index.tsx

// ...생략
const router = createBrowserRouter([
  {
    element: <AppLayout />,
    children: [
      { path: "/", element: <App /> },
      { path: "/create", element: <AnotherPage /> }
    ]
  }
]);

root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
)

AppLayout.tsx

import { Outlet, useOutletContext } from "react-router-dom";
import { MessageInstance } from "antd/lib/message/interface";

export const AppLayout = () => {
  const [messageApi, contextHolder] = message.useMessage();
  return (
    <>
      {contextHolder}
      <Outlet context={{ messageApi }} />
    </>
  );
};

AnotherPage.tsx

import { useNavigate, useOutletContext } from "react-router-dom";
import { Button } from "antd";

export const AnotherPage = () => {
  const { messageApi } = useOutletContext();
  const navigate = useNavigate();

  return (
    <>
      <h3>Create Page</h3>
      <Button
        onClick={() => {
          messageApi.success("게시글이 등록되었습니다!");
          navigate(-1);
        }}
      >
        등록하기
      </Button>
    </>
  );
};

타입 오류

그러나 위와 같이 사용하면 useOutletContext를 사용하는 시점에서 타입 오류가 발생한다.


useOutletContext를 typescript와 함께 사용하기

다행이 공식문서에 해당 케이스에 대한 해결방법이 잘 나와있다. context에 타입을 제한해주고, 타입을 포함한 custom hook을 만들어 useOutletContext를 사용하기를 권장하고 있다.
공식문서의 예시를 위의 코드에 적용해보았다.

AppLayout.tsx

import { Outlet, useOutletContext } from "react-router-dom";
import { MessageInstance } from "antd/lib/message/interface";

type ContextType = { messageApi: MessageInstance };

export const AppLayout = () => {
  const [messageApi, contextHolder] = message.useMessage();
  return (
    <>
      {contextHolder}
      <Outlet context={{ messageApi } satisfies ContextType} />
    </>
  );
};

// for typescript
export const useMessageApi = () => useOutletContext<ContextType>();

AnotherPage.tsx

import { useNavigate } from "react-router-dom";
import { Button } from "antd";
import { useMessageApi } from "./AppLayout.tsx";

export const AnotherPage = () => {
  const { messageApi } = useMessageApi();
  const navigate = useNavigate();

  return (
    <>
      <h3>Create Page</h3>
      <Button
        onClick={() => {
          messageApi.success("게시글이 등록되었습니다!");
          navigate(-1);
        }}
      >
        등록하기
      </Button>
    </>
  );
};



원래는 redux를 이용하여 messageApi를 전달해주어야 하나 고민했는데, useOutletContext를 이용해 쉽게 해결할 수 있어서 좋았다. 새삼 react-router-dom은 내가 생각하는 것보다 많은 기능을 제공해주는구나 싶었다. 공식문서를 좀 더 꼼꼼이 읽어보아야지.



참고자료

profile
우리 블로그 정상영업합니다.

0개의 댓글