Frontend/기타

React Router의 메타 정보 저장소, Handle

제주도랏맨 2025. 4. 27. 15:15

 

React Router의 메타 정보 저장소, Handle

리액트 라우터를 사용하면서, 한 번 쯤은 권한 인증이나 역할 체크, 아니면 공통 레이아웃에 대한 고민을 해 본 적 있을 것이다.

나 역시도 회사에서 리액트 라우터를 사용하면서, 이런 부분에 대한 고민을 마주친 적이 많았다.

예를 들어 로그인 여부가 필요한 라우팅은 어떻게 처리할지, 모바일 UI에서 헤더의 타이틀을 어떻게 바꿔줄지,

아니면 중첩된 라우팅 사이에서 헤더-푸터 레이아웃의 존재 여부를 쉽게 관리할 수 없을지 말이다.

 

그런데 이런 문제를 개선할 수 있는 방법을 최근에 발견하게 되었는데 바로 vue-router의 meta 필드였다.

우연히 본 블로그에서 vue-router의 경우, meta 속성을 통해 로그인 권한을 관리하는 것을 보고

`이렇게 하면 각 라우터마다 정보를 부여해서 라우터가 중첩되어 있어도 쉽게 관리할 수 있겠다`라는 생각이 들었던 것이다.

그렇게 리액트 라우터에는 비슷한 역할을 하는 속성이 없나 찾아보다가 handle이라는 속성이 있는 것을 발견했다.

 

놀랍게도, 그리고 안타깝게도 handle 속성에 대한 명세는 리액트 라우터 공식 문서에 존재하지 않는다.

그래서 나 같은 사람들이 이 속성을 쓰면 좋은 기능들을 다른 방식으로 어찌저찌 구현하게 된게 아닌가 싶은데

이 속성에 대한 논의는 무려 2022년에 올라왔고 같은 해 6.4 버전이 등장하면서 공식에 포함되었다.

 

그나마 이 속성에 대한 명세를 찾을 수 있는 건 remix의 문서로 리액트 라우터와 remix가 통합되면서

remix 쪽 명세를 리액트 라우터 쪽에 추가하지 않은게 아닐까 하는 추측을 해본다.

 

Handle 정의하기

// https://api.reactrouter.com/v7/interfaces/react_router.NonIndexRouteObject.html

interface NonIndexRouteObject {
    action?: boolean | ActionFunction<any>;
    caseSensitive?: boolean;
    children?: RouteObject[];
    Component?: null | ComponentType<{}>;
    element?: ReactNode;
    ErrorBoundary?: null | ComponentType<{}>;
    errorElement?: ReactNode;
    handle?: any;                                   // handle 속성
    hasErrorBoundary?: boolean;
    HydrateFallback?: null | ComponentType<{}>;
    hydrateFallbackElement?: ReactNode;
    id?: string;
    index?: false;
    lazy?: LazyRouteFunction<RouteObject>;
    loader?: boolean | LoaderFunction<any>;
    path?: string;
    shouldRevalidate?: ShouldRevalidateFunction;
}

// https://api.reactrouter.com/v7/interfaces/react_router.PathRouteProps.html

interface PathRouteProps {
    action?: boolean | ActionFunction<any>;
    caseSensitive?: boolean;
    children?: ReactNode;
    Component?: null | ComponentType<{}>;
    element?: ReactNode;
    ErrorBoundary?: null | ComponentType<{}>;
    errorElement?: ReactNode;
    handle?: any;                                   // handle 속성
    hasErrorBoundary?: boolean;
    HydrateFallback?: null | ComponentType<{}>;
    hydrateFallbackElement?: ReactNode;
    id?: string;
    index?: false;
    lazy?: LazyRouteFunction<NonIndexRouteObject>;
    loader?: boolean | LoaderFunction<any>;
    path?: string;
    shouldRevalidate?: ShouldRevalidateFunction;
}

 

handle 속성은 RouteObject 혹은 RouteProps 타입에 위처럼 정의되어 있다.

안타깝게도 타입은 제네릭으로 정의되어있지 않고 any로 정의되어 있어 사용 시 타입 캐스팅을 해서 사용해야한다.

type이 any라서 어떤 타입이든 상관은 없지만, 나는 vue-router를 참고해서 객체 형태로 사용한다.

 

// RouteObject 형태

{
  path: "/router-path",
  element: <Element />,
  handle: {
    header: true,
    footer: true,
    title: "Page Title",
  },
}

// JSX Route 컴포넌트 형태

<Route
  path="/route-path"
  element={<Element />}
  handle={{ header: true, footer: false, title: "Page Title" }}
/>

 

RouteObject로 사용할 때나 혹은 JSX 컴포넌트 형태로 라우트를 정의할 때 위처럼 handle을 정의해주면 사용할 수 있다.

 

Handle 가져오기

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

export const Layout = () => {
  const matches = useMatches();
  const currentHandle = matches[matches.length - 1]?.handle as {
    header: boolean;
    footer: boolean;
    title?: string;
  };

  return (
    <>
      {currentHandle?.header && <header>{currentHandle?.title}</header>}
      <Outlet />
      {currentHandle?.footer && <footer>Footer</footer>}
    </>
  );
};

 

정의된 handle의 정보를 가져오는 것은 `useMatches` 훅을 사용하여 가져올 수 있다.

`useMatches` 훅은 라우터 객체 배열을 탐색하여 정의된 라우터와 일치하는 라우터들을 가져오는 훅인데

현재 라우터와 그 라우터의 상위에 속하는 모든 라우터를 가져와 배열로 return한다.

 

가장 처음엔 root 라우터의 정보를가 가장 마지막에는 현재 표시 중인 라우터의 정보를 가져오기 때문에

가장 마지막를 가져와 handle을 가져오면 현재 라우터에 정의된 handle을 가져올 수 있다.

(아쉽게도 any 타입이라 따로 타입 캐스팅을 해줘야한다.)

 

결론

다들 고민해봤을 부분인데 나 역시 리액트 라우터가 동작하면 되지 라는 마음으로 사용해왔기 때문인지

이런 기능이 있음에도 몰라서 못 썼다는게 아이러니했다. 물론 문서에 없는 것도 한 몫하지만...

앞으로 리액트 라우터를 사용할 때 한 번 쯤은 handle 속성을 떠올려보는 것은 어떨까?