일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Git
- 공부
- 취업
- 알고리즘
- css
- 프론트엔드
- 코딩테스트
- Next.js
- 개발자
- html
- JavaScript
- React
- DOM
- dynamic import
- 비동기
- csr
- TypeScript
- 자바스크립트
- React Query
- error
- 취업준비
- http
- Vite
- Sass
- 코딩
- 차이
- SSR
- 에러
- 백준
- Browser
- Today
- Total
minTech
[React] vite를 이용한 프로젝트 최적화 여행기( feat.rollup-plugin-visualizer ) 본문
이번 동아리 프로젝트를 진행하면서,,
✈️ 최적화 여행을 하게 된 계기
나의 PR을 올리기 전에 항상 lint 검사와 "npm run build"를 통해 빌드를 해서 사전에 github actions에서 잡힐 오류들을 미리 검사하곤 하였다. 하지만 어느샌가부터 빌드 시 아래와 같은 경고 문구가 발생했다.
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
오잉또잉,,
경고창을 대애애충보면 빌드 완료 후에 dist 파일에 생성된 번들의 크기가 500kB가 넘어 이에 대한 경고와 함께 아래 해결 방법 몇시가지 제시되어 있다. 무려 내가 했던 프로젝트의 js 파일은 기준치(500kB)의 약 2배인 907kB,,,,!
비록 프로젝트를 한창 진행중일 때는 최적화보다 당장의 기능 구현에 급급했기에,, 프로젝트를 모두 마무리하고 최적화를 진행하였다.
우선 이렇게 본격적인(?) 최적화라는 것을 거의 처음 시도해보는 나에게 당장의 이 코드만 보고 어느 파일을 어떻게 최적화하라는 것인지 알 수 가 없었다. 그래서 구글링한 결과, vite의 경우 esbuild를 통해 사전 번들링을 수행하고, rollup을 통해 배포를 위한 프로덕션용 번들링이 수행된다. 따라서 Rollup의 번들의 크기를 시각적으로 확인할 수 있는 대표적인 라이브러리인 rollup-plugin-visualizer과 vite-bundle-visualizer 중에서 선택하기로 결정하였다.
rollup-plugin-visualizer vs vite-bundle-visualizer
두 개의 비교를 위해 npm trends를 통해 다운로드 수와 최근에 업데이트한 날짜를 확인하였다.
위에서 확인할 수 있다시피 rollup-plugin-visualizer 이 업데이트 날짜도 더 최신이며, 다운로드 수도 압도적으로 많았다.
그래서 이를 통해 번들의 크기를 시각적으로 분석하고, 최적화를 진행하기로 정했다.
rollup-plugin-visualizer
-> https://www.npmjs.com/package/rollup-plugin-visualizer/v/5.6.0
설치는 아래 명령어를 입력하여 진행하였다.
npm i rollup-plugin-visualizer
설치 후에는 vite.config.js 파일에서 설정을 아래와 같이 추가로 넣어준다.
plugins: [
// ... 그 외 설정들
[visualizer() as PluginOption],
],
이렇게 설정까지 완료해주면 이제 사용이 가능한데,
먼저 npm run build를 통해 빌드를 진행해주면 src 폴더 내에 아래와 같이 stats.html 파일이 생성되어있을 것이다.
나는 이 파일을 열고, Go Live를 눌러 라이브 서버를 열어서 확인하였다.
( 없으면 extends 탭을 통해 "Live server" 를 검색하여 설치해주기!! )
파일을 열어주면 다음과 같이 번들의 크기에 따라 각기 다른 사이즈를 가진 네모 상자들이 보이게 된다.
최적화 진행해보기!
1. visualizer를 통해 확인하지는 못하지만, 먼저 번들의 크기가 비교적 컸던 css 파일부터 최적화를 진행해보았다.
vite.config.js 파일의 rollup 옵션을 설정 중에 treeShake라는 설정이 있다. 이는 사용하지 않는 코드를 제거해주는 설정인데 이를 true 값으로 넣어 활성화하였다.
또한 vite 의 빌드 옵션 중 cssCodeSplit 설정을 활성화하여 css 파일의 코드분할이 이루어지도록 하였다.
export default defineConfig({
build: {
rollupOptions: {
treeshake: true, // rollup의 treeshake 활성화
},
cssCodeSplit: true, // css의 코드분할 활성화
},
});
이를 모두 진행하였더니 약 25kB 줄었다.. ㅎㅎ
미세하지만 초기 번들 사이즈를 줄이고, 이러한 설정이 있다는 것을 알았다는 것에 의의 두기,,
이제 본격적으로 visualizer를 통해 번들의 사이즈를 분석해보며, 큰 파일들의 번들 사이즈를 줄여보자!
2. 모든 페이지 lazy import 적용하기
처음에는 router-dom 관련 번들 사이즈가 굉장히 크길래 이를 줄이기 위해 "초기 로딩 속도를 줄이면 성능이 좋겠지~" 하고 막연히 routes.ts 파일의 모든 페이지를 lazy import하여 사용자가 필요로 할 때만 요청되도록 하였다.
‼️ 주의
react 공식문서를 보면 임포트하려는 lazy 컴포넌트는 default 내보내기로 내보내져 있어야 한다고 나와있다.
즉, lazy import로 import 하려는 컴포넌트는 export가 아니라 export default를 사용하여야한다.
(난 기존에 모든 페이지를 export로 내보내기를 적용해서 다 바꾸느라 애를 먹었다..)
// route.ts
const Main = lazy(() => import('./pages/main/main'));
const Signup = lazy(() => import('./pages/sign-up/Signup'));
const Login = lazy(() => import('./pages/login/Login'));
lazy import를 적용할 때는 react의 suspense를 통해 해당 컴포넌트를 로딩하는 동안에 렌더링할 컴포넌트를 지정해주어야한다.
나는 모든 페이지에 lazy import를 적용하였기 때문에 App.tsx에 전체적으로 Suspense를 감싸 fallback으로 미리 구현해두었던 로딩페이지를 넣어 해당 페이지가 로딩되는 동안 LoadingPage가 렌더링되도록 하였다.
import { RouterProvider } from 'react-router-dom';
import { routes } from './routes';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ToastProvider } from './contexts/toastContext';
import { Suspense } from 'react';
import LoadingPage from './pages/loading/LoadingPage';
import { HelmetProvider } from 'react-helmet-async';
function App() {
const queryClient = new QueryClient();
return (
<HelmetProvider>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<ToastProvider>
<Suspense fallback={<LoadingPage />}> // 로딩 페이지 렌더링
<RouterProvider router={routes}></RouterProvider>;
</Suspense>
</ToastProvider>
</QueryClientProvider>
</HelmetProvider>
);
}
export default App;
모두 lazy import를 통해 dynamic import를 적용시킨 후에 다시 visualizer를 통해 어떻게 변했는지 보았다.
일단 npm run build를 통해 build를 시켜보았을 때는 처음에 빌드시켰을 때 js 파일이 하나로 압축되던 것과 달리 여러 개로 분할되어 번들되었음을 확인할 수 있다. 결론적으로 맨 아래의 통합적인 js 파일의 번들 사이즈는 500kB로 줄어 경고창은 뜨지 않았다.
아래는 visualizer를 통해 확인한 모습이다. 전과 달리 하나하나 페이지마다 상자가 컬러별로 분리되어있음을 확인하였다.
최적화 성공!!!
이라고 생각했지만 다시 생각해보니 이렇게 모든 페이지를 Lazy import를 통해 동적으로 가져오게 되면 번들링의 의미가 과연있을까? 싶었다.
lazy import 를 사용하면 과연 좋은가?
lazy import가 초기 로딩 속도를 줄여주지만 이를 남발하는 것은 좋지 않다.
왜냐하면 번들링의 의미는 여러 개의 파일을 하나로 압축하여 요청하는 네트워크의 수를 감소시키기 위해 생긴 것인데 모든 페이지를 이렇게 갖고 오게 되면 번들링의 의미를 잃어버리게 된다.
또한 자주 사용하는 페이지도 동적으로 가져오게 되면 페이지 전환 시 로딩 속도가 느려지기 때문에 사용자의 경험성 줄어들 뿐더러, 요청 네투워크 수도 늘어나 오히려 성능적인 축면에서 비효율적일 수 도 있다.
결론적으로는 lazy import를 적절한 곳에 사용해야한다.
lazy import를 적절한 곳에 사용하자 !!!
lazy import의 경우에는 사용자가 자주 방문하지 않는 페이지나 무거운 페이지에 적용한다.
우리 프로젝트의 경우에는 눈에 띌 정도로 무거운 페이지는 없었기에 자주 방문하지 않는 로그인, 회원가입, 결제 완료 페이지 등에 lazy import를 적용하였다.
적용하였더니?
결국 다시 원상복구다..
그렇기 때문에 다시 난 visualizer를 통해 다른 파일의 번들 사이즈를 줄여야겠다고 생각했다.
다시 보았더니 react quill,,, 의 크기가 생각보다 굉장히 컸다. 이 라이브러리는 모든 페이지에서 사용하는 것도 아닌데 굉장히 큰 비율을 차지한다.. 라고 생각해서 다음 타깃은 react-quill-new 로 정했다. 👀
(그리고 생각보다 이 quill.snow.css 파일이 용량이 크다..)
그래서 나는 기존에 'react-quill-new' 라이브러리에서 import 하던 ReactQuill에 바로 lazy import를 적용시키지 못하기 때문에 따로ReactQuill과 내가 새로 커스텀한 QuillToolbar를 import하여 새로 공통 컴포넌트로 빼주었다.
import { forwardRef } from 'react';
import { QuillToolbar } from '../../my-page/portfolio/QuillToolbar';
import ReactQuill from 'react-quill-new';
import 'react-quill-new/dist/quill.snow.css';
interface QuillEditorPropsType {
className: string;
onChangeHandler: (content: string) => void;
id?: string;
value?: string;
placeholder?: string;
}
const QuillEditor = forwardRef<ReactQuill, QuillEditorPropsType>(
({ className, onChangeHandler, id, value, placeholder, ...rest }, ref) => {
const modules = {
toolbar: { container: '#toolbar' },
};
return (
<div>
<QuillToolbar />
<ReactQuill
modules={modules}
placeholder={placeholder || '내용을 입력하세요'}
onChange={onChangeHandler}
id={id}
value={value}
ref={ref}
{...rest}
className={className}
/>
</div>
);
},
);
QuillEditor.displayName = 'QuillEditor';
export default QuillEditor;
이렇게 빼준 후에, react quill을 사용하던 페이지에서 모두 import에서 lazy import로 변환하였다.
const QuillEditor = lazy(
() => import('../../components/common/quillEditor/QuillEditor'),
);
모두 적용 시킨 후 다시 빌드 해주고 visualizer를 확인했다.
QuillEditor.js 로 번들이 따로 분리되어 500kB가 안넘는 것을 확인할 수 있다!!!
visuallizer를 보았을 때는 quill이 아직 있지만 그 전과 뭔가 느낌이 다르다.
이제 진짜 최적화 성공!!
웹 사이트의 성능 검사를 통해 수치적으로 봐도 좋아졌음을 알 수 있다.
( 검색엔진 최적화 부분은 메타 태그를 통해 올린 것이므로 별개 )
이 외에 추가적으로 설정했던 것도 있는데 react router dom 관련 파일의 번들 사이즈를 감소를 위해서 manualChunks를 활용하여 react-router-dom을 별도 번들로 분리하였다.
// vite.config.js
build: {
rollupOptions: {
treeshake: true,
output: {
manualChunks: {
router: ['react-router-dom'], // React Router를 별도 번들로 분리
},
},
},
cssCodeSplit: true,
},
번들링 최적화는 처음이라 많이 서투르고 모르는 것도 많았지만, 그만큼 알게된 것도 많아져 뿌듯했다.
lazy import의 개념과, 적용하는 상황에 대해 알게 되었으니 다음에 react를 사용한 프로젝트 진행 시 무거운 컴포넌트를 import할 때 적용해야겠다고 생각했다. 또한 visualizer를 통해 번들 사이즈를 시각적으로 단계마다 확인하면서 진행하니 훨씬 도움이 많이 되었고, 어떤 파일의 최적화를 진행해야할 지 바로 결정하는데 도움이 되었다!
📚 참고 자료
https://velog.io/@code-bebop/dynamic-import%EC%99%80-React.lazy
https://www.npmjs.com/package/rollup-plugin-visualizer/v/5.6.0
'React' 카테고리의 다른 글
[React] Vite에 대해 알아보자! (5) | 2024.10.08 |
---|---|
[React] Prettier 적용이 안된다면? (1) | 2024.10.04 |
[React] 컬러 피커(color-picker) 사용하기 (1) | 2024.05.25 |
[React] react portal로 모달창 만들기 (feat. 이벤트 버블링) (0) | 2024.05.08 |
[React. Error] 'error:03000086:digital envelope routines::initialization error' (0) | 2024.04.09 |