CRA에서 Vite로 마이그레이션 하기
Table of Contents
개요
CRA
로 이루어진 사내 프로젝트를 Vite
로 마이그레이션 했다.
그 과정과 결과, 트러블슈팅을 공유하고자 한다.
마이그레이션 이유
사내 프로젝트는 Webpack 4
로 이루어진 react-scripts v4
와 Craco
조합을 사용하고 있었다. 이를 Vite
로 마이그레이션 하기로 결정한 이유는 다음과 같다.
1. CRA의 느린 속도
CRA
와 react-scripts
는 강력한 도구임에는 틀림 없다. 하지만 그 내부의 Webpack
은 무겁고 느린 편에 속한다.
사내 프로젝트의 로컬 개발 환경 구동과 HMR 시간은 수 초 이상 소요되었다.
배포 시간은 최대 15분이 찍히고 있었다. 그 중 프론트엔드 프로젝트의 빌드가 최대 2분 씩 걸리고 있었다.
개발 환경 구동 시간은 10초 이상 씩 걸리고 있었으며 HMR
시간 역시 수 초 이상 소요되고 있었다.
전반적으로 프로젝트 규모가 커질 수록 느려지고 있음이 틀림 없었다.
Vite의 빠른 속도
대신에 Vite
가 Native ES Modules
+ Rollup
구조를 채택하여 속도가 매우 빠르다는 점에 주목했다. Vite
는 어플리케이션의 모듈을 의존성 패키지와 소스 코드로 나눠서 구동한다.
먼저 의존성 패키지는 ES Build
로 사전에 트랜스파일링한다. ES Build
는 Go
언어로 작성되어 Webpack
대비 최대 100배 더 빠른 속도를 제공한다.
소스 코드는 네이티브 ES Modules
를 활용하여 브라우저에서 import
할 수 있다. 즉, 브라우저가 번들러의 작업을 일부 수행한다. Vite
는 브라우저가 요청하는 소스 코드를 즉시 변환하여 제공한다.
2. 번들러의 옵션 활용이 번거롭다
Webpack
의 기능을 추가로 사용하려면 eject
를 하거나 Craco
등 써드 파티 라이브러리에 크게 의존해야 한다. 즉, 경우에 따라 커스텀 구성이 번거로울 수 있었다.
실제로 CRA
기반 사내 프로젝트에서 React.lazy()
를 사용하여 간단한 코드 스플리팅을 하는데도 에러를 뿜어내어 Craco
로 해결했던 경험도 있었다.
아직은 CRA
의 기본 세팅을 제외하면 Webpack
의 의존성이 거의 없었지만, 프로젝트가 확장되면서 비슷한 문제를 마주칠 가능성이 크다고 판단했다.
또한 작성일 기준으로 react-scripts
라이브러리의 마지막 업데이트 일은 22년 4월로 1년이 넘게 업데이트가 이루어지지 않고 있다.
따라서 CRA
기반의 어플리케이션은 변화가 빠른 프론트엔드 생태계에 대한 대응이 어렵다고 판단했다.
3. React 팀에서 더이상 권장하지 않는 분위기
새로 개편된 React
공식 문서에서 CRA
는 더이상 언급되지 않는다. 대신 React 기반의 여러 프레임워크 중 하나를 선택할 것을 권장하고 있다.
만약 프레임워크를 고르지 못했다면 Vite
나 Parcel
등의 번들러를 활용하여 수동으로 React
어플리케이션을 만들어도 좋다고 언급하고 있다.
If you’re still not convinced, or your app has unusual constraints not served well by these frameworks and you’d like to roll your own custom setup, we can’t stop you—go for it! Grab react and react-dom from npm, set up your custom build process with a bundler like Vite or Parcel, and add other tools as you need them for routing, static generation or server-side rendering, and more.
현재는 새로운 프레임워크를 도입할 만한 여유가 없었기에 vite
마이그레이션을 우선적으로 진행하기로 했다.
마이그레이션 과정
1. Vite 설치
yarn add -D vite @vitejs/plugin-react
package.json 의 scripts 에서 실행 명령어를 vite로 변경해야 한다.
craco start
를 vite
로, craco build
를 vite build
로 변경한다.
1. vite.config.ts 생성
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});
2. vite.env.d.ts 생성
/// <reference types="vite/client" />
/// <referencetypes="vite-plugin-svgr/client" />
3. tsconfig types 추가
{
compilerOptions: {
...,
"types": ["vite/client"]
}
}
4. index.html 수정
index.html
은 public 폴더에서 최상단 폴더로 이동해야 한다.
index.html
내의 %PUBLIC_URL%
은 모두 제거해야 한다.
root div 바로 밑에 index.tsx
를 추가해야 한다.
<!-- index.html -->
...
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
...
</body>
2. react-scripts 및 Craco 제거
1. react-scripts 및 Craco 제거
yarn remove react-scripts @craco/craco
2. craco.config.js 제거
craco.config.js
를 제거한다.
3. ENV 환경 변수 설정
1. 환경 변수 프리픽스 변경
CRA
는 REACT_APP_
프리픽스를 사용하는 반면, Vite
는 VITE_
프리픽스를 사용한다.
또한 환경 변수 사용 시 CRA
는 process.env.
프리픽스를 사용하는 반면, Vite
는 import.meta.env.
프리픽스를 사용한다.
이에 맞춰 모든 환경 변수의 프리픽스를 변경했다.
2. react-app-env.d.ts 제거
CRA
에서 제공하는 react-app-env.d.ts
를 제거하고 env.d.ts
를 새로 생성했다.
interface ImportMetaEnv {
VITE_TEMP_ENV: string
...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
4. vite.config.ts 설정 추가
프로젝트 환경에 맞게 몇 가지 설정을 추가했다.
1. localhost에서 https 사용 설정
localhost
에서 https
를 사용해야 했기에 basic-ssl
플러그인을 추가했다.
yarn add -D @vitejs/plugin-basic-ssl
// vite.config.ts
import basicSsl from '@vitejs/plugin-basic-ssl';
...
export default defineConfig(() => {
return {
...
server: {
https: true,
},
plugins: [react(), basicSsl()],
};
});
2. svgr 설정
SVG 컴포넌트 사용을 위해 svgr
을 추가했다.
yarn add -D vite-plugin-svgr
// vite.config.ts
import basicSsl from '@vitejs/plugin-basic-ssl';
import svgr from 'vite-plugin-svgr'
...
export default defineConfig(() => {
return {
...
server: {
https: true,
},
plugins: [
react(),
basicSsl(),
svgr({
svgrOptions: {
icon: true,
},
})
],
};
});
3. tsconfigPaths 설정
절대경로 import를 위해 tsconfig의 paths를 그대로 사용하도록 설정했다.
yarn add -D vite-tsconfig-paths
// vite.config.ts
import basicSsl from '@vitejs/plugin-basic-ssl';
import svgr from 'vite-plugin-svgr'
import tsconfigPaths from 'vite-tsconfig-paths';
...
export default defineConfig(() => {
return {
...
server: {
https: true,
},
plugins: [
react(),
basicSsl(),
svgr({
svgrOptions: {
icon: true,
},
}),
tsconfigPaths()
],
};
});
5. CRA eslint 설정 제거
eslint extends 항목 중 react-app
을 제거한다.
이 때 주의할 점은 CRA에서 제공하는 eslint 설정이 사라지기 때문에 린트 에러가 발생할 수 있다.
이를 위해 필요한 플러그인, rules 등을 추가로 설정해야할 수 있다.
6. 테스트 환경 세팅
테스트를 위해 Vitest
를 도입했다.
yarn add -D vitest
// package.json
{
...
"scripts": {
...
"test": "vitest test"
}
}
추가 설정
기본적인 설정은 끝났지만 몇 가지 설정을 추가했다.
1. Storybook 설정
Storybook 역시 vite 기반으로 마이그레이션했다.
간단한 명령어로 가능하다.
npx sb@next init --builder storybook-builder-vite
2. 빌드 관련 추가 설정
빌드 시 더 자세한 정보를 얻기 위한 설정을 추가했다.
1. bundle-analyzer 추가
yarn add -D rollup-plugin-visualizer
bundle analyzer는 빌드 시 자동으로 실행되며 filename 항목에서 저장 경로를 설정할 수 있다.
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig(() => {
return {
plugins: [
...,
visualizer({
template: 'treemap', // or sunburst
open: true,
gzipSize: true,
brotliSize: true,
filename: './bundle-analyze/analyze.html', // will be saved in project's root
}) as PluginOption,
],
};
});
2. 빌드 소요 시간 출력
빌드 시 소요 시간을 알기 위한 플러그인을 추가했다.
yarn add -D vite-plugin-time-reporter
import timeReporter from 'vite-plugin-time-reporter';
export default defineConfig(() => {
return {
plugins: [..., timeReporter()],
};
});
트러블 슈팅
Vite
를 도입한 후 모든게 순탄하지만은 않았다.
1. Node.js 런타임 환경 전용 모듈이 브라우저에서 동작하지 않는 이슈
Node.js 런타임 환경 전용 모듈이란 path
, fs
, events
, http
, https
등등 브라우저가 아닌 Node.js
런타임 환경에서만 동작하는 모듈이다. 따라서 브라우저 환경에서 정상 동작하지 않을 수 있다.
이러한 현상에 대해서 이미 여러 개의 이슈가 올라왔고, 그 중 하나에서 에반 유는 다음과 같이 답변했다.
This is a wont fix, because aws-amplify is legit trying to import and use Node-only builtins when it's actually intended to be a client library. Unlike webpack 4, Vite does not shim Node built-ins. Note this means aws-amplify will break in webpack 5 as well because it no longer shims Node built-ins. aws-amplify should stop relying on Node built-ins if it actually is meant to be used in the browser.
이러한 답변에 기반해보면 기존의 CRA
에서는 정상 동작했던 이유는 다음과 같다고 볼 수 있다.
사내 프로젝트의 CRA
는 Webpack 4
에 의존하고 있었는데 Webpack 4
까지는 Node.js
런타임 환경 전용 모듈을 브라우저에서도 동작할 수 있도록 호환성 처리를 해주고 있었기 때문이다. 하지만 Webpack 5
부터는 더 엄격한 모듈 시스템이 도입되어 이러한 처리가 제공되지 않는다. 이는 Vite
도 마찬가지이므로 예기치 못한 이슈가 발생했었다.
이 이슈는 index.html
과 vite.config.ts
에 다음과 같은 설정을 추가하여 대응했다.
<body>
<div id="root"></div>
<script>
window.global = window;
</script>
<script type="module" src="/src/index.tsx"></script>
</body>
Node.js
의 전역 객체 및 모듈 관련 기능을 브라우저 환경에서 사용할 수 있게끔 폴리필 처리를 해주는 플러그인을 설치 및 적용했다. 또한 브라우저 환경에서 전역 변수 global을
window
로 설정했다.
./runtimeConfig'
모듈은 Node.js의 내장 모듈이기 때문에 브라우저에서 접근이 불가능하다. 따라서 브라우저 모듈인 ./runtimeConfig.browser
로 해석되도록 수정했다.
// 다음의 2개 플러그인 설치가 필요하다.
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';
export default defineConfig({
define: {
...(process.env.NODE_ENV === 'development' ? { global: 'window' } : {}),
},
optimizeDeps: {
esbuildOptions: {
define: {
global: 'globalThis',
},
plugins: [
NodeGlobalsPolyfillPlugin({
process: true,
buffer: true,
}),
NodeModulesPolyfillPlugin(),
],
},
},
resolve: {
alias: {
'./runtimeConfig': './runtimeConfig.browser',
},
},
});
2. 낮은 버전 브라우저 / 모바일 기기에서 동작하지 않는 이슈
낮은 버전의 브라우저 또는 모바일 기기에서 웹 어플리케이션이 동작하지 않는 이슈가 있었다.
이 이슈는 Iphone 6+
, IOS 12.5.7
환경에서 발생했다.
옵셔널 체이닝 문법이 인식되지 않아서 어플리케이션이 멈춘 것으로 파악했고, 최신 문법을 이해하지 못하는건가 싶었다.
따라서 legacy 환경에서 Vite
어플리케이션이 동작할 수 있도록 호환성 처리를 해주는 플러그인 @vitejs/plugin-legacy
를 추가했다. 이슈 자체는 구형 IOS
에서 발생했지만 범용적인 적용을 위해 target을 IE
로 잡았다.
이 처리로 인해 빌드 소요 시간이 약 1분 정도 늘어났다..😢
import legacy from '@vitejs/plugin-legacy';
export default defineConfig(() => {
return {
...
plugins: [
...
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
};
});
3. 타입 체크가 되지 않는 이슈
Vite는 .ts 파일에 대해서 트랜스파일링만 수행하며, 타입 검사는 IDE와 빌드 프로세스에서 수행된다고 가정합니다.
vite는 타입 체크를 하지 않는다. 트랜스파일링은 Vite의 on demand HMR 컨셉과 일치한다. 변경된 모듈만 다시 트랜스파일링하여 교체할 수 있다. 반면 타입 체크는 모든 모듈에 대해 수행되어야 한다. 변경된 모듈에만 타입 에러가 발생한다는 보장이 없기 때문이다. 하지만 타입 체크를 매번 한다면 Vite는 빠른 속도를 제공할 수 없게 된다. 따라서 Vite 팀에서는 타입 체크는 별도로 수행할 것을 권장한다.
Vite의 역할은 소스 모듈을 가능한 빠르게 브라우저에서 실행할 수 있는 형태로 변환하는 것입니다. 따라서 이를 위해 Vite의 변환 파이프 라인에서 정적 분석 검사를 분리하는 것을 권장합니다.
따라서 다음의 2가지 방법으로 타입 체크를 수행하도록 했다.
- 빌드 명령어에
tsc --noEmit
을 추가하여 빌드 시 타입 체크가 이루어지도록 했다. - vite-plugin-checker 플러그인을 추가하여 브라우저에서 타입 에러가 보여지도록 했다.
다만 빌드 시 tsc --noEmit
이 추가되니까 빌드 시간이 소폭 증가했다.
결과 및 후기
Vite
를 도입한지 약 2주가 지났고 현재는 모두가 만족하면서 사용하고 있다. 특히 빠른 개발 속도가 주는 만족감이 매우 크다.
- 빌드 시간 : 약 3분 -> 약 1분 20초 (약 55.56% 감소)
- 로컬 환경 구동 시간 : 12초 -> 0.3초 (약 97.5% 감소)
- 배포 시간 : 약 10분 -> 약 8분 (약 20% 감소)
다만, CRA
에서 Vite
로의 마이그레이션은 프로젝트에 매우 큰 영향을 미치는 작업이다. 따라서 철저한 검증이 동반되어야 한다. 나는 이 점을 간과했고 별도의 QA 검증 요청을 하지 않았다. 그 결과, 정기 배포가 코 앞에 왔는데도 이슈가 계속 발생했다. 우려의 목소리가 나오기 시작했고 결국 정기 배포를 하루 앞두고 프로젝트 전체 QA 검증이 진행되었다.
더이상의 이슈는 발견되지 않아서 가슴을 쓸어내렸지만, 서비스에 큰 변화를 일으켜놓고 검증 요청조차 하지 않은 내 자신을 크게 반성했다.