내가 프로젝트에서 최적화를 진행했던 방법들 (With React+Vite)

Seju·2023년 12월 31일
12

Optimization

목록 보기
1/1

🏊‍♂️개요


이글은 프로젝트를 진행하면서 LightHouse 기반 성능최적화를 다룬 내용입니다.


먼저 최적화를 진행 하기 전 처참한 성능..


🥗 1. 폰트 최적화


먼저, 성능최적화를 위해 폰트최적화를 먼저 진행했다.

  • 현재 프로젝트에선 웹폰트를 url로 받아와 폰트를 렌더링하고 있었는데,
  • 브라우저 렌더링 과정에서 웹폰트가 적용되지않아 네트워크 탭에서 웹 페이지 로딩시간을 잡아먹었으며,
  • FOLT , 웹 폰트 로딩이 완료되기 전까지 사용자에게 텍스트가 보이지 않는 현상이 발생했다.

💡 여기서 FOLT(Flash of Invisible Text)란?
웹 페이지가 로드되는 동안 웹 폰트가 아직 완전히 로드되지않았을때 발생하는 현상
이때, 브라우저는 해당 텍스트를 눈에 보이지 않는 상태로 두는데, 사용자 입장에선 웹 폰트가 로드되기 전까지 페이지의 일부 또는 전체 텍스트가 보이지 않는 상태가 되므로 좋지 않은 UX를 제공할 수 있다.


내가 진행한 폰트최적화는?

1. 먼저 폰트의 형식 중 가장 압축률이 좋은 WOFF2 폰트로 바꿔서 적용했다.

  • WOFF2폰트는 WOFF 폰트에 비해 압축률이 30% 정도 더 좋아 성능상의 이점이 존재한다.

2. font-display : swap 적용

  • font-display : swap 속성을 적용함으로써, 웹 폰트가 로딩되는 시간동안 시스템폰트로 대체해서 보여주며, 최종적으로 FOLT 현상을 방지시켰다.
@layer base {
  html {
    font-family: Pretendard;
    src: url("https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff2")
      format("woff2");
    font-size: 1rem;
    font-display: swap;

    @screen tablet {
      font-size: 0.875rem;
    }

    @screen mobile {
      font-sizep: 0.75rem;
    }
  }
}


🎉 2. 코드 분할(Code Splitting)


코드분할을 하는 이유는?

리액트와 같은 Single Page Application은 프로젝트 빌드 시 하나의 JavaScript파일로 번들링된다.

  • 이렇게 하나의 파일로 번들링된 결과물로 배포시 처음 진입할 때 모든 페이지에 대한 정보를 불러오고, 이는 초기 로딩을 느리게 만드는 결과를 초래하게된다.
  • 이렇듯, React는 기본적으로 Single Page Appliction이므로 코드 분할을 통해 성능적으로 이점을 챙길 수 있게된다.

동적 import와 lazy 함수

먼저, 프로젝트 내부에선 모든 컴포넌트가 모여있는 곳이 routes 파일이므로 해당 파일을 허브로 만들었다.

  • 이후, 컴포넌트의 import 부분을 lazy 함수와 동적 import로 매핑해서 리팩토링하였다.

동적 import
import() 함수는 JavaScript의 동적 임포트 기능이다.
이로 인해 모듈을 비동기적으로 불러올 수 있다

lazy 함수
React에서 제공하는 lazy함수는 컴포넌트를 동적 임포트하여 불러온다.
해당 함수는 Promise를 리턴하는 함수를 인자로 받으며, 해당 Promise가 resolve 될 경우 컴포넌트를 렌더링할 수 있다

import {lazy} from "react";
const RootLayout = lazy(() => import("@/layout/RootLayout"));
const Cart = lazy(() => import("@/views/Cart"));
const Customer = lazy(() => 
const CustomerCreate = lazy(() => 
import {Route} from "react-router-dom";
import {createRoutesFromElements} from "react-router-dom";
import {createBrowserRouter} from "react-router-dom";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<RootLayout />}>
      <Route index element={<Main />} />
      <Route path="/signUp" element={<SignUp />} />
      <Route path="/findStore" element={<FindStore />} />
    </Route>
  )
);

export default router;

이후, routes 부분을 import 구문과 lazy함수로 래핑한후, 상위 컴포넌트(여기선 App.jsx)를Suspense로 래핑하고, fallback 컴포넌트를 부여해야한다.

  • Suspense 컴포넌트의 children으로 매핑했던 RouterProvider를 자식으로 넣는다.
  • 여기서 fallback은 컴포넌트가 lazy함수로 인해 지연로딩 될 때 대체해서 보여줄 UI를 담아준다.
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {Suspense} from "react";
import {RouterProvider} from "react-router-dom";
import {HelmetProvider} from "react-helmet-async";
import {Toaster} from "react-hot-toast";
import router from "./routes/routes";
import JijoSpinner from "./components/JijoSpinner";



function App() {
  return (
    <>
      <HelmetProvider>
        <QueryClientProvider client={queryClient}>
          <Suspense fallback={<JijoSpinner />}>
            <RouterProvider router={router}></RouterProvider>
          </Suspense>
        </QueryClientProvider>
      </HelmetProvider>
      <Toaster />
    </>
  );
}

export default App;

👷 3. Vite 환경에서 텍스트 압축 진행하기


여기서 텍스트 압축이란?
데이터를 저장하거나 전송할 때 사용되는 기술이다.
텍스트 파일의 크기를 효과적으로 압축해, 저장공간을 절약하고, 네트워크를 통한 데이터 전송속도 또한 향상시킨다.
결과적으로 텍스트압축 또한 성능상의 이점을 가져다준다.

텍스트 압축을 위해 vite 플러그인인 vite-plugin-compression 라이브러리가 제공된다.

  • 이를 통해 vite.config.js 내부에 해당 플러그인을 추가함으로써, Vite 환경에서 빌드 시 생성되는 Assets 파일들을 압축해, 서버로부터 클라이언트로 전송 시 필요한 파일 크기들을 줄여줄 수 있게된다.
  • 아래 코드에선 plugins 배열 내부에 viteCompression을 넣고있다.
    • 여기서 alogoritm 속성은 압축 알고리즘을 지정하고 기본적으로 gzip이 사용된다.
    • 두번째론 ext인데, 해당 속성은 압축된 파일의 확장자를 지정한다. gzip 알고리즘 사용시에는 .gz라는 확장자명을 사용하게 된다.

import react from "@vitejs/plugin-react";
import {defineConfig} from "vite";
import viteCompression from "vite-plugin-compression";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    jsconfigPath(),
    viteCompression({
      // 압축 알고리즘 지정, 기본적으로는 'gzip'을 사용
      algorithm: "gzip",
      // 압축된 파일의 확장자를 '.gz'로 설정
      ext: ".gz",
    }),
  ],

  css: {
    devSourcemap: true,
    modules: {
      generateScopedName: isDev
        ? "[name]_[local]__[hash:base64:5]"
        : "[hash:base64:4]",
    },
  },

});

🪄 4. 청크파일 최적화


여기서 청크란?
청크(Chunck)는 웹 애플리케이션의 코드 또는 리소스를 작은 단위로 나눈것을 의미한다.
일반적으로 청크는 Webpack, Rollup, Vite와 같은 모던 빌드 도구에 의해 생성되고, 이 도구들은 애플리케이션의 자바스크립트, 스타일시트, 이미지, 폰트등을 분할하여 청크로 만들게 된다.

rollup-plugin-visualizer 설치하기

vite환경에선 해당 라이브러리로 번들 사이즈를 분석할 수 있다.

pnpm i -D rollup-plugin-visualizer

vite.config.js에서 빌드의 plugin 에서 설치한 visualizer 라이브러리를 추가한다.

  • 해당 프로젝트에선 vite에서 제공하는 rollupOptions 내부에서 구성하였다
// import * as path from 'node:path'
import react from "@vitejs/plugin-react";
import {defineConfig} from "vite";
import {env} from "node:process";
import {splitVendorChunkPlugin} from "vite";
import jsconfigPath from "vite-jsconfig-paths";
import {createHtmlPlugin} from "vite-plugin-html";
import {loadEnv} from "vite";
import viteCompression from "vite-plugin-compression";
import {visualizer} from "rollup-plugin-visualizer";


// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    jsconfigPath(),
    splitVendorChunkPlugin(),
    viteCompression(),
  ],

  build: {
    rollupOptions: {
      plugins: [
        visualizer({
          open: true,
          gzipSize: true,
          brotliSize: true,
        }),
      ],
    },
  },
});

이후 빌드시 visualizer 라이브러리에 의해 현재 코드들이 어떤식으로 분리가 되었는지 확인할 수 있다.

기존 코드분할로 분리된 청크파일들

manualChunks로 라이브러리 청크

기본적으로 vite는 자동적으로 파일들의 청크를 진행해주지만, 추가적으로 더 디테일하게 manualChunks 옵션을 통해 라이브러리 청크를 진행할 수 있다.

  • 적용방법은 다음과 같다.
  • rollupOptions.output.manualChunks에서 관심사별로 라이브러리 분리 후 추가
    • manualChunks 옵션은 Rollup 번들러의 설정 옵션 중 하나로, 이를 통해 개발자가 직접 청크(chunk)를 어떻게 분할할지 정의할 수 있다.

export default defineConfig({
  plugins: [
    react(),
    jsconfigPath(),
    viteCompression(),
  ],

  // 빌드 시, 청크 파일 생성 매뉴얼 구성
  build: {
    rollupOptions: {
      plugins: [
        visualizer({
          open: true,
          gzipSize: true,
          brotliSize: true,
        }),
      ],
	  output: {
        manualChunks: {
          react: ["react", "react-dom", "zustand", "@tanstack/react-query"],
          reactRouter: ["react-router-dom"],
          animations: ["framer-motion", "gsap", "aos"],
          swiperBundle: ["swiper"],
        },
      },
    },
  },
});

🌏 5. 이미지 최적화하기


마지막으로 최적화를 진행할 부분은 이미지이다.

이미지 지연로딩 (Lazy Image)

  • 현재 프로젝트에선 웹 내부에 다량의 이미지가 추가되어있고, 이로 인해 각각의 이미지들이 많은 크기를 차지하고 있어 최적화가 필요로했다.
  • 이미지 최적화기법은 많은 방법이 있지만, 나는 LazyImage 라는 컴포넌트를 만들어 프로젝트 내 모든 <img />태그에 적용시켰다.
    • 해당 방법은 IntersectionObserver API를 사용해 사용자의 뷰포트에 이미지가 나타날 때까지 해당 이미지의 로딩을 지연하는 방식으로 사용했다.
    • 이를 통해, 초기 페이지 로딩 시간을 단축하고, 사용하지 않을 리소스에 대한 네트워크 요청 또한 감소되는것을 경험했다.

Lazy Image 컴포넌트

import { useState } from "react";
import { useEffect } from "react";
import { useRef } from "react";

function LazyImage({ src, alt, className, width = "auto", height = "auto" }) {
  const [isLoading, setLoading] = useState(false);

  const imgRef = useRef(null);
  const observer = useRef();

  const intersectionObserver = (entries, io) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        io.unobserve(entry.target);
        setLoading(true);
      }
    });
  };

  useEffect(() => {
    observer.current = new IntersectionObserver(intersectionObserver);
    imgRef.current && observer.current.observe(imgRef.current);
  }, []);

  return (
    <div className="w-full">
      <img width={width} height={height} ref={imgRef} src={isLoading ? src : null} alt={alt} className={className} />
    </div>
  );
}

export default LazyImage;

.WebP 파일로 이미지 포맷 변환하기

  • 이과정에선, 현재 팀원들이 고생이 많았다.
  • 기존에 서버에 저장되 있던 png 확장자로 되있던 이미지들을 Squoosh를 통해 전부 .webp로 변환했기때문이다.

WebP 이미지 포멧으로 얻을 수 있는 성능상 이점은?

  • WebP 이미지 포맷은 구글에서 개발한 이미지 포맷으로, 웹에서 이미지 성능 최적화를 위해 설계되었다.
  • WebP는 기존의 JPEG,PNG,GIF 포멧에 비해 상당히 작은 파일 크기를 가지면서도 동일하거나 더 나은 이미지 품질을 제공할 수 있게된다.

물론 LazyImage 컴포넌트와 WebP 이미지 사용과 달리 이미지 최적화 기법은 무수히 많이 존재한다.

🚀 마지막으로

위의 5가지 방법들을 모두 수행한 후 빌드 후 LightHouse에서 성능점수를 다시 확인해보았다.

  • 기존 31점이였던 성능이 98점으로 많이 올라온 모습을 확인할 수 있게되었다.

98점으로 성능이 향상되었다.

profile
Talk is cheap. Show me the code.

0개의 댓글