Project Soljik DevLog
  • Project Soljik 소개 문서
  • 프로젝트 소개
    • 솔직한 온라인 쇼핑, Soljik
    • 기술 스택
    • 컨벤션
  • 최적화
    • 이미지 최적화
    • Lazy Loading / Code Splitting
    • 렌더링 최적화
  • 트러블 슈팅
    • BaaS(Firebase) 환경에서의 세션 관리
    • Firebase 저장소의 CORS 에러 해결
    • 다음 우편번호 API 적용하기
Powered by GitBook
On this page
  • Lazy Loading / Code Splitting 의 필요성
  • 최적화 방법
  • 코드 예시
  • 성능 측정
  • 결론
  1. 최적화

Lazy Loading / Code Splitting

Vite + React 프로젝트의 빌드파일에 적용된 lazy loading, code splitting 내용을 정리한 문서입니다.

Lazy Loading / Code Splitting 의 필요성

현재 프로젝트 코드는 메인 라우터에서 2개의 서브 라우터(비회원, 회원 라우터)를 사용하는 구조입니다.

각각의 서브 라우터에는 path 별로 매칭된 페이지 컴포넌트들이 존재하며, 이 페이지들의 코드와 외부 라이브러리 코드들은 프로젝트 빌드시에 하나의 index.js 파일로 빌드됩니다.

이러한 방식의 초기 로딩 시간을 지연시키는 원인이 됩니다.

  1. 모든 코드들이 하나의 파일로 합쳐지기 때문에 용량이 커진다.

  2. 어떤 페이지에 접근하든 무조건 전체 코드를 한번에 불러와야 한다.

이러한 문제를 해결하기 위해 Lazy Loading 과 Code Splitting 을 프로젝트에 적용합니다.

Firebase SDK 는 v9 이상부터 모듈화된 API 구조를 제공합니다. 이 프로젝트에서는 대부분의 페이지에서 Firebase 가 사용되므로, auth, firestore, storage 등 각각의 기능에 해당하는 모듈을 정적 import 방식으로 불러와 사용합니다.

import 예시
import { signInWithEmailAndPassword } from 'firebase/auth';

최적화 방법

최적화의 필요성 내용을 바탕으로 크게 2가지 방식으로 최적화를 진행합니다.

1

Lazy Loading

페이지 별 Lazy Loading 을 적용해, 현재 페이지의 코드만 불러오도록 합니다.

  • React.lazy 메서드를 활용해 컴포넌트에 Lazy Loading 을 적용합니다.

  • 적용 대상은 회원/비회원 페이지 컴포넌트 단위로 적용합니다.

2

Code Splitting

가장 큰 비중을 차지하는 Firebase SDK 를 비롯한 라이브러리들을 분리하여 빌드합니다.

  • 정적 import 방식과 Vite 의 빌드 옵션 중 rollupOptions - output - manualChunks 를 사용합니다.


코드 예시

  1. 라우팅을 관리하는 프로젝트의 메인 라우터 코드입니다.

MainRouter.tsx
import { Route, Routes } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import ProtectedRouteForSigning from './ProtectedRouteForSigning';
import Loading from '@/pages/loading/Loading';
import ProtectedRouteForMember from './ProtectedRouteForMember';
import NotFound from '@/pages/notfound/NotFound';

const Signin = lazy(() => import('@/pages/signin/Signin'));
const Signup = lazy(() => import('@/pages/signup/Signup'));
const Home = lazy(() => import('@/pages/buyer/home/Home'));
const Category = lazy(() => import('@/pages/buyer/category/Category'));
const History = lazy(() => import('@/pages/buyer/history/History'));
const Purchase = lazy(() => import('@/pages/buyer/purchase/Purchase'));
const Items = lazy(() => import('@/pages/seller/items/Items'));
const Management = lazy(() => import('@/pages/seller/management/Management'));
const Registration = lazy(
  () => import('@/pages/seller/registration/Registration'),
);
const Update = lazy(() => import('@/pages/seller/update/Update'));
const Detail = lazy(() => import('@/pages/buyer/detail/Detail'));

const MainRouter = () => {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route element={<ProtectedRouteForSigning />}>
          <Route path='/signin' element={<Signin />} />
          <Route path='/signup' element={<Signup />} />
        </Route>
        <Route element={<ProtectedRouteForMember />}>
          <Route path='/' element={<Home />} />
          <Route path='/category' element={<Category />} />
          <Route path='/detail/:id' element={<Detail />} />
          <Route path='/history' element={<History />} />
          <Route path='/purchase' element={<Purchase />} />
          <Route path='/items' element={<Items />} />
          <Route path='/management' element={<Management />} />
          <Route path='/registration' element={<Registration />} />
          <Route path='/update' element={<Update />} />
        </Route>
        <Route path='*' element={<NotFound />} />
      </Routes>
    </Suspense>
  );
};

export default MainRouter;

잘못된 경로 접근 시 사용될 NotFound 페이지를 제외하고는 전부 페이지 단위로 React.lazy 메서드를 사용합니다.

또한 Suspense 기능을 활용해 페이지를 로딩할 때 Loading 컴포넌트를 출력해서 사용자에게 로딩중임을 알려줍니다.

  1. Vite 의 빌드 설정을 관리하는 vite.config.ts 파일입니다.

vite.config.ts
import { defineConfig, PluginOption } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [react(), visualizer() as PluginOption],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          react: ['react', 'react-dom'],
          firebase: ['firebase/app'],
          'firebase-auth': ['firebase/auth'],
          'firebase-firestore': ['firebase/firestore'],
          'firebase-storage': ['firebase/storage'],
          carousel: ['react-slick'],
          notification: ['sonner'],
        },
      },
    },
  },
  // ... 기타설정
});

먼저 Vite 의 플러그인으로 빌드 결과를 시각화해주는 rollup-plugin-visualizer 를 설정합니다.

이후 rollupOptions - output - manualChunks 옵션을 활용해 사용할 라이브러리들을 직접 청크로 설정할 수 있습니다.

먼저 react 와 관련된 라이브러리들을 묶고, firebase 의 모듈들은 각각 따로 묶어서 청크 용량을 최소화하면서 용도에 따라 분리해 불러올 수 있도록 합니다.

마지막은 ui 에 사용될 react-slick 과 sonner 라이브러리도 각각 carousel, notification 청크로 설정합니다.


성능 측정

Lazy Loading / Code Splitting 적용 전

아래는 로고나 정적 이미지 파일, 폰트들을 제외한 프로젝트의 빌드 파일들입니다.

dist/index.html                                             0.72 kB │ gzip:   0.39 kB     
dist/assets/index-2d6kTBOk.css                             13.61 kB │ gzip:   3.90 kB
dist/assets/index-C_Hsjm93.js                           1,005.58 kB │ gzip: 274.18 kB

면 html, css, js 파일이 존재하고 그 중 index-C_Hsjm93.js 파일은 용량이 거의 274.18 kB 에 달합니다.

이는 사용자가 어떤 페이지에 접근하던 상관없이 무조건 274.18 kB 용량의 JS 파일을 불러와야 한다는 것을 의미합니다. 아래는 실제 각각의 웹 페이지에 접근했을 때, 불러오는 JS 파일을 개발자 도구로 확인한 결과입니다.

어떤 페이지를 가도 똑같이 275kB 의 index.js 파일을 불러오는것을 확인할 수 있습니다.

다음으로 index.js 파일 내부를 더 자세히 살펴보겠습니다.

Firebase 가 전체의 55% 정도를 차지하고 있어서 분리가 시급하고, 나머지 라이브러리들은 각각 0~6% 정도를 차지하고 있습니다.

Lazy Loading / Code splitting 적용 후

아래는 로고나 정적 이미지 파일, 폰트들을 제외한 프로젝트의 빌드 파일들입니다.

dist/index.html                                             1.21 kB │ gzip:  0.50 kB
dist/assets/index-2d6kTBOk.css                             13.61 kB │ gzip:  3.90 kB
dist/assets/useQuery-Ba8W4N3q.js                            0.10 kB │ gzip:  0.11 kB
dist/assets/ClampedP-C7xh1IXK.js                            0.32 kB │ gzip:  0.22 kB
dist/assets/index-Lz0b4wwX.js                               0.48 kB │ gzip:  0.32 kB
dist/assets/categoryService-xk2yduZk.js                     0.53 kB │ gzip:  0.29 kB
dist/assets/Checkbox-B20LpKzc.js                            0.53 kB │ gzip:  0.34 kB
dist/assets/firebaseUtils-CvSOZ24t.js                       0.55 kB │ gzip:  0.38 kB
dist/assets/v4-DvF23Exx.js                                  0.82 kB │ gzip:  0.45 kB
dist/assets/Spinner-BFTgiwOH.js                             0.85 kB │ gzip:  0.45 kB
dist/assets/orderService--MsW_ovq.js                        1.00 kB │ gzip:  0.53 kB
dist/assets/HorizontalSelect-BPPDzBld.js                    1.05 kB │ gzip:  0.65 kB
dist/assets/TextArea-BKGqkNtY.js                            1.33 kB │ gzip:  0.64 kB
dist/assets/index-29cW7QyD.js                               1.55 kB │ gzip:  0.74 kB
dist/assets/index-YX3xpOEZ.js                               2.17 kB │ gzip:  0.97 kB
dist/assets/createRegisterObject-BoE1JipF.js                2.49 kB │ gzip:  0.88 kB
dist/assets/VerticalCard-91wCRs0o.js                        2.86 kB │ gzip:  1.26 kB
dist/assets/useMutation-BYcGktsq.js                         2.95 kB │ gzip:  1.25 kB
dist/assets/index-Dx2kbDJU.js                               3.15 kB │ gzip:  1.45 kB
dist/assets/index-C_oZg3Fm.js                               3.57 kB │ gzip:  1.35 kB
dist/assets/Signup-_fkdCUJs.js                              3.73 kB │ gzip:  1.99 kB
dist/assets/Category-DIb5YdCG.js                            4.22 kB │ gzip:  1.94 kB
dist/assets/Signin-CDwnqyHZ.js                              4.48 kB │ gzip:  2.31 kB
dist/assets/productService-DNPR8JT9.js                      4.64 kB │ gzip:  1.84 kB
dist/assets/Registration-ChDwYoyu.js                        6.83 kB │ gzip:  2.91 kB
dist/assets/Update-DjzvJuRZ.js                              7.44 kB │ gzip:  3.24 kB
dist/assets/Header-CP9-qU4J.js                              7.46 kB │ gzip:  2.50 kB
dist/assets/Items-Cc2kHIJ3.js                               7.79 kB │ gzip:  2.99 kB
dist/assets/useBaseQuery-NPai320v.js                        9.51 kB │ gzip:  3.40 kB
dist/assets/History-BYdOFnhx.js                            10.10 kB │ gzip:  3.65 kB
dist/assets/Management-9dMaBHzj.js                         11.84 kB │ gzip:  4.81 kB
dist/assets/Home-IpMyTNSq.js                               12.48 kB │ gzip:  4.40 kB
dist/assets/Purchase-1J6x2UtF.js                           13.10 kB │ gzip:  5.55 kB
dist/assets/Detail-D5twswEM.js                             15.55 kB │ gzip:  5.70 kB
dist/assets/Input-Bjm9xofC.js                              22.54 kB │ gzip:  8.79 kB
dist/assets/notification-DfJNDfFa.js                       28.68 kB │ gzip:  8.35 kB
dist/assets/carousel-Bj3VKddu.js                           65.85 kB │ gzip: 17.62 kB
dist/assets/index-xyGLF-Vn.js                             115.10 kB │ gzip: 40.77 kB
dist/assets/react-DcL22WG5.js                             141.84 kB │ gzip: 45.59 kB
dist/assets/firebase-app-BVXKbYYP.js                       42.73 kB │ gzip: 10.04 kB
dist/assets/firebase-auth-C16K3SW_.js                     125.39 kB │ gzip: 25.42 kB
dist/assets/firebase-firestore-MENO9QEK.js                296.26 kB │ gzip: 74.22 kB
dist/assets/firebase-storage-yW-6QTKp.js                   36.26 kB │ gzip:  9.05 kB

빌드 파일 목록을 살펴보면 다양한 JS 파일들이 존재합니다.

  1. 페이지 단위 청크 - React.lazy 메서드를 적용한 페이지 컴포넌트

  2. 라이브러리 청크 - vite.config.ts 의 manualChunks 옵션에 맞게 분리된 청크와 자동 분리된 다양한 라이브러리

  3. 공통 유틸/컴포넌트 청크 - 여러 파일에서 import되지만, entry가 따로 없어서 Rollup이 공통 청크로 만들어낸 파일(index-XXXX.js)

  4. 단일 기능 청크 - 작은 함수나 훅, 서비스 단위로 export하는 모듈이 특정 페이지에서만 import된 경우(useQuery-XXXX.js, useMutation-XXXX.js, productService-XXXX.js , etc...)

  5. UI 컴포넌트 청크 - UI 구성 요소가 React.lazy 또는 특정 페이지에서만 import된 경우(Checkbox-XXXX.js, TextArea-XXXX.js , etc...)

​최대 크기의 청크파일은 74.22 kB 파일로 firebase 의 firestore 모듈에 해당하는 청크파일입니다.

분리 전인 274.18 kB 에 비하면 27% 수준으로 줄었습니다.

물론 청크가 여러개로 쪼개졌기 때문에 단일 파일로 비교하기보다 실제 네트워크 상에서 불러오는 JS 파일들을 개발자 도구로 살펴보고 비교해보겠습니다.


결론

React.lazy 메서드와 Vite 의 빌드 옵션을 사용해서 최적화를 진행해보았습니다.

각 페이지에서 불러오는 JS 파일의 총 용량을 최적화 이전과 비교해보면 다음과 같습니다.

페이지
최적화 이전
최적화 이후
비교

로그인

275 kB

156 kB

-43.3 %

회원 홈페이지

275 kB

247 kB

-10.2 %

구매자 전용

275 kB

252 kB

-8.4 %

판매자 전

275 kB

246 kB

-10.5 %

최적화 이전과 비교했을 때 최소 8.4%, 최대 43.3 % 까지 불러오는 빌드 파일의 용량이 줄었습니다.

로그인 페이지는 firebase 모듈 중 app, auth 두가지만 사용하므로 용량이 많이 줄었지만, 회원들이 사용하는 페이지들은 firebase 의 모듈을 거의 다 사용하기 때문에 용량의 변화가 크지 않은것을 확인했습니다.

Previous이미지 최적화Next렌더링 최적화

Last updated 2 months ago

를 활용하여 빌드 파일 내부의 라이브러리들의 구성을 살펴보았습니다.

rollup-plugin-visualizer
페이지 별 Network 로그
빌드 파일 내부 구성
페이지 별 Network 로그후