코드 스플린팅
코드 스플리팅
파일을 분리하는 작업
❗더 나은 사용자 경험을 위해 코드를 비동기적으로 로딩하는 방법
예를 들어 페이지가 /main, /about, /post 이렇게 세 가지 페이지로 이루어진 SPA를 개발한다고 할 때
/main으로 들어가는 동안 /about이나 /post 페이지 정보는 사용자에게 필요하지 않을 확률이 높다.
💡 그러한 파일들을 분리하여 지금 사용자에게 필요한 파일만 불러올 수가 있다면 로딩도 빠르게 이루어지고 트래픽도 줄어 사용자 경험이 좋아질 수가 있다.
지금 당장 필요한 코드가 아니면 따로 분리시켜서, 나중에 필요할 때 불러와서 사용할 수 있다.
리액트에서 코드 스플리팅
React.lazy
💡 컴포넌트를 렌더링하는 시점에 비동기적으로 로딩할 수 있게 해주는 유틸 함수이다.
Suspense
💡 리액트 내장 컴포넌트로 코드 스플리팅 된 컴포넌트를 로딩하고, 로딩이 끝나지 않았을 때 보여줄 UI를 설정할 수 있다.
fallback이라는 props를 통해 로딩 중에 보여줄 JSX 문법을 지정할 수 있다.
lazy는 컴포넌트를 동적으로 로드하게 해주며, Suspense는 로드 중인 상태를 처리합니다.
라우터 + 코드 스플린팅
App.js
import {RouterProvider} from 'react-router-dom'; // RouterProvider는 생성된 라우터를 애플리케이션에 주입하는 컴포넌트입니다.
import root from "./router/root"; // ./router/root 모듈에서 root 라우터 객체를 가져옵니다. root는 root.js에서 설정한 라우터 객체입니다.
function App() {
return (
/*
RouterProvider 컴포넌트를 사용하여 root 라우터를 애플리케이션에 주입합니다.
router={root} 속성을 통해 root 라우터 객체를 RouterProvider에 전달합니다.
이렇게 하면 RouterProvider는 root 라우터 객체에 정의된 경로와 컴포넌트에 따라 라우팅을 관리합니다.
*/
<RouterProvider router={root}/>
);
}
export default App;
root.js
import {createBrowserRouter} from "react-router-dom"; //createBrowserRouter 함수는 브라우저 기반의 라우터를 생성하는 데 사용
import {Suspense,lazy} from "react"; // React의 Suspense와 lazy를 가져옵니다. 이 두 함수는 동적 import(코드 스플리팅)를 통해 컴포넌트를 로드할 때 사용됩니다.
const Loading = <div>Loading...</div> //로딩 상태를 나타내는 간단한 React 요소를 정의합니다. 컴포넌트를 동적으로 로드하는 동안 사용자에게 로딩 메시지를 표시하는 데 사용됩니다.
/*
Main 컴포넌트를 동적으로 로드하기 위해 lazy 함수를 사용
lazy 함수는 함수를 인자로 받고, 전달인자 함수는 동적 import()함수를 이용하여 코드이고
이 함수는 실행 중에 모듈을 동적으로 불러올 수 있고,동적 가져오기를 사용하면 애플리케이션에 필요한 모듈을 필요한 시점에 로드할 수 있으므로, 대
규모 애플리케이션에서 초기 로드 속도를 향상시키고 필요하지 않은 모듈의 로드를 지연시킬 수 있다.
동적 import의 반환값은 promise이며 , lazy는 promise를 반환하는 함수를 받아서 동적으로 로드된 컴포넌트를 React 컴포넌트로 사용할 수 있도록 래핑합니다.
MainPage라는 컴포넌트를 Main 변수에 할당한다. 이를 통해 MainPage 컴포넌트를 Main이라는 이름으로 사용할 수 있다.
*/
const Main = lazy(() => import("../pages/MainPage"))
const About = lazy(() => import("../pages/AboutPage"))
const root = createBrowserRouter([ // createBrowserRouter 함수에 라우트 설정을 전달하여 라우터를 생성합니다.
{
path:"", // path는 경로를 정의합니다. 여기서는 기본 경로("", 즉 /)를 설정
/*
element는 해당 경로에서 렌더링할 컴포넌트를 정의합니다. 여기서는 Suspense 컴포넌트를 사용하여 로딩 상태를 처리합니다.
suspense 컴포넌트는 동적으로 로드되는 컴포넌트를 감싸고, fallback 속성을 통해 로딩 중에 표시할 컴포넌트 또는 리액트 요소를 지정가능하다. 여기서는 리액트 요소로 지정
Suspense -> 동적으로 로드되는 컴포넌트가 로드되는 동안 로딩 상태를 표시합니다. fallback 속성을 통해 로딩 중에 표시할 컴포넌트를 지정합니다.
동적 import로 인해 컴포넌트 로드가 지연될 때 사용자 경험을 향상시킵니다.
기본경로 url요청이 되면 MainPage 컴포넌트가 동적import가 되고, 동적 로딩중에 Loading 컴포넌트가 보이게된다.
동적로딩으로 초기번들을 줄여서 속도를 빠르게 할 수 있다.
*/
element:<Suspense fallback={Loading}><Main/></Suspense>
},
{
path:"about",
element: <Suspense fallback={Loading}><About/></Suspense>
}
])
export default root;
SPA , <Link>
React로 작성된 애플리케이션은 일반적으로 SPA (Single Page Application)입니다.
SPA에서는 전체 페이지를 다시 로드하지 않고, 클라이언트 측에서 라우팅을 관리하여 애플리케이션의 성능과 사용자 경험을 향상시킵니다.
주소창을 통한 접근과 전체 애플리케이션 로딩
직접 주소 입력:
사용자가 localhost:3000/about과 같이 브라우저 주소창에 직접 입력하여 접근하면, 서버는 해당 URL을 처리하고 애플리케이션의 처음부터 다시 로드됩니다.
이 과정에서 React 애플리케이션이 초기화되고, 필요한 모든 컴포넌트가 로드됩니다.
리액트 라우터를 통한 네비게이션:
React 애플리케이션 내부에서 <Link> 컴포넌트를 사용하여 네비게이션하면, 페이지 전체를 다시 로드하지 않고 필요한 컴포넌트만 로드합니다.
이는 클라이언트 측 라우팅을 통해 가능하며, React Router가 이를 관리합니다.
<Link>와 <a> 태그의 차이점
<Link> 컴포넌트:
react-router-dom에서 제공하는 <Link> 컴포넌트는 클라이언트 측 네비게이션을 위해 사용됩니다.
<Link>를 클릭하면 페이지 전체를 다시 로드하지 않고, URL을 변경하면서 해당 경로에 맞는 컴포넌트만 로드합니다.
<a> 태그:
<a> 태그를 사용하면 기본적으로 브라우저는 해당 URL로 요청을 보내고 전체 페이지를 다시 로드합니다.
SPA에서는 이러한 동작이 불필요하고 비효율적이므로, <Link> 컴포넌트를 사용하는 것이 좋습니다.
코드 스플리팅과 동적 로딩
코드 스플리팅:
Suspense와 lazy를 사용하여 컴포넌트를 동적으로 로드할 수 있습니다.
이는 코드 스플리팅 (code splitting)이라 불리며, 초기 로딩 시간을 줄이고 필요한 컴포넌트만 로드하는 데 도움이 됩니다.
예를 들어, lazy(() => import('./SomeComponent'))를 사용하면 SomeComponent는 실제로 필요할 때 로드됩니다.
로딩중에는 Suspense의 fallback 속성으로 지정한 로딩 상태 컴포넌트를 표시합니다.
MainPage.js
/*
React는 SPA로서 브라우저 주소창을 통해 컴포넌트를 출력할 수 있습니다.
주소창 변경은 애플리케이션 전체 로딩과 처리를 의미합니다.
SPA에서는 새 창을 열거나 '새로고침'하는 것에 주의해야 합니다.
따라서 React Router를 사용할 때는 <a> 태그 사용을 피해야 합니다.
'localhost:3000/about'과 같이 주소창에 직접 입력해서 접근하면
React가 처음부터 실행됩니다. 하지만 <Link>를 사용하면
React가 처음부터 실행될 필요 없이 해당 주소를 위한 컴포넌트만 로딩하면 됩니다.
React는 실행 시 모든 컴포넌트를 로딩하지 않기 위해
Suspense와 lazy를 사용하여 분할 로딩을 합니다. 이를 코드 스플리팅이라고 합니다.
*/
import {Link} from 'react-router-dom';
const MainPage = () => {
return (
<div>
<div className="flex">
<Link to={'/about'}>About</Link>
</div>
<div className=" test-3xl">
<div>Main Page</div>
</div>
</div>
);
}
export default MainPage;
AboutPage.js
const AboutPage = () => {
return(
<div className=" text-3xl">About Page</div>
)
}
export default AboutPage;
레이아웃 컴포넌트
프로젝트에서는 공통의 레이아웃 템플릿을 구성하고 메뉴 구조를 만들어서 자주 사용하는 링크들에 대한 처리를 재사용 할 수 있도록 구성한다.
BasicLayout.js
== <BasicLayout> 컴포넌트
children props를 받아서 레이아웃 내부의 메인 콘텐츠를 표시합니다.
<BasicLayout> 컴포넌트는 기본적인 레이아웃을 구성하며, <header>와 메인 콘텐츠, 사이드바로 구분됩니다.
const BasicLayout = ({children}) => { // 리액트는 가장 겉의 태그가 하나여야하므르 <>라는 빈태그를 이용하여 여러태그를 같은레벨에 존재하게한다.
return(
<>
<header className="bg-teal-400 p-5">
<h1 className="text-2xl md:text-4xl">
Header
</h1>
</header>
<div className="bg-white my-5 w-full flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
<main className="bg-sky-300 md:w-2/3 lg:w-3/4 px-5 py-40">
{children}
</main>
<aside className="bg-green-300 md:w-1/3 lg:w-1/4 px-5 py-40">
<h1 className="text-2xl md:text-4xl">
Sidebar
</h1>
</aside>
</div>
</>
)
}
export default BasicLayout;
MainPage.js
MainPage 컴포넌트에 레이아웃 컴포넌트를 적용
import BasicLayout from "../layouts/BasicLayout";
const MainPage = () => {
return (
<BasicLayout>
<div className=" text-3xl">Main page</div>
</BasicLayout>
);
}
export default MainPage;
상단 메뉴 컴포넌트
레이아웃에서 header에 들어갈 부분은 별도의 컴포넌트를 구성해서 활용합니다.
BasicMenu.js
import {Link} from "react-router-dom";
const BasicMenu = () => {
return (
<nav id='navbar' className=" flex bg-blue-300">
<div className="w-4/5 bg-gray-500">
<ul className="flex p-4 text-white font-bold">
<li className={"pr-6 text-2xl"}>
<Link to={'/'}>Main</Link>
</li>
<li className={"pr-6 text-2xl"}>
<Link to={'/about'}>About</Link>
</li>
</ul>
</div>
<div className={"w-1/5 flex justify-end bg-orange-300 p-4 font-medium"}>
<div className={"text-white text-sm m-1 rounded"}>
Login
</div>
</div>
</nav>
);
}
export default BasicMenu;
위의 BasicMenu 컴포넌트를 BasicLayout 컴포넌트에 추가
header부분을 BasicMenu 컴포넌트로 대체한다.
import BasicMenu from "../component/menus/BasicMenu";
const BasicLayout = ({children}) => { // 리액트는 가장 겉의 태그가 하나여야하므르 <>라는 빈태그를 이용하여 여러태그를 같은레벨에 존재하게한다.
return(
<>
<BasicMenu/>
<div className="bg-white my-5 w-full flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
<main className="bg-sky-300 md:w-2/3 lg:w-3/4 px-5 py-40">
{children}
</main>
<aside className="bg-green-300 md:w-1/3 lg:w-1/4 px-5 py-40">
<h1 className="text-2xl md:text-4xl">
Sidebar
</h1>
</aside>
</div>
</>
)
}
export default BasicLayout;
모듈화와 라우팅 관리
1. 애플리케이션 컴포넌트의 증가와 복잡성
대규모 React 애플리케이션에서는 다양한 기능과 컴포넌트가 필요하게 됩니다. 이로 인해 단일 파일 내에서 모든 컴포넌트를 관리하기 어려워지며, 코드의 유지보수성과 확장성이 저하될 수 있습니다.
2. 모듈화된 기능 구성
애플리케이션을 관리하기 위해 기능을 모듈로 나누는 것이 좋습니다. 예를 들어, 게시판, 회원 관리, 상품 관리 등의 기능을 각각 독립적인 모듈로 구성할 수 있습니다. 각 모듈은 자체적으로 필요한 컴포넌트와 상태를 포함하며, 일반적으로 하나의 폴더 내에 관련 파일을 모아서 관리합니다.
3. 모듈 내 메뉴 구조
각 모듈은 자체적인 메뉴 구조를 가질 수 있습니다. 예를 들어, 게시판 모듈은 게시글 목록, 글쓰기 폼 등의 메뉴를 포함할 수 있습니다. 회원 모듈은 로그인, 회원 가입, 프로필 관리 등의 메뉴를 포함할 수 있습니다. 이렇게 각 모듈은 자체적으로 필요한 기능을 구현하고, 이를 통합하여 전체 애플리케이션을 구성합니다.
4. React-Router의 <Outlet> 활용
React-Router는 SPA(Single Page Application)에서 네비게이션을 관리하는 라이브러리입니다. <Outlet>은 React-Router v6에서 도입된 요소로, 중첩된 라우트 구조에서 하위 라우트들을 렌더링하는 데 사용됩니다.
투두 링크 추가
<li className={"pr-6 text-2xl"}>
<Link to={'/todo/'}>Todo</Link>
</li>
/todo 가 아닌 /todo/로 작성한 이유는?
- React Router에서도 이와 같은 관례를 따릅니다. '/todo/'와 같이 슬래시로 끝나는 경로는 해당 경로가 디렉토리를 나타내고, 하위 경로들이 존재할 수 있음을 의미합니다.
- 따라서 '/todo/'를 사용하면, 예를 들어 '/todo/add', '/todo/edit' 등의 하위 경로들을 쉽게 추가할 수 있습니다
투두라는 모듈에서 사용할 인덱스페이지
import {Outlet} from "react-router-dom";
import BasicLayout from "../../layouts/BasicLayout";
const IndexPage = () => {
return ( // <BasicLayout> 컴포넌트 안에 들어간 요소들은 React 컴포넌트의 children props로 전달됩니다.
<BasicLayout>
<div className="w-full flex m-2 p-2 ">
<div className="text-xl m-1 p-2 w-20 font-extrabold text-center underline">LIST</div>
<div className="text-xl m-1 p-2 w-20 font-extrabold text-center underline">ADD</div>
</div>
<div className="flex flex-wrap w-full">
<Outlet/>
</div>
</BasicLayout>
); // <Outlet />: React Router의 <Outlet> 컴포넌트는 현재 경로에 맞는 하위 컴포넌트들을 렌더링합니다.
}
export default IndexPage;
라우터에 추가
const TodoIndex = lazy(() => import("../pages/todo/IndexPage"));
{
path:"about",
element: <Suspense fallback={Loading}><About/></Suspense>
},
<Outlet>을 활용하면 중첩적인 라우팅 설정시 레이아웃을 유지할 수 있습니다.
이를 확인하기 위해 'todo/list'와 같이 중첩적인 경로를 처리해보겠습니다.
투두 리스트페이지 컴포넌트
const ListPage = () =>{
return (
<div className="p-4 w-full bg-orange-200 ">
<div className="text-3xl font-extrabold">
Todo List Page Component
</div>
</div>
);
}
export default ListPage;
React-Router의 중첩라우팅
React-Router는 하나의 경로 설정에서 children 속성을 이용해서 하위로 중첩적인 경로를 지정할 수 있습니다.
const TodoList = lazy(() => import("../pages/todo/ListPage"));
{
path:"todo",
element: <Suspense fallback={Loading}><TodoIndex/></Suspense>,
children:[
{
path:"list",
element:<Suspense><TodoList></TodoList></Suspense>
}
]
}
정리
{
path:"todo",
element: <Suspense fallback={Loading}><TodoIndex/></Suspense>,
children:[
{
path:"list",
element:<Suspense fallback={Loading}><TodoList></TodoList></Suspense>
}
]
}
todo 경로로 request가 되면
TodoIndex 컴포넌트가 로딩되고
const TodoIndex = lazy(() => import("../pages/todo/IndexPage"));
[ TodoIndex 컴포넌트 == IndexPage 컴포넌트 ]
IndexPage 컴포넌트에서는 BasicLayout 컴포넌트를 사용한다.
BasicLayout 의 children 에는
IndexPage 가 < BasicLayout > 태그안에 적어둔 요소들이 들어간다 [ children이라는 props으로 들어가게된다.]
그리고 그 todo/list 로 요청이 들어오면
IndexPage의 <Outlet>에는 라우터설정에서 children 속성에 설정해둔 path:"list"의 element가 들어간다.
즉 그냥 /todo로 요청이 온다면 Outlet부분이 치환되지않고 렌더링되고
/todo/list처럼오면 /todo 라우팅설정아래 /list children을 찾아 outlet 부분과 치환하여 렌더링해준다.
위와 같이 라우팅 설정에 children 속성을 이용하면 중첩적인 라우팅 설정을 적용할 수 있지만 페이지가 많아지면
root.js파일이 너무 복잡해진다.
그러므로 별도의 함수에서 children 속성값을 해당하는 설정을 반환하는 방식을 사용한다.
router/todo/todoRouter.js
import { Suspense, lazy } from "react";
const Loading = <div>Loading....</div>
const TodoList = lazy(() => import("../../pages/todo/ListPage"))
const todoRouter = () => {
return [
{
path: "list",
element: <Suspense fallback={Loading}><TodoList/></Suspense>
}
]
}
export default todoRouter;
todoRouter()의 반환값을 이용하여 children 속성을 설정한다.
{
path:"todo",
element: <Suspense fallback={Loading}><TodoIndex/></Suspense>,
children: todoRouter()
}
const todoRouter = () => {
return [
{
path: "list",
element: <Suspense fallback={Loading}><TodoList/></Suspense>
},
{
path:"",
element: <Navigate replace to="list"/>
}
]
}
todoRouter의 children ""경로에 대한 설정을 추가해준다.
root.js의
{
path:"todo",
element: <Suspense fallback={Loading}><TodoIndex/></Suspense>,
children: todoRouter()
}
해당 설정과 함께보면
기본경로인 todo로 이동하게되면
todo/list로 이동하게된다.
url param 사용
/todo/read/33과 같이
특정번호 todo 조회하기 위한 라우팅 설정 추가
Todo 목록 페이지에서 조회 페이지로 이동 시 경로 변경
예: '/todo/read/33'와 같이 데이터가 있는 경로
React-Router에서 ':'를 활용하여 데이터 전달
{
path:"read/:tno",
element: <Suspense fallback={Loading}><TodoRead/></Suspense>,
}
React-Router에서 ':'를 활용하여 데이터 전달된
33과 같은 파라미터를 받아 처리하기
경로 처리를 위한 useParams( )
→ 특정 번호의 경로 사용 시, 컴포넌트에서 주소창의 일부 활용 필요.
→ React-Router의 useParams( )로 지정된 변수 추출 가능.
import { useParams } from "react-router-dom";
const ReadPage = () => {
const {tno} = useParams()
return (
<div className="text-3xl font-extrabold">
Todo Read Page Component {tno}
</div>
);
}
export default ReadPage;
url에 ? 이후에 들어가는
쿼리스트링은 useSearchParams( )를 이용
import { useSearchParams } from "react-router-dom";
const ListPage = () => {
const [queryParams] = useSearchParams()
const page = queryParams.get("page") ? parseInt(queryParams.get("page")) : 1
const size = queryParams.get("size") ? parseInt(queryParams.get("size")) : 10
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">
Todo List Page Component {page} --- {size}
</div>
</div>
);
}
export default ListPage;
useNavigate( )
→ React-Router를 사용할 때, 링크(<Link>)로 이동하는 경우도 있지만 동적 데이터 처리로 이동하는
경우가 더 많음.
→ useNavigate( )를 활용하여 프로그램을 통해 데이터 동적 이동 처리.
import {Outlet , useNavigate} from "react-router-dom";
import BasicLayout from "../../layouts/BasicLayout";
import { useCallback } from "react";
const IndexPage = () => {
const navigate = useNavigate()
const handleClickList = useCallback(() => { navigate({ pathname:'list'}) })
const handleClickAdd = useCallback(() => { navigate({ pathname:'add' }) })
return (
<BasicLayout>
<div className="w-full flex m-2 p-2 ">
<div className="text-xl m-1 p-2 w-20 font-extrabold text-center underline"
onClick={handleClickList}> LIST </div>
<div className="text-xl m-1 p-2 w-20 font-extrabold text-center underline"
onClick={handleClickAdd}> ADD </div>
</div>
<div className="flex flex-wrap w-full"> <Outlet/> </div>
</BasicLayout>
);
}
export default IndexPage;
\
댓글