코드 스플리팅이란 전체 애플리케이션을 더 작은 청크(파일) 단위로 나누고, 특정 시점에 필요한 청크들만 동적으로 불러와서 사용하는 방식으로 애플리케이션을 최적화 하는 방법이다. 이를 통해 초기 로딩 시간을 줄이고, 사용자가 더 빠르게 콘텐츠에 접근할 수 있도록 한다.
SPA(Single Page Application)의 경우, 사용자가 애플리케이션에 처음 진입할 때 진입점이 되는 HTML 파일을 불러오고 위에서부터 코드를 실행하며 resource를 불러와 페이지를 렌더링한다.따라서 초기 렌더링시 굳이 바로 사용하지 않을 resource도 불러오기 때문에 초기 렌더링이 사용자 입장에서 불필요하게 늘어날 수 있다. 따라서 필요할 때마다 resource를 불러오는 방식으로 청크를 나누면 초기 렌더링 시간이 줄어들어 사용자 경험이 개선이 된다.
모든 파일을 코드 스플리팅을 한다면 초기 로딩 속도는 많이 줄어들겠지만 이후에 애플리케이션을 사용할 때마다 resource를 불러와 사용자 경험이 오히려 떨어질 수 있다. 따라서 여러가지 기준을 세워서 코드 스플리팅을 해야한다.
react-errror-boundary
라이브러리를 사용한다const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const Dotenv = require('dotenv-webpack');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const progressPlugin = require('progress-webpack-plugin');
module.exports = {
...
// 설정 부분
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
// components
import { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
import LandingPage from '@/pages/Landing/LandingPage';
import MainPage from '@/pages/Main/MainPage';
import ChannelContainer from '@/pages/Main/_compoenets/Content/ChannelContainer';
import Channel from '@/pages/Main/Channel/Channel';
import FallBack from '@/components/Molecules/FallBack';
import SignInPage from '@/pages/Signin/SignInPage';
const Profile = lazy(() => import('@/pages/Main/Profile/Profile'));
const EditChannels = lazy(() => import('@/pages/Main/EditChannels/EditChannels'));
const NotFoundPage = lazy(() => import('@/pages/NotFound/NotFoundPage'));
export default function Router() {
// view
return (
<Suspense fallback={<FallBack />}>
<Routes>
<Route path='/' element={<LandingPage />} />
<Route path='/signIn' element={<SignInPage />} />
<Route path='/main' element={<MainPage />}>
<Route path='/main' element={<ChannelContainer />} />
<Route path='/main/channel/:channelId' element={<Channel />} />
<Route path='/main/Profile' element={<Profile />} />
<Route path='/main/EditChannels' element={<EditChannels />} />
</Route>
<Route path='*' element={<NotFoundPage />} />
</Routes>
</Suspense>
);
}
Suspense 컴포넌트는 lazy로 감싸진 import문으로 해당 컴포넌트, 페이지가 불러오기 전까지 보여줄 컴포넌트이다.
코드 스플리팅 전
import React, { useEffect, useState } from 'react';
function UserProfile() {
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/user');
const result = await response.json();
setData(result);
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
}
fetchData();
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error loading data</div>;
}
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
export default UserProfile;
컴포넌트 내부 코드가 복잡하다.
코드 스플리팅 이후
import React, { useEffect, useState } from 'react';
function UserProfile() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/user');
const result = await response.json();
setData(result);
}
fetchData();
}, []);
if (!data) {
throw new Promise((resolve) => setTimeout(resolve, 1000)); // 임시 로딩 처리
}
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
export default UserProfile;
import React, { lazy, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
const UserProfile = lazy(() => import('./UserProfile'));
function LoadingFallback() {
return <div>Loading...</div>;
}
function ErrorFallback({ error, resetErrorBoundary }: { error: Error, resetErrorBoundary: () => void }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<LoadingFallback />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}
export default App;
resetErrorBoundary
를 이용하면 에러가 나기 전으로 되돌려 다시 컴포넌트를 렌더링해 데이터 패칭을 다시 하게 끔 만들 수 있다.// components
import { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
import LandingPage from '@/pages/Landing/LandingPage';
import MainPage from '@/pages/Main/MainPage';
import ChannelContainer from '@/pages/Main/_compoenets/Content/ChannelContainer';
import Channel from '@/pages/Main/Channel/Channel';
import FallBack from '@/components/Molecules/FallBack';
import SignInPage from '@/pages/Signin/SignInPage';
const Profile = lazy(() => import('@/pages/Main/Profile/Profile'));
const EditChannels = lazy(() => import('@/pages/Main/EditChannels/EditChannels'));
const NotFoundPage = lazy(() => import('@/pages/NotFound/NotFoundPage'));
export default function Router() {
// view
return (
<Suspense fallback={<FallBack />}>
<Routes>
<Route path='/' element={<LandingPage />} />
<Route path='/signIn' element={<SignInPage />} />
<Route path='/main' element={<MainPage />}>
<Route path='/main' element={<ChannelContainer />} />
<Route path='/main/channel/:channelId' element={<Channel />} />
<Route path='/main/Profile' element={<Profile />} />
<Route path='/main/EditChannels' element={<EditChannels />} />
</Route>
<Route path='*' element={<NotFoundPage />} />
</Routes>
</Suspense>
);
}
위에서 본 코드인데 Profile, EditChannels, NotFoundPage의 경우 유저가 자주 들어갈 페이지가 아니기 때문에 코드 스플리팅으로 지연로딩이 되게 끔 했다.// libraries
import styled from 'styled-components';
import { lazy } from 'react';
// components
import Header from './_compoenets/Header/Header';
import SideBar from './_compoenets/SiderBar/SideBar';
import { Outlet } from 'react-router-dom';
import YoutubeIframePlayer from './_compoenets/YoutubeIframePlayer/YoutubeIframePlayer';
const MusicBar = lazy(() => import('./_compoenets/MusicBar/MusicBar'));
export default function MainPage() {
// view
return (
<Wrapper>
<Header />
<Main>
<SideBar />
<Right>
<Outlet />
</Right>
</Main>
<MusicBar />
<YoutubeIframePlayer />
</Wrapper>
);
}
...Styled-components
MusicBar의 경우 음악이 재생 될 때만 보여지기 때문에 적용했다.먼저 리액트 쿼리를 사용하는 경우 Suspense와 ErrorBoundary를 사용할때는 useSuspenseQuery
와 QueryClient
의 throwOnError: true
옵션을 사용해야한다. (자세한건 공식 문서 보기)
import styled from 'styled-components';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
interface QueryErrorBoundaryProps {
children: React.ReactNode
}
export default function QueryErrorBoundary({ children }: QueryErrorBoundaryProps) {
const { reset } = useQueryErrorResetBoundary();
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<Wrapper role='alert'>
<ErrorMessage>{error.message}</ErrorMessage>
<ResetComponentButton onClick={resetErrorBoundary}>재시도</ResetComponentButton>
</Wrapper>
)}
>
{children}
</ErrorBoundary>
);
}
// libraries
import { lazy, Suspense } from 'react';
import styled from 'styled-components';
// components
import SubscribeButton from './SubscribeButton/SubscribeButton';
import EditButton from './EditButton/EditButton';
import FallBack from '@/components/Molecules/ComponentFallBack';
import QueryErrorBoundary from '@/components/Molecules/QueryErrorBoundary';
const MusicContainer = lazy(() => import('./MusicContainer'));
// images
import addSquareSvg from '@/images/svg/add-square.svg';
// hooks
import useMusicList from './MusicList.hook';
import AddMusicGuide from './AddMusicGuide';
export default function MusicList() {
// logics
const { isEditMode, channelId, setIsEditMode, handleCreateMusicButtonButtonClick, handleEditConfirmButtonClick } =
useMusicList();
// view
return (
<Wrapper>
<Header>
<Left>
<SubscribeButton channelId={channelId} />
<EditButton
isEditMode={isEditMode}
handleEditConfirmButtonClick={handleEditConfirmButtonClick}
setIsEditMode={setIsEditMode}
/>
</Left>
<CreateMusicButton onClick={handleCreateMusicButtonButtonClick}>
<img src={addSquareSvg} alt='음악 생성 버튼 이미지' />
</CreateMusicButton>
<AddMusicGuide />
</Header>
<QueryErrorBoundary>
<Suspense fallback={<FallBack />}>
<MusicContainer isEditMode={isEditMode} />
</Suspense>
</QueryErrorBoundary>
</Wrapper>
);
}
...
import { useSuspenseQuery } from '@tanstack/react-query';
import { Music } from '../../types/music';
import { getMusicsByChannelId } from '../services/channel';
import queryKeys from '../queryKey';
export default function useGetMusicsByChannelId(channelId: string | undefined) {
const queryKey = queryKeys.musicList.allMusicList;
return useSuspenseQuery<Music[], Error>({
queryKey: queryKey,
queryFn: () => getMusicsByChannelId(channelId),
});
}
Suspense
와 ErrorBoundary
를 사용해 코드 스플리팅을 하면서도 로딩 상태와 에러 처리를 고급지게(?) 처리할 수 있다. 단 몇가지 세팅이 필요하다.https://velog.io/@wildcatco/비동기-데이터-패칭의-로딩-에러-상태-분리하기-with-Tanstack-Query-Suspense-Error-Boundary