Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

mimizae 님의 블로그

RouteHandle은 뭐고, 왜 라우트 단에서 헤더를 관리할 수 있을까? 본문

FE

RouteHandle은 뭐고, 왜 라우트 단에서 헤더를 관리할 수 있을까?

mimizae 2026. 2. 3. 17:03

37기 DIVE SOPT에서 진행한 프로젝트 CareNA 프로젝트 초기 세팅 과정을 담은 글입니다! 🙌🏻

💭 해당 구조는 웹 파트 리드님의 아이디어로, 스스로 떠올린 아이디어는 아니지만! 처음 접해본 패턴이라 동작 원리와 설계 의도를 이해해보고자 했다. 단순히 사용하는 데 그치지 않고 왜 이런 방식으로 설계했는지, 어떤 문제를 해결하려는 구조인지를 이해하기 위해 관련 문서와 코드를 참고하며 혼자 정리해보는 시간을 가졌다.
그 과정에서 라우트를 단순한 경로 정의가 아니라 페이지 메타데이터를 관리하는 단위로 확장해볼 수 있다는 관점을 얻었다…!!!

 


💡 문제점

페이지마다 헤더가 다르다…!!

  • 홈: 로고 + 마이페이지 아이콘
  • 마이페이지: 뒤로가기 + 타이틀
  • 로그인: 헤더 없음
  • etc...

React Router 초기 세팅을 진행하면서 페이지마다 layout을 분리하는 게 어떤지? 하는 제안을 했었는데, 그 원인 또한 페이지마다 헤더가 달랐기 때문이었다...!!

(Router 초기 세팅을 하며 현재 mvp 단에서는 페이지 수가 많지 않아 큰 문제는 아니지만, 스프린트를 거듭할 수록 유지보수에 어떤 구조가 좋을지 고민했었다.)

 

이걸 구현하려고 하면 보통 다음과 같은 선택지들을 고민하게 된다.

  • 페이지 컴포넌트 안에서 <Header />를 직접 렌더링
  • 전역 상태로 현재 페이지 타입을 관리
  • URL을 기준으로 if/else 분기

하지만 이러한 접근 방식들은 공통적으로 페이지의 UI 설정이 라우트 구조와 분리되고, 페이지 수가 늘어날수록 조건 분기가 복잡해진다는 문제를 안고 있었다.

무엇보다도 이 페이지는 어떤 성격의 페이지인가?라는 정보를 코드를 통해 직관적으로 파악하기 어렵다는 점이 가장 큰 아쉬움이었다…!!


💡 React Router의 handle이란?

React Router에서 각 라우트 객체에는 handle이라는 임의의 데이터를 담을 수 있는 필드가 있다.

{
  path: "/home",
  element: <HomePage />,
  handle: { header: "main" }
}

 

여기서 중요한 점은!! handle라우팅에 관여하지 않는다는 점이다.

경로 매칭, 리다이렉트와는 무관하며 오직 개발자가 자유롭게 정의하는 메타데이터 공간이라고 볼 수 있다!

 

handle을 활용해 각 페이지가 어떤 형태의 헤더를 가져야 하는지를 페이지 컴포넌트가 아닌 라우트 정의 단계에서 선언하도록 했다.

이를 통해 페이지 UI에 대한 설정을 라우트 구조 안으로 자연스럽게 끌어올릴 수 있었다!!!!

 

export interface RouteHandle {
  header?: HeaderVariant;
  title?: string;
}

 

RouteHandle 인터페이스는 이 handle에 어떤 정보가 들어갈 수 있는지를 명확히 정의하기 위한 타입이다.

헤더의 형태나 타이틀과 같이 레이아웃에서 공통으로 참조해야 하는 값들을 타입으로 제한함으로써, 라우트 설정 단계에서의 실수를 줄이고 Layout 컴포넌트에서도 안정적으로 해당 정보를 사용할 수 있게 된다!!


💡 음? 근데 이 정보는 어떻게 Layout까지 전달되는 거지?

여기서 등장하는 게 useMatches()다!!

const matches = useMatches();

 

Layout 컴포넌트에서는 useMatches() 훅을 사용해 현재  URL과 매칭된 모든 라우트 정보루트부터 가장 깊은 자식 라우트까지 배열로 얻게 된다!!

 

예를 들어, 아래와 같은 라우트 구조를 가지고 있는 /my-page에 접속한다면

 <App>
    <ErrorBoundary>
      <AppProvider>
        <AuthInitializerProvider>
          ...
                <ProtectedRoute>
                  <Layout>
                    <MyPage />
                  </Layout>
                </ProtectedRoute>
              ...
        </AuthInitializerProvider>
      </AppProvider>
    </ErrorBoundary>
  </App>
[
  { handle: undefined },          // ErrorBoundary
	...
  { handle: undefined },          // ProtectedRoute
  { handle: undefined },          // Layout
  { handle: { header: "back", title: "마이페이지" } } // MyPage
]

 

ErrorBoundary → … → ProtectedRoute → Layout → MyPage 순서로 라우트가 매칭되며, 각 라우트에 정의된 handle 정보 역시 이 배열에 함께 포함된다!!

 

그래서 Layout에서는 이 배열의 가장 마지막 요소를 현재 페이지로 판단하고,

const currentMatch = matches[matches.length - 1]; // MyPage!

 

를 통해 현재 페이지에 해당하는 라우트의 handle을 가져와서 어떤 헤더를 렌더링할지 결정한다!!

 

이 방식 덕분에 Layout은 현재 URL이나 페이지 컴포넌트의 내부 구현을 알 필요 없이!!! 라우트 정의에 선언된 메타 정보만을 기준으로 공통 UI를 제어할 수 있게 된다.


💡 이 패턴의 핵심 장점

이 구조의 가장 큰 장점은 역할과 책임이 명확하게 분리된다는 점이다!!

 

페이지 컴포넌트는 화면과 비즈니스 로직에만 집중하고, 라우트는 페이지의 성격과 메타 정보를 설명하며, Layout은 이 정보를 해석해 공통 UI를 제어한다.

{
	path:ROUTE_PATH.MY_PAGE,
	element:<MyPage />,
	handle: {
		header:"back",
		title:"마이페이지",
	},
}

 

특히 이러한 라우트 정의를 살펴보는 것만으로도 각 페이지가 어떤 성격의 화면인지 쉽게 파악할 수 있다!!!

마이페이지 라우트에 뒤로가기 헤더와 타이틀이 함께 선언되어 있는 것을 보면, 해당 페이지가 메인 페이지가 아닌 서브 페이지라는 사실을 코드 레벨에서 바로 이해할 수 있을 것이다.


💡 전체 코드

전체 코드를 살펴보며 흐름을 더 확실히 이해해 보자

/**
 * 헤더 변형 타입
 * - "main": 메인 페이지용 (로고 + 마이페이지 아이콘)
 * - "back": 서브 페이지용 (뒤로가기 + 타이틀)
 * - "none": 헤더 없음
 */
 
export type HeaderVariant = "main" | "back" | "none";

/**
 * 라우트 핸들 인터페이스
 * - useMatches()로 현재 라우트의 handle을 가져와서 Layout에서 헤더를 동적으로 렌더링
 */

export interface RouteHandle {
  header?: HeaderVariant;
  title?: string;
}

export const router = createBrowserRouter([
  {
    path: ROUTE_PATH.LOGIN,
    element: <LoginPage />,
  },
  {
    element: <ProtectedRoute />,
    children: [
      {
        path: ROUTE_PATH.LAYOUT,
        element: <Layout />,
        children: [
          {
            index: true,
            element: <Navigate to={ROUTE_PATH.HOME} replace />,
           },
           {
             path: ROUTE_PATH.HOME,
             element: <HomePage />,
             handle: { header: "main" } satisfies RouteHandle,
            },
            {
              path: ROUTE_PATH.MY_PAGE,
              element: <MyPage />,
              handle: {
                header: "back",
                title: "마이페이지",
              } satisfies RouteHandle,
            },
            {
              path: "*",
              element: <></>,
             },
           ],
         },
       ],
     },
   ]
 );

 

import { Outlet, useMatches } from "react-router";
import { Header } from "@/shared/ui/navigations/header";
import type { RouteHandle } from "./router";

export const Layout = () => {
	/**
	 * useMatches(): 현재 URL과 매칭된 모든 라우트 정보를 배열로 반환
	 * 예: /home 접속 시 → [{ path: "/" }, { path: "home", handle: { header: "main" } }]
	 *
	 * matches[matches.length - 1]: 가장 마지막(가장 깊은) 라우트 = 현재 페이지
	 */
	const matches = useMatches();
	const currentMatch = matches[matches.length - 1];
	const headerConfig = currentMatch?.handle as RouteHandle | undefined;
	const showHeader = headerConfig?.header !== "none";

	return (
		<>
			<Header variant={headerConfig?.header} title={headerConfig?.title} />
			<main className={showHeader ? "pt-[5.6rem]" : ""}>
				<Outlet />
			</main>
		</>
	);
};

 

위와 같이 Layout 컴포넌트에서 useMatches() 훅으로 현재 url에 대한 라우트 구조 정보를 끌어와 어떤 페이지 컴포넌트를 렌더링하고 있는지 알아내어서, currentMatch에 저장한다.

 

const headerConfig = currentMatch?.handle as RouteHandle | undefined;

 

이후 currentMatch(현재 컴포넌트)에 포함된 handle 값을 가져와, 이 프로젝트에서 미리 정의해 둔 RouteHandle 인터페이스 형태로 해석한다. 그리고 현재 페이지에서 사용할 헤더 설정(variant, title)을 추출한다!!

export interface RouteHandle {
  header?: HeaderVariant;
  title?: string;
}
<Header variant={headerConfig?.header} title={headerConfig?.title} />

 

이렇게 추출한 설정 값을 Header 컴포넌트에 전달하여, 페이지마다 서로 다른 헤더 variant와 title을 렌더링한다.

const showHeader = headerConfig?.header !== "none";

 

마지막으로, showHeader 값을 기준으로 헤더가 있을 때만 padding-top 적용시켜 페이지 컴포넌트들은 레이아웃 신경 안 써도 되도록 했다!

 

 

'FE' 카테고리의 다른 글

마이페이지 min-height 문제  (0) 2026.02.10
ticker 구현기  (0) 2026.02.09
React Route 초기 세팅  (0) 2026.02.02
Tanstack Query 초기 세팅  (0) 2026.01.29
swagger-typescript-api 적용기  (0) 2026.01.28