회고

Vite 마이그레이션 회고

[FE] Lighthouse 2024. 7. 2. 15:21

들어가며

보통의 리팩토링 작업은 그다지 큰 단위의 작업이 아니기에 회고를 작성하지는 않지만 이번 Vite 마이그레이션 작업은 정말 크고 중요한 리팩토링이자 리팩토링의 최종 목표였기 때문에 회고 글을 작성한다.
 
4월 중순에 팀에 합류하고 프로젝트를 살펴보고 Vite로 마이그레이션 하면 좋겠다 라는 생각을 했다. 더불어 React와 TypeScript 버전도 최신 버전으로 버전 관리를 해야겠다는 생각도 들었다. 그 이유는 다음과 같았다.

  1. 너무 오래 걸리는 개발 서버 구동시간
  2. 오래 걸리는 빌드 시간
  3. 그 당시 최적화는 됐지만 eject된 CRA는 버전 관리가 어렵고 프로젝트의 복잡도만 늘어난다.
  4. 메인 라이브러리인 React와 TypeScript의 버전 관리가 안돼서 서드 파티 라이브러리의 버전 관리 또한 안됨. 이럴수록 프로젝트는 레거시가 된다.

합류 후 프로젝트 코드를 살펴보고 이런 저런 불편함을 느꼈기 때문에 5월 10일에 Vite 마이그레이션이 가능한지 테스트 PoC를 진행했다. 그 당시는 라이브러리 정리도, React와 TypeScript 버전도 관리가 안되어 있었기 때문에 불가능이라는 생각을 가지고 진행을 했었고, 추후 Vite 마이그레이션을 위해 필요한 액션 아이템을 리스트 업 해보자 라는 생각으로 진행을 했다.
 
테스트를 진행해본 결과 당장 리스트 업 된 액션 아이템은 다음과 같았다.

  1. 죽은 코드, 파일, 라이브러리 제거 (사용하지 않는 코드)
    - 사용하지 않는 코드와 파일, 라이브러리는 커밋 당시에 제거를 해야 한다고 생각하지만 프로젝트가 정리가 안되어 있었기 때문에 반드시 정리가 필요. 또한 사용하지 않는 라이브러리는 버전 관리가 안되기 때문에 peer-dependency로 인해 마이그레이션에 걸림돌이 될 확률이 높음.
  2. react-data-export 라이브러리 제거
    - 프로젝트에서 Excel 추출을 프론트엔드에서 하고 있었는데 react-data-export 라는 라이브러리를 사용하고 있었음. 해당 라이브러리는 4년전에 유지보수가 멈춘 레거시.
  3. 프로젝트 내 jQuery를 사용하고 있는 부분 대체
    - 프로젝트의 VMConsole 기능을 위해 VMWare에서 제공해주는 SDK를 사용하고 있는데 해당 SDK에서 jQuery, jQuery-ui를 사용하고 있음. jQuery 라이브러리 자체를 프로젝트 src 디렉토리 하위에서 관리하고 있어서 번들링 될 때 해당 라이브러리도 함께 번들링 되고 있음.
  4. CommonJS 사용하는 부분 대체
    - Vite 6버전 부터는 CJS를 지원하지 않기에 CJS를 사용하고 있는 컴포넌트를 반드시 리팩토링 해야 함.
  5. react-lodable 라이브러리 대체
    - 코드 스플리팅을 위해 react-lodable 라이브러리를 사용하고 있음. 해당 라이브러리 또한 4년 전 유지보수가 멈춘 라이브러리임. React.lazy API를 사용해 코드 스플리팅을 진행하는 것으로 변경
  6. React, TypeScript 버전 업그레이드
    - 최신 라이브러리 기능을 사용하기 위해서 반드시 관리되어야 함

테스트 결과

죽은 코드, 파일, 라이브러리 제거

먼저 죽은 코드와 파일을 제거해주는 일은 간단했다. 지금 사용하지 않는 코드 (어디에서도 참조하고 있지 않은 코드)는 추후에도 사용하지 않을 확률이 높다. 만약 추후에 사용해야 한다면 깃으로 히스토리 관리가 되기 때문에 쉽게 복구할 수 있으니 반드시 제거해주자.
 
라이브러리 또한 마찬가지이다. 지금 사용하지 않는 라이브러리는 버전 관리가 안되기 때문에 프로젝트에 방해만 된다. 온보딩 기간에 사용하지 않는 라이브러리를 리스트 업 했었고 유지보수가 멈춘 라이브러리와 사용하지 않는 라이브러리를 마이그레이션 과정에서 정리를 했다. 그 결과 최종적으로 약 60개 정도의 라이브러리를 정리한 것을 확인할 수 있다.

// 이전 디펜던시
   "dependencies": {
      "@ag-grid-community/client-side-row-model": "23.2.1",
      "@ag-grid-community/core": "23.2.1",
      "@emotion/core": "^11.0.0",
      "@emotion/react": "^11.8.1",
      "@emotion/styled": "^11.8.1",
      "@hookform/resolvers": "^3.3.4",
      "@material-ui/core": "^4.11.0",
      "@material-ui/lab": "^4.0.0-alpha.56",
      "@monaco-editor/react": "^4.4.1",
      "@react-dnd/invariant": "^3.0.0",
      "@toast-ui/editor": "^2.5.2",
      "@toast-ui/react-editor": "^2.5.2",
      "@types/d3": "^7.4.0",
      "@types/d3-hexbin": "^0.2.3",
      "ag-grid-community": "23.2.1",
      "ag-grid-react": "^23.2.1",
      "apexcharts": "3.35.4",
      "axios": "^0.22.0",
      "chroma-js": "^2.4.2",
      "classnames": "^2.3.1",
      "codemirror": "^5.63.1",
      "core-js": "^3.21.1",
      "corepack": "^0.14.0",
      "crypto-browserify": "^3.12.0",
      "crypto-js": "^4.0.0",
      "cytoscape": "^3.23.0",
      "cytoscape-canvas": "^3.0.1",
      "cytoscape-context-menus": "^4.1.0",
      "cytoscape-cxtmenu": "^3.4.0",
      "cytoscape-edgehandles": "^4.0.1",
      "cytoscape-grid-guide": "^2.3.3",
      "cytoscape-node-html-label": "^1.2.2",
      "cytoscape-popper": "^2.0.0",
      "d3": "^7.6.1",
      "d3-force-3d": "^3.0.2",
      "d3-hexbin": "^0.2.2",
      "date-fns": "^2.16.1",
      "dayjs": "^1.11.0",
      "dotenv": "^8.2.0",
      "dotenv-webpack": "^8.0.0",
      "env-cmd": "^10.1.0",
      "faker": "^4.1.0",
      "file-selector": "^0.6.0",
      "fxjs": "^0.21.2",
      "he": "^1.2.0",
      "html-react-parser": "^0.13.0",
      "html2canvas": "^1.0.0-rc.7",
      "immer": "^9.0.12",
      "ip": "^1.1.5",
      "ip-utils": "^2.4.0",
      "iput": "^1.1.2",
      "is-cidr": "^4.0.2",
      "jquery": "^3.6.3",
      "jquery-ui": "^1.13.2",
      "jsencrypt": "^3.2.1",
      "jspdf": "^2.3.1",
      "lodash": "^4.17.21",
      "monaco-editor": "^0.33.0",
      "mqtt": "^4.3.7",
      "net": "^1.0.2",
      "objectorarray": "^1.0.5",
      "outqource-components": "^1.0.11",
      "outqource-redux": "^1.0.2",
      "pnp-webpack-plugin": "^1.7.0",
      "prop-types": "^15.8.1",
      "qs": "^6.9.4",
      "query-string": "^7.1.1",
      "react": "^16.13.1",
      "react-apexcharts": "^1.3.7",
      "react-color": "^2.19.3",
      "react-custom-scrollbars-2": "^4.4.0",
      "react-data-export": "^0.6.0",
      "react-datepicker": "^3.4.1",
      "react-dnd": "^11.1.3",
      "react-dnd-html5-backend": "^11.1.3",
      "react-dom": "^16.13.1",
      "react-drag-sizing": "^0.1.0-alpha.1",
      "react-draggable": "^4.4.4",
      "react-dropzone": "^12.0.4",
      "react-error-boundary": "^3.1.4",
      "react-force-graph-2d": "^1.23.11",
      "react-grid-layout": "1.3.4",
      "react-helmet": "^6.1.0",
      "react-hook-form": "^7.51.0",
      "react-idle-timer": "^4.6.2",
      "react-intl": "^5.4.6",
      "react-is": "^17.0.2",
      "react-lazylog": "^4.5.3",
      "react-loadable": "^5.5.0",
      "react-modal": "^3.14.4",
      "react-multi-toggle": "^1.1.0",
      "react-popper-tooltip": "^4.3.1",
      "react-qr-code": "^2.0.7",
      "react-query": "^3.34.19",
      "react-resizable": "^3.0.4",
      "react-router": "5.2.1",
      "react-router-dom": "6.8",
      "react-select": "^5.7.3",
      "react-slick": "^0.28.1",
      "react-text-mask": "^5.4.3",
      "react-toastify": "^6.0.8",
      "react-tooltip": "^4.2.21",
      "react-use": "^17.3.2",
      "recharts": "^1.8.5",
      "regenerator-runtime": "^0.13.9",
      "semver": "^7.3.8",
      "shortid": "^2.2.16",
      "slick-carousel": "^1.8.1",
      "stompjs": "^2.3.3",
      "styled-components": "^5.3.3",
      "sweetalert2": "^9.17.0",
      "swiper": "9.4.1",
      "tippy.js": "^6.3.7",
      "tslib": "^2.4.0",
      "validator": "^13.7.0",
      "vis-react": "^0.5.1",
      "xterm": "^4.18.0",
      "xterm-addon-attach": "^0.6.0",
      "yaml": "^1.10.2",
      "yup": "^1.4.0",
      "zustand": "^3.7.1"
   },
   "devDependencies": {
      "@babel/core": "^7.10.5",
      "@babel/preset-env": "^7.10.4",
      "@babel/preset-react": "^7.10.4",
      "@babel/preset-typescript": "^7.10.4",
      "@babel/runtime": "^7.17.9",
      "@types/chroma-js": "^2.1.0",
      "@types/cytoscape": "^3.19.9",
      "@types/cytoscape-canvas": "^3.0.0",
      "@types/cytoscape-context-menus": "^4.1.0",
      "@types/cytoscape-edgehandles": "^4.0.0",
      "@types/faker": "^4.1.12",
      "@types/lodash": "^4.14.158",
      "@types/qs": "^6.9.4",
      "@types/query-string": "^6.3.0",
      "@types/react": "^16.9.43",
      "@types/react-dom": "^16.9.8",
      "@types/react-helmet": "^6.0.0",
      "@types/react-loadable": "^5.5.6",
      "@types/react-router-dom": "5.3.3",
      "@types/recharts": "^1.8.14",
      "@types/redux-logger": "^3.0.9",
      "@types/shortid": "^0.0.29",
      "@types/stompjs": "^2.3.5",
      "@typescript-eslint/eslint-plugin": "^5.0.0",
      "@typescript-eslint/parser": "^5.0.0",
      "babel-loader": "^8.1.0",
      "clean-webpack-plugin": "^3.0.0",
      "connect-api-mocker": "^1.10.0",
      "copy-webpack-plugin": "^6.0.3",
      "css-loader": "^3.6.0",
      "eslint": "^8.21.0",
      "eslint-plugin-react": "^7.26.1",
      "eslint-plugin-simple-import-sort": "^12.1.0",
      "file-loader": "^6.0.0",
      "fork-ts-checker-webpack-plugin": "^5.0.9",
      "html-webpack-plugin": "^4.3.0",
      "mini-css-extract-plugin": "^0.9.0",
      "optimize-css-assets-webpack-plugin": "^6.0.0",
      "preload-webpack-plugin": "3.0.0-beta.3",
      "prettier": "^2.5.1",
      "resolve-url-loader": "^5.0.0",
      "sass": "^1.42.1",
      "sass-loader": "10.2.0",
      "source-map-loader": "^1.0.1",
      "style-loader": "^1.2.1",
      "terser-webpack-plugin": "^4.2.3",
      "ts-loader": "^8.0.1",
      "typescript": "^3.9.7",
      "typescript-plugin-css-modules": "^3.4.0",
      "url-loader": "^4.1.1",
      "webpack": "^4.43.0",
      "webpack-bundle-analyzer": "^4.5.0",
      "webpack-cli": "^3.3.12",
      "webpack-dev-server": "^3.11.0",
      "webpack-merge": "^5.7.3",
      "xlsx": "^0.18.2"
   },
// 이후 디펜던시
   "dependencies": {
      "@ag-grid-community/client-side-row-model": "23.2.1",
      "@ag-grid-community/core": "23.2.1",
      "@hookform/resolvers": "^3.6.0",
      "@monaco-editor/react": "^4.6.0",
      "@react-dnd/invariant": "^3.0.1",
      "@toast-ui/editor": "^2.5.4",
      "@toast-ui/react-editor": "^2.5.4",
      "ag-grid-community": "23.2.1",
      "ag-grid-react": "^23.2.1",
      "apexcharts": "^3.49.2",
      "axios": "^0.22.0",
      "chroma-js": "^2.4.2",
      "classnames": "^2.5.1",
      "codemirror": "^5.65.16",
      "core-js": "^3.37.1",
      "corepack": "^0.14.2",
      "crypto-js": "^4.2.0",
      "cytoscape": "^3.29.2",
      "cytoscape-canvas": "^3.0.1",
      "cytoscape-context-menus": "^4.1.0",
      "cytoscape-cxtmenu": "^3.5.0",
      "cytoscape-edgehandles": "^4.0.1",
      "cytoscape-grid-guide": "^2.3.3",
      "cytoscape-node-html-label": "^1.2.2",
      "cytoscape-popper": "^2.0.0",
      "d3": "^7.9.0",
      "d3-force-3d": "^3.0.5",
      "d3-hexbin": "^0.2.2",
      "date-fns": "^2.30.0",
      "dayjs": "^1.11.11",
      "exceljs": "^4.4.0",
      "html-react-parser": "^0.13.0",
      "html2canvas": "^1.4.1",
      "immer": "^9.0.21",
      "ip": "^1.1.9",
      "ip-utils": "^2.4.0",
      "is-cidr": "^4.0.2",
      "jsencrypt": "^3.3.2",
      "jspdf": "^2.5.1",
      "lodash": "^4.17.21",
      "mqtt": "^5.7.3",
      "qs": "^6.12.1",
      "react": "^18.3.1",
      "react-apexcharts": "^1.4.1",
      "react-color": "^2.19.3",
      "react-custom-scrollbars-2": "^4.5.0",
      "react-datepicker": "^3.8.0",
      "react-dnd": "^16.0.1",
      "react-dnd-html5-backend": "^16.0.1",
      "react-dom": "^18.3.1",
      "react-drag-sizing": "^0.2.1",
      "react-draggable": "^4.4.6",
      "react-dropzone": "^12.1.0",
      "react-error-boundary": "^3.1.4",
      "react-grid-layout": "1.3.4",
      "react-hook-form": "^7.52.0",
      "react-idle-timer": "^4.6.4",
      "react-intl": "^5.25.1",
      "react-lazylog": "^4.5.3",
      "react-modal": "^3.16.1",
      "react-popper-tooltip": "^4.4.2",
      "react-qr-code": "^2.0.15",
      "react-query": "^3.34.19",
      "react-resizable": "^3.0.5",
      "react-router": "^6.24.0",
      "react-router-dom": "^6.24.0",
      "react-select": "^5.8.0",
      "react-slick": "^0.28.1",
      "react-text-mask": "^5.5.0",
      "react-toastify": "^6.2.0",
      "react-tooltip": "^4.5.1",
      "react-use": "^17.5.0",
      "recharts": "^2.12.7",
      "semver": "^7.6.2",
      "shortid": "^2.2.16",
      "slick-carousel": "^1.8.1",
      "sweetalert2": "^9.17.4",
      "swiper": "9.4.1",
      "tippy.js": "^6.3.7",
      "vis-react": "^0.5.1",
      "xterm": "^4.19.0",
      "xterm-addon-attach": "^0.6.0",
      "yaml": "^1.10.2",
      "yup": "^1.4.0",
      "zod": "^3.23.8",
      "zustand": "^3.7.2"
   },
   "devDependencies": {
      "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
      "@types/chroma-js": "^2.4.4",
      "@types/cytoscape": "^3.21.4",
      "@types/cytoscape-canvas": "^3.0.3",
      "@types/cytoscape-context-menus": "^4.1.4",
      "@types/cytoscape-edgehandles": "^4.0.4",
      "@types/d3": "^7.4.3",
      "@types/d3-hexbin": "^0.2.5",
      "@types/exceljs": "^1.3.0",
      "@types/lodash": "^4.17.5",
      "@types/qs": "^6.9.15",
      "@types/react": "^18.3.3",
      "@types/react-dom": "^18.3.0",
      "@types/react-modal": "^3.16.3",
      "@types/react-router-dom": "5.3.3",
      "@types/recharts": "^1.8.29",
      "@types/shortid": "^0.0.29",
      "@typescript-eslint/eslint-plugin": "^5.62.0",
      "@typescript-eslint/parser": "^5.62.0",
      "@vitejs/plugin-react": "^4.3.1",
      "esbuild-plugin-react-virtualized": "^1.0.4",
      "eslint": "^8.57.0",
      "eslint-plugin-react": "^7.34.3",
      "eslint-plugin-simple-import-sort": "^12.1.0",
      "jquery": "^3.7.1",
      "prettier": "^2.8.8",
      "sass": "^1.77.6",
      "sass-loader": "14.2.1",
      "typescript": "5.4.5",
      "vite": "^5.3.1",
      "vite-plugin-checker": "^0.7.0",
      "vite-plugin-svgr": "^4.2.0",
      "vite-tsconfig-paths": "^4.3.2"
   },

react-data-export 대체

위에서 언급했듯이 react-data-export 라이브러리는 유지보수가 멈춘 라이브러리다. 또한 exceljs를 활용해 엑셀을 커스텀 할 수 있으면 좋겠다라는 요구사항이 있어 exceljs를 사용하는 방식으로 프로젝트를 리팩토링 했다. 해당 과정은 아래에 문서 링크를 남겨놓겠다.
[React]ExcelJS 커스텀 훅으로 사용하기
 

프로젝트 내 jQuery를 사용하고 있는 부분 대체

사실 프로젝트에서 가장 흉측한 부분이라고 생각이 들면서도 겁이 났던 액션 아이템이였다. jQuery는 2020년에 코딩을 처음 배울 때 접했던 라이브러리인데 React내부에 존재하고 있다는게 아리송 했다. 하지만 레거시에도 이유가 있는 법이다. 프로젝트의 VMConsole 기능에서 VMWare가 제공하는 SDK를 사용하고 있는데 해당 SDK가 jQuery, jQuery-ui를 사용하고 있기 때문이다. 보통은 jQuery를 사용하기 위해 CDN을 이용한다. 하지만 우리 프로젝트의 특성 상 솔루션이 폐쇄망에 설치 되기 때문에 CDN을 이용할 수 없었고 빌드 타임에 라이브러리가 함께 번들링 되어야 했다.
 
처음 적용하고자 했던 방법은 jQuery를 디펜던시로 내려받아서 사용하는 것 이었다. 하지만 이 방법은 개발 서버에서는 적용이 됐지만 빌드 후 프로덕션에서 적용이 안되는 문제가 발생했다. 이 방법을 해결하기 위해 Vite의 정적 에셋들은 public 폴더 하위에서 관리된다는 점을 참고해 jQuery, jQuery-ui min 파일을 public/assets/js 하위에 위치한 후 index.html에서 스크립트 태그를 활용해 주입하는 방식으로 해결을 했다. 이 방식을 활용하니 window객체에 jQuery가 잘 주입이 되어 해당 기능이 문제없이 동작하는 것을 확인했다.

CommonJS 사용하는 부분 대체

CJS를 대체하는 일도 쉬운 일은 아니였다. 서드파티 라이브러리에서 ESM형태로 export를 지원하지 않으면 해결하기 어려운 문제이기 때문이다. 우리 프로젝트 내에 특정 한 라이브러리가 CJS만 지원해 문제가 있었지만 이 부분만 ESLint 규칙을 무시하는 방식으로 에러를 일단락 했다. 지금 당장 문제가 있는 것은 아니지만 추후에 문제가 발생할 소지가 있기 때문에 지속해서 관리가 필요할 것 같다.

react-lodable 라이브러리 대체

위 라이브러리 또한 유지 보수가 멈춘 라이브러리다. 반드시 대체가 됐어야 했고, React의 lazy API를 활용해 코드 스플리팅을 대체했다. React 18버전 부터는 Suspense가 상당히 개선됐기 때문에 코드 스플리팅 또한 상당히 안정성 있지 않을까 싶다.

React, TypeScript 버전 업그레이드

프로젝트 내 메인 라이브러리의 버전 관리는 필수이다. React 프로젝트 특성 상 반드시 서드파티 라이브러리를 사용하게 되는데, 메인 라이브러리의 버전 관리가 안된다면 peer-dependency로 인해 서드파티 라이브러리의 버전 또한 관리가 어렵다. package파일 내에 resolutions을 활용해 특정 버전을 고정 시킬 수 있지만 이 방법 또한 임시 방편이라는 생각이 들고 근본적인 방법은 메인 라이브러리의 버전 관리라고 생각한다.

마이그레이션을 위한 고려 사항

크게 마주친 문제점들을 해결을 했고 이제 실제로 Vite로 마이그레이션 하기 위해 고려해야 할 사항을 살펴보자면 다음과 같다.

  1. Node.js 버전 (최소 18버전 이상)
  2. Eject해 최적화 시킨 CRA 프로젝트를 커버할 수 있는지
  3. 마이그레이션 이후 빌드 결과물이 문제 없이 실행이 되는지
  4. CI/CD 스크립트 수정 (도커 파일도 함께 수정)

프로젝트에서 패키지 매니저로 Yarn Berry를 사용하고 있었는데 이번 기회에 Pnpm을 도입해 패키지 매니저를 교체하자는 의견이 나와서 Pnpm도 함께 도입했다.

마이그레이션 이후 변경 사항들

  • Node.js 버전 (16버전 → 20버전)
  • Package Manager 교체 (Yarn Berry → Pnpm)
  • Webpack, Babel 설정 제거 (플러그인 삭제)
  • React 버전 (16.13.1 버전 → 18.3.1 버전)
  • TypeScript 버전 (3.9.7버전 → 5.4.5 버전)
  • Vite 5.3.1 버전 설치 및 번들러 교체 (Rollup)

마이그레이션 이후 이점

패키지 매니저 교체

기존 패키지 매니저인 Yarn Berry는 깃을 활용해 디펜던시를 관리한다. 그렇기 때문에 프로젝트가 진행될 수록 깃에 주는 부하는 기하수급적으로 늘어난다. 추후 프론트엔드 프로젝트의 구조를 모노레포로 변경할 가능성도 고려해 Pnpm으로 패키지 매니저를 변경하는 것을 고려했다. Yarn Berry 역시 모노레포의 Workspace를 지원하지만 Workspace의 자잘한 버그들이 존재한다는 후기를 확인했을때 조금 더 안정성 있는 Pnpm을 사용하는 것이 어떨까 하는 생각도 들었기 떄문이다.
Pnpm은 의존성 라이브러리를 글로벌 저장소에 설치를 하고 프로젝트에서는 링크를 통해 해당 라이브러리를 사용한다. 그렇기 때문에 위의 문제를 해결할 수 있었고 프로젝트를 클론 받을때 마다 라이브러리 zip 파일도 함께 내려받아 클론 과정이 오래 걸린다는 단점도 해결하게 됐다.
https://medium.com/wantedjobs/yarn-classic%EC%97%90%EC%84%9C-pnpm%EC%9C%BC%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0-with-turborepo-7c0c37cb3f9e
https://engineering.ab180.co/stories/yarn-to-pnpm

빌드 후 번들링 결과물 사이즈 감소

Webpack을 활용해 번들링을 했을 때 번들링 사이즈를 비교해보자. 좌측이 Webpack의 결과물이고 우측이 Vite의 결과물이다. 37.9MB 의 결과물에서 18.4MB로 사이즈가 줄어들었다. 프론트엔드에서 가장 중요한 것 중에 하나가 번들링 사이즈 최적화 인데 마이그레이션 이후 가장 유의미한 결과이지 않을까 생각한다.

기존의 번들링 사이즈
마이그레이션 이후 번들링 사이즈

 

개발 서버 구동 시간 감소

개발 서버 구동 시간이 엄청나게 단축됐다. Webpack은 개발 서버 구동을 위해 번들링을 수행하고 번들링 된 결과물을 실행한다. Vite는 개발 서버 구동을 위해 의존성 모듈을 ESBuild를 활용해 사전 번들링을 수행하고 실제 개발 서버는 네이티브 ESM을 활용해 브라우저가 요청한 모듈을 변환해 보내준다. ESBuild는 Go언어로 작성이 되어서 상당히 빠르게 사전 번들링이 수행된다.
아래는 기존의 개발 서버 구동 시간이다. 개발 서버가 구동 된 후 초기 페이지가 뜨기 까지 정확히 1분 16초가 걸렸다.

 
아래는 Vite의 개발 서버 구동 시간이다. 개발 서버가 구동 되고 초기 페이지가 뜨기 까지 14초가 걸렸다. 약 1분 가량의 시간이 단축됐다

페이지 리로드 없는 HMR

Webpack을 사용해 개발 서버를 구동할 때 느꼈던 불편한 점은 HMR이였다. Hot Module Reload 기능은 프론트엔드 개발에 있어서 없어서는 안되는 기능이지만 Webpack의 HMR은 모듈이 변경 됐을 경우 번들링을 다시 수행하기 때문에 페이지가 반드시 리로드 됐었다. Vite의 HMR은 번들링 없이 네이티브 ESM을 활용해 브라우저가 요청한 모듈을 변환 후 보내주기 때문에 페이지 리로드가 없다. 이때 HTTP Request Header을 활용해 변경 점이 없는 모듈은 캐싱을 활용하기 때문에 더더욱 빠르게 HMR이 적용된다.

빌드 시간 단축

로컬 기준으로 빌드 시간이 약 40초 가량 단축됐다. 기존의 빌드 수행 시간은 1분 26초가 걸렸고, Vite 마이그래이션 이후 빌드 수행 시간은 46초 이다.
젠킨스 서버에서의 빌드 시간도 50% 가량 단축됐다. 기존의 CRA 프로젝트는 빌드 시간이 2분 이상 소요가 됐었는데 Vite 마이그레이션 이후 정확히 1분으로 단축됐다.
아래 동영상은 로컬에서 측정한 결과이다.

Zero Configuration

Vite의 빌드 과정은 Zero Configuration으로 최적화가 잘 되어있다. 그렇기 때문에 기존에 Webpack을 통해 설정해줬던 부분들은 알아서 커버가 됐고, 중복되는 서드파티 라이브러리의 chunk파일을 분리하는 최적화만 진행했다.

React 최신 버전 도입

React 18버전에는 Concurrent Mode, Automatic Batching이 도입됐다. 이로 인해서 UX가 상당히 개선이 됐는데 드디어 해당 기능을 도입할 수 있게 됐다.
또한 올해 19버전이 출시가 예정되어 있는데 19버전이 출시된 후 stable 된다면 문제 없이 도입이 가능할 것 이라는 생각을 한다.

React 18 버전의 실상을 파헤치다. - 오픈소스컨설팅 테크블로그 %

안녕하세요. Playce Dev팀 강동희 입니다. 22년 4월 React 의 버전이 18버전으로 메이저 업그레이드 되었습니다. 버전이 올라가면서 무엇이 어떻게 변화 했는지 집중적으로 살펴봅시다!

tech.osci.kr

서드파티 라이브러리 버전 관리 용이

메인 라이브러리 최신화로 인해 이제 Zustand, Tanstack/react-query 라이브러리의 버전을 최신 버전으로 관리할 수 있게 됐다.

마치며

마이그레이션 한 프로젝트는 5년이상 유지보수 중인 프로젝트이다. 5년이라는 시간동안 많은 개발자들을 거쳐갔고 정리되지 않았던 레거시를 하나하나 정리해 프로젝트에서 때를 벗겨냈다는 생각을 한다.

회고록에 대한 팀장님의 언급
리팩토링 회고록을 보신 수석님의 반응
프론트엔드 파트 리드님의 반응

 
이번 마이그레이션 과정이 TabCloudit 프론트엔드의 도약점이 되지 않을까? 라는 생각을 한다. React 18버전의 Automatic Batching으로 인해 대시보드의 랜더링 퍼포먼스가 상당히 개선될 것 같다는 기대감도 갖게 된다. 앞으로의 액션 아이템을 리스트업 해보자면 다음과 같다.

  • Zustand 버전 관리
  • Tanstack/react-query 버전 관리
  • Table 컴포넌트 교체 (ag-grid → Tanstack/react-table)
  • Radix-ui를 도입해 서드 파티 UI 라이브러리 관리 포인트 일원화