프로젝트 진행 과정 중에, 맡은 파트가 생각보다 일찍 끝나기도 했고 페이지 자체에 이미지가 많이 들어가기 때문에 성능 개선에 대한 관심이 생겼다. 생각난 김에 lighthouse를 바로 돌려봤는데 점수가
보다시피 Performance 부분의 점수가 36점이다 하하
점수가 왜 이렇게 낮을까... 살펴보니까
첫번째로, Reduce unused Javascript인데 직역하면 사용하지 않는 자바스크립트 파일을 줄이라는 뜻이다.
가장 위의 용량이 큰 파일을 보면 bundle.js라고 되어있는데 bundle이란 뭘까?
번들링이란 말 그대로 '어떤 것들을 묶는다'
라는 의미인데, 모듈화 되어있는 파일들을 묶어주는 과정을 말한다.
스크립트의 크기가 점점 커지고 복잡해지면서 하나의 파일로 관리하기에는 복잡성이 너무 커지고 작업의 효율성을 위해서 파일을 역할에 따라 분리하는게 일반적이다.
분리된 모듈들은 각각의 파일들이기 때문에 서로 연결시켜주기 위해 모듈 내부에 import로 외부 모듈의 기능을 가져오고, export로 외부 모듈에서의 접근을 허용하는 등의 과정을 거친다.
그럼 분리된채로 import, export 문을 활용하여 연결되어 있게끔 만들었으니까 이대로 쓰면 되는거 아닌가요? 왜 분리 했던걸 다시 합치는거죠?
// hello.js
var word = 'Hello';
// world.js
var word = 'World';
<html>
<head>
<script src="./source/hello.js"></script>
<script src="./source/world.js"></script>
</head>
<body>
<div id="root"></div>
<script>
document.querySelector('#root').innerHTML = word;
</script>
</body>
</html>
위와 같이 2개의 자바스크립트 파일이 분리되어 있고, 같은 이름의 변수를 사용하고 있다. 그래서 전역 범위를 가지는 두 변수의 충돌이 발생하고 마지막으로 호출한 world.js 에서 선언된 word만 적용되는 상황이 일어난다!
지금 같이 단순한 상황이 아니라, 자바스크립트 파일이 100개, 1000개가 있다면 이는 심각한 문제가 된다.
대부분의 현재 주요 브라우저에서는 각 호스트 당 한 번에 요청을 보낼 수 있는 횟수가 6회로 제한되어 있다.
회색 막대는 브라우저가 6개의 연결 제한을 기다리는 동안 요청이 큐에 대기하는 시간을 표시하고, 노란색 막대는 첫 번째 바이트에 대한 요청 시간이다. 즉, 요청을 보내고 서버에서 첫 번째 응답을 받는 데 걸린 시간이다. 파란색 막대는 서버에서 응답 데이터를 수신하는 데 걸린 시간을 표시한다.
출처: https://docs.microsoft.com/ko-kr/aspnet/mvc/overview/performance/bundling-and-minification
이렇게 나눠서 요청을 보내게 되면, 브라우저의 연결 제한에 따라 대기 시간이 발생하게 되고 이는 성능 저하로 이어질 수 있다.
그러므로, 번들링을 하면 파일 수가 적어지므로 HTTP 요청 수가 줄어들고 페이지 로드 성능이 향상될 수 있다!
CRA(Create-React-App)으로 시작할 경우, 기본적으로 Webpack 및 Babel 설정이 되어있는데 번들링이 되면서 모든 자바스크립트 파일이 하나의 자바스크립트 파일(bundle.js)로 합쳐지게 된다.
이 번들 파일은 프로젝트가 커질수록 용량이 커지며, 페이지에 처음 접속할 때 자바스크립트 파일을 불러오는 CSR의 특성 상 시간이 오래 걸릴 수 있다는 문제가 생긴다.
https://velog.io/@ctdlog/서버-사이드-렌더링SSR-클라이언트-사이드-렌더링CSR이란
따라서, 이를 개선하기 위한 방법으로 React에서는 코드 분할(Code Splitting)에 대해 공식문서에서 설명하고 있다.
React.lazy() 함수를 사용하면 동적 import를 사용해서 컴포넌트를 렌더링 할 수 있다.
import OtherComponent from './OtherComponent';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
그런데, 모든 import에다가 lazy를 걸어서 분할해야 하는걸까? 코드 분할을 어느 곳에 도입할지 어떻게 결정할지에 대한 고민이 생기는데 React 공식 문서에서는 Route-based code splitting이라는 방법을 제시한다.
말 그대로 Route(페이지)에 기반한 코드 분할 방법이다.
그래서 나도 공식문서대로, Route-base code splitting을 진행했고, 전과 달리 페이지 별 로딩 시간이 생겼기 때문에 Suspense를 통해서 직접 만든 로딩 스피너를 보여주게끔 했다.
// Route-based Code Splitting
const Home = lazy(() => import('pages/Home'));
const Login = lazy(() => import('pages/Login'));
const SignUp = lazy(() => import('pages/SignUp'));
...
const Router = () => (
<BrowserRouter>
<Suspense fallback={<Spinner />}>
<Routes>
{/* GlobalLayout -> Header + Footer */}
<Route element={<GlobalLayout />}>
{/* 인증 여부 상관없이 접속 가능한 페이지 정의 (비로그인) */}
<Route path='/' element={<Home />} />
<Route path='/login' element={<Login />} />
<Route path='/signup' element={<SignUp />} />
...
</Route>
</Routes>
</Suspense>
</BrowserRouter>
);
export default Router;
이렇게 해서, 코드 분할을 통한 번들 사이즈 줄이기는 끝났고 아까 봤던 lighthouse 첫뻔째 사진에 bundle.js 아래에 있었던 react-icons와 2번 째 사진에 있던 Properly size images 문제도 해결해보자!
아이콘을 사용하기 위해서 react-icons라는 라이브러리를 사용했는데, 사용하는 아이콘 숫자에 비해 라이브러리의 chunk 사이즈가 너무 큰 게 문제가 됐다.
알고 보니까, react-icons는 icons 종류별(Bootstrap, Font-awesome.. 등)로 구분되어 있으며 종류별로 하나의 js 파일에 아이콘 전체를 포함하고 있었다. 이 때문에 Font-awesome에 있는 아이콘 하나만 가져오려고 해도 Font-awesome 아이콘들을 담고 있는 전체 파일이 필요했고, 이에 따라 성능 문제가 발생한 것이다.
그래서 react-icons에서는 @react-icons/all-files
라는 별도의 라이브러리를 제공한다.
@react-icons/all-files
라이브러리는 아이콘 별로 자바스크립트 파일을 별도로 가지고 있게 때문에, 빌드 시 트리 쉐이킹 방식으로 더 적은 크기의 chunk를 만들 수 있다고 한다.
import { FaAngleDoubleLeft, FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from "react-icons/fa";
import { FaAngleDoubleLeft } from '@react-icons/all-files/fa/FaAngleDoubleLeft';
import { FaAngleDoubleRight } from '@react-icons/all-files/fa/FaAngleDoubleRight';
import { FaAngleLeft } from '@react-icons/all-files/fa/FaAngleLeft';
import { FaAngleRight } from '@react-icons/all-files/fa/FaAngleRight';
직접 해보니까, 기존 방식과 달리 아이콘 별로 import를 각각 하는게 번거롭다는 점과
react-icons
에 있는 아이콘이@react-icons/all-files
라이브러리에는 없는게 있어서 다른 아이콘들을 찾아봐야 되는 단점이 조금 있긴 하다..
이거는 사실 간단하게 해결할 수 있는 문제인데, 배너에 있는 이미지들이 바뀌지 않는 이미지들이라 소스 코드에 이미지 파일들을 넣어두고, import 해서 사용했다.
그런데 이미지가 고화질이다 보니까 용량이 꽤 됐고, 이에 따른 성능 이슈가 생겨서 이미지 파일을 직접 넣어서 쓰는 것보다는 기존에 프로젝트에 사용하던 AWS S3에 올려두고 파일 대신 링크로 대체했다.
크게, 다음과 같이 3가지 과정을 거친 결과..
- Route-base Code Splitting
- react-icons 번들 사이즈 줄이기
- 이미지 파일 대신 AWS S3를 통해 링크로 대체하기
기존의 30점이였던 Performance 점수가 93점까지 상승했다 😁
https://lihano.tistory.com/17
https://humanwater.tistory.com/1
https://perfectacle.github.io/2016/11/18/Module-bundling-with-Webpck/
https://hoons-up.tistory.com/63
https://eratosthenes.tistory.com/2