브라우저에서 Native ESM 방식을 지원하기 전까지는 Javascript 모듈화를 브라우저 레벨에서 진행할 수 없었고, 해당 소스 모듈을 브라우저에서 지원하기 위해서 require/IIFE/import,expor를 통한 모듈화 문법을 이용하여서 여러개의 파일로 합쳐주거나 의미 있는 단위로 묶어주는 기능을 번들링이라고 하며, 현재는 Webpack, Rollup 그리고 Parcel을 도구들이 지원하고 있습니다.
하지만 애플리케이션이 점점 더 발전함에 따라서 JS 도구들의 성능 병목 현상이 발생, HMR 사용하더라도 변경된 파일 적용 시 수초가 걸려서 개발자 생산성을 떨어뜨는 문제가 발생하고 있습니다.
ESM(ECMAScript Modules)
ESM은 모듈화 문법인 import, export를 별도의 도구 없이 브라우저 자체에서 소화해 낼 수 있는 모듈 방식을 의미한다. 만약 아래와 같은 코드를 웹팩과 같은 번들러 없이 브라우저에서 실행하면 에러가 발생합니다.
// app.jsimport { sum } from 'test.js'console.log(sum(1,2));<script src="app.js></script>
예전에는 브라우저가 import와 export를 해석할 수 있는 능력이 없었지만 이제는 다음과 같이 속성을 추가하면 브라우저에서 import와 export를 소화할 수 있게 되었습니다.
<script type="module" src="app.mjs></script>
Vite 특징
콜드 스타트 방식의 개발 서버 구동에 대한 성능 개선
Dependencies: node_modulesd와 같이 내용이 바뀌지 않는 소스코드에 대해서 esbuild 이용한 사전 번들링 기능 제공
esbuild는 webpack, parcel과 같은 기존의 번들러 대비 10-100배 빠른 속도를 제공
esbuild 로 node_modules 의 모든 패키지를 사전 번들링하여 아래와 같이 serving 하고 캐시
// Dependencies package import 할 경우import dayjs from 'dayjs';// vite esm 지원을 위한 변환/캐싱/node_modules/.vite/deps/dayjs.js?v=b09621d8
Source code: Native ESM 방식을 적용해서 브라우저가 모듈을 직접 가져오고 변환 및 제공하도록 처리
HMR 성능 개선
기존 번들러 기반에서는 소스 코드 변경 시 다시 번들링 과정을 다시 수행하다보니 성능이 느려지는 이슈를 Native ESM 방식을 사용하여 변경된 모듈만 교체해서 성능 개선
HTTP 헤더를 활용하여 페이지 로드 속도 개선(요청 횟수 최소화)
304 Not Modified: 클라이언트 캐시 사용
Cache-Control: max-age=31536000, immutalble 이용한 캐시 사용
프로덕션 환경 Rollup 사용
가장 빠른 성능을 자랑하는 esbuild이지만 아쉬운 생태계와 브라우저용 번들링에서는 안정성이 떨어진다는 평가가 있어서 vite에서는 프로덕션 환경에서 Rollup 번들러를 사용(트리 셰이킹, 지연로딩, 파일 분활 등 성능 이점 제공)
Rollup은 v4에서 파서를 SWC 전환 → Rust 포팅인 Rolldown 만드는 작업을 진행 중이며 개발이 완료된다면 esbuild를 모두 대체 가능(개발/빌드 사이에 불이치 제거 목표)
Setting the Stage with Vite Installationvite: 모던 웹 프로젝트 빌드 도구
@vitejs/plugin-react: @vitejs/plugin-react는 vite에서 React를 사용할 때 필요한 구성을 추가하여 React 애플리케이션의 빠른 개발 및 번들링을 지원
vite-tsconfig-paths: tsconfig.json 파일에 설정된 경로 별칭을 Vite에서도 적용할 수 있도록 지원. vite-tsconfig-paths를 사용하면 TypeScript 설정에 정의된 경로 별칭이 Vite에서도 인식되어 모듈 경로를 해석할 때 사용 가능
vite-plugin-svgr
SVG를 React 컴포넌트로 변환하는 Vite 플러그인
단, 3.3.0 이후 버전 설치 시 빌드, 타입 에러 발생(3.3.0 사용 시 아래와 같은 svgr을 리액트 컴포넌트로 변경 가능)
import { ReactComponent as ChannelLogo } from '../../assets/artwork-illust-pm-assign-info-channel-talk.svg';
vite-plugin-checker
vite를 사용하는 프로젝트에서 타입 체크, ESLint, VLS (Volar Language Server) 등을 포함한 여러 검사 기능을 제공하는 플러그인
비동기로 체크를 하기 때문에 개발 서버 환경 로딩 속도에 영향을 주지 않는 장점이 있다.
yarn add vite @vitejs/plugin-react vite-tsconfig-paths vite-plugin-svgr@3.3.0
yarn add vite-plugin-checker -D
// <root>/vite.config.jsimport { defineConfig, loadEnv } from 'vite';import path from 'path';import react from '@vitejs/plugin-react';import viteTsconfigPaths from 'vite-tsconfig-paths';import svgr from 'vite-plugin-svgr';import { createHtmlPlugin } from 'vite-plugin-html';import checker from 'vite-plugin-checker';export default defineConfig(config => { const env = loadEnv(config.mode, process.cwd(), ''); return { build: { outDir: 'build', // build 폴더명 변경(기본 dist) target: 'es2015', // es6 target transpile sourcemap: false, // 소스맵 생성 여부 rollupOptions: { output: { manualChunks(id) { // chunks 파일을 만들 경우 아래와 같이 청크명과 필터를 걸어주세요. if (id.includes('/react')) { return 'venders'; } }, }, onwarn(warning, defaultHandler) { // sourcemap: true 설정할 경우 node_modules에 소스맵 없는 경우에 대한 경고 무시를 위해 추가 if (warning.code === 'SOURCEMAP_ERROR') { return; } defaultHandler(warning); }, }, }, define: { global: 'globalThis', // 글로벌 객체를 빈 객체로 대체(global is not defined error 에러에 대한 방어코드) }, plugins: [react(), viteTsconfigPaths(), svgr()], };});
Environment Variables
VITE라는 prefix를 설정하지 않을 경우 import.meta.env 에서 값을 얻어 올 수 없습니다.
기본적으로 속성값으로 DEV(vite), PROD(vite build)을 제공해줌으로 개발/빌드 환경 구분이 가능합니다.
ChangeEnv Variables and Modes
// .env Files.env # loaded in all cases.env.local # loaded in all cases, ignored by git.env.[mode] # only loaded in specified mode.env.[mode].local # only loaded in specified mode, ignored by git// Modes# .env.production.liveVITE_ENVIRONMENT=live// command mode option flag"build:dev": "yarn vite --mode production.dev","build:rc": "yarn vite --mode production.rc","build:live": "yarn vite --mode production.live",
Add module vite-plugin-checker
CRA 환경에서 내부적으로 fork-ts-checker-webpack-plugin 플러그인을 바탕으로 비동기로 타입 체크를 해주고 있지만, VITE 환경에서는 vite-plugin-checker 직접 설치 후 vite.config.js 구성에 추가해줘야 합니다.
import.meta.glob dynamic load
Vite의 import.meta.glob을 사용하여 모든 아이콘을 동적으로 가져올 수 있습니다.
const icons = import.meta.glob('./*Icon.tsx');const iconComponentPath = './aIcon.tsx';const Icon = icons && icons[iconComponentPath as string] ? lazy( () => icons[iconComponentPath as string]() as Promise<{ default: ComponentType; }>, ) : null;
sass-resources-loader(dart sass) → vite config preprocessorOptions.sass
공통적으로 사용되는 스타일을 전역적으로 적용할 때 사용하는 sass-resources-loader를 vite에서는 preprocessorOptions.sass로 대체가 가능
// CRArules: [ { test: /\\.scss$/, use: [ { loader: 'sass-resources-loader', options: { resources: [ path.resolve(__dirname, './sass.scss'), ], }, }, ], include: path.resolve(__dirname, './src'), exclude: /node_modules/, },]// vite.config.jscss: { preprocessorOptions: { scss: { // Next line will prepend the import in all you scss files as you did with your vite.config.js file additionalData: ` @import "./sass.scss"; `, }, },},
단계별 에러 케이스 정리
@forward rules must be written before any other rules 에러
@forward 구문이 다른 모든 규칙(@import, @use, 스타일 선언) 보다 위에 위치하도록 수정
@forward
Dart Sass에서 도입된 Sass 모듈 시스템의 일부로, 모듈화된 스타일시트를 구성하고 관리하는 데 중요한 역할을 합니다.
@import 방식과는 다르게, @forward는 Sass 코드의 모듈화와 재사용성을 더 쉽게 관리할 수 있도록 돕습니다.
@use: 모듈을 가져오면서 네임스페이스를 사용해 충돌을 방지하고, 명시적으로 필요한 부분만 가져올 수 있습니다.
@import는 파일을 단순히 포함하는 방식으로, 여러 번 포함될 수 있고 변수나 믹스인의 충돌 위험이 있습니다.
// reset.scss@forward 'reset';// ./sass.scss@use 'color';css: { preprocessorOptions: { scss: { // Next line will prepend the import in all you scss files as you did with your vite.config.js file additionalData: ` @forward 'reset'; @use './color.scss' as *; @use './sass.scss' as * `, }, },},
setting module scss name for Vite(선택)
CRA에서는 SCSS 모듈을 사용할 때, 파일명과 클래스명을 결합해 고유한 클래스명을 자동으로 생성해주만, VITE 에서는 클래스명으로만 생성되는 규칙을 다르게 가져가고 있음
규칙: [파일명][원래 클래스명][hash:base64:5]
단, 서비스 내에서 module scss를 전역으로 선택하는 경우가 없다면 굳이 세팅을 해줄 필요가 없습니다.(내부적으로는 세팅을 하지 않을 시 [클래스명, hash]값을 토대로 유니크한 값으로 자동 변경)
import styles from './Test.module.scss';const Test = () => { return ( <div className={styles.container}></div> );};export default Test;// 변경 전class="Test_container__28qL6"// 변경 후//// vite.config.jsconst namespaceUUID = v4();css: { ... modules: { // 파일명과 클래스명을 포함한 고유한 이름 생성 generateScopedName: (name, filename) => { // 파일 경로 기반 고유한 해시 생성 const file = path.basename(filename).replace('.module', '').replace(/\.[^/.]+$/, ''); // UUID 생성 후 Base64 인코딩 const hash = Buffer.from(v5(`${file}_${name}`, namespaceUUID)).toString('base64').slice(0, 5); return `${file}_${name}__${hash}`; }, },},
server(proxy, outside file)
CRA proxy 설정을 VITE proxy로 대체가 가능합니다.
VITE 환경 외에 외부 폴더/파일 접근 시 fs.allow 객체로 추가 시 dev server에서 접근이 가능합니다.
기존에는 proxy 패스를 배열로 관리했지만 vite에서는 각각 패스로 관리를 해줘야합니다.
external file link settings(alias)
CRA에서 외부 파일내에 패스(tsconfig.json)를 접근 시 현재 프로젝트 환경 기준으로 패스를 바라보지만,
VITE에서는 외부 파일 기준으로 패스를 접근하다보니 alias 처리에 이슈가 있어서 tsconfig path가 아닌 절대 경로 패스로 재 지정해야 합니다.
예)
setting external file access permissions
CRA에서는 외부파일 접근 시 내부 config 파일에서 ModuleScopePlugin 플러그인에 추가해주는 번거로움이 있었지만,
VITE에서는 빌드 과정에서는 외부 파일 접근이 자유로우나, 개발 환경에서는 외부 파일 패스 경로를 설정해줘야합니다.
단, 파일 패스 설정 시 외부 파일 패스외에 내부 파일 패스도 같이 설정해줘야 합니다.
fs: { // 허용할 디렉터리 추가 (여기에 외부 파일 경로를 추가) allow: [ '../outside', '/' ],},
setting baseurl for Vite
CRA에서는 내부적으로 BaseURL(PUBLIC_URL)을 기준으로 리소스 패스 경로를 설정해주지만, VITE에서는 해당 경로를 base 속성값으로 변경해줘야합니다.(env 환경 변수에 base url 패스를 지정해주세요.)
단, DEV 환경에서는 내부 리소스 패스로 설정해야함으로 패스를 지정하지 말아야합니다.
vite 연동을 위한 스토리북 패키지 추가
기존 CRA 환경에서는 @storybook/react 패키지 명령어를 사용해서 문제가 없었지만,
VITE에서는 @storybook/react-vite 패키지를 사용하면서 storybook 패키지를 추가적으로 설치가 필요합니다.
또한, storybook 연동 패키지가 6버전 사용 시 ‘storybook-channel-mock.js:7 Uncaught TypeError: import_channels.default is not a constructor’ 와 같은 에러가 발생하게 됩니다.
꼭 최신 버전인 8버전 이상에 패키지를 설치를 해주세요
// CRA - @storybook/react"storybook": "start-storybook -p 9009 -s public",// VITE - @storybook/vitestaticDirs: ['../public'], // 이전 버전(v6) -s public 옵션과 동일
webpackFinal → ViteFinal 마이그레이션
preprocess scss, 허용할 파일 패스 연동
// webpackconfig.module.rules.push({ test: /\.scss$/, use: [ { loader: 'sass-resources-loader', options: { resources: [path.resolve(__dirname, '../sass.scss')], }, }, ], include: path.resolve(__dirname, '../src'), exclude: /node_modules/,});// Viteconst { mergeConfig } = await import('vite');return mergeConfig(config, { // Merge custom configuration into the default config const { mergeConfig } = await import('vite'); css: { preprocessorOptions: { scss: { // Next line will prepend the import in all you scss files as you did with your vite.config.js file additionalData: ` @forward 'reset'; @use './color.scss' as *; @use './sass.scss' as * `, }, }, },
외부 파일 접근
// VITEserver: { fs: { // 허용할 디렉터리 추가 (여기에 외부 파일 경로를 추가) allow: [ '../outside', '/' ], },},
storybook code migration processingNo longer inferring default values of args(v6.3)
버전 업데이트가 되면서 argTypes에서 기본값 세팅을 할 수 없고 args에서 세팅하는걸로 변경되었습니다. 빌드 에러가 발생하진 않지만, 확인해보시면 undefined 설정되서 들어가게 되니 꼭 마이그레이션 시 변경 해주세요.
빠른 성능: esbuild 기반으로 Jest보다 빠른 테스트 실행.(jest: Babel)
ESM 지원: 최신 ES 모듈을 기본적으로 지원.(ES 방식 지원 가능하나 호환성 이슈 발생)
타입스크립트 통합: 추가 설정 없이 TypeScript와 자연스럽게 통합.
간단한 모킹 API: 직관적이고 쉽게 사용할 수 있는 mocking 기능 제공.
Vite와의 호환성: Vite 설정 및 플러그인을 그대로 활용.
현대적 기능: HMR과 빠른 watch 모드 지원.
작은 학습 곡선: Vite 사용자에게 친숙한 설정.
우수한 개발자 경험: 오류 보고 및 테스트 피드백이 현대적이고 상세함.
Installation
yarn add -D vitest jsdom @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event// 기존 Jest 테스트 관련 패키지 삭제yarn remove jest babel-jest
setting
기존에 package.json/jest.setup.js 설정된 테스트 구성을 vitest.config.js로 이전
// 변경 전//// package.json"jest": { "collectCoverageFrom": [ "!<rootDir>/node_modules/" ], "moduleNameMapper": { "lodash-es": "lodash", }, "transformIgnorePatterns": [ "/node_modules/", ], "setupFilesAfterEnv": [ "<rootDir>/jest.setup.js" ]},// 변경 후//// vitest.config.jsexport default defineConfig({ resolve: { alias: { "lodash-es": "lodash", }, }, plugins: [ react(), viteTsconfigPaths(), // tsconfig 파일 패스 연동 ], test: { environment: 'jsdom', // React 테스트 환경을 위해 설정 globals: true, // Jest와 동일한 글로벌 변수를 사용 setupFiles: './src/setupTests.ts', // setup 파일 },});// src/setupTest.tsimport '@testing-library/jest-dom';
기존 리액트 테스트 환경 테스트를 위해서 test 환경을 꼭 json 으로 설정해주세요.
타입스크립트 타입에 vitest 추가해주세요.
Types
vitest는 네임스페이스 속성을 지원하지 않기 때문에 직접 유형을 가져와야합니다.
// 변경 전let fn: jest.Mock<(name: string) => number>;// 변경 후import type { Mock } from 'vitest';let fn: Mock<(name: string) => number>;
Timeout
// 변경 전jest.setTimeout(5_000);// 변경 후vi.setConfig({ testTimeout: 5_000 });
package.json test commands
"test": "vitest","test:run": "vitest run",
MSW Migration Guide
vitest 테스트와 msw 0.43.1 버전 같이 사용 시 아래 같은 문구에 에러가 발생
vitest에서 ECMAscript 모듈을 지원하고 있고 msw 이전버전에서는 미지원함에 따른 에러 발생으로 추측(버전을 최신 버전으로 변경해서 수정)
vitest Package subpath './lib' is not defined by "exports" in node_modules/headers-polyfill/package.json
Installation
msw 버전을 최신버전으로 업그레이드 함에 따른 storybook에 사용되는 storybook-addon도 동일하게 업그레이드 처리해야 합니다.
msw-storybook-addon 관련 스토리부 설정을 기존과 동일합니다.
yarn add -D msw@2.4.11 msw-storybook-addon@2.0.3
msw code migration processingworker-imports
// 변경 전import { setupWorker } from 'msw';// 변경 후import { setupWorker } from 'msw/browser';
Response resolver arguments
// 변경 전rest.get('/resource', (req, res, ctx) => {});// 변경 후import { http } from 'msw';http.get('/resource', ({ request }) => { const url = new URL(request.url); const productId = url.searchParams.get('id');});
Response declaration
// 변경 전rest.get('/resource', (req, res, ctx) => { return res(ctx.status(200), ctx.json({ id: 'abc-123' }));});// 변경 후import { http } from 'msw';http.get('/resource', () => { return new Response(JSON.stringify({ id: 'abc-123' }), { headers: { 'Content-Type': 'application/json', }, });});// 변경 후 2import { http, HttpResponse } from 'msw';export const handlers = [ http.get('/resource', () => { return HttpResponse.json({ id: 'abc-123' }, { status: 200 }); }),];
Request params
// 변경 전rest.get('/post/:id', req => { const { id } = req.params;});// 변경 후import { http } from 'msw';http.get('/post/:id', ({ params }) => { const { id } = params;});
Request cookies
// 변경 전rest.get('/resource', req => { const { token } = req.cookies;});// 변경 후import { http } from 'msw';http.get('/resource', ({ cookies }) => { const { token } = cookies;});
Request Body
// 변경 전rest.post('/resource', req => { // The library would assume a JSON request body // based on the request's "Content-Type" header. const { id } = req.body;});// 변경 후import { http } from 'msw';http.post('/user', async ({ request }) => { // Read the request body as JSON. const user = await request.json(); const { id } = user;});