webpack 에서 vite로 마이그레이션

제이밍·2025년 8월 9일
1
post-thumbnail

모노레포 환경에서 CRA → Vite 마이그레이션 여정기

안녕하세요! 오늘은 복잡한 모노레포 환경에서 Create React App(CRA)에서 Vite로 마이그레이션한 과정을 단계별로 차근차근 정리해보려고 합니다.

프로젝트 구조 소개

저희 키즈노트의 인앱 프로젝트는 다양한 마이크로 서비스들로 구성된 모노레포 환경입니다. services 디렉토리 하위에는 14개의 서비스가 있어요.

  • benefit: 혜택 메인 서비스
  • benefit-photomall: 키즈노트북 미리보기 미션
  • benefit-battle: 밸런스게임 혜택 미션
  • erp: 전자문서 관리 서비스
  • leadform: 리드폼 서비스
  • schoolbus: 안심승하차 서비스

...그 외 다양한 서비스들 존재

각 서비스는 완전히 독립적인 React 애플리케이션으로 동작합니다.

공통 패키지 활용

모노레포의 장점을 살리기 위한 @internals 공통 패키지도 있어요

  • @internals/components: 모든 서비스에서 사용할 수 있는 공용 UI 컴포넌트들
  • @internals/hooks: 자주 쓰이는 커스텀 React 훅들
  • @internals/libs: 유틸리티 함수들, API 관련 client들

1단계: 기존 CRA 설정 파악하기

마이그레이션을 시작하기 전에 기존 설정들을 분석했습니다.

기존 Package.json 스크립트

{
  "scripts": {
    "dev": "craco start --config craco.config.js",
    "build": "env-cmd --recursive --silent -f .env.production.${PHASE_ENV:-local} craco build --config craco.config.js",
    "analyze": "source-map-explorer 'build/static/js/*.js'",
    "test": "jest"
  }
}

cracoenv-cmd를 함께 사용하여 복잡한 환경 변수 관리를 하고 있었습니다.

환경 변수 관리

.env-cmdrc.js에서는 10개 이상의 서로 다른 배포 환경을 관리하고 있었어요

module.exports = defineEnv({
  prod: {
    REACT_APP_API_HOST: 'https://api.example.com',
    REACT_APP_ADS_API_HOST: 'https://ads-api.example.com',
    // ... 기타 API 호스트들
  },
  sandbox01: {
    REACT_APP_API_HOST: 'https://api-sandbox-01.example.com',
    // ... 샌드박스 환경 설정
  },
  dev01: {
    REACT_APP_API_HOST: 'https://api-dev-01.example.com',
  },
  // sandbox02, sandbox03, dev02, dev03 등등...
});

Craco 설정

CRA 환경에서 커스텀을 위한 Craro 설정

// craco.config.js
module.exports = {
  plugins: [
    {
      plugin: sentryPlugin, // 에러 트래킹을 위한 Sentry
      options: { context: path.join(__dirname, '../..') },
    },
    {
      plugin: rewireBabelLoader, // 내부 패키지 트랜스파일링
      options: {
   ...
      },
    },
  ],
  webpack: {
    configure: {
      resolve: {
...
      },
    },
  },
  style: {
   ...
  },
};

2단계: Vite 패키지 추가 및 설정

Package.json 수정

{
  "name": "benefit-photomall",
  "type": "module",  // ES 모듈 지원
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "jest"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.2.1",
    "@sentry/vite-plugin": "^3.0.0",
    "vite": "^5.1.4",
    "vite-plugin-svgr": "^4.2.0"
  }
}

제거된 기존 CRA 의존성들

  • @craco/craco → 더 이상 필요 없음
  • @sentry/craco-plugin@sentry/vite-plugin으로 교체
  • craco-babel-loader → Vite가 자동 처리
  • env-cmd-enhanced → Vite 방식으로 변경
  • react-scripts → CRA 핵심 패키지
  • tsconfig-paths-webpack-plugin → Vite 기본 지원

3단계: Vite 설정 파일 구성

가장 중요한 부분인 Vite 설정을 구성했습니다:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import { defineEnv } from './.env-cmdrc.js';

const envConfigs = defineEnv;

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  
  const normalizeEnvKey = (env: string) => env.replace(/-/g, '');
  const phaseEnv = process.env.PHASE_ENV || mode;
  const normalizedPhaseEnv = normalizeEnvKey(phaseEnv);
  const config = envConfigs[normalizedPhaseEnv as keyof typeof envConfigs];

  return {
    base: '/benefit-photomall/',
    
    plugins: [
      react({ jsxRuntime: 'automatic' }),
      svgr({
        svgrOptions: {
          exportType: 'named',
          ref: true,
          svgo: false,
          titleProp: true,
        },
        include: '**/*.svg',
      }),
    ],

    resolve: {
      // 중복 패키지 방지
      dedupe: [
        'react',
        'react-dom', 
        'react-router-dom',
        '@emotion/react',
        '@emotion/styled',
        'dayjs',
        'lodash',
      ],
      alias: {
        '@/assets': resolve(__dirname, './src/assets'),
        '@/components': resolve(__dirname, './src/components'),
        '@/hooks': resolve(__dirname, './src/hooks'),
        '@/lib': resolve(__dirname, './src/lib'),
        // 모노레포 내부 패키지들
        '@internals/components': resolve(__dirname, '../../internals/components'),
        '@internals/libs': resolve(__dirname, '../../internals/libs'),
        '@internals/hooks': resolve(__dirname, '../../internals/hooks'),
      },
    },
    
    optimizeDeps: {
      include: ['react', 'react-dom'],
      exclude: ['@internals/components', '@internals/libs', '@internals/hooks'],
    },
    
    server: {
      port: 3000,
      https: env.VITE_SSL_KEY_PATH && env.VITE_SSL_CERT_PATH
        ? {
            key: readFileSync(env.VITE_SSL_KEY_PATH),
            cert: readFileSync(env.VITE_SSL_CERT_PATH),
          }
        : undefined,
      host: 'localhost',
      open: true,
      strictPort: true,
    },
    
    build: {
      outDir: 'build',
      sourcemap: phaseEnv === 'prod',
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
            router: ['react-router-dom'],
            query: ['@tanstack/react-query'],
            emotion: ['@emotion/react', '@emotion/styled'],
            utils: ['lodash', 'dayjs', 'axios'],
          },
        },
      },
    },
    
    // 환경 변수 빌드 시점 주입
    define: {
      'process.env.NODE_ENV': JSON.stringify(mode),
      'import.meta.env.VITE_API_HOST': JSON.stringify(config.VITE_API_HOST),
      'import.meta.env.VITE_ADS_API_HOST': JSON.stringify(config.VITE_ADS_API_HOST),
      // ... 기타 환경 변수들
    },
  };
});

4단계: 코드 변경 사항

환경 변수 접근 방식 변경

// Before (CRA)
const apiHost = process.env.REACT_APP_API_HOST;

// After (Vite)  
const apiHost = import.meta.env.VITE_API_HOST;

환경 변수 네이밍 정리

// Before
REACT_APP_ADS_API_HOST: 'https://api.example.com'
REACT_APP_PW_API_HOST: 'https://api.example.com'

// After
VITE_API_HOST: 'https://api.example.com'
VITE_ADS_API_HOST: 'https://api.example.com'

SVG Import 방식 개선

// Before
import { ReactComponent as Alert } from '@/assets/alertTriangle.svg';

// After
// vite svg 세팅을 이용하면 기존 CRA 방식과 동일하게 사용 할 수 있어요.
import Alert from '@/assets/alertTriangle.svg?react';

TypeScript 설정 업데이트

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext", 
    "jsx": "react-jsx",
    "useDefineForClassFields": true
  }
}

HTML 템플릿 위치 변경

기존 public/index.html을 루트로 이동

<!-- index.html -->
<head>
  %VITE_GTM_HEAD%
  <script>
    window.SENTRY_RELEASE = { id: '%VITE_SENTRY_RELEASE%' };
  </script>
</head>
<body>
  %VITE_GTM_NOSCRIPT%
  <div id="app"></div>
  <script type="module" src="/src/index.tsx"></script>
</body>

5단계: 마주친 도전 과제들

1. 내부 패키지 처리

모노레포의 @internals 패키지들은 TypeScript 소스로 직접 참조되기 때문에,
alias 설정과 optimizeDeps 설정이 중요했습니다!

2. 환경 변수 처리 일관성

10개가 넘는 배포 환경 설정을 모두 Vite 방식으로 옮기면서도 기존 배포 파이프라인과의 호환성을 유지해야 했습니다.

3. Sentry 플러그인 교체

공식 @sentry/vite-plugin으로 교체하면서 설정 방식이 달라졌지만, 라이브러리 교체만으로 간단하게 교체할 수 있었어요.

마이그레이션 결과

성능 개선 결과

측정 항목CRAVite개선 효과
개발 서버 시작15초1초94% 단축
배포 시간1분 16초38초50% 단축
메인 번들 크기296KB132KB56% 감소

빌드 결과 비교

CRA 빌드 결과

  • main.js: 296.41 kB
  • 총 번들 크기: ~365 kB

Vite 빌드 결과 (청크별 분리)

  • index: 123.44 kB (메인 앱)
  • vendor: 91.78 kB (React + React DOM)
  • utils: 40.50 kB (유틸리티)
  • 총 번들 크기: ~132 kB

배포 파이프라인 단순화

# Before
env-cmd --recursive --silent -f .env.production.${PHASE_ENV} craco build --config craco.config.js

# After  
yarn workspace benefit-photomall build --mode ${PHASE_ENV}

마무리

이번 Vite 마이그레이션은 단순한 빌드 툴 교체를 넘어서 모노레포 환경에서의 개발 경험을 개선할 수 있는 의미 있는 작업이었습니다.

주요 성과

  • 개발 생산성 향상: 거의 즉시 시작되는 개발 서버
  • 빌드 성능 개선: 50% 단축된 배포 시간
  • 번들 최적화: 청크 분리를 통한 캐싱 효율성 증대
  • 개발 경험 개선: 더 간단해진 설정과 명령어

복잡한 환경 변수 관리와 내부 패키지 처리 등 모노레포 특유의 도전 과제들을 하나씩 해결해나가면서 많은 것을 배울 수 있었습니다. 이제 이 경험을 바탕으로 다른 서비스들도 순차적으로 Vite로 마이그레이션해 나갈 계획입니다!

profile
모르는것은 그때그때 기록하기

0개의 댓글