Rollup에서 Vite로의 마이그레이션

정동환·2024년 9월 27일

티맥스 업무일지

목록 보기
6/8
post-thumbnail

1. 서론

현재 사내 코어 팀의 일원으로 일하고 있습니다. 저희 팀은 회사 전체와 그룹사에서 광범위하게 사용되는 공통 라이브러리를 개발하고 있습니다. 이 라이브러리는 다양한 사업 요구사항에 맞춰 지속적으로 업데이트되고 배포되고 있습니다.

저희 팀은 빌드 시간과 HMR(Hot Module Replacement) 시간이 60초 정도로 긴 문제를 겪고 있었습니다. 이 글에서는 이 문제를 어떻게 해결했는지 자세히 설명하고자 합니다.

2. 문제 자세히 살펴보기

2.1 빌드 시간 문제

저희 팀이 겪은 가장 큰 문제는 긴 빌드와 HMR 시간이었습니다. 컴퓨터 사양에 따라 차이가 있었지만, 빠르면 50초, 길면 2-3분까지 빌드 시간이 소요되었습니다. 이는 작은 변경사항을 테스트하는 데에도 상당한 시간이 필요하다는 것을 의미했습니다.

2.2 개발 과정에서의 불편함

저희 팀은 라이브러리를 개발하면서 yarn link를 자주 사용해야 했습니다. 이는 우리 라이브러리를 사용하는 프로젝트나 테스트 프로젝트에 실시간으로 변경사항을 확인하기 위함이었습니다. 하지만 매번 변경사항을 저장할 때마다 전체 빌드가 실행되어 HMR도 빌드 시간만큼 오래 걸렸습니다.

2.3 팀 전체의 생산성 저하

이러한 문제로 인해 저희 팀과 라이브러리를 사용하는 다른 팀들의 작업 시간 중 약 50%가 빌드와 HMR 대기 시간으로 소모되었습니다. 이는 전체적인 개발 생산성을 크게 저하시켰습니다.

3. 원인

3.1 이전 롤업 설정 파일 분석

rollup.config.js

import alias from '@rollup/plugin-alias';
import { babel } from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import url from '@rollup/plugin-url';
import svgr from '@svgr/rollup';
import path from 'path';
import del from 'rollup-plugin-delete';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';
import { terser } from 'rollup-plugin-terser';
import typescript from 'rollup-plugin-typescript2';

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

process.env.BABEL_ENV = 'production';

function setUpRollup({ input, output }) {
  return {
    input,
    output,
    external: ['@wapl/superapp-websocket', /@babel\/runtime/],
    preserveModules: output.format === 'cjs',
    plugins: [
      peerDepsExternal(),
      resolve({
        extensions,
        browser: true,
      }),
      postcss({
        plugins: [],
      }),
      alias({
        entries: {
          '@': path.resolve(__dirname, 'src'),
        },
      }),
      json(),
      commonjs({
        include: /node_modules/,
        extensions: [...extensions, '.js'],
      }),
      typescript({
        useTsconfigDeclarationDir: true,
        tsconfigOverride: {
          exclude: ['**/test/*', '**/*.spec.{ts,tsx}', 'vitest.config.ts', '**/__mocks__/*'],
        },
        clean: true,
        check: false,
      }),
      babel({
        extensions,
        babelHelpers: 'runtime',
        exclude: 'node_modules/**',
        plugins: ['@babel/plugin-transform-runtime'],
      }),
      terser(),
      svgr(),
      url({ limit: 100000000000000000 }),
      del({ targets: 'dist/dts/*' }),
      replace({
        preventAssignment: true,
        'process.browser': true,
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
    ],
  };
}

export default [
  setUpRollup({
    input: './src/index.ts',
    output: [
      {
        dir: 'dist', // 'output.file'을 'output.dir'로 변경
        format: 'cjs',
        entryFileNames: 'index.cjs.js', // 파일 이름을 지정,
      },
      {
        dir: 'dist',
        format: 'esm',
        entryFileNames: 'index.esm.js',
      },
    ],
  }),
];

우리 팀은 기존에 Rollup을 사용하고 있었습니다. Rollup 설정 파일(rollup.config.js)을 자세히 분석한 결과, 여러 플러그인들이 사용되고 있었고, 특히 rollup-plugin-typescript2가 사용되고 있었습니다.

각각의 파일들이 어떤 역할을 하는지는 이 블로그에 자세하게 나와있습니다.

블로그 가장 아래에 나와있듯이 저희 팀에서 사용하고 있던 rollup-plugin-typescript-2는 타입스크립트 일정 버전 이후로 심각하게 느린 이슈가 있었습니다. 관련 이슈

3.2 개선방안

  1. rollup을 그대로 사용하되 rollup-plugin-typescript-2 의존성을 제거
  2. 다른 번들러를 사용하는 방법

1번 방안도 고려해봤지만 저희 팀에 Rollup을 잘 아는 사람이 없기 때문에 기존대로 유지보다 최신 번들러로 교체하는 게 낫다는 판단을 했습니다.

2번 방안에서 여러 번들러들을 검토했습니다. 주요 후보로는 tsup와 Vite가 있었는데, tsup은 CSS가 섞여 있는 경우 사용을 지양하라는 공식 문서의 권고를 고려하여 최종적으로 Vite를 선택했습니다. 또한 커뮤니티의 크기, 업데이트 속도 등도 Vite의 장점이었습니다.

3.3 마이그레이션 과정

packge.json

의존성 파일들이 좀 많은 관계로 마이그레이션 과정과 관련된 의존성들만 남겨두고 생략했습니다. 이번에 마이그레이션을 진행하며 사용하지 않는 의존성들도 함께 제거했습니다.

주요변경사항은 다음과 같습니다.

  • "type": "module" 추가
  • 의존성 파일들 중 불필요한 것들 제거
  • Vite 관련 의존성 추가 (vite, @vitejs/plugin-react, vite-plugin-dts 등)
  • 스크립트 명령어 수정 (예: "start": "vite build --watch", "build": "tsc && vite build")
{
   ...
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "types": "dist/dts/src/index.d.ts",
  "files": [
    "dist/**/*"
  ],
  "dependencies": {
    "@babel/runtime": "^7.24.4",
    ...
    "react-scripts": "5.0.0",
    "rollup-plugin-visualizer": "^5.11.0",
    ...
  },
  "peerDependencies": {
    ...,
    "@emotion/babel-plugin": "^11.7.2",
    "@rollup/plugin-alias": "^3.1.9",
    "@rollup/plugin-babel": "^6.0.3",
    "@rollup/plugin-commonjs": "^23.0.3",
    "@rollup/plugin-json": "^5.0.2",
    "@rollup/plugin-node-resolve": "^13.1.2",
    "@rollup/plugin-replace": "^4.0.0",
    "@rollup/plugin-url": "^8.0.2",
    "@svgr/rollup": "^6.5.1",
    ...
    "@types/styled-components": "^5.1.19",
    "@zerollup/ts-transform-paths": "^1.7.18",
    ...
    "eslint-import-resolver-typescript": "^2.5.0",
    "eslint-plugin-import": "^2.25.4",
    "eslint-plugin-react": "^7.28.0",
    "rollup": "^2.63.0",
    "rollup-plugin-delete": "^2.0.0",
    "rollup-plugin-livereload": "^2.0.5",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-postcss": "^4.0.2",
    "rollup-plugin-serve": "^1.1.0",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-typescript2": "^0.31.1",
    "typescript": "^4.4.2",
  },
  "scripts": {
    "start": "rollup -w -c",
    "build": "rollup -c",
    "prepublishOnly": "yarn && yarn build",
    "format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"",
    "prepare": "husky install",
    "version": "yarn format && git add -A src",
    "postversion": "git push && git push --tags",
    "clean:branch": "sh ./scripts/clean_branch.sh",
    "clean:cache": "rm -rf node_modules && yarn cache clean .",
    "reInstall": "yarn clean:cache && yarn",
  },
  "eslintConfig": {
    "extends": [
      "react-app"
    ]
  },
  "resolutions": {
    "@types/react": "^18.2.20"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "sideEffects": false
}

after

{
  ...
  "type": "module",
  "main": "./dist/index.cjs.js",
  "module": "./dist/index.es.js",
  "types": "./dist/dts/src/index.d.ts",
  "files": [
    "dist"
  ],
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
    "@vitejs/plugin-react": "^4.3.0",
    ...
    "typescript": "^5.4.5",
    "vite": "^5.2.12",
    "vite-plugin-dts": "^3.9.1",
    "vite-tsconfig-paths": "^4.3.2",
    ...
  },
  "scripts": {
    "start": "vite build --watch",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "prepublishOnly": "yarn install && yarn build",
    "prepare": "husky install",
    "clean:branch": "sh ./scripts/clean_branch.sh",
    "clean:cache": "rm -rf node_modules && yarn cache clean",
    "install:clean": "yarn clean:cache && yarn",
    "test": "vitest run",
    "test:watch": "vitest --coverage",
    "test:ui": "vitest --ui --coverage"
  },
  "sideEffects": false
}

vite.config.ts

Rollup 설정을 Vite 설정으로 변환했습니다. 주요 설정 내용은 다음과 같습니다:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import dts from 'vite-plugin-dts';
import path from 'path';
import tsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    dts({
      insertTypesEntry: true,
    }),
    cssInjectedByJsPlugin(),
    tsconfigPaths(),
  ],
  build: {
    lib: {
      entry: 'src/index.ts',
      ...
      fileName: (format) => `index.${format}.js`,
    },
    rollupOptions: {
      external: [
        'react',
        'react-dom',
        '사내_라이브러리_1',
        '사내_라이브러리_2',
        'mobx',
        'mobx-react-lite',
      ],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
    emptyOutDir: true,
  },
});

tsconfig.json

tsconfig.json 파일을 Vite에 맞게 수정했습니다.

before

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "esnext"],
    "outDir": "dist",
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@wapl/ui": ["node_modules/@wapl/ui"],
      "@mui/material": ["node_modules/@mui/material"]
    },
    "declaration": true,
    "declarationDir": "dist/dts",
    "typeRoots": ["@types", "node_modules/@types", "node_modules"], //https://github.com/vitest-dev/vitest/issues/3835
    "plugins": [{ "transform": "@zerollup/ts-transform-paths" }],
    "types": ["vitest/globals"]
  },
  "declarationMap": true,
  "include": [
    "src/**/*",
    ".eslintrc.json",
    "tests/**/*",
    "src/custom.d.ts",
    "vitest.config.ts"
  ],
  "exclude": ["node_modules", "dist"]
}

after

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "module": "ESNext",
    "skipLibCheck": true,

    "moduleResolution": "Node",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    "allowJs": true,
    "esModuleInterop": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

tsconfig.node.json

{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "strict": true
  },
  "include": ["vite.config.ts"]
}

vite-env.d.ts

src내부에 해당 파일을 추가하거나 tsconfig.json 내 compilerOptions.types 옵션에 vite/client를 명시해 줄 수도 있습니다. 링크

/// <reference types="vite/client" />

개선 결과

마이그레이션 후 빌드 시간과 HMR 시간이 크게 개선되었습니다:

빌드 시간

  • 이전: 짧으면 50초, 길면 2,3분

  • 이후: 짧으면 10초, 길면 20초

HMR

  • 이전: 짧으면 50초, 길면 2,3분

  • 이후: 짧으면 2~3초, 길면 10초

결론

Vite로의 번들러 마이그레이션을 통해 저희 팀은 빌드 시간과 HMR 시간을 크게 단축시킬 수 있었습니다. 이는 개발 생산성을 크게 향상시켰으며, 팀원들의 작업 효율성도 높아졌습니다.

이번 경험을 통해 개발 환경 최적화의 중요성을 다시 한 번 깨달았습니다. 기술 스택을 주기적으로 검토하고 필요에 따라 과감히 개선하는 것이 중요함을 알 수 있었습니다.

profile
Software developer

0개의 댓글