Vue로 만든 UI 라이브러리를 Rollup으로 번들링 해본 후기

Sian·2021년 9월 7일
0

SpaceONE FrontEnd 개발

목록 보기
4/6
post-thumbnail

기존에 사용하던 UI 라이브러리(Spaceone Design system)는 vue-cli를 통해 웹팩으로 번들링을 했었는데, 라이브러리를 번들링할 때는 롤업을 사용하는 것이 좋을 것 같아 새롭게 번들링을 시도하였다.

글을 시작하기 전에 우선 결론을 말하자면 번들링 자체는 성공했지만, 이를 콘솔에 적용하는 것은 어려웠고 한 엔트리만을 사용한 번들링이었기에 기존 웹팩 빌드와 큰 차이가 없어서(빌드 시간에는 유의미한 차이가 있었다!) 롤업 번들링을 적용하지 않도록 결정하였다.
하지만 롤업 번들링을 경험하고, 트러블 슈팅했던 과정 자체는 유의미했기 때문에 후기를 작성한다.

롤업 번들링을 위한 절차

  1. 어떤 loader나 plugin들이 필요한 지 확인하고 rollup과 함께 설치
  2. rollup.config 작성
  3. package.json 수정
  4. build 명령어를 통한 번들링

1. 번들링에 필요한 플러그인/패키지 📚

- rollup
- rollup-plugin-vue
- rollup-plugin-typescript2
- @rollup/plugin-alias
- @rollup/plugin-node-resolve : 외부 노드 모듈을 사용 시 (node_modules 디렉토리)
- @rollup/plugin-commonjs : 외부 노드 모듈이 es6 으로 변환되지 않았을 경우 es6 으로 변환
- rollup-plugin-postcss : postcss 사용 시 
- postcss-import
- @rollup/plugin-json: json 사용 시 (i18n 등)
- @rollup/plugin-replace: NODE_ENV 등을 replace할 때
- rollup-plugin-terser: 번들을 minify할 때 
- @rollup/plugin-image: JPG, PNG, GIF, SVG, and WebP files import 시 
- @rollup/plugin-dynamic-import-vars: lazy loading을 하는 컴포넌트에 적용

적어놓고 보니 상당히 많은 플러그인과 패키지가 필요한데, postcss나 dynamic import 등의 여러 조건들이 있기 때문이고 더 규모가 작은 프로젝트에 적용한다면 꼭 필요한 플러그인들만 설치하는 것이 좋다.

2. config 작성 ⚙️

export default {
    input: './src/index.ts',
   output: [
        {
            name: 'spaceone-design-system',
            format: 'umd',
            file: pkg.unpkg,
            globals: {
                vue: 'Vue',
                '@vue/composition-api': 'VueCompositionApi',
                'vue-i18n': 'VueI18n',
                'vue-svgicon': 'icon',
                'v-tooltip': 'VTooltip',
                'vue-notification': 'Notifications',
            },
            inlineDynamicImports: true,
            exports: 'named',
        }],
    external: ['@vue/composition-api',
        'vue-i18n',
        'vue-svgicon',
        'velocity-animate',
        'vue-notification',
        'vue-fragment',
        'v-tooltip', 'vue'],
    plugins: [
        alias({ entries: [{ find: '@', replacement: `${__dirname}/src/` }] }),
        vue({
            css: false, // Dynamically inject css as a <style> tag
            //css를 false로 두고, 아래 postcss로 처리
            compileTemplate: true, // Explicitly convert template to render function
            preprocessStyles: true,
            preprocessOptions: {
                sass: { // sass 적용
                    includePaths: ['./node_modules'],
                },
            },
            transformAssetUrls: true,
        }),
        // [Rollup Plugin Vue](https://rollup-plugin-vue.vuejs.org/)
        postcss({
            extract: true,
            config: {
                path: './postcss.config', //postcss config 작성 path
            },
            plugins: [cssimport(), autoprefixer(), tailwind()],
        }),
        json(),
        image(),
        terser(), // minifies generated bundles
        commonjs(),
        nodeResolve(),
        multi(),
        typescript({
            tsconfig: 'tsconfig.json',
        }),

    ],

};

초기에 작성했던 rollup config 파일이다.

이 config로 빌드를 하며 겪었던 트러블 슈팅에 대해 몇 가지 적어보고자 한다.

💥 css/postcss 관련 이슈

빌드를 할 때 css 부분에서 unexpected token 이슈가 지속적으로 발생한다.
이는 여러 가지 이유가 있는데, 우선 postcss의 @import 문법을 다루기 위해 postcss-import를 설치하고, 해당 플러그인을 postcss 플러그인 옵션에 넣어준다.

또한 vue의 css는 false로 처리해야 postcss 플러그인의 옵션들을 사용할 수 있다.

웹팩에서는 postcss의 option 내부에 sass loader로 설정해주었는데, 이를 같은 방식으로 use:['sass'] 와 같이 postcss option에 설정해주었더니 unexpected token 에러(css를 해석하지 못함)가 지속적으로 발생하여 vue 플러그인에 아래와 같이 작성해주었다.

 preprocessOptions: {
	sass: {
		includePaths: ['./node_modules'],
	},
},

또한 옵션에 대해 잘 파악하지 못하고, 다른 기술블로그들을 참고해서 postcss의 내부에

        	extract: false,
            config: { ..생략
            }
            modules: true,
            use: ['sass'],
            plugins: [autoprefixer()],
            writeDefinitions: true,
            namedExports: true,

이렇게 중구난방으로 옵션들을 작성했었는데, 이렇게 작성하니 error: TypeError: node.getIterator is not a function 와 같은 에러가 지속적으로 발생하였다.

우선 잘 알아보면, writeDefinitions는 모든 처리된 css 파일에 대해 .css.d.ts 파일을 만들어주는 것으로, extract: true와 함께 사용되어야 한다.(너무 당연하게도..)
그런데 위에 작성한 것을 보면 extract: false로 되어있었기에 이를 수정하고 css.d.ts 옵션은 필요하지 않다고 생각하여 제거했더니 이슈가 해결되었다.

💥 typescript/json 관련 이슈

typescript 플러그인을 쓸 때, commonjs는 불가능하고, 해당 플러그인은 es2015와 esnext만 가능하다. 이는 commonjs로 module 부분을 작성하면 터미널에서 확인할 수 있다.
json을 사용하고 싶을 때는 json plugin을 설치해주고, json()을 plugin 부분에 넣어주어야 한다.

💥 externals와 globals

externals는 작성하고, globals는 작성하지 않으면
(!) Missing global variable names
위와 같은 warning이 발생한다. 따라서 externals와 globals를 동일하게 작성해줄 수 있도록 한다.

💥 lazy loading 관련 이슈

rollup의 inlineDynamicImports(Create single bundle when using dynamic imports)만 사용하면 lazy loading이 가능할 것이라고 생각했지만, 실제로 번들링 결과 lazy loading이 적용되지 않았다.

예시 상황은 아래와 같다.

const lottieFile = await import(`./p-lotties/${props.name}.json`);

이런 식으로 await import를 해서 로티나 가변적인 레이아웃을 적용하는 코드가 있는데, 이걸 롤업으로 번들링하면

const r=await import(`./p-lotties/${e.name}.json`);

결과물이 이렇게 떨어지고, 번들링된 결과물을 콘솔에서 적용해보면

These relative modules were not found:

  • ./p-lotties in ./node_modules/@spaceone/design-system/lib/spaceone-design-system.esm.js

당연하게 path를 찾지 못하는 이슈가 발생한다.
이를 처리하기 위해 @rollup/plugin-dynamic-import-vars라는 플러그인을 설치했고, 해당 플러그인을 plugins 부분에 작성해준다. (typescript와 commonjs 뒤에 작성해야 동작한다.)
번들링된 파일을 열어보면 variableDynamicImportRuntime0$1(path) 이런식으로 바뀌어 있다.

위의 rollup config가 다소 정돈되어 있지 않아보일 뿐더러 umd가 아닌 다른 모듈도 조금 더 쉽게 config를 바꾸고 번들링할 수 있도록 하기 위해 최종적으로는 아래와 같이 config를 작성하였다. (https://dev.to/shubhadip/vue-3-component-library-270p 참고)

import vue from 'rollup-plugin-vue';
import commonjs from '@rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
import resolve from '@rollup/plugin-node-resolve';
import multi from '@rollup/plugin-multi-entry';
import alias from '@rollup/plugin-alias';
import image from '@rollup/plugin-image';
import postcss from 'rollup-plugin-postcss';
import cssimport from 'postcss-import';
import json from '@rollup/plugin-json';
import autoprefixer from 'autoprefixer';
import tailwind from 'tailwindcss';
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import nodePolyfills from 'rollup-plugin-polyfill-node';
import replace from '@rollup/plugin-replace';
import pkg from './package.json';

const config = {
    plugins: {
        preVue: [
            alias({
                entries: [{ find: '@', replacement: `${__dirname}/src/` }],
                customResolver: resolve({
                    extensions: ['.js', '.jsx', '.vue'],
                }),
            }),
            image(),
        ],
        replace: {
            'process.env.NODE_ENV': JSON.stringify('production'),
            __VUE_OPTIONS_API__: JSON.stringify(true),
            __VUE_PROD_DEVTOOLS__: JSON.stringify(false),
        },
        vue: {
            css: false, // Dynamically inject css as a <style> tag
            compileTemplate: true, // Explicitly convert template to render function
            preprocessStyles: true,
            style: {
                postcssCleanOptions: {
                    disable: true,
                },
                preprocessOptions: {
                    sass: {
                        includePaths: ['./node_modules'],
                    },
                },
            },
            template: {
                // isProduction: true,
                transformAssetUrls: true,
            },
        },
        postVue: [
            resolve({
                extensions: ['.js', '.jsx', '.ts', '.vue', '.svg'],
            }),
            // [Rollup Plugin Vue](https://rollup-plugin-vue.vuejs.org/)
            postcss({
                extract: true,
                config: {
                    path: './postcss.config',
                },
                plugins: [cssimport(), autoprefixer(), tailwind()],
            }),
        ],
    },
};

export default {
    input: './src/index.ts',
    output: [
        {
            name: 'spaceone-design-system',
            format: 'umd',
            file: pkg.unpkg,
            globals: {
                vue: 'Vue',
                '@vue/composition-api': 'VueCompositionApi',
                'vue-i18n': 'VueI18n',
                'vue-svgicon': 'icon',
                'v-tooltip': 'VTooltip',
                'vue-notification': 'Notifications',
            },
            inlineDynamicImports: true,
            exports: 'named',
        }],
    external: ['@vue/composition-api',
        'vue-i18n',
        'vue-svgicon',
        'velocity-animate',
        'vue-notification',
        'vue-fragment',
        'v-tooltip', 'vue', '@babel/runtime'],
    plugins: [
        json(),
        // nodeResolve({ extensions: ['.js', '.ts', '.svg'] }),
        replace(config.plugins.replace),
        ...config.plugins.preVue,
        vue(config.plugins.vue),
        ...config.plugins.postVue,
        commonjs(),
        typescript({
            tsconfig: 'tsconfig.json',
        }),
        // terser(), // minifies generated bundles
        nodePolyfills(),
        dynamicImportVars(),
        multi(),
    ],
};

preVue 단계에서 alias나 replace로 path와 환경변수를 바꿔주고, vue에서 필요한 플러그인들을 적용하고, postVue 단계에서 postcss 등의 처리를 해준다.

package.json 수정 🛠

  "main": "dist/spaceone-design-system.umd.js",
  "typings": "lib/index.d.ts",
  "module": "lib/spaceone-design-system.esm.js",
  "unpkg": "lib/spaceone-design-system.umd.js",
  
  "scripts": {
  		...
        "build:rollup": "NODE_ENV=production rollup --config rollup.config.js"
  }

이렇게 수정해주고 build:rollup 명령어를 통해 번들링한다. (main은 보통 esm이나 cjs로 많이 작성하는 것이 좋다.)

결과 ✨


이런 긴 과정을 거치면 이렇게 번들링 된 파일이 떨어진다.


기존에 걸리던 빌드 시간이 3분이 넘던 것에 대비해 30~1분 내외로 빌드를 할 수 있게 되었다!

또한 번들 사이즈도
이렇게 절반 가량 감소하였다.

빌드 시간도, 번들 사이즈도 획기적으로 줄어들었지만 롤업을 선택하지 못한 이유는 두 가지가 있다.
1. rollup-vue-plugin 버전
2. postcss 관련 이슈

번들링된 결과물을 적용하면

이런 에러가 찍힌다. 이걸 구글에 검색해 본 결과,

  • The problem is that you're using Vue 2, but the Vue transformer supports only Vue 3. Please update to Vue 3 and everything should work fine.
  • Confirming that the latest release (3.0.0-alpha.10) solved this. Thank you for the quick response!
  • Component versions 2+ use vue 3. I'm not sure older versions will be supported, so you should upgrade to vue 3 if you can.

위와 같이 vue3에서 해결되었다는 얘기들이 나온다.(깃헙 이슈 등 참고)
그렇다면 vue2를 쓰는 플러그인에서 왜

const _withId$c = /*#__PURE__*/Vue$1.withScopeId("data-v-06a80b9f");

이런 코드가 나왔으며, 왜 동작하지 않을까?

이 에러가 나온 곳을 확인했더니, vue-notification이라는 vue2 전용 라이브러리에서 해당 에러를 발생시키는 것을 확인하였다. 이는 composition api에서는 삭제된 compiler 속성을 사용하는 것으로 추정된다.
또한 scopeId <- 라는 부분에 착안하여, css들이 scoped를 가진 것들을 모두 제거했더니 해당 에러가 사라졌다.

이런 여러 트러블슈팅 과정을 거치면서, 과연 이렇게 생태계가 작고 유지보수가 잘 되지않는(공식에서 밀어주는 웹팩 or vite와 비교해서) 번들링을 하는 게 맞을까? 라는 고민이 들었다. 기존에도 웹팩으로 모두 빌드하고 있었고, 지금 당장은 잘 돌아갈지라도 계속적인 문제 상황이 생길 때 이렇게 버전 이슈가 많고 대응해야 하는 게 많다면 유지보수를 할 수 있을까?
구글에 전혀 나오지 않는 내용들을 빌드된 파일 내부 코드를 다 뒤지고, github issue들을 모두 읽어보고, 공개된 코드들을 다 읽어보며 트러블슈팅하는 게 앞으로도 유지보수가 가능한 걸까? 🤔

또한, vue3에 breaking change들이 너무 많은 시점에 이런 vue2/composition api에 종속된 환경에 적합하게 적용해놓은 번들링을 했다가 vue3 마이그레이션 할 때 또 하나의 레거시를 양산하는 것이 아닐까 고민이 되었다.

그래서 우선은 PoC로 남겨두고, vue3로 마이그레이션을 먼저 진행한 후 번들링을 하거나 아니면 vue3에서 공식적으로 가장 밀어주는 vite를 사용해보거나 해야겠다는 생각이 들었다.

하지만 조금 더 트러블슈팅을 해보긴 할 것 같다.. 😢

0개의 댓글