[Vue + Vuetify] Optimization CSS Bundling

Jan 02, 2024
[Vue + Vuetify] Optimization CSS Bundling

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, SCSSCSS 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도 사용되고 있으므로 sassscss를 분리해서 설정을 적용해주었다.
      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` 구문을 별도로 타겟팅할 수 있습니다.
       
    • Vuetify를 사용하면 src/styles 폴더 안에 variables.scss 파일에 정의된 변수들을 프로젝트 전체에 자동으로 적용해준다.
    • HCR styles 폴더 구조
    • notion image
      If you have not installed Vuetify, check out the quick-start guide. Once installed, create a folder called sassscss or styles in your src directory with a file named variables.scss or variables.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하면 스타일이 대량으로 중복될 수 있다. 컴포넌트 개수 만큼 실행되기 때문에 변수에 대해서만 정의해야한다.
      • notion image
      • 기존 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

      • 현재 HCR에서는 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번만 호출되는 것을 확인할 수 있다.
    • build time 비교
      • 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';
       
    • build time 비교
      • 유의미한 차이를 발견하지 못했다…
      time type \ server type
      dev
      prod
      before
      88436ms
      110.03s.
      after
      92,895ms
      108.11s.
      before: parsed size 1.67mb
      notion image
      after
      notion image

      최종

      되도록 쉬운 이해를 위해 변수는 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"; `, }, }, },
       

      참고자료

      css/sass/scss 차이에 관혀여: https://cocoon1787.tistory.com/843
      sass-loader additionalData에 관하여: https://imkh.dev/scss-prepand/
       
Share article

veganee