Create React App에서 Vite로 마이그레이션하기

김유경·2025년 5월 26일

들어가며

테스트 자동화는 점점 더 중요해지고 있지만, 기존 Create React App(CRA) 기반의 테스트 환경은 최신 스택과의 호환성에 여러 문제를 가져왔습니다. 저희 프로젝트에서도 테스트 코드 작성 도중 다음과 같은 오류를 마주하게 되었습니다.

Cannot find module 'react-router-dom' from 'src/setupTests.js'

이 에러는 테스트 환경에서 react-router-dom 모듈을 정상적으로 import하거나 mock하지 못할 때 발생하며, 근본적인 원인은 Jest(CommonJS 기반)와 react-router-dom(ESM 기반)의 모듈 시스템 충돌입니다.


CRA + Jtest 환경의 문제

1. CommonJS vs ESM 충돌

react-router-dom@v6부터는 ESM(ES Modules) 형식으로 배포되지만, CRA에서 사용하는 Jest는 기본적으로 CommonJS 환경입니다. 이로 인해 아래와 같은 mock 코드에서 충돌이 발생합니다.

jest.mock('react-router-dom', () => {
  const actual = jest.requireActual('react-router-dom');  // ⛔ 충돌 발생
  return {
    ...actual,
    useNavigate: jest.fn(),
  };
});

2. Babel 설정 제한

CRA는 내부적으로 Babel을 설정하지만 사용자에게 노출하지 않기 때문에, ESM 모듈을 제대로 변환할 수 없습니다. babel.config.js를 직접 적용하려면 CRA를 eject해야 했고, --experimental-vm-modules, babel-jest, esm 같은 복잡한 설정이 추가로 필요했습니다.

3. react-router-dom v7+의 구조적 변화

v7에서는 Routes, Outlet, useNavigate 등 많은 API가 구조적으로 변경되어, 기존 Jest mock 방식과 호환되지 않으며 테스트 구성 자체가 복잡해집니다.


왜 Vite로 마이그레이션했을까?

기존 CRA 환경에서 어떻게든 테스트 환경을 맞춰보려고 여러 시도를 했습니다.

  1. jest.config.js 수동 작성 + transform 설정
  2. react-router-dom 모듈 직접 mock 처리
  3. CRA eject 후 babel.config.js 설정

😢 하지만 모두 설정이 복잡하였으며, 결국 테스트 환경을 완전히 구축하지 못했습니다.

😃 그에 반해, Vite는?

  1. 기본적으로 ESM을 지원하므로 react-router-dom v6+과의 충돌이 없음
  2. 빠른 빌드 속도와 HMR(Hot Module Replacement) → 개발 효율 증가
  3. Vitest라는 테스트 러너를 통해 ESM 기반 테스트 환경을 쉽게 구성 가능

🚀 그래서 Vite + Vitest로 전환하게 되었습니다.

참고로, react-router-dom 버전을 낮춰보는 시도도 해봤습니다. react-router-dom 버전을 v5v6.3으로 낮추면 Jest 환경에서 테스트는 되지만, 최신 라우터 API를 쓰지 못하는 단점이 컸습니다. 그리고 이는 어디까지나 임시방편이었고, 장기적으로는 ESM을 지원하는 Vite + Vitest 조합으로 옮겨가는 것이 더 합리적인 방향이라고 판단했습니다.


마이그레이션 과정

기존 CRA 프로젝트 구조 요약

project/
├── public/                  # 정적 파일 (favicon, 이미지 등)
├── src/
│   ├── components/          # 공통 UI 컴포넌트
│   ├── pages/               # 라우팅 단위별 페이지
│   ├── routes/              # AppRouter 등 라우팅 설정
│   ├── utils/               # 유틸리티 함수 및 Context
│   ├── index.js             # 엔트리포인트
│   ├── App.test.js          # 기본 테스트 코드
│   └── setupTests.js        # 테스트 환경 설정
├── package.json
├── postcss.config.js
└── tailwind.config.js

‼️ 참고로, 기존에는 JSX 문법을 .js 파일에 작성해도 CRA에서는 문제없이 동작했습니다. 하지만 Vite에서는 .jsx 확장자를 명확히 구분하지 않으면 모듈 해석에 오류가 생길 수 있어 마이그레이션 중 .js → .jsx 확장자 변경 작업이 필요했습니다.

# JSX 태그가 포함된 파일만 .jsx로 변경
find src -name "*.js" | while read file; do
  if grep -q '<[A-Za-z]' "$file"; then
    mv "$file" "${file%.js}.jsx"
  fi
done

1단계: 패키지 의존성 변경

제거할 패키지

{
  "dependencies": {
    // 제거
    "react-scripts": "5.0.1"
  },
  "devDependencies": {
    // Jest 관련 패키지들도 함께 제거 (Vitest로 대체)
    "@testing-library/jest-dom": "^5.16.4",
    "jest": "^27.5.1"
  }
}

추가할 패키지

{
  "devDependencies": {
    // Vite 관련
    "vite": "^5.0.0",
    "@vitejs/plugin-react": "^4.2.0",
    
    // Vitest (Jest 대체)
    "vitest": "^1.0.0",
    "@vitest/ui": "^1.0.0",
    "@vitest/coverage-v8": "^1.0.0",
    "jsdom": "^23.0.0",
    
    // 기타 필요한 패키지들
    "@types/node": "^20.0.0"
  }
}

스크립트 수정

{
  "scripts": {
    "start": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  }
}

2단계: Vite 설정 파일 생성

프로젝트 루트에 vite.config.js (혹은 .mjs) 파일을 생성합니다.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  // React 플러그인 설정 (자동 JSX 런타임 + Babel 플러그인 포함)
  plugins: [
    react({
      jsxRuntime: 'automatic', // React 17+ 자동 런타임
      jsxImportSource: 'react', // JSX 소스 명시
      babel: {
        plugins: [
          ['@babel/plugin-transform-react-jsx', { runtime: 'automatic' }]
        ]
      }
    })
  ],

  // 테스트 환경 설정 (Vitest)
  test: {
    globals: true, // describe, it, expect 등 글로벌로 사용
    environment: 'jsdom', // 브라우저 시뮬레이션 환경
    setupFiles: ['./src/setupTests.js'], // 테스트 초기 설정 파일
    include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], // 테스트 파일 인식 확장자
    css: true, // CSS import 허용
    transformMode: {
      web: [/\.[jt]sx?$/] // 웹 전용 변환 규칙
    }
  },
  
  // 모듈 해석 설정
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src') // @ → src 경로로 대체
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx'] // 자동 확장자 해석
  },

  // 환경 변수 정의
  define: {
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
  }
});

3단계: HTML 파일 수정

public/index.html을 수정하여 Vite에서 요구하는 형태로 변경합니다. Vite는 CRA와 달리 public/index.html이 아닌 루트 경로의 index.html을 진입점으로 사용합니다. 또한, 진입점 파일이 JSX 문법을 포함한 경우 .jsx 확장자를 명시해야 하며, 실제 파일명에 따라 /src/index.jsx 또는 /src/main.jsx로 지정해주면 됩니다.

기존 CRA 형태

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <!-- ... -->
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Vite 형태로 수정

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <!-- %PUBLIC_URL% 제거 -->
    <!-- ... -->
  </head>
  <body>
    <div id="root"></div>
    <!-- 진입점 스크립트 추가 -->
    <script type="module" src="/src/index.jsx"></script>
  </body>
</html>

4단계: 환경 변수 설정

CRA 방식

// .env
REACT_APP_API_URL=https://api.example.com

// 사용
const apiUrl = process.env.REACT_APP_API_URL

Vite 방식

// .env
VITE_API_URL=https://api.example.com

// 사용
const apiUrl = import.meta.env.VITE_API_URL

5단계: import 문 수정

불필요한 .js 확장자 제거

1. React import에 .js 확장자 추가 문제

// 잘못된 형태
import React from 'react.js'
import { useState } from 'react.js'

// 올바른 형태
import React from 'react'
import { useState } from 'react'

2. CSS 모듈 import 문제

// 잘못된 형태
import styles from './Component.module.css.js'

// 올바른 형태
import styles from './Component.module.css'

3. 라이브러리 import 문제

// 잘못된 형태
import { BrowserRouter } from 'react-router-dom.js'

// 올바른 형태
import { BrowserRouter } from 'react-router-dom'

6단계: 테스트 설정 마이그레이션

Jest에서 Vitest로 마이그레이션합니다.
기존 CRA에서는 .test.js 파일에 JSX를 포함해도 문제없었지만, Vite에서는 .jsx로 확장자를 명시하지 않으면 오류가 발생할 수 있어 테스트 파일도 함께 확장자를 변경했습니다.

setupTests.js 수정

// 기존 Jest 설정
import '@testing-library/jest-dom/extend-expect'

// Vitest 설정으로 변경
// jest-dom의 커스텀 매처 (toBeInTheDocument 등) 등록
import '@testing-library/jest-dom'

Mock 예시 (Vitest 방식)

vi.mock('react-router-dom', async (importOriginal) => {
  const actual = await importOriginal()
  return {
    ...actual,
    useNavigate: () => vi.fn(),
    useLocation: () => ({ pathname: '/' }),
  }
})

테스트 파일 수정

대부분의 테스트 코드는 그대로 사용할 수 있지만, 일부 Jest 특화 기능은 수정이 필요할 수 있습니다.


마이그레이션 중 발생한 주요 이슈들

1. import 확장자 문제

가장 빈번하게 겪었던 문제 중 하나는 import 문에 불필요하게 .js 확장자가 포함된 경우였습니다. 기존 CRA 환경에서는 대부분의 파일이 .js 확장자로 작성되어 있었고, JSX 문법이 포함되어 있어도 별다른 문제가 발생하지 않았습니다.

하지만 Vite는 ESM 기반으로 동작하기 때문에 모듈 경로를 보다 엄격하게 해석합니다. JSX가 포함된 .js 파일이 .jsx로 명확히 구분되지 않으면, 모듈 해석 오류나 경로 충돌이 발생할 수 있습니다. 이로 인해 실제 코드에서는 import 경로에 .js 확장자가 남아 있는 경우 오류가 발생하는 일이 많았습니다.

따라서 마이그레이션 과정에서 JSX를 사용하는 .js 파일들을 .jsx로 변경하고, 모든 import 문에서 확장자를 제거하는 작업이 필요했습니다.

해결 방법: 프로젝트 전체에서 정규표현식을 사용해 일괄 수정

# 예시: VS Code에서 정규표현식 검색/치환 사용
# 검색: from '(.+)\.js'
# 치환: from '$1'

2. 환경 변수 마이그레이션

모든 REACT_APP_ 접두사를 VITE_로 변경하고, process.envimport.meta.env로 수정해야 합니다.

3. 절대 경로 import 설정

CRA의 src 기반 절대 경로를 Vite에서도 사용하려면 별도 설정이 필요합니다.

// vite.config.js
resolve: {
  alias: {
    '@': resolve(__dirname, 'src')
  }
}

참고 자료

0개의 댓글