-
Notifications
You must be signed in to change notification settings - Fork 5
useRoutes로 라우팅 관리하기
현재 아래와 같이 App.jsx에서 모든 라우팅을 관리하고 있습니다. 아래처럼 라우팅 페이지를 관리했을 때 다음과 같은 불편함을 느꼈습니다.
- App.jsx은 엔트리 포인트로 폴더 구조 및 App전체에 사용되고 있는 기능을 보여야 한다고 생각합니다. 하지만 App.jsx에서 모든 페이지 관련 라우팅을 관리하다보니 프로젝트에서 전체적으로 사용하는 기능을 한 눈에 파악하기 어려웠습니다.
- 아래와 같이 페이지가 5개만 넘어가도 가독성이 좋지 못함을 알 수 있습니다.
- 중첩된 라우터가 무엇인지, 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;
- /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);
};
이전에는 <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);
}
이전에는 아래와 같이 <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);
};
현재 프로젝트 폴더구조는 다음과 같습니다.
# 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/
🏠 Home
- FE : MSW를 활용한 API Mocking
- FE : useRoutes로 라우팅 관리하기
- FE : UI/UX 용어 정리
- FE : Storybook 활용(feat. Chormatic을 활용한 협업)
- FE : 상태관리 migration (redux-→ react context api)
- FE : CORS란?(우리가 겪은 문제들)
- FE : 도메인별 Api 파일 구분 및 공통 에러 핸들러 생성
- FE : 사용자에게 API상태를 UI로 알려주자
- BE : java stream API 를 이용해 코드 가독성 제고
- BE : AOP를 활용해 특정 DTO에 정보 추가
- BE : DB 직접 조회를 줄이기 위한 노력