[Vue + Vuetify] Optimization CSS Bundling
Jan 02, 2024
issue
. 예컨대
header-title-text
css class가 적용된 <span>
tag의 경우 style이 약 65번 호출되고 있다src/views/Desktop/HeaderControllerWrapper.vue
<template> <div class="header-controller-wrapper"> <!-- header controller의 공통 소스 --> <BreadCrumbs></BreadCrumbs> <keep-alive> <component :is="computedComponent" > <template #pageTitle> //이 부분 <span class="header-title-text"> <span> {{ getterPageTitle }} </span> </span> 생략... </template> </component> </keep-alive> </div> </template>
- 페이지 로드 시간을 줄여 사용자 경험과 개발 경험을 향상시키기 위해 css bundling(모듈들의 의존성 관계를 파악하여 그룹화시켜주는 작업) 최적화를 통해 해당 style이 한번만 load되도록 할 필요가 있다.
Solution
- 현재 HCR에서는 style을 SASS/SCSS + Vuetify 기본 제공 styles로 관리하고 있다
- CSS(CSS : Cascading Style Sheets - 종속형 시트)는 프로젝트의 크기가 커지고 고도화될수록 유지보수에 큰 어려움이 생긴다.
- 불필요한 선택자(Selector)
- 연산 기능 한계
- 구문(Statement)의 부재의 문제점
- SASS/SCSS(SASS : Syntactically Awesome Style Sheets - 문법적으로 멋진 스타일시트/SCSS : Sassy CSS - SASS보다 좀 더 넓은 범용성과 CSS의 호환성의 장점을 가짐)
- 변수(Variable) 할당
- 중첩(Nesting) 구문
- 모듈화(Modularity)
- 믹스인(Mixins)
- 확장&상속(Extend/Inheritance)
- 연산자(Operators)
SASS
,SCSS
를CSS pre-processor
(전처리기)라고도 하는데 스크립팅 언어이기 때문에SASS
,SCSS
로 작성된 파일들은 곧바로 웹에 적용될 수는 없다. 웹은 기본적으로 CSS파일로 동작하므로 별도의 컴파일 과정을 거친 다음 CSS파일로 변환하여 사용하게 된다.
- Vuejs에서 sass를 사용하는 방법
- sass와 sass-loader 설치하기
npm install -D sass-loader sass
- 간단한 설치 만으로 사용이 가능한 이유는 vue-loader에서 기본으로 설정되어 있는 webpack 설정 때문에 패키지 설치 후 컴포넌트 내에서 lang 속성(
<style lang="scss" scoped>
)을 지정해주면 자동으로 Loader를 사용하여 바로 사용할 수 있다.
//vue cli verstion vue --version @vue/cli 5.0.8 //package.json "dependencies": { "vue": "2.7", 생략 }, "devDependencies": { "sass": "~1.32.8", "sass-loader": "^10.1.1", 생략
-
scss
및sass
파일을 웹팩으로 빌드할 때 사용되는 로더가scss-loader
다. - 스타일 코드에 자주 작성되는 색상 값이나 픽셀 값을 변수용 파일에 작성해서 다른
scss
파일에서 변수 파일을import
한 후에 해당 변수를 사용하는 방법이 일반적이다. 하지만 변수 파일을 매번import
해줘야하는 번거로움이 있고 믹스인 등 다른 기능들을 모아놓은 파일을 또 불러와야 한다면 매번import
문과 파일의 경로까지 써줘야하는 불편함이 있다. _mobile.scss
예시
$app-bar-height: map.get($spacers, 14); $tab-height: map.get($spacers, 12); $bottom-navigation-height: map.get($spacers, 14); $divider-height: 1px; $main-dismiss-height: $app-bar-height + $tab-height + $bottom-navigation-height; $bottom-button-height: map.get($spacers, 13); $mobile-screen-max-width: 600px; $dialog-margin: map.get($spacers, 2); $covered-all-z-index: 9999;
scss-loader
에서는 전역 sass
/scss
스타일 및 변수 설정할 수 있는additionalData
옵션을 제공한다. (9버전 미만 에서는 prepandData
를 사용하고 그 위 버전은 additionalData
옵션명을 사용한다.)/vue.config.js
module.exports = { ...getConfig(), css: { extract: false, loaderOptions: { // 전역 scss 스타일 및 변수 설정 sass: { additionalData: ` @use "sass:map" @import '~vuetify/src/styles/styles.sass' @import '@/styles/_mobile.scss' `, }, scss: { additionalData: ` @use "sass:map"; @import '~vuetify/src/styles/styles.sass'; @import '@/styles/variables.scss'; // GLOBAL SETTING @import '@/styles/_mobile.scss'; @import '@/styles/_mixins.scss'; `, }, }, }, 생략 };
- 현재 HCR에서는
sass
도 사용되고 있으므로sass
와scss
를 분리해서 설정을 적용해주었다.
by default the `sass` option will apply to both syntaxes because `scss` syntax is also processed by sass-loader underlyingly but when configuring the `prependData` option `scss` syntax requires an semicolon at the end of a statement, while `sass` syntax requires none in that case, we can target the `scss` syntax separately using the `scss` option 기본적으로 `sass` 옵션은 두 구문 모두에 적용됩니다. `scss` 구문도 기본적으로 sass-loader에 의해 처리되기 때문입니다.하지만prependData
옵션을 구성할 때scss
구문은 명령문 끝에 세미콜론이 필요하지만sass
구문에는 없음이 필요합니다.이 경우, `scss` 옵션을 사용하여 `scss` 구문을 별도로 타겟팅할 수 있습니다.
src/styles
폴더 안에 variables.scss
파일에 정의된 변수들을 프로젝트 전체에 자동으로 적용해준다.If you have not installed Vuetify, check out the quick-start guide. Once installed, create a folder calledsass
,scss
orstyles
in your src directory with a file namedvariables.scss
orvariables.sass
. The vuetify-loader will automatically bootstrap your variables into Vue CLI’s compilation process, overwriting the framework defaults. 설치가 완료되면 src 디렉터리에 sass, scss 또는 styles라는 폴더를 만들고 variables.scss 또는 variables.sass라는 이름의 파일을 만듭니다. vuetify-loader는 프레임워크 기본값을 덮어쓰면서 변수를 Vue CLI의 컴파일 프로세스에 자동으로 부트스트랩합니다. https://v2.vuetifyjs.com/en/features/sass-variables/#vue-cli-install
variables.scss
에 import하면 스타일이 대량으로 중복될 수 있다. 컴포넌트 개수 만큼 실행되기 때문에 변수에 대해서만 정의해야한다. - 기존
variables.scss
@use 'sass:map'; @import '~vuetify/src/styles/styles.sass'; // GLOBAL SETTING @import '~@/styles/mobile'; @import '~@/styles/mixins'; // FONT $heading-font-family: 'Roboto', 'Noto Sans KR', sans-serif !important; $body-font-family: 'Roboto', 'Noto Sans KR', sans-serif !important; // LAYOUT $header-height: 64px; $lnb-width: 180px; $page-tab-height: 48px; $page-content-width: 1400px; // MEDIA QUERY $sm-screen-max-width: 640px; $lg-screen-min-width: $sm-screen-max-width + 1; // Z-INDEX $z-index-overlay: 110; $z-index-header: 100; $z-index-order-wrapper: 90; $z-index-worker-wrapper: 90; $z-index-lnb: 90; $z-index-map-wrapper: 50; // BUTTON // https://vuetifyjs.com/en/api/v-btn/#sass $btn-letter-spacing: 0; // PAGE: map-control $order-wrapper-width: 402px; $worker-wrapper-width: 276px; $order-info-card-width: 440px; $drawer-button-height: 56px; // DIALOG $order-detail-dialog-width: 460; // Component @import '~@/styles/dialog'; @import '~@/styles/button'; @import '~@/styles/textDesignSystem'; // reset vuetify .v-pagination__navigation, .v-pagination__item { box-shadow: none !important; border: 1px solid map.get($grey, 'lighten-2') !important; } // UTIL .wh-nw { white-space: nowrap; } .wh-pl { white-space: pre-line; } .main-content { overflow: auto; border-top: 1px solid map.get($grey, 'lighten-2'); padding: 16px; }
variables.scss
에는 전역으로 사용하는 변수만 남기고 일반 css class/import문은 global.scss
로 옮겨서 관리한다.// FONT $heading-font-family: 'Roboto', 'Noto Sans KR', sans-serif !important; $body-font-family: 'Roboto', 'Noto Sans KR', sans-serif !important; // LAYOUT $header-height: 64px; $lnb-width: 180px; $page-tab-height: 48px; $page-content-width: 1400px; // MEDIA QUERY $sm-screen-max-width: 640px; $lg-screen-min-width: $sm-screen-max-width + 1; // Z-INDEX $z-index-overlay: 110; $z-index-header: 100; $z-index-order-wrapper: 90; $z-index-worker-wrapper: 90; $z-index-lnb: 90; $z-index-map-wrapper: 50; // BUTTON // https://vuetifyjs.com/en/api/v-btn/#sass $btn-letter-spacing: 0; // PAGE: map-control $order-wrapper-width: 402px; $worker-wrapper-width: 276px; $order-info-card-width: 440px; $drawer-button-height: 56px; // DIALOG $order-detail-dialog-width: 460;
Result
variables.scss, _mobile.scss, _mixins.scss
파일에 global scss
변수들을 관리하고 있다.src/styles/global.scss
에서 프로젝트 전역에 쓰이는 변수명을 제외한 그밖의 일반 css class/import문을 관리한다.// Component @import '~@/styles/dialog'; @import '~@/styles/button'; @import '~@/styles/textDesignSystem'; * { box-sizing: border-box; ::before { box-sizing: border-box; } ::after { box-sizing: border-box; } } img, embed, object, video { max-width: 100%; } .d-ib { display: inline-block; } .va-m { vertical-align: middle; } // reset vuetify .v-pagination__navigation, .v-pagination__item { box-shadow: none !important; border: 1px solid map.get($grey, 'lighten-2') !important; } // UTIL .wh-nw { white-space: nowrap; } .wh-pl { white-space: pre-line; } .main-content { overflow: auto; border-top: 1px solid map.get($grey, 'lighten-2'); padding: 16px; }
src/plugins/vuetify.ts
에서 src/styles/global.scss
를 import한다.src/plugins/vuetify.ts
import Vuetify from 'vuetify'; import { ko, en } from 'vuetify/lib/locale'; import { mobileScreenMaxWidth } from '@/styles/export.scss'; import '@/styles/global.scss'; //이 부분
header-title-text
css class가 1번만 호출되는 것을 확인할 수 있다.time type \ server type | dev | prod |
before | 88436ms | 110.03s. |
after | 96,237ms | 130.54s. |
Vuetify 최적화
//before import Vuetify from 'vuetify'; import { ko, en } from 'vuetify/lib/locale';
//after import Vuetify from 'vuetify/lib'; import ko from 'vuetify/lib/locale/ko'; import en from 'vuetify/lib/locale/en';
- 유의미한 차이를 발견하지 못했다…
time type \ server type | dev | prod |
before | 88436ms | 110.03s. |
after | 92,895ms | 108.11s. |
before: parsed size 1.67mb
after
최종
되도록 쉬운 이해를 위해 변수는 variable에 모아서 해결하는 방식으로 변경
variables.scss
@import '~vuetify/src/styles/styles.sass'; @import '~@/styles/mobile'; @import '~@/styles/mixins';
vue.config.js
module.exports = { ...getConfig(), css: { extract: false, loaderOptions: { // TODO 모바일 한정이며,추후 모바일 작업 시 scss로 변경 후 삭제 필요 sass: { additionalData: ` @use "sass:map" `, }, scss: { additionalData: ` @use "sass:map"; `, }, }, },
관련 PR link
참고자료
vue-cli docs: https://cli.vuejs.org/guide/css.html
vuetify2 docs: https://v2.vuetifyjs.com/en/features/sass-variables/
css/sass/scss 차이에 관혀여: https://cocoon1787.tistory.com/843
sass-loader additionalData에 관하여: https://imkh.dev/scss-prepand/
vuetify sass 설정에 관하여: https://velog.io/@adc0612/vue-프로젝트-vuetify-sass-설정
Share article