
리액트 프로젝트 산출물을 빌드 할때 코드 분할 하는 법에 대하여
브라우져를 통해 사용자가 첫 페이지에 접속을 하면 index.html & main.js파일을 다운로드 한다. 이때 로딩속도는 main.js의 용량과 비례하므로 js를 정해진 용량 이하로 분할 하여 빌드후
각 페이지에 필요한 파일로 다운 받게하여 속도 향상을 할수 있다.
...
- import { BrowserRouter as Router } from "react-router-dom";
import { HashRouter as Router } from "react-router-dom";
...
정적 서비스를 위해 라우터를 변경 해준다.
yarn build
./build folder
├─ build
├─ v0.0.1
├─ main.js
├─ main.js.LICENSE.txt
└─ index.html
LICENSE.txt파일이 생성되지 않도록 설정 추가
...
const TerserPlugin = require("terser-webpack-plugin");
...
optimization: {
minimize: true,
minimizer: [new TerserPlugin({ extractComments: false })]
}
...
yarn build

./src/components/PageLoader.tsx
import React, { SVGProps } from "react";
const PageLoader = () => {
return (
<div className="flex flex-col items-center justify-center flex-1 w-full h-[500px] transition-colors duration-500 bg-gradient-to-b from-blue-100 via-white to-white dark:from-gray-900 dark:via-gray-950 dark:to-black">
{/* 회전하는 로고 아이콘 */}
<div className="mb-6">
<LoadingTwotoneLoop className="w-16 h-16 text-gray-600 opacity-90" />
</div>
{/* 텍스트 애니메이션 */}
<div className="flex items-center space-x-1 text-xl font-semibold tracking-wide text-gray-600 dark:text-blue-300">
<span>페이지를 불러오는 중입니다</span>
<span className="text-2xl animate-bounce">.</span>
<span className="animate-bounce [animation-delay:0.2s] text-2xl">.</span>
<span className="animate-bounce [animation-delay:0.4s] text-2xl">.</span>
</div>
</div>
);
};
export default PageLoader;
export function LoadingTwotoneLoop(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="10em" height="10em" {...props}>
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path strokeDasharray="16" strokeDashoffset="16" d="M12 3c4.97 0 9 4.03 9 9">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="16;0"></animate>
<animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"></animateTransform>
</path>
<path strokeDasharray="64" strokeDashoffset="64" strokeOpacity=".3" d="M12 3c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9c0 -4.97 4.03 -9 9 -9Z">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="1.2s" values="64;0"></animate>
</path>
</g>
</svg>
);
}
...
import React, { Suspense } from "react";
import GNB from "@/components/Navigations/GNB";
import Routes from "@/config/Routes";
import { BrowserRouter as Router } from "react-router-dom";
import PageLoader from "./components/PageLoader";
const App = () => {
return (
<div className="flex flex-row justify-center min-h-screen">
<div className="flex flex-col flex-1 ">
<Router>
<GNB />
<div className="flex flex-1 w-full text-black transition-colors duration-300 bg-white dark:bg-gray-900 dark:text-gray-100">
<Suspense fallback={<PageLoader />}>
<Routes />
</Suspense>
</div>
</Router>
</div>
</div>
);
};
export default App;
파일이 로드된 후 랜더링이 될수 있도록 Suspense로 감싸준다.
import Home from "@/pages/Home";
import NotFound from "@/pages/NotFound";
import React from "react";
import { useRoutes } from "react-router-dom";
const Category1 = React.lazy(() => import("@/pages/Category1"));
const Page1 = React.lazy(() => import("@/pages/Category1/Page1"));
const Page2 = React.lazy(() => import("@/pages/Category1/Page2"));
const Page3 = React.lazy(() => import("@/pages/Category1/Page3"));
const Routes = () => {
const element = useRoutes([
{ path: "/", element: <Home /> },
{
path: "/category1",
element: <Category1 />,
children: [
{ path: "/category1/page1", element: <Page1 /> },
{ path: "/category1/page2", element: <Page2 /> },
{ path: "/category1/:slug", element: <Page3 /> }
]
},
{ path: "*", element: <NotFound /> }
]);
return element;
};
export default Routes;
optimization: {
minimize: true,
minimizer: [new TerserPlugin({ extractComments: false })],
splitChunks: {
chunks: "all"
}
}
splicChucks 옵션을 추가 해준다.
import file대신 lazy 함수를 이용해 import 할경우 webpack에서 코드분할을 자동으로 해준다.
yarn build
├─ build
├─ v0.0.1
├─ 887.main.js
└─ main.js
└─ index.html

위 빌드 결과물처럼 자동으로 분할되어 파일이 생긴다.
Routes.tsx에서 페이지들을 선언한뒤에 GNB또는 하위 메뉴바에서 링크를 등록을 해주는 과정이 다소 번거로울 수 있으므려 Routes.tsx에서 정의 된 json데이터를 사용 할수 있도록 변경하자.
import React from "react";
// 동적 로딩이 아닌 정적 컴포넌트 (즉시 import)
import Dashboard from "@/pages/Dashboard";
import NotFound from "@/pages/NotFound";
// 동적 import를 통한 코드 스플리팅 (React.lazy + Suspense 필요)
const Category2 = React.lazy(() => import("@/pages/Category2"));
const Category3 = React.lazy(() => import("@/pages/Category3"));
const Category4 = React.lazy(() => import("@/pages/Category4"));
const Category4Page1 = React.lazy(() => import("@/pages/Category4/Category4Page1"));
const Category4Page2 = React.lazy(() => import("@/pages/Category4/Category4Page2"));
const Category4Page3 = React.lazy(() => import("@/pages/Category4/Category4Page3"));
/**
* 실제 라우트 데이터 정의
* - 라우터 구성 및 메뉴, 권한 처리 등 다목적으로 활용 가능
*/
export const RouteData = {
Dashboard: { name: "Dashboard", icon: "☀️", disable: false, path: "/", element: <Dashboard /> },
Category2: { name: "Category2", icon: "☀️", disable: false, path: "/category2", element: <Category2 /> },
Category3: { name: "Category3", icon: "☀️", disable: false, permision: ["admin"], path: "/category3", element: <Category3 /> },
Category4: {
name: "Category4",
icon: "☀️",
disable: false,
path: "/category4",
element: <Category4 />,
children: {
Category4Page1: { name: "Category4Page1", icon: "", disable: false, path: "/category4/page1", element: <Category4Page1 /> },
Category4Page2: { name: "Category4Page2", icon: "", disable: false, path: "/category4/page2", element: <Category4Page2 /> },
Category4Page3: { name: "Category4Page3", icon: "", disable: false, path: "/category4/:slug", element: <Category4Page3 /> }
}
},
NotFound: { name: "NotFound", icon: "", disable: true, path: "*", element: <NotFound /> }
};
import { RouteObject, useRoutes } from "react-router-dom";
/**
* 라우트 기본 타입 (RouteObject에서 children 제외 후 확장)
* - name: 라벨용 이름
* - disable: 비활성화 여부 (메뉴 숨김 등)
* - permision: 권한 리스트 (ex: ["admin"])
*/
export interface ExRouteObjectBase extends Omit<RouteObject, "children"> {
name: string;
icon: string;
disable?: boolean;
permision?: string[];
}
/**
* 라우트 객체 타입
* - children은 key-value 구조로 선언하여 참조 편의성 확보
*/
export interface ExRouteObject extends ExRouteObjectBase {
children?: Record<string, ExRouteObject>; // 중첩 라우팅도 key-value로 관리
}
/**
* 최상위 라우트 모음: 각 항목은 키를 기반으로 직접 참조 가능
*/
export type RouteDataAtts = Record<string, ExRouteObject>;
/**
* cleanRoutes:
* - key-value 구조의 RouteDatas를 React Router가 요구하는 배열 형태로 변환
* - children도 재귀적으로 변환
*/
export function cleanRoutes(routes: RouteDataAtts): RouteObject[] {
return Object.values(routes).map(({ path, element, children }) => ({
path,
element,
children: children ? cleanRoutes(children) : undefined
}));
}
/**
* availableRouteDatas:
* - disable이 아닌 라우트만 반환
* - 메뉴 표시, 퍼미션 검사용
*/
export function availableRouteDatas(routes: RouteDataAtts): ExRouteObject[] {
return Object.values(routes).filter(r => !r.disable);
}
/**
* Routes 컴포넌트:
* - useRoutes()를 통해 라우트 요소 생성
* - cleanRoutes()를 통해 라우터용 포맷으로 변환
*/
const Routes = ({ data }: { data: RouteDataAtts }) => {
const element = useRoutes(cleanRoutes(data));
return element;
};
export default Routes;
{ name: "", visible: true, permision: ["admin"], path: "/", element: }
여기서는 단순하기 메뉴명만 표기 하도록 하겠다.
import React, { useEffect, useState } from "react";
import { NavLink } from "react-router-dom";
import { availableRouteDatas, RouteDataAtts } from "@/config/Routes";
export default function GNB({ data }: { data: RouteDataAtts }) {
const menus = availableRouteDatas(data);
const [isDark, setIsDark] = useState(() => document.documentElement.classList.contains("dark"));
const toggleDark = () => {
const newVal = !isDark;
document.documentElement.classList.toggle("dark", newVal);
localStorage.setItem("theme", newVal ? "dark" : "light");
setIsDark(newVal);
};
useEffect(() => {
const saved = localStorage.getItem("theme");
if (saved === "dark") {
document.documentElement.classList.add("dark");
setIsDark(true);
}
}, []);
return (
<header className="relative z-50 flex items-center justify-between w-full h-16 px-8 text-gray-900 bg-white border-b border-gray-200 shadow dark:bg-gray-900 dark:border-gray-700 dark:text-white">
<nav className="flex items-center space-x-6">
<span className="text-lg font-bold tracking-tight">REACT</span>
{menus.map((item, idx) =>
item.children ? (
<div key={idx} className="relative group">
<span className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800">{item.icon + " " + item.name}</span>
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 shadow-lg rounded border dark:border-gray-700 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto transition-opacity z-10 min-w-[160px]">
{availableRouteDatas(item.children).map((child, i) => (
<NavLink key={i} to={child.path} className={({ isActive }) => `block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${isActive ? "font-semibold" : ""}`}>
{child.name}
</NavLink>
))}
</div>
</div>
) : (
<NavLink key={idx} to={item.path} className={({ isActive }) => `px-3 py-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 ${isActive ? "font-semibold" : ""}`}>
{item.icon + " " + item.name}
</NavLink>
)
)}
</nav>
<div className="flex items-center space-x-4 text-sm">
<button onClick={toggleDark} className="px-3 py-1 text-black bg-gray-200 rounded dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 dark:text-white">
{isDark ? "🌙 다크" : "☀️ 라이트"}
</button>
<span className="text-gray-500 dark:text-gray-300">👤 운영자</span>
<button className="px-3 py-1 text-white bg-red-500 rounded hover:bg-red-600">로그아웃</button>
</div>
</header>
);
}
import React from "react";
import { NavLink } from "react-router-dom";
import { availableRouteDatas, RouteDataAtts } from "@/config/Routes";
export interface SideNavBarProps {
data: RouteDataAtts;
}
export default function SideNavBar({ data }: SideNavBarProps) {
const menus = availableRouteDatas(data);
return (
<aside className="w-48 border-r border-gray-200 dark:border-gray-700">
<nav className="">
{menus.map((item, idx) => (
<NavLink key={idx} to={item.path} className={({ isActive }) => `block px-3 py-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 ${isActive ? "font-bold bg-gray-100 dark:bg-gray-800 dark:text-white" : ""}`}>
{item.icon + " " + item.name}
</NavLink>
))}
</nav>
</aside>
);
}
import React from "react";
import SideNavBar from "@/components/Navigations/SideNavBar";
import PageLayout from "@/components/PageLayout";
import { Outlet } from "react-router-dom";
import { RouteData } from "@/config/RouteData";
const Category4 = () => {
return (
<PageLayout className="flex-row p-0">
<SideNavBar data={RouteData.Category4.children} />
<Outlet />
</PageLayout>
);
};
export default Category4;
import React, { Suspense } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import GNB from "@/components/Navigations/GNB";
import Routes from "@/config/Routes";
import PageLoader from "@/components/PageLoader";
import { RouteData } from "@/config/RouteData";
const App = () => {
return (
<div className="flex flex-row justify-center min-h-screen">
<div className="flex flex-col flex-1 ">
<Router>
<GNB data={RouteData} />
<div className="flex flex-1 w-full text-black transition-colors duration-300 bg-white dark:bg-gray-900 dark:text-gray-100">
<Suspense fallback={<PageLoader />}>
<Routes data={RouteData} />
</Suspense>
</div>
</Router>
</div>
</div>
);
};
export default App;