react-router-dom 의 useMatches

김강민·2025년 8월 16일

개발

목록 보기
23/32
post-thumbnail

react-router-dom의 useMatcheshandle로 똑똑하게 라우팅하기

react-router-dom을 사용해 중첩 라우팅(Nested Routing)을 구성하다 보면, "자식 페이지의 정보를 부모 레이아웃에서 어떻게 알 수 있을까?"라는 흔한 문제에 부딪힌다.

예를 들어, 현재 페이지의 URL에 따라 부모인 Layout 컴포넌트가 동적으로 페이지 제목(title)을 변경해야 하는 경우이다.

처음 이 문제를 마주했을 때, 나는 라우트 관련 파일을 분리한다는 생각을 하지 못했고, 각 페이지 라우트에 개별적으로 Layout 컴포넌트를 적용해야 한다고 당연하게 생각했다.

또한, MainPage와 그 외 페이지가 서로 다른 헤더를 가져야 했기 때문에, Header 컴포넌트가 type이라는 prop을 받아 동적으로 다른 헤더를 렌더링하도록 구현했다.

// 동적으로 다른 헤더를 보여주는 Header 컴포넌트
import { MainHeader, PageHeader } from '../header';

type HeaderType = 'main' | 'page';

type Props = {
  type: HeaderType;
  title?: string;
};

const HEADER_COMPONENTS = {
  main: MainHeader,
  page: PageHeader,
} as const;

export const Header = ({ type, title }: Props) => {
  const HeaderComponent = HEADER_COMPONENTS[type];

  return <HeaderComponent title={title} />;
};

이러한 초기 접근 방식은 처음에는 동작하는 것처럼 보였지만, 곧바로 아래와 같은 명확한 한계에 부딪히게 되었다.

AS-IS: Layout에 Prop 직접 전달하기

초기 접근 방식에 따라, 라우트 설정에서 Layout 컴포넌트에 직접 pageTitle과 같은 prop을 전달했다.

// router.tsx (AS-IS)
const router = createBrowserRouter([
  // ...
  {
    path: ROUTER_PATH.COMMUNITY,
    // 👇 Layout에 직접 title을 prop으로 전달
    element: <Layout pageTitle='커뮤니티' />,
    children: [
      {
        index: true,
        element: <CommunityPage />,
      },
    ],
  },
  // ...
]);

문제점

이 방식은 프로젝트를 진행하면서 몇 가지 명확한 한계점을 드러냈다.

  • 유지보수의 어려움: 새로운 페이지를 추가하거나 기존 페이지의 제목을 변경할 때마다, 페이지 컴포넌트뿐만 아니라 항상 router.tsx 파일까지 함께 수정해야 했다. 이는 매우 번거로운 작업이었다.
  • 관심사 분리 원칙 위배: Layout 컴포넌트가 어떤 title을 보여줄지에 대한 정보가 Layout 자신이 아닌, 외부의 router.tsx 파일에 흩어져 있었다. Layout의 책임이 분산되어 코드의 응집도가 떨어졌다.

TO-BE: useMatcheshandle로 개선하기

이러한 불편함을 해결하기 위해 react-router-dom v6.4부터 도입된 useMatches 훅과 handle 프로퍼티를 사용했다.

useMatches 공식 문서 정의

먼저 공식 문서의 정의를 살펴보자.
react-router-dom : useMatches

function useMatches(): UIMatch[]

현재 활성화된 라우트 매치 배열을 반환합니다. 부모/자식 라우트의 loaderData나 라우트의 handle 프로퍼티에 접근할 때 유용합니다.

여기서 UIMatch는 현재 URL과 일치하는 각 라우트 객체(경로, 파라미터, 그리고 우리가 사용할 handle 등)에 대한 정보를 담고 있는 객체이다.

  • handle: 라우터를 설정할 때, 각 라우트(route) 객체에 우리가 원하는 데이터를 담을 수 있는 프로퍼티이다.
  • useMatches: 현재 URL과 일치하는(match) 모든 라우트 객체들의 정보를 UIMatch 배열로 반환하는 훅이다. 이 배열을 통해 상위 컴포넌트는 하위 컴포넌트의 handle에 접근할 수 있다.

이 두 가지를 조합하면, 라우터 설정 파일이 모든 경로와 그에 따른 메타데이터(제목 등)를 관리하는 '진실의 원천(Single Source of Truth)'이 되고, Layout 컴포넌트는 그 정보를 가져다 쓰기만 하는 이상적인 구조를 만들 수 있다.

1단계: 라우터에 handle 정보 추가하기

가장 먼저, 각 페이지 라우트에 handle 프로퍼티를 추가하여 필요한 정보(여기서는 title)를 심어준다.

// router.tsx (TO-BE)
const router = createBrowserRouter([
  {
    path: '/',
    element: <AuthRoute />,
    children: [
      // ...
      {
        element: <Layout pageType='Page' />, // 이제 Layout에 title을 넘기지 않는다.
        children: [
          {
            path: 'clip',
            element: <ClipPage />,
            // 👇 각 라우트에 handle 프로퍼티를 추가!
            handle: { title: '클립 저장' },
          },
          {
            path: 'search',
            element: <SearchPage />,
            handle: { title: '검색' },
          },
        ],
      },
    ],
  },
]);

2단계: Layout 컴포넌트에서 useMatches로 정보 가져오기

다음으로, 부모인 Layout 컴포넌트가 useMatches 훅을 사용해 현재 활성화된 자식 라우트의 handle 정보를 읽어오도록 수정한다.

// Layout.tsx
import { Outlet, useMatches } from 'react-router-dom';
// ...

type RouteHandle = {
  title?: string;
};

export const Layout = ({ pageType }: { pageType: 'Main' | 'Page' }) => {
  const matches = useMatches();
  const handle = matches[matches.length - 1]?.handle as RouteHandle | undefined;
  const title = handle?.title;

  return (
    <PageLayout>
      <Header type={pageType} title={title} />
      <PageContainer>
        <Outlet />
      </PageContainer>
      <NavigateBar />
    </PageLayout>
  );
};

matches 배열은 최상위 라우트부터 현재 라우트까지의 모든 정보를 담고 있으며, matches[matches.length - 1]을 통해 가장 마지막에 매치된, 즉 현재 화면에 보이는 페이지의 라우트 정보에 접근할 수 있다.

결론

useMatcheshandle을 사용하는 이 패턴은 다음과 같은 명확한 장점을 제공한다.

  • 관심사 분리: Layout 컴포넌트는 더 이상 하위 페이지들의 경로와 제목을 알 필요가 없다. 오직 "현재 라우트의 handle에서 title을 가져와 렌더링한다"는 책임만 가진다.
  • 중앙화된 관리: 모든 라우팅 정보와 메타데이터가 router.tsx 파일 한곳에서 관리되므로, 새로운 페이지를 추가하거나 기존 페이지의 제목을 변경할 때 이 파일 하나만 수정하면 된다.

이처럼 useMatches는 중첩 라우팅 구조에서 컴포넌트 간의 데이터 흐름을 매우 선언적이고 효율적으로 만들어주는 편리한 기능이었다.

세상에는 참 특이한 기능이 많다..

profile
인생은 프레임워크처럼, 공부는 라이브러리처럼

0개의 댓글