이 글은 vue-cli(vue3, typescript, composition API)로 구성되어 webpack5 환경의 프로젝트를 vite 3버전 환경으로 마이그레이션하는데 구글링하고 직접 헤딩하여 완성되기까지의 경험을 기록한 내용이다.
BE 서버 환경이 불안정한 경우 서버를 재시작해야하는 경우가 빈번한 경우 환경에 따라 API 서버가 자주 변경되는 경우에는 FE환경에서 개발하는데 약간의(?) 불편함을 겪곤한다.
이 부분을 해결하기 위해 .env 파일로 여러 환경을 커버하거나 여러 서버를 띄우는 등의 임시 방편으로 처리해왔다. 하지만 HMR로 커버가 안되는 부분이 존재하였으며, 프로젝트가 비대해짐에 따라 빌드 속도가 점점 느려지게 되었다.
이러한 문제점이 지속되면서 동료개발자들의 불만 제기도 있었으며, 느린 CI 속도와 효율적이지 않는 FE 환경 구성, 느린 빌드 속도 등 다양한 부분들을 개선하기 위해서 마이그레이션을 하게되었다.
약 130,000 ~ 140,000 ms (대략 2분 넘음)
약 110,000 ~ 120,000 ms (2분 언저리)
약 2,000 ~ 3,000 ms
약 41,000 ms
약 4,000 ms
콘솔에 출력되지는 않지만 곧바로 반영
결과적으로 stylelint(lintOnStart)가 없는 환경에서의 개발용 빌드 시간은 약 27.5 ~ 30배 더 빠른 효과를 보였다.
vite2에서 3으로 크게 메이저버전이 업데이트되었다. vite3으로 마이그레이션하기 전에 변경점을 간단하게 정리해보자. 영어를 발번역한 글이므로 참고만 하면 될 것 같다.
Vite는 EOL(End-of-life)에 도달한 Node v12를 더이상 지원하지 않는다. Node는 14.18+
버전을 사용해야한다. node v14의 마지막 버전은 14.19.3 이므로 이 버전을 사용하기로 생각했다. 기존 프로젝트들이 node v12를 사용하는 경우도 존재하므로, nvm을 사용하여 다중 노드 환경을 구성해준다.
프로덕션 버전으로 빌드 및 번들링 시 소스코드가 최신 JS를 지원하는 환경에서 동작한다고 가정하고 진행된다. 기본적으로 Vite는 Native ES Module, Native ESM의 동적 Import, import.meta를 지원하는 브라우저를 대상으로 하고 있다.
Chrome >= 87
Firefox >= 78
Safari >= 13
Edge >= 88
npm create vite@latest "프로젝트명"
vue
, react, preact, lit, sveltevue-ts
framework에 lit, svelte도 추가되었다.
cd "프로젝트명"
npm install
npm run dev
정상적으로 프로젝트가 돌아가는 것을 확인한다. 이 때, 디펜던시에 있는 라이브러리들의 버전을 최신화하였다.
vite 3으로 업데이트되면서 기본 port가 3000에서 5173으로 변경되었다.
scripts.dev 명령어의 값을 "vite"에서 "vite --host"로 변경해주자. 이는 Local뿐만 아니라 Network방식을 허용하여 접속할 수 있게 변경하기 위함이다.
우선 폴더를 그대로 복사 붙여넣기한다. src/App.vue, src/main.ts 파일 내용도 복붙한다.
이미 vite로 마이그레이션할 계획으로 프로젝트의 폴더구조를 개선하였다. vite 환경의 프로젝트를 생성한 후에 기본적인 폴더 구조를 파악하고나서 기존 프로젝트의 폴더 구조와 비교/분석 후에 복붙할 때 사이드이펙트가 발생하지 않도록 스무스하게 진행될 수 있도록 개선하였다.
결과적으로는 거의(?) 문제가 발생하지 않았다. index.html이나 favicon같은 문제가 약간 있었지만 아래에 그 부분에 대해 작성하였다.
기존 프로젝트의 디펜던시들을 복사해준다. 그 뒤 vite에 맞게 package.json 파일에 필요한 라이브러리를 변경한다. 필요한 라이브러리는 업데이트를 진행하였다.
// package.json
"@vue/cli-plugin-babel": "~0.0.0",
"@vue/cli-plugin-eslint": "~0.0.0",
"@vue/cli-plugin-router": "~0.0.0",
"@vue/cli-plugin-vuex": "~0.0.0",
"@vue/cli-service": "~0.0.0",
// package.json
"sass-loader": "^0.0.0"
babel-eslint
, 폴리필 관련 디펜던시인 core-js
삭제하였다.// package.json
{
"name": "프로젝트명",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
// ...
},
// ...
}
"type": "module"
이 기본적으로 추가되었다.--host
옵션도 추가해주었다.처음에 파일을 복붙하고 나서 path url 부분에 빨간줄이 그어지며 resolve 관련한 알림이 나타난다. alias의 문제인데 이는 아래의 방식으로 해결할 수 있다.
vite.config.js
파일에 세팅해준다.// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
resolve: {
alias: [
{ find: '@', replacement: '/src' },
]
},
plugins: [vue()]
})
구글링하면 path를 사용해 path.resolve로 alias를 추가하는 방법이 많이 나온다. 하지만 기본적으로 위 코드처럼 alias를 지원해주기 때문에 불필요하게 path 라이브러리가 없어도 된다.
tsconfig.json
파일에 다음과 같이 추가한다.baseUrl, paths 옵션을 추가한다.
// tsconfig.json
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
}
},
// ...
}
// @ts-ignore
주석 사용하기. 이 방법도 임시방편인 것 같다.vite-env.d.ts
파일이 존재한다. 다음과 같이 라이브러리명을 선언해준다.// vite-env.d.ts
declare module '라이브러리명';
vite-env.d.ts 파일에 선언하는 방식으로 진행하였다. @ts-ignore 주석은 꼼수와 같은 방법이라고 생각되었기 때문이다.
우선 vite에서도 .env
파일의 명명규칙이 유지된다. 파일을 복붙하였을 때 파일명은 그대로 사용하면 된다. 하지만 파일 내 변수명은 변경이 필요하다.
vue3 환경에서는 VUE_APP_*
접두사를 사용해서 환경변수를 선언하는데, VITE_*
로 변경되었으며 그에 따라 환경변수명도 변경한다.
vue-cli에서 사용되었던 process.env.ㅁㅁㅁ
형태의 환경변수를 변경해야한다. vite의 환경 변수는 import.meta.env
객체를 이용해 접근하도록 되어있다.
대표적인 환경변수를 다음과 같이 변경한다. 프로젝트 전체 검색을 통해 변경한다.
.env 파일의 내용을 다음과 같이 변경한다.
위에서 언급한대로 파일을 복붙하는데 경로가 달라진 파일은 index.html이다. public 폴더 내부에 있는 index.html 파일의 위치가 프로젝트로 루트로 변경되었다.
추가 번들링 없이 index.html 파일이 앱의 진입점이 되게끔 의도적으로 변경되었기 때문이다. vite는 개발모드 시 esbuild를 사용하기 때문에 <script type="module" src="/src/main.ts"></script>
코드가 html 파일의 body 하단에 위치하게 된다.
public/favicon.icon이 public/vite.svg로 대체되었다. 그리고 index.html 파일에서 favicon을 사용하는 코드를 변경해야한다.
<link rel="icon" href="<%= BASE_URL %>favicon.icon">
이렇게 BASE_URL을 사용하는데, 이 부분은 변경이 필요하다. index.html이 번들링하지 않으므로 일반적으로 사용하는 방식처럼 코드를 변경해주자.
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
vite.config.js 파일의 resolve.extensions에 기본값은 ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json']
이다. .vue 확장자는 vue3에서도 생략하지 않도록 되어있으며, 기본 값을 그대로 유지해준다. 추후 필요한 확장자가 있는 경우 vite.config.js에 추가해준다.
webpack 환경이 아니기 때문에 vue-router에서 사용되는 dynamic import의 webpackChunkName
코드를 제거한다. 프로젝트의 메뉴가 많기 때문에 router 파일도 모두 모듈화하였으며, 제거할 코드들을 일일히 살펴보며 오타가 없는지 잘 지워졌는지 신경쓰면서 코드를 삭제하였다.
vite는 모던 브라우저만을 타깃으로 하기 때문에, 표준을 준수하도록 native css를 사용하도록 권고된다. 다시말해 CSS Pre-processors를 기본적으로 지원하지 않는다는 뜻이다. 다만 필요한 경우에는 어렵지 않게 설치해서 바로 사용할 수 있다.
# .scss and .sass
npm add -D sass
# .less
npm add -D less
# .styl and .stylus
npm add -D stylus
Vue SFC를 사용하는 경우 설치후 별다른 설정없이 <style lang="sass">
와 같은 css 전처리기를 바로 사용할 수 있다. Sass나 Less에서의 @import alias
나 url()
도 사용가능하다.
위 이미지는 webpack의 sass-loader 공식문서 내용에 일부이다. ~
를 사용하는 것은 deprecated되었으며, 코드에서 제거하는 것을 권장한다. 이미 구식 코드에서 사용 중이기 때문에 역사적인 이유(?)와 불필요한 사이드이펙트 발생을 막기때문에 호환성을 지원한다.
sass-loader의 change log중 일부이다. 11.0.0 버전이 되면서 위와 같은 내용이 적용된다.
프로젝트의 alias 중 가장 유명한 별칭은 at(@)이다. webpack 설정으로 @
을 src 폴더를 가리키게 세팅해준다. tilde를 사용하는 ~@
는 sass 구문에서 alias를 사용하기 위한 prefix이다. ~는 sass의 기능이 아니고 sass-loader에서 구현되는 기능이다. 그러므로 sass-loader의 문법을 수정해야한다. vite에는 sass의 package만 필요할 뿐 loader는 필요없다.
해결책
~@
를 /src
로 치환하는 alias로 설정하여 scss파일 내 url(~@/...)
코드를 정상적으로 읽을 수 있게 되었다.
~@
에서 ~
를 제거하는 방식으로 진행하려고 했다. 하지만 프로젝트 코드 중 모든 ~@
를 제거하기에 너무 노가다스럽다는 생각을 하였다. 혹시 ~@
를 sass의 alias로 세팅하면 *.scss
파일 내 코드를 수정없이 사용할 수 있지 않을까 생각하여 검색하게 되었다. vite > github > issue에서 검색한 내용을 힌트로 삼아 다음과 같은 코드로 해결하게 되었다.
export default defineConfig({
resolve: {
alias: [
{ find: '@', replacement: '/src' },
{ find: /^~@/, replacement: '/src' }
]
},
// ...
})
물론 프로젝트 내 ~@를 @로 치환하는 방법이 가장 빠르고 단순하다. @로 변경하는 방법을 추천한다. 위 방식은 sass에서 사용되는 alias를 추가하기 위해서 조사한 내용이다.
위 과정까지 적용하면 vite 서버는 정상적으로 작동한다. 하지만 실제 localhost로 접속해보면 sass의 문제가 발생한다. 뭔가 sass의 전역 변수가 말썽을 부린 것 같다. 환경 설정이 잘못 설정된 것일까?
Undefined variable.
╷
53 │ border-radius: $border-radius-base;
│ ^^^^^^^^^^^^^^^^^^^
╵
vite.config.ts
에 css.preprocessorOptions를 추가해보자. (참고 : https://vitejs.dev/config/#css-preprocessoroptions).
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
resolve: {
alias: [
{ find: '@', replacement: '/src' },
]
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/styles/main.scss";'
}
}
},
plugins: [vue()]
})
위의 방식대로 css.preprocessorOptions에 scss 경로를 걸어줬다. 하지만 또다른 문제가 발생하고 말았다.
위와 같이 scss 옵션을 세팅하고나서 dev 환경에서 style에 대한 문제가 발생한다. 같은 스타일이 반복되게 중복되는 현상이 생기는데, 이 문제는 vite github > issue(https://github.com/vitejs/vite/issues/4448, https://github.com/vitejs/vite/issues/7504)에 올라와있는 이슈이다. 프로젝트 내 scss 파일에 대한 이해가 필요하다. 우선 vite.config.ts
파일의 css.preprocessorOptions.scss.additionalData에 대해 알아보자.
additionalData 옵션 설명
additionalData 옵션은 다음과 같이 정의할 수 있다. (https://github.com/vitejs/vite/issues/4448#issuecomment-1110997673)
vite는 각각의 sass, scss, css import를 webpack과 동일한 방식으로 개별적으로 처리한다. 프로젝트 내 js, ts파일에서 import된 모든 sass, scss, css 파일 앞에 additionalData가 추가된다는 뜻이다.
현재 프로젝트의 scss 구조
위 문제를 해결하기 위해 우선 프로젝트의 scss구조를 알아보자. util 폴더에 있는 부분은 scss의 공통 변수와 함수가 들어있다.
scss 구조를 참고하여 설정을 변경
그러므로 모든 스타일 앞에 추가하는 역할을 하는 scss.additionalData 옵션에는 saas의 공통 variables(변수들), mixins(함수들)에 해당하는 util성 scss파일을 등록한다.
// vite.config.ts
export default defineConfig({
// ...
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/styles/utils/index";'
}
}
},
// ...
그리고 나머지 공통 스타일은 main.ts
파일에 추가한다.
// main.ts
import { createApp } from 'vue';
import App from '@/App.vue';
import router from '@/router';
import { store, key } from '@/store';
import '@/styles/main.scss';
const app = createApp(App);
// ...
app.mount('#app');
vite.config.ts의 additionalData 옵션과 main.ts에 공통 스타일 import를 통해 scss가 정상적으로 반영되는 것을 볼 수 있다.
vite 3버전의 기본 프로젝트 구조의 main.ts는 다음과 같다.
import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app')
프로젝트 스타일 파일은 main.ts에 추가해준다. sass의 유틸성 변수, 함수는 vite.config.ts파일의 css.preprocessorOptions.scss.additionalData에 추가해준다.
vite 환경에 Restful API를 위한 proxy 설정을 해준다. vite.config.ts
파일에 server.proxy의 값을 추가해준다.
export default defineConfig({
// ...
server: {
proxy: {
'/api/v1': {
target: 'http://0000.0000.000/',
},
'/api/v2': {
target: 'http://0000.0000.000/',
},
},
},
// ...
})
vite의 proxy 설정은 webpack의 proxy 설정과 거의 같다. 다만 다른점은 속성명이 devServer
가 server
로 변경된 것 밖에 없다.
target의 값을 문자열이 아닌 환경 변수로 설정하는 방법은 다음과 같다.
"@types/node" 패키지 설치도 필요하다.
위와 같은 방식은 proxy.URL.target의 값을 문자열로만 세팅할 수밖에 없다. 하지만 vite.config.ts
파일 안에서 .env
파일로부터 환경 변수 값을 꺼내올 수 없다. 또한 ImportMeta interface에는 env가 존재하지 않아 import.meta.env도 사용할 수 없다.
동적 프록시 설정을 위해 위 이미지처럼 세팅을 해준다. process.env를 사용하기 위해서는 @types/node
패키지도 필요하다. 설치해주도록 하자.
ESLint와 ESLint를 위한 vue plugin을 설치한다.
npm i --D eslint eslint-plugin-vue
그리고 ESLint 설정을 위한 파일을 생성한다. 무의식적으로 .eslintrc.js 파일을 만들어서 아래와 같이 추가하였다.
// .eslintrc.js
module.exports = {
env: {
node: true,
},
extends: [
'plugin:vue/vue3-recommended',
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
}
}
이상하게도 위와 같은 에러가 발생한다. package.json 파일 내 "type": "module",
속성이 추가된 것을 기억하는가? eslint 설정 파일의 확장자를 변경해야한다.
vue-cli 프로젝트에서 존재했던 .eslintrc.js
파일의 확장자를 .eslintrc.cjs
로 변경해준다.
그리고 IDE(webstorm)의 auto fix를 위한 eslint configuration files의 파일 path도 변경된 확장자 파일에 맞게 변경해준다. 그리고 기존의 eslint의 룰을 추가해준다.
@vue/eslint-config-typescript
이 config는 @vue/cli, create-vue 설정에서 사용하도록 특별히 설계되었으며, 외부 사용을 위한 것이 아니다. 설치되어있다면 지워주도록 하자.
eslint를 기본적으로 airbnb 룰에 따라 개발하되, rules에 추가적인 규칙을 추가해주는 방향으로 설정하려고 한다.
Typescript를 위해서 다음과 같은 플러그인/파서를 설치해준다.
npm i --D @typescript-eslint/eslint-plugin @typescript-eslint/parser
@typescript-eslint/eslint-plugin : Typescript 고유의 규칙을 포함하는 플러그인
@typescript-eslint/parser : ESLint가 Typescript 코드를 린트하도록 허용하는 파서
TypeScript 3.8부터 type-only imports, exports를 위한 새로운 문법이 추가되었다. type을 export할 때 다음과 같이 사용해야한다.
import type { SampleType } from './sampleModule.ts';
export type { SampleType };
type을 import/export하는 모든 코드에 type을 일일히 다 붙여줄 수가 없기 때문에, type-only 옵션을 수정해준다.
// tsconfig.json
{
"compilerOptions": {
// ...
"isolatedModules": false, // true가 기본 값이지만 false로 변경해준다.
// ...
}
}
시간이 된다면 type을 따로 나누고 isolatedModules옵션을 true로 변경해야할 것이다.
위의 옵션 외에도 추가적으로 다른 옵션들을 추가해준다.
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"noImplicitAny": false,
"isolatedModules": false,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
eslint의 몇몇 주요 설정들 정리
env 옵션은 전역 변수를 위한 것이고, parserOptions는 syntax를 위한 것이다.
vite 환경의 .eslintrc.cjs 내용
eslint-plugin-vue 패키지의 공식문서의 유저가이드의 내용은 다음과 같다.
vue-eslint-parser
를 parser 옵션에 사용하고, @typescript-eslint/parser
를 parserOptions.parser에 사용한다.
그 후에 vue-cli 환경의 eslintrc.js 파일의 rules를 vite 환경의 .eslintrc.cjs 파일의 rules로 옮겨준다.
// .eslintrc.cjs
module.exports = {
root: true,
env: {
node: true,
jest: true,
'vue/setup-compiler-macros': true, // API와 같은 defineProps 호환성을 위함
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-airbnb', // 위에서 설치한 패키지
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
rules: {
// 룰을 추가해준다.
}
}
webstorm에서 eslint 규칙을 읽어 auto fix 하는 기능을 위해 IDE 세팅을 적용한다.
.eslintrc.cjs 파일을 설정파일로 추가한다. 하지만 다음과 같은 문제가 발생한다.
eslint에서 Typescript 절대경로 Import에 '@'를 인식하지 못하는 문제가 발생한다.
IDE의 auto fix 옵션을 설정하기 전까지는 typescript 파일 경로에 alias '@'가 들어있는 경우 문제가 발생하지 않았다. 하지만 옵션을 켜는 경우 경로를 Ctrl+Click하여 파일을 찾아갈 수는 있지만, 다음과 같은 에러가 발생한다.
ESLint: Missing file extension for "@/utils/types"(import/extensions)
이 문제를 해결하기 위해서는 eslint-import-resolver-typescript
패키지 설치가 필요하다.
npm i -D eslint-import-resolver-typescript
이렇게 설치한 후에 .eslintrc.* 파일에 다음과 같이 추가한다.
// .eslintrc.cjs
module.exports = {
// ...
"settings": {
"import/resolver": {
"typescript": {}
}
}
// ...
}
위처럼 세팅하면 코드를 작성할 때, eslintrc에 세팅한 룰대로 prettier처럼 auto fix가 설정된다.
기존 프로젝트에서 stylelint를 사용하고 있으며, 특히 css attr의 ordering과 코드의 3depth 초과를 방지하기 위해서 사용 중이다.
사용 여부에 대해 의견이 분분하지만 이미 사용 중이기 때문에 상태를 유지하기 위해서 vite만의 stylelint plugin을 설치해준다.
npm install -D stylelint vite-plugin-stylelint
그리고 기존에 사용했던 stylelint.config.js의 확장자를 .cjs로 변경한다. 이는 package.json의 "type": "module"
옵션으로 인한 설정이다.
이제는 vite.config.jts 파일에서 StylelintPlugin을 추가해준다.
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import StylelintPlugin from 'vite-plugin-stylelint';
// ...
plugins: [
vue(),
StylelintPlugin({
include: ['src/**/*.{vue,scss}'],
exclude: ['node_modules'],
lintOnStart: true,
emitErrorAsWarning: true,
}),
],
});
};
webpack에서 사용했던 StyleLintPlugin의 파라미터의 속성이 달라졌다. 위에서 참고로 걸어놓은 깃허브 페이지의 문서를 읽어보면서 사용할만한 옵션을 설정해보자.
직접 적은 코드는 lint가 잘못된 경우 error가 아닌 warning이 발생되도록 한 옵션이다. 또한 npm run dev
개발 서버가 시작될 때 stylelint가 한 번 돌도록 옵션을 설정하였다.
vite에서 lintOnStart를 켠 경우
[vite] warning: Stylelint is linting all files in the project becauselintOnStart
is true. This will significantly slow down Vite.
워닝이 발생한다. 이 부분은 조금 개선이 필요하다.
정말 대단하십니다.. 👍