Skip to content

useRoutes로 라우팅 관리하기

YUNHO edited this page Jan 2, 2023 · 1 revision

1. 기존까지 사용했던 라우팅 관리의 문제점

현재 아래와 같이 App.jsx에서 모든 라우팅을 관리하고 있습니다. 아래처럼 라우팅 페이지를 관리했을 때 다음과 같은 불편함을 느꼈습니다.

  1. App.jsx은 엔트리 포인트로 폴더 구조 및 App전체에 사용되고 있는 기능을 보여야 한다고 생각합니다. 하지만 App.jsx에서 모든 페이지 관련 라우팅을 관리하다보니 프로젝트에서 전체적으로 사용하는 기능을 한 눈에 파악하기 어려웠습니다.
  2. 아래와 같이 페이지가 5개만 넘어가도 가독성이 좋지 못함을 알 수 있습니다.
  3. 중첩된 라우터가 무엇인지, hoc(publicRouter, privateRotue)가 적용된 컴포넌트는 무엇인지 한 눈에 파악하기 어렵습니다.
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ROUTES } from './constant';
import Layout from './Layout';
import Main from './pages/Main';
import About from './pages/About';
import Fruits from './pages/Fruits';
import Apple from './pages/Apple';
import Banana from './pages/Banana';

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path={ROUTES.HOME} element={<Layout />}>
          <Route index element={<Main />} />
          <Route path={ROUTES.ABOUT} element={<About />} />
          <Route
            path={ROUTES.FRUITS.INDEX}
            element={<PublicRoute Component={Fruits} restricted={restricted} />}
          >
            <Route
              path={ROUTES.FRUITS.APPLE}
              element={<PrivateRtoue Component={Apple} restricted={restricted} />}
            />
            <Route path={ROUTES.FRUITS.BANANA} element={<Banana />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}
// Layout.tsx
import { Link, Outlet } from 'react-router-dom';
import { ROUTES } from './constant';

const Layout = () => {
  return (
    <div>
      <ul style={{ display: 'flex', flexDirection: 'column' }}>
        <Link to={ROUTES.HOME}>HOME</Link>
        <Link to={ROUTES.ABOUT}>ABOUT</Link>
        <Link to={ROUTES.FRUITS.INDEX}>FRUITS</Link>
        <Link to={ROUTES.FRUITS.APPLE}>APPLE of FRUITS</Link>
        <Link to={ROUTES.FRUITS.BANANA}>BANANA of FRUITS</Link>
      </ul>
      <Outlet />
    </div>
  );
};

export default Layout;
// Fruits.tsx
import { Outlet } from 'react-router-dom';

const Fruits = () => {
  return (
    <div>
      <h1>Fruits</h1>
      <Outlet />
    </div>
  );
};

export default Fruits;
수정 전 App.tsx (😅 주의 : 가독성에 눈이 아플 수 있습니다)
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { ROUTE } from 'constant/route.constant';
import Login from 'pages/Login';
import Main from 'pages/Main';
import SignUp from 'pages/SignUp';
import UserBoard from 'pages/UserBoard';
import TeamBoard from 'pages/TeamBoard';
import UserPost from 'pages/UserPost';
import TeamPost from 'pages/TeamPost';
import Callback from 'pages/Callback';
import EditUserProfile from 'pages/EditUserProfile';
import EditTeamProfile from 'pages/EditTeamPost';
import MyList from 'pages/MyList';
import NewPost from 'pages/NewTeamPost';
import MyPost from 'pages/MyPost';
import NotFound from 'pages/NotFound';
import EssentialInfo from 'pages/EssentialInfo';
import AssignLayout from 'layouts/layoutAssign';
import Nickname from 'pages/EssentialInfo/SubPages/Nickname';
import Skills from 'pages/EssentialInfo/SubPages/Skills';
import ProfileImage from 'pages/EssentialInfo/SubPages/ProfileImage';
import SessionJob from 'pages/EssentialInfo/SubPages/SessionJob';
import Slogan from 'pages/EssentialInfo/SubPages/Slogan';
import BelongTeam from 'pages/EssentialInfo/SubPages/BelongTeam';
import Introduction from 'pages/EssentialInfo/SubPages/Introduction';
import Portfolio from 'pages/EssentialInfo/SubPages/Portfolio';
import EssentialCallback from 'pages/EssentialInfo/SubPages/EssentialCallback';
import OAuthCallback from 'pages/OAuthCallback';
import OAuthFail from 'pages/OAuthFail';
import PublicRoute from 'hoc/PublicRoute';
import PrivateRoute from 'hoc/PrivateRoute';
import Layout from 'layouts/layout';

function App() {
  return (
    <Router>
      <Routes>
        <Route element={<AssignLayout />}>
          <Route path={ROUTE.SIGN_UP} element={<PublicRoute Component={SignUp} restricted />} />
          <Route path={ROUTE.LOGIN} element={<PublicRoute Component={Login} restricted />} />
        </Route>
        <Route path={ROUTE.HOME} element={<Layout />}>
          <Route index element={<PublicRoute Component={Main} restricted={false} />} />
          <Route
            path={ROUTE.USER}
            element={<PublicRoute Component={UserBoard} restricted={false} />}
          />
          <Route
            path={ROUTE.TEAM}
            element={<PublicRoute Component={TeamBoard} restricted={false} />}
          />
          <Route path={ROUTE.PROFILE} element={<PrivateRoute Component={EditUserProfile} />} />
          <Route
            path={ROUTE.USER}
            element={<PublicRoute Component={UserBoard} restricted={false} />}
          />
          <Route
            path={ROUTE.TEAM}
            element={<PublicRoute Component={TeamBoard} restricted={false} />}
          />
          <Route path={ROUTE.PROFILE} element={<PrivateRoute Component={EditUserProfile} />} />
          <Route path={ROUTE.LOGIN} element={<PublicRoute Component={Login} restricted />}>
            <Route
              path={ROUTE.ESSENTIAL_INFO.INDEX}
              element={<PublicRoute Component={EssentialInfo} restricted />}
            >
              <Route index element={<Navigate to={ROUTE.ESSENTIAL_INFO.NICKNAME} replace />} />
              <Route
                path={ROUTE.ESSENTIAL_INFO.NICKNAME}
                element={<PublicRoute Component={Nickname} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.SKILL}
                element={<PublicRoute Component={Skills} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.PROFILE_IMAGE}
                element={<PublicRoute Component={ProfileImage} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.SESSION_JOB}
                element={<PublicRoute Component={SessionJob} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.SLOGAN}
                element={<PublicRoute Component={Slogan} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.BELONG_TEAM}
                element={<PublicRoute Component={BelongTeam} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.CONTENT}
                element={<PublicRoute Component={Introduction} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.PROTFOLIO}
                element={<PublicRoute Component={Portfolio} restricted />}
              />
              <Route
                path={ROUTE.ESSENTIAL_INFO.CALLBACK}
                element={<PublicRoute Component={EssentialCallback} restricted />}
              />
            </Route>
          </Route>
          <Route path={ROUTE.SIGN_UP} element={<PublicRoute Component={SignUp} restricted />} />
          <Route path={ROUTE.MY_LIST} element={<PrivateRoute Component={MyList} />} />
          <Route
            path={ROUTE.NEW_POST}
            element={<PrivateRoute Component={NewPost} restricted={false} />}
          />
          <Route path={ROUTE.MY_POST} element={<PrivateRoute Component={MyPost} />} />
          <Route
            path={`${ROUTE.USER}/:userId`}
            element={<PublicRoute Component={UserPost} restricted={false} />}
          />
          <Route
            path={`${ROUTE.TEAM}/:teamId`}
            element={<PublicRoute Component={TeamPost} restricted={false} />}
          />
          <Route
            path={`${ROUTE.TEAM_EDIT}/:teamId`}
            element={<PrivateRoute Component={EditTeamProfile} />}
          />
          <Route path={ROUTE.OAUTH_CALLBACK} element={<OAuthCallback />} />
          <Route path={ROUTE.OAUTH_FAIL} element={<OAuthFail />} />
          <Route path={ROUTE.CALLBACK} element={<Callback />} />
          <Route path={ROUTE.NOTFOUND} element={<NotFound />} />
        </Route>
      </Routes>
    </Router>
  );
}

export default App;

2. 목표

  • /src/routes에서 객체로 routes 구조를 관리하기
  • 중첩된 라우터는 한 눈에 알아볼 수 있도록 관리하기

3. 결과 예시

3-1. /src/routes에서 객체로 routes 구조를 관리하기

react-router-dom-v6에서 제공하는 useRoutes를 활용해 /src/routes/index.jsx에서 모든 라우팅을 관리하도록 했습니다.

const routes = [
  {
    path: ROUTES.HOME,
    element: <Main />,
  },
  {
    path: ROUTES.ABOUT,
    element: <About />,
  },
  {
    path: ROUTES.FRUITS.INDEX + ROUTES.ALL,
    element: <FruitRouter />,
  },
];

const Router = () => {
  const element = [
    {
      element: <Layout />,
      children: routes,
    },
  ];
  return useRoutes(element);
};

3-2. private라우터와 public라우터 등 기능에 따라 구분하기

이전에는 <Route>컴포넌트 마다 아래와 같이 Public인지 Private인지 element를 감싸는 형태로 코드를 작성했습니다.

// App.jsx
  <Route path={ROUTE.SIGN_UP} element={<PublicRoute Component={SignUp} restricted />} />
  <Route path={ROUTE.LOGIN} element={<PublicRoute Component={Login} restricted />} />
  <Route path={ROUTE.PROFILE} element={<PrivateRoute Component={EditUserProfile} />} />

반복되는 코드를 Array.map을 활용해 줄이고, 별도의 파일에서 관리하도록 수정했습니다.

로그인한 유저만 접근할 수 있는 privateRoutes

// /src/routes/privateRoutes.js
const routes = [
  {
    path: ROUTE.PROFILE, // 브라우저에서 라우팅되어야하는 경로
    element: EditUserProfile, // 컴포넌트
    restricted: true, // hoc에서 사용할 속성
  },
  // 생략
];
const privateRoutes = routes.map(({ path, element, restricted }) => ({
  path,
  element: <PrivateRoute Component={element} restricted={restricted} />,
}));

모든 유저가 접근할 수 있는 public Routes

// /src/routes/publicRoutes.js
const routes = [
  {
    path: ROUTE.SIGN_UP,
    element: SignUp,
    restricted: true,
  },
  {
    path: ROUTE.LOGIN + ROUTE.ALL,
    element: LoginRoute,
    restricted: true,
  },
  // 생략
];
const publicRoutes = routes.map(({ path, element, restricted }) => ({
  path,
  element: <PublicRoute Component={element} restricted={restricted} />,
}));

프로젝트에서 사용하는 모든 라우팅을 모으는 곳(src/routes/index.jsx)

export default function Router() {
  const element = [
    {
      element: <Layout />,
      children: [...publicRoutes, ...privateRoutes],
    },
    ...etcRoutes,
  ];
  return useRoutes(element);
}

3-3. 중첩된 라우터는 한 눈에 알아볼 수 있도록 관리하기

이전에는 아래와 같이 <Route>컴포넌트 내부에 들여쓰기 방식을 통해서 중첩된 라우터를 구분하거나 해당 페이지 하단에 <Route>를 둬서 중첩된 라우터를 관리했습니다.

<Route>와

처음에 언급한 것처럼 App.jsx에서 관리하는 라우팅이 많아질수록 가독성이 떨어져서 구조를 파악하기 힘들고 유지보수가 어려워집니다.

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
          <Route path={ROUTES.FRUITS.INDEX} element={<Fruits />}>
            <Route path={ROUTES.FRUITS.APPLE} element={<Apple />} />
            <Route path={ROUTES.FRUITS.BANANA} element={<Banana />} />
          </Route>
      </Routes>
    </BrowserRouter>
  );
}

// Fruits.tsx
import { Outlet } from "react-router-dom";

const Fruits = () => {
  return (
    <div>
      <h1>Fruits</h1>
      <Outlet />
    </div>
  );
};

export default Fruits;

별도의 페이지에서 관리

App.jsx에서 모든 라우팅을 관리한다고 예상할 수 있는데 중첩된 라우팅은 멀리 떨어진 /pages/Fruits에서 관리하는 사실을 예상하기 어렵습니다. (응집도가 떨어진다고 할 수 있습니다.)

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
          <Route path={ROUTES.FRUITS.INDEX} element={<Fruits />} />
      </Routes>
    </BrowserRouter>
  );
}

// Fruits.tsx
import { Outlet } from "react-router-dom";

const Fruits = () => {
  return (
    <div>
      <h1>Fruits</h1>
      <Routes>
          <Route path={ROUTES.FRUITS.APPLE} element={<Apple />} />
          <Route path={ROUTES.FRUITS.BANANA} element={<Banana />} />
      </Routes>
    </div>
  );
};

export default Fruits;

개선방법

라우팅을 관리하는 폴더내부에서 아래와 같이 매핑해줍니다. (FruitRouter)

// /src/routes/index.jsx
const fruitsRoutes = [
  { path: ROUTES.FRUITS.APPLE, element: <Apple /> },
  { path: ROUTES.FRUITS.BANANA, element: <Banana /> },
];

const Fruits = ({ children }) => {
  return (
    <div>
      <h1>Fruits</h1>
      {children}
    </div>
  );
};

const FruitRouter = () => {
  return (
    <>
      <Fruits>
        <Routes>
          {fruitsRoutes.map(({ path, element }) => (
            <Route key={path} path={path} element={element} />
          ))}
        </Routes>
      </Fruits>
    </>
  );
};

const routes: RouteProps[] = [
  {
    path: ROUTES.HOME,
    element: <Main />,
  },
  {
    path: ROUTES.ABOUT,
    element: <About />,
  },
  {
    path: ROUTES.FRUITS.INDEX + ROUTES.ALL,
    element: <FruitRouter />,
  },
];

const Router = () => {
  const element = [
    {
      element: <Layout />,
      children: routes,
    },
  ];
  return useRoutes(element);
};

react-router-mangement

4. 프로젝트에 어떻게 적용했을까

프로젝트 구조

현재 프로젝트 폴더구조는 다음과 같습니다.

# src/routes
├── EssentialInfoRoute.jsx /# 필수정보입력페이지 중첩라우터
├── LoginRoute.jsx # 로그인 페이지 중첩 라우터
├── EtcRoutes.js # callback 페이지와 같은 기타 라우터 모음
├── PrivateRoutes.js # 로그인 하지 않은 유저는 접근할 수 없는 라우터 모음
├── PublicRoutes.js # 로그인한 유저는 접근할 수 없는 라우터 모음
└── index.jsx

프로젝트에서 사용되는 페이지 레이아웃은 크게 Global Naviation Bar(GNB)이 포함된 페이지와 포함되지 않은 페이지, 2개로 나눌 수 있습니다. GNB가 포함된 페이지는 로그인,회원가입, 유저(팀)보드와 같은 페이지가 있으며, GNB가 포함되지 않은 페이지는 소셜로그인 성공/실패 페이지, Not Found, Callback페이지 등이 있습니다.

useRoutes의 children 기능을 활용해, 반복되는 layout 구조를 src/layouts 컴포넌트의 <Outlet /> 으로 렌더링 될 수 있게 구분했습니다.

src/routes/index.js

export default function Router() {
  const element = [
    {
      path: ROUTE.HOME, // 메인페이지는 특수한 처리가 필요해 별도로 분리
      element: <Main />,
      restricted: false,
    },
    {
      element: <LayoutWithHeader />, // GNB헤더가 필요한 레이아웃
      children: [...publicRoutes, ...privateRoutes],
    },
    {
      element: <LayoutFullPage />, // GNB헤더가 필요하지 않은 레이아웃
      children: [...etcRoutes],
    },
  ];
  return useRoutes(element);
}

src/layouts/LayoutWithHeader.jsx

export default function LayoutWithHeader() {
  return (
    <S.AppContainer>
      <S.Header>
        <GlobalNavigation />
      </S.Header>
      <S.Main>
        <Outlet />
      </S.Main>
    </S.AppContainer>
  );
}

중첩라우터는 다음과 같이 구성했습니다.

src/router/EssentialInfoRoute.jsx

// 생략
const essentialInfoNestedRoutes = [
  {
    path: ROUTE.ESSENTIAL_INFO.NICKNAME,
    element: Nickname,
    restricted: true,
  },
  {
    path: ROUTE.ESSENTIAL_INFO.SKILL,
    element: Skills,
    restricted: true,
  },
  // 생략
];

export default function EssentialInfoRoute() {
  return (
    <EssentialInfo>
      <Routes>
        <Route index element={<Navigate to={ROUTE.ESSENTIAL_INFO.NICKNAME} replace />} />
        {essentialInfoNestedRoutes.map(({ path, element, restricted }) => (
          <Route
            key={path}
            path={path}
            element={<PublicRoute Component={element} restricted={restricted} />}
          />
        ))}
      </Routes>
    </EssentialInfo>
  );
}

// src/pages/EssentialInfo/index.js
function EssentialInfo({ children }) {
  return (
    <S.Layout ref={layoutRef} onClick={handleClickLayout}>
      {/* 생략 */}
      {children}
    </S.Layout>
  );
}

반복되는 hoc는 배열을 만든 뒤 Array.prototype.map을 통해 가독성 및 유지보수가 용이하게 관리하고 있습니다.

src/router/PrivateRoutes.js

import React from 'react';
// 생략

const routes = [
  {
    path: ROUTE.PROFILE,
    element: EditUserProfile,
    restricted: true,
  },
  // 생략
];

const publicRoutes = routes.map(({ path, element, restricted }) => ({
  path,
  element: <PublicRoute Component={element} restricted={restricted} />,
}));

export default publicRoutes;

참고자료

https://milooy.wordpress.com/2016/03/03/underscore-vs-dash/

https://kimchanjung.github.io/programming/2020/06/22/react-router-overlab-routing/

https://ui.dev/react-router-route-config

Clone this wiki locally