React Router 구조 개선하기

jinvicky·2024년 4월 23일
0

Intro


회사 프로젝트를 Vue3로 진행하는데 부분 프론트 개발자로 협업하면서, 몇 달 전에 짰던 React router를 개선할 여지가 있다고 판단하여 구조적인 개선을 해보았다.

Before Refactoring


function App() {
   return (
     <>
       <NavBar />
       <Router />
       <Footer />
     </>
   );
}

기존에도 동일하게 react-router-dom 라이브러리를 사용했고,
공통인 네비게이션 바와 푸터는 전역에 적용되도록 위와, 같이 Router 태그 바깥에 넣었다.

import { BrowserRouter, Routes, Route } from "react-router-dom";
import BlogList from "./post/BlogList";
import CmsList from ".//cmsList/CmsList";
import MainPage from ".//main/MainPage";
import SignInPage from ".//signIn/page";
import MyInquiryList from ".//bbs/inquiry/InquiryList";
import MyCmsList from "./myPage/myCms/MyCmsList";
import InquiryForm from ".//bbs/inquiry/InquiryForm";
import ApplyCms from "./apply/ApplyForm";
import ReviewPage from ".//review/page";
import SigninKakaoLoading from "./signIn/loading/SigninKakaoLoading";
import SignOutLoading from ".//signIn/signOut";
import NotFound from "../error/notFound";
import NoticeDetail from ".//bbs/notice/NoticeDetail";
import InquiryDetail from ".//bbs/inquiry/InquiryDetail";
import MyReviewList from "./review/MyReviewList";
import SigninTwitterLoading from "./signIn/loading/SigninTwitterLoading";
import TossSuccess from "./payment/tossSuccess";
import SigninNaverLoading from "./signIn/loading/SigninNaverLoading";
import MyCmsDetail from "./myPage/myCms/MyCmsDetail";
import TossPayment from "./payment/TossPayment";

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<MainPage />} />
        <Route path="/posts" element={<BlogList />} />
        <Route path="/commissions" element={<CmsList />} />
        <Route path="/reviews" element={<ReviewPage />} />
        <Route path="/sign/in" element={<SignInPage />} />
        <Route path="/apply/:cmsId" element={<ApplyCms />} />
        <Route path="/mypage/cms" element={<MyCmsList />} />
        <Route path="/mypage/cms/:cmsApplyId" element={<MyCmsDetail />} />
        <Route path="/mypage/inquiry" element={<MyInquiryList />} />
        <Route path="/mypage/inquiry/form" element={<InquiryForm />} />
        <Route path="/mypage/inquiry/:inqId" element={<InquiryDetail />} />
        <Route path="/mypage/reviews" element={<MyReviewList />} />
        <Route path="/notice/:noticeId" element={<NoticeDetail />} />
        <Route path="/login/kakao" element={<SigninKakaoLoading />} />
        <Route path="/login/naver" element={<SigninNaverLoading />} />
        <Route path="/login/twitter" element={<SigninTwitterLoading />} />
        <Route path="/sign/out" element={<SignOutLoading />} />
        <Route path="/toss/success" element={<TossSuccess />} />
        <Route path="/toss/payment" element={<TossPayment />} />

        {/* 404 error page */}
        <Route path="/*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

이것이 기존 라우터 컴포넌트다.
Route 태그는 pathelement 속성(attribute)을 가진다.
내 코드를 프로젝트 코드와 보면서 내것은 상당히 날것(raw)같고 뭉쳐있다는 생각이 들었다.

내가 생각했던 문제점

  • 기능(도메인)별로 router가 묶여 있었으면 좋겠다. 지금 너무 보기에 헷갈린다.
  • lazy로 성능 개선을 하고 싶다.
  • (기타) 왜 ../../의 상대경로로 했지? 이러면 위치 옮길 때 골치아픈데, 게다가 위치 파악이 제대로 안됨

...오늘 짠 코드는 나와 신이 알지만, 어제 짠 코드는 신만이 안다는 말이 떠올랐다.

After Refactoring

상대 경로를 절대 경로로 바꾸기

일단 상대 경로부터 바꿨다. @를 붙인 alias를 등록해서 src를 기준으로 한다.
저번에 포스팅을 하긴 했지만 간략히 다시하겠다.

  • vite.config.ts를 수정한다.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@router': '/src/router',
      '@views': '/src/views',
      '@components': '/src/components'
    }
  },
  css: {
    preprocessorOptions: {
      sass: {
        additionalData: '@import "@/assets/scss/_variables";',
      },
    },
  },
})

테스트로 view, components를 등록해 보았다.

  • ts.config.json을 수정한다.
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": false,
    "baseUrl": ".",
    "paths": {
      "@router/*": [
        "src/router/*"
      ],
      "@views/*": [
        "src/views/*"
      ], 
      "@components/*":[
        "src/components/*"
      ]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

baseUrl 설정을 꼭 해야 한다. .으로 설정하더라고.

route.ts 작성하기

보통 도메인별로 domain.routes.ts를 별도 ts 파일로 작성한다.
내 토이 프로젝트는 라우팅이 그렇게 많지 않아서 테스트로 1개의 파일에 작성해 보도록 하겠다.

나는 routes.ts 파일을 src/router에 작성했다.


import React, { lazy } from 'react';
import { RouteObject } from 'react-router-dom';

/**
 * 진입페이지
 */
const entryRoutes: RouteObject[] = [
    {path: "/", Component: lazy(() => import('@views/EntryPage')),}
];

const statsRoutes: RouteObject[] = [
    {path: "/stats", Component: lazy(() => import('@views/MonthlyStats'))}
]

/**
 * 모든 라우팅 
 */
const pageRoutes = [
    ...entryRoutes,
    ...statsRoutes
]

export {
    pageRoutes,
}

lazy()를 써서 지연 로딩을 구현하고 동시에 기존 라우터처럼 길어지는 page import문을 줄일 수 있었다.

타입으로는 RouteObject를 사용했다. path, Component를 사용했다. (에러 시 ts가 검출)

전체적으로 pageRoutes에서 기존 라우팅 배열들을 합쳐서 export해준다.
이 방법은 mypage는 mypage별로, application은 application별로 그룹핑을 한 효과가 있다.

공통 컴포넌트가 들어가는 Layout 만들기

import { Suspense } from "react";
import { Outlet } from "react-router-dom";

import NavBar from "@components/navbar/index";
import Footer from "@components/footer/Footer";
import CommonLoading from "@components/loading";

function Layout () {
    return <>
        <NavBar />
        <Suspense fallback={<CommonLoading />}>
            <Outlet />
        </Suspense>
        <Footer/>
    </>
}

export default Layout;

(지금 보니 CommonLoading 별로다. 이름 나은 것 없나)
Outlet 컴포넌트는 react-router-dom 의 내장 컴포넌트다.
Suspense 컴포넌트로 감싸서 불러오기 중에는 로딩 컴포넌트를 띄우다가 lazy()를 썼던 routes를 화면에 띄운다.

그러면 저 Outlet 컴포넌트에는 뭐가 들어가나?

최종 App.tsx 적용

import { BrowserRouter as Router, Routes, Route, createBrowserRouter, RouterProvider } from 'react-router-dom';
import { pageRoutes } from "./routers/routes";
import Layout from "@components/global/Layout";
import NotFound from '@views/NotFound';

function App () {
  const router = createBrowserRouter([
    {
      element: <Layout />,
      errorElement: <NotFound />,
      children: pageRoutes
    },
  ])

  return (
    <RouterProvider router={router} />
  )
}

export default App;

정답은 pageRoutes의 컴포넌트가 Outlet에 들어가는 것이 된다.
errorElement는 지정되지 않은 router 경로에 접근할 경우 발생한다. 그래서 404 페이지 컴포넌트를 추가해주면 된다.

컴포넌트만 최소한으로 만들어서 테스트해볼 수 있다고 생각한다.

Outro


아래 블로그 추천
https://semaphoreci.com/blog/routing-layer-react

여기에 통계 페이지를 더해서 커미션 정산 액셀 파일을 통계를 내서 보여주고 싶은데 고민중이다.

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글