행동대장 성능 : bundle main.js 스크립트 크기 줄이기
성능 이슈 이어서!
랜딩 페이지에서 필요하지 않은 스크립트를 제거해서 랜딩 페이지 초기 로딩 시에 자바스크립트 실행 시간을 줄이고 네트워크 요청량도 줄이는 효과를 얻기 위해서 이번에는 Main 스크립트를 줄여보는 최적화를 진행했다.
현재 메인 스크립트 성능
그렇다면 역시 현재 성능을 먼저 체크해보면
npm run build를 했을 때는(g-zip 적용 전) 688KB가 나왔고, cloudfront에서 g-zip을 적용한 후 결과는 219KB이고 시간은 18ms가 걸렸다.
목표는 200KB 이하로 떨어뜨리기
현재 성능을 측정했으니 다음으로는 어떤 방법으로 개선했는지를 주제 별로 적어보려고 한다.
1. 코드 스플리팅
코드 스플리팅이란 지금 당장 필요하지 않는 코드를 분리해서 필요한 곳에서 코드를 불러올 수 있도록 여러 개의 작은 청크(chunk)로 나누는 기술이다. 예로 들면 랜딩 페이지에서는 이벤트 페이지가 필요하지 않으므로 랜딩 페이지가 포함된 스크립트에서 이벤트 페이지를 분리해내는 것이다.
react에서는 동적 import를 사용하면 코드를 분리할 수 있다.
const Home = lazy(() => import('./pages/Home/Home'));
위 예시처럼 Home 컴포넌트를 lazy import하게 되면 main.js에서 Home 컴포넌트와 관련된 코드를 다른 파일로 분리할 수 있게 된다. 다만 주의점은 lazy를 사용하기 위해 상위 컴포넌트에 Suspense를 감싸줘야 한다는 것이다.
<Suspense fallback={<div style={{ width: '100vw', height: '100vh' }}>loading...</div>}>
<Home />
</Suspense>
우리 프로젝트에서는 이미 코드 스플리팅이 적용되어있다. 하지만 페이지 추가된 컴포넌트에 스플리팅이 되어있지 않아서 추가로 랜딩 페이지에서 필요하지 않은 컴포넌트에 스플리팅을 적용했다.
코드 스플리팅 개선 결과
추가로 생성된 페이지 컴포넌트에 lazy loading을 적용한 결과 28KB를 더 줄일 수 있었다.
2. 랜딩 페이지에서 사용하지 않는 Tanstack-Query 분리
행동대장은 서버 상태 관리 라이브러리로 Tanstack-Query를 사용한다. 하지만 랜딩 페이지에서는 백엔드 서버로 api 요청을 보내는 기능이 없다. 즉 랜딩 페이지에서는 Tanstack-Query가 필요하지 않은데 Tanstack-Query 관련 스크립트가 main에 포함되어있는 상황이다.
그래서 이를 분리해서 백엔드 서버로 api를 요청하는 컴포넌트에서만 Tanstack-Query 스크립트를 불러오게 한다면 main을 더 줄일 수 있을 것 같아 진행했다.
먼저 전반적으로 사용하는 App 컴포넌트부터 살펴보면
const App: React.FC = () => {
return (
<HDesignProvider>
<UnPredictableErrorBoundary>
<Global styles={GlobalStyle} />
<ErrorCatcher>
<QueryClientBoundary>
<ReactQueryDevtools initialIsOpen={false} />
<NetworkStateCatcher />
<ToastContainer />
<AmplitudeInitializer>
<Outlet />
</AmplitudeInitializer>
</QueryClientBoundary>
</ErrorCatcher>
</UnPredictableErrorBoundary>
</HDesignProvider>
);
};
export default App;
ps : 행동대장의 에러처리 전략이 궁금하다면
여기서 주목할 것은 ErrorCatcher와 QueryClientBoundary, ToastContainer이다. 백엔드 서버로 api 요청 시 에러가 발생하면 QueryClientBoundary에서 error를 발생시키고 ErrorCatcher에서 이를 감지해서 Toast를 띄우는 기능이다. 즉 이 세 개의 컴포넌트는 백엔드로 api를 요청하지 않는다면 필요없는 컴포넌트인 것이다. 메인 페이지에서는 이 기능이 필요없는데 프로젝트의 전반적으로 사용되는 코드에 포함되어있으니 비효율적이라고 판단. 이를 분리해보기로 했다.
EssentialQueryApp 정의
const EssentialQueryApp: React.FC = () => {
return (
<ErrorCatcher>
<QueryClientBoundary>
<ReactQueryDevtools initialIsOpen={false} />
<ToastContainer />
<Outlet />
</QueryClientBoundary>
</ErrorCatcher>
);
};
export default EssentialQueryApp;
Tanstack-Query를 사용해야하는 컴포넌트에서는 EssentialQueryApp 컴포넌트를 부르도록 분리했다. 그리고 EssentialQueryApp은 랜딩 페이지에서 필요하지 않은 컴포넌트이므로 동적 import를 적용시켜줬다.
const EssentialQueryApp = lazy(() => import('./EssentialQueryApp'));
서비스 전반적으로 필요한 컴포넌트는 최상단에서 불러주고 메인 페이지에는 EssentialQueryApp을 적용시키지 않았다.
그리고 그 외 페이지들은 백엔드로 api 요청을 날리므로 EssentialQueryApp을 적용시켜서 QueryClient가 필요한 컴포넌트에만 Tanstack-Query 스크립트가 포함되도록 변경했다.
import App from './App';
const EssentialQueryApp = lazy(() => import('./EssentialQueryApp'));
const router = createBrowserRouter([
{
path: '',
element: (
<Suspense>
<App />
</Suspense>
),
children: [
{
index: true,
path: ROUTER_URLS.main,
element: <MainPage />,
},
{
element: <EssentialQueryApp />,
children: [
{
path: ROUTER_URLS.createEvent,
element: <CreateEventFunnel />,
},
....
],
},
],
},
]);
Tanstack-Query 코드 분리 개선결과
main에서 Tanstack-query 관련 기능을 분리한 결과 23KB를 추가로 감소시킬 수 있었다.
무의식적으로 QueryClientProvider를 전역적으로 선언해서 사용하고 있다면 이를 분리해서 필요한 곳에서만 부를 수 있도록 개선해봐도 좋을 것 같다.
3. Tree Shaking
Tree Shaking 나무를 흔드는 것이다.
프로젝트에서 사용하고 있지 않은 코드를 덜어내서 불필요한 코드를 제거하는 최적화 기법이다.
이해를 돕기 위헤 자세하게 예시를 들어보면 모듈을 선언하고 모듈 내 100개의 함수가 있다고 가정해보자
function haengdong1() {}
function haengdong2() {}
function haengdong3() {}
...
function haengdong100() {}
export default {
haengdong1,
....
haengdong100
}
이 모듈에서 내가 필요한 것은 haengdong일 때 아래와 같이 모듈을 import해서 함수를 사용한다고 해보자
import haengdongFunction from './module';
haengdongFunction.haengdong1();
우리는 haengdong1이 필요해서 함수를 가져왔지만 실제로 import 문에서 100개의 함수를 전부 가져오게 됐고 나머지 99개의 함수들은 불필요하지만 import 문에 의해 불러와지게 됐다.
tree shaking을 하기 위해선 export default가 아닌 named export를 사용해서 export하고, import 구문에서 구조 분해 할당을 사용해서 실제로 필요한 함수만 import 해야한다.
export function haengdong1() {}
export function haengdong2() {}
export function haengdong3() {}
export function haengdong100() {}
import {haengdong1} from './module';
haengdong1();
그러나..
우리 코드.. 이미 export default를 사용해서 export 하고 있는 코드가 많다. 이를 전반적으로 뜯어고치는 비용을 생각하면 차라리 성능 개선을 하지 않는 것이 나을 정도이다.
다행히 Webpack의 Tree Shaking 공식문서를 보면 package.json에 sideEffects: false 속성을 추가하게 되면 webpack이 tree shaking을 할 때 사용하지 않는 export는 제거해도 괜찮다는 것을 webpack에 알려줄 수 있다고 한다. 그래서 export default를 해주고 있어도 실제로 사용하지 않고 있다면 webpack이 tree shaking을 해주게 된다. 휴우..
Tree Shaking 개선 결과
Tree Shaking을 진행해준 결과 31KB를 추가로 감소시킬 수 있었다.
그럼 지금까지 적용한 3가지 결과를 전부 반영한 g-zip 후 main 스크립트의 크기는 목표치에 도달했을까?
main bundle 최적화 결과
g-zip이 적용된 결과 main bundle을 보면 194KB이고 시간은 11ms가 측정됐다.
개선하기 전보다 bundle 크기는 219KB에서 194KB로 25KB를 줄였으며, 시간은 18ms에서 11ms로 7ms를 줄일 수 있었다.