번들 사이즈 2배 이벤트

MSW 설정하면서 번들사이즈다 2배가 된 문제가 발생했습니다. 원래 보통 300kb 정도되는 번들 사이즈가 600정도로 커졌습니다.

원래 MSW는 개발하면서 백엔드를 mocking하는 것입니다. 그래서 개발하는 동안에만 있어야 하고 build에는 포함되면 안됩니다. 이 문제를 해결하는 글입니다.

참고로 일반적으로 권장하는 번들 사이즈는 500kb 미만입니다.

문제: 번들사이즈 2배 이벤트

MSW를 설정하면 번들사이즈가 2배가 됩니다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ThemeProvider } from '@emotion/react';
import theme from './styles/theme.ts';
import queryClient from './libs/queryClient.ts';
// highlight-start
import { worker } from './mocks/worker.ts';

if (process.env.NODE_ENV === 'development') {
  /**
   * 실제 DB랑 통신을 확인하고 싶으면 아래 worker를 주석처리
   * api가 안정화 되면 아래 설정 {onUnhandledRequest: 'bypass'}은 해제
   * @see https://stackoverflow.com/questions/68024935/msw-logging-warnings-for-unhandled-supertest-requests
   */
  worker.start({ onUnhandledRequest: 'bypass' });
}
// highlight-end
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
      <ReactQueryDevtools />
    </QueryClientProvider>
  </React.StrictMode>
);

개발하는 동안 MSW를 활성화하는 설정입니다.

vite v4.3.9 building for production...
✓ 633 modules transformed.
dist/index.html                                     0.72 kB │ gzip:   0.39 kB
dist/assets/index-103ff5dd.js                       0.08 kB │ gzip:   0.09 kB
dist/assets/PageHeading-858f23b9.js                 0.13 kB │ gzip:   0.13 kB
dist/assets/index-8b0137af.js                       0.32 kB │ gzip:   0.29 kB
dist/assets/index-87bbd3d1.js                       0.65 kB │ gzip:   0.46 kB
dist/assets/index-32e984bf.js                       0.91 kB │ gzip:   0.51 kB
dist/assets/index-d8a3b847.js                       1.25 kB │ gzip:   0.67 kB
dist/assets/index-e552a5ba.js                       1.29 kB │ gzip:   0.75 kB
dist/assets/index-7c7fe7b8.js                       1.57 kB │ gzip:   0.99 kB
dist/assets/index-a90939a3.js                       3.12 kB │ gzip:   1.42 kB
dist/assets/index-f613d10c.js                       3.52 kB │ gzip:   1.69 kB
dist/assets/Input-8c62094b.js                       4.51 kB │ gzip:   1.82 kB
dist/assets/react-error-boundary.esm-37156e7c.js   17.20 kB │ gzip:   5.85 kB
dist/assets/index-11b4e058.js                     645.95 kB │ gzip: 201.37 kB

문제가 되는 번들 사이즈입니다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ThemeProvider } from '@emotion/react';
import theme from './styles/theme.ts';
import queryClient from './libs/queryClient.ts';
// highlight-start
import { worker } from './mocks/worker.ts';

if (process.env.NODE_ENV === 'development') {
  /**
   * 실제 DB랑 통신을 확인하고 싶으면 아래 worker를 주석처리
   * api가 안정화 되면 아래 설정 {onUnhandledRequest: 'bypass'}은 해제
   * @see https://stackoverflow.com/questions/68024935/msw-logging-warnings-for-unhandled-supertest-requests
   */
  worker.start({ onUnhandledRequest: 'bypass' });
}
// highlight-end
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
      <ReactQueryDevtools />
    </QueryClientProvider>
  </React.StrictMode>
);

개발하는 동안 MSW를 활성화하는 설정입니다.

vite v4.3.9 building for production...
✓ 359 modules transformed.
dist/index.html                                     0.72 kB │ gzip:   0.39 kB
dist/assets/index-103ff5dd.js                       0.08 kB │ gzip:   0.09 kB
dist/assets/PageHeading-2d3c7743.js                 0.13 kB │ gzip:   0.13 kB
dist/assets/index-76e731ac.js                       0.32 kB │ gzip:   0.29 kB
dist/assets/index-5977a381.js                       0.65 kB │ gzip:   0.46 kB
dist/assets/index-5302ca3b.js                       0.91 kB │ gzip:   0.51 kB
dist/assets/index-a077f0df.js                       1.25 kB │ gzip:   0.67 kB
dist/assets/index-364e022a.js                       1.29 kB │ gzip:   0.75 kB
dist/assets/index-9eabc667.js                       1.57 kB │ gzip:   0.99 kB
dist/assets/index-19083c5b.js                       3.12 kB │ gzip:   1.42 kB
dist/assets/index-c7499265.js                       3.52 kB │ gzip:   1.69 kB
dist/assets/Input-85139c49.js                       4.51 kB │ gzip:   1.82 kB
dist/assets/react-error-boundary.esm-5a8d437e.js   17.20 kB │ gzip:   5.85 kB
dist/assets/index-dcf1326b.js                     305.53 kB │ gzip: 101.05 kB

시도: MSW Tree shaking을 위한 여정

검색: Tree shaking does not work in vite library mode

rollup-plugin-visualizer 설치

직접 도움되는 것은 아니였지만 유용한 라이브러입니다. 일단 남겨두겠습니다.

rollup-plugin-visualizer

import { defineConfig, type PluginOption } from 'vite';
export default defineConfig({
  plugins: [visualizer() as PluginOption],
});
/// <reference types="vitest" />
/// <reference types="vite/client" />

import react from '@vitejs/plugin-react-swc';
import { defineConfig, loadEnv, type PluginOption } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
import path from 'path';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const { VITE_PORT } = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [react(), visualizer() as PluginOption],
    server: { port: parseInt(VITE_PORT) },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
      },
    },
    test: {
      globals: true,
      environment: 'jsdom',
      setupFiles: ['./src/setup.ts'],
    },
  };
});

이렇게 설정했습니다. state.html이 출력됩니다. 파일 구경하면 번들 사이즈별로 볼 수 있습니다.

검색: Mock Service Worker Tree shaking

다시보면 이 방법이 해결에 제일 가까웠습니다.

Mock Service Worker로 만드는 모의 서버

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

// 개발 환경에서만 실행되도록 환경 변수를 확인하는 과정이 필요하다.
if (process.env.NODE_ENV === 'development') {
  const { worker } = require('./mocks/worker');
  worker.start();
}

ReactDOM.render(<App />, document.getElementById('root'));

검색: msw not being tree shaken from client bundle (dead code elimination bug)

{
  // 생략
  "browser": {
    "./.msw": false
  }
}

별로 도움 안되었습니다.

해결: 동적으로 import하기

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ThemeProvider } from '@emotion/react';
import theme from './styles/theme.ts';
import queryClient from './libs/queryClient.ts';

/**
 * 실제 DB랑 통신을 확인하고 싶으면 아래 worker를 주석처리
 * api가 안정화 되면 아래 설정 {onUnhandledRequest: 'bypass'}은 해제
 * @see https://stackoverflow.com/questions/68024935/msw-logging-warnings-for-unhandled-supertest-requests
 * import는 비동기 함수이기 때문 async await와 즉시 실행함수가 필요함
 */
// highlight-start
(async function () {
  if (process.env.NODE_ENV === 'development') {
    // tree shaking을 위해 await import 활용
    const { worker } = await import('./mocks/worker.ts');
    worker.start({ onUnhandledRequest: 'bypass' });
  }
})();
// highlight-end
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
      <ReactQueryDevtools />
    </QueryClientProvider>
  </React.StrictMode>
);

당황스럽게 간단했습니다. 테스트는 성공적으로 동작하고 번들사이즈 2배 이벤트는 종료되었습니다.

즉시 실행함수에 async await도 적용해서 비동기로 동작하는 import로 worker를 가져옵니다.

vite v4.3.9 building for production...
✓ 635 modules transformed.
dist/index.html                                     0.72 kB │ gzip:   0.39 kB
dist/assets/index-103ff5dd.js                       0.08 kB │ gzip:   0.09 kB
dist/assets/PageHeading-d6ffa854.js                 0.13 kB │ gzip:   0.13 kB
dist/assets/index-2e281901.js                       0.32 kB │ gzip:   0.29 kB
dist/assets/index-21727d9f.js                       0.65 kB │ gzip:   0.46 kB
dist/assets/index-f13856c9.js                       0.91 kB │ gzip:   0.51 kB
dist/assets/index-327db79f.js                       1.25 kB │ gzip:   0.67 kB
dist/assets/index-e0d49bcb.js                       1.29 kB │ gzip:   0.75 kB
dist/assets/index-02c71055.js                       1.60 kB │ gzip:   1.01 kB
dist/assets/index-e90ed05b.js                       3.12 kB │ gzip:   1.42 kB
dist/assets/index-ecd6e71f.js                       3.52 kB │ gzip:   1.69 kB
dist/assets/Input-a9c99b9d.js                       4.51 kB │ gzip:   1.82 kB
dist/assets/react-error-boundary.esm-e555e35f.js   17.20 kB │ gzip:   5.85 kB
dist/assets/index-e38efc25.js                     305.50 kB │ gzip: 101.04 kB

원래 보여야 하는 번들 사이즈입니다. 이거로 번들사이즈 2배 이벤트는 끝났습니다.

How to make your JavaScript Bundle Smaller

예전에 본 튜토리얼인데 다시봐도 유용합니다.

학습: lazy 이외 다른 tree shaking

오늘도 자바스크립트는 저를 실망시키지 않습니다.

vite에서는 top level await를 사용할 수 있지만 브라우저에서는 지원하지 않습니다.

(async function () {
  if (process.env.NODE_ENV === 'development') {
    const { worker } = await import('./mocks/worker.ts');
    worker.start({ onUnhandledRequest: 'bypass' });
  }
})();

이런 패턴으로 tree shaking합니다. 개발할 때만 실행하기 때문에 build할 때 무시합니다.

vite의 강력함 같습니다.