[React] CRA 프로젝트를 vite로 마이그레이션하기 (2) - after

Gyuhan Park·2024년 1월 11일
1

react

목록 보기
2/11

💭 TMI

엄청난 충돌과의 싸움에서 이기고 돌아왔다...라이브러리가 많을수록 마이그레이션하기 어렵고 라이브러리 간에 의존성이 높을수록 하나만 버전을 올리기 어려웠다. 추후 개발을 진행할 때 라이브러리를 제거하고 직접 구현하는 방향을 고려하고 있다.

기존의 프로젝트에서 하나씩 수정하다보니 package.json을 공유하게 되서 특정 부분의 오류를 찾기 어려웠다. 그래서 npm create vite@latest [프로젝트 명] --template react-ts 로 새로 생성하여 옮기기로 결정하였다.

vite로 마이그레이션, node와 typescript 버전 올리는 것에 초점을 맞췄다. 라이브러리 최신화, package.json 분리 작업을 진행하였는데, 지원하지 않는 경우 수정해보고 어렵다 판단하였을 땐 기존 버전을 유지하였다.

😇 트러블 슈팅

❌ npm ERR! Could not resolve dependency

npm ERR! Could not resolve dependency:
npm ERR! peer react@"^15.3.0 || ^16.0.0 || ^17.0.0" from react-swipeable-views@0.14.0

일단 react 18 버전이 나온 지 약 2년이 지났기 때문에 안정화되었다고 생각하여 버전업을 시도하였다. 또한 18 버전에서 지원하는 automatic batching, concurrency mode, suspense 등의 내용들이 흥미롭고 성능 향상에 도움이 될 것이라고 판단하였다.
하지만 위와 같은 react 18을 지원하지 않는 라이브러리들이 존재하였다. react-swipeable-views 의 경우 직접 구현을 고려하여 주석처리해보았지만 @mui/base 등의 스타일링 코드에서도 오류가 발생하여 추후에 라이브러리 직접 구현 후 버전업을 진행할 예정이다.

❌ Warning: Received false for a non-boolean attribute loading.

Warning: Received false for a non-boolean attribute loading.

If you want to write it to the DOM, pass a string instead: loading="false" or loading={value.toString()}.

If you used to conditionally omit it with loading={condition && value}, pass loading={condition ? value : undefined} instead.

emotion 또는 styled-components 를 사용할 때 boolean 값을 넘겨 조건부 스타일링을 적용할 때 발생하는 오류다.
emotion 입장에서는 loading이 boolean props로 인지하지 못한다. emotion에서는 이를 $ 를 변수 앞에 prefix로 붙여 boolean값을 명시해주는 방식을 사용한다.

Before

interface Props extends ButtonUnstyledProps {
  loading?: boolean;
  ...
}
export const Button: React.FC<Props> = ({ children, ...props }) => (
  <ButtonUnstyled component={CustomButtonRoot} {...props}>
    {props.loading ? ...}
  </ButtonUnstyled>
);

After

interface Props extends ButtonUnstyledProps {
  $loading?: boolean;
  ...
}
export const Button: React.FC<Props> = ({ children, ...props }) => (
  <ButtonUnstyled component={CustomButtonRoot} {...props}>
    {props.$loading ? ...}
  </ButtonUnstyled>
);

https://styled-components.com/docs/api#transient-props

❌ Component selectors can only be used ...

Component selectors can only be used in conjunction with @emotion/babel-plugin, the swc Emotion plugin, or another Emotion-aware compiler transform.

emotion을 컴파일러가 인식하지 못한다. nextjs에서 발생하는 오류 자료밖에 없어서 애를 먹었다. 최신 버전의 Next.js는 babel 대신 swc를 이용해서 트랜스파일 하기 때문에 babel을 사용하려면 플러그인을 설정해줘야 한다.
vite에서도 emotion을 babel로 트랜스파일하기 위해 @emotion/babel-plugin을 명시적으로 적용해줘야 한다.

vite.config.js

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react({
      jsxImportSource: '@emotion/react',
      babel: {
        plugins: ['@emotion/babel-plugin'],
      },
    }),
    ...
  ],
  ...
});

https://emotion.sh/docs/@emotion/babel-plugin

❌ jsx pragma @jsxImportSource @emotion/react

Babel 트랜스파일러한테 JSX 코드를 변환할 때, React의 jsx()함수가 아니라 Emotion의 jsx()함수를 대신 사용하기 위해 명시해야 한다.

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

컴포넌트마다 작성하는 대신 config 설정을 할 수 있다.

// tsconfig.json
"compilerOptions": {
	"jsxImportSource": "@emotion/react",
    ...
}

https://emotion.sh/docs/typescript
https://velog.io/@remon/React-Emotion-%EC%84%A4%EC%B9%98-%ED%9B%84-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%98%88%EC%A0%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0

❌ does not provide an export named 'ReactComponent

svg 파일을 import 할 수 없다는 에러가 발생한다.

index.ts:2 Uncaught SyntaxError: The requested module '/src/assets/icons/nav_home.svg?import' does not provide an export named 'ReactComponent

현재 프로젝트에서는 index 파일을 두고 svg 파일을 컴포넌트처럼 사용하고 있었다.
하지만 vite에서는 svg 를 인식하지 못하였고 svg 확장자를 svg.d.ts 파일에서 전역으로 선언하여 처리하는 방법으로도 해결되지 않았다ㅠ
따라서 index로 묶어 import하는 방법 대신 svg 파일을 직접 import 하였고, 라이브러리 문서를 참고하여 class를 적용하는 경우 리액트 컴포넌트로 불러왔다. svg파일뒤에 ?react 를 붙이면 리액트 컴포넌트로 사용할 수 있다.

Before

// assets/index.ts
export { ReactComponent as BackIcon } from './back.svg';

// conponent.tsx
import { Icons } from '@/assets;
...
return (
	<Icons.BackIcon className="absolute-center" />
	...
  )

After

// conponent.tsx
import BackIcon from '@/assets/icons/back.svg?react';
...
return (
	<BackIcon className="absolute-center" />
	...
)

vite.config.js

import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';

// https://vitejs.dev/config/
export default defineConfig({![](https://velog.velcdn.com/images/ghenmaru/post/5807097c-98ef-42a3-9052-b7b0daf26892/image.png)

  plugins: [
    ...
    svgr({ include: '**/*.svg?react' }),
  ],
  ...
});

tsconfig.json

"compilerOptions": {
	"types": ["vite/client", "vite-plugin-svgr/client", "node"],
    ...
}

https://www.npmjs.com/package/vite-plugin-svgr

😎 패키지 정리 완료

typescript : 4.1.2 -> 5.2.2
node : 16 -> 18, 20 모두 호환

@emotion/react : 11.7.1 -> 11.11.3
@emotion/styled : 11.6.0 -> 11.11.0
axios : 0.21.1 -> 1.6.5
date-fns : 2.25.0 -> 3.2.0
mobx : 6.3.2 -> 6.12.0
mobx-react-lite : 3.2.1 -> 4.0.5
quill-image-uploader : 1.2.2 -> 1.3.0
react-responsive-carousel : 3.2.21 -> 3.2.23
react-virtuoso : 2.6.0 -> 4.6.2
use-long-press : 1.0.1 -> 3.2.0

Before

{
"dependencies": {
    "@craco/craco": "^6.4.3",
    "@emotion/react": "^11.7.1",
    "@emotion/styled": "^11.6.0",
    "@mui/base": "^5.0.0-alpha.69",
    "@mui/icons-material": "^5.4.2",
    "@mui/material": "^5.4.0",
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "axios": "^0.21.1",
    "cross-env": "^7.0.3",
    "date-fns": "^2.25.0",
    "emotion-normalize": "^11.0.1",
    "mobx": "^6.3.2",
    "mobx-react-lite": "^3.2.1",
    "quill-image-uploader": "^1.2.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hook-form": "^7.12.1",
    "react-quill": "^1.3.5",
    "react-responsive-carousel": "^3.2.21",
    "react-router-dom": "^5.2.0",
    "react-scripts": "4.0.3",
    "react-simple-image-viewer": "^1.2.1",
    "react-swipeable-views": "^0.14.0",
    "react-virtuoso": "^2.6.0",
    "source-map-explorer": "^2.5.3",
    "typescript": "^4.1.2",
    "use-long-press": "^1.1.1",
    "web-vitals": "^1.0.1"
  },
	"devDependencies": {
	    "@emotion/babel-plugin": "^11.7.2",
	    "@types/react-router-dom": "^5.1.8",
	    "@types/react-swipeable-views": "^0.13.1",
	    "@typescript-eslint/parser": "^4.32.0",
	    "cra-bundle-analyzer": "^0.1.1",
	    "eslint-config-prettier": "^8.3.0",
	    "eslint-import-resolver-typescript": "^2.5.0",
	    "eslint-plugin-import": "^2.25.4",
	    "eslint-plugin-prettier": "^3.4.0",
	    "prettier": "^2.3.1",
	    "speed-measure-webpack-plugin": "^1.5.0",
	    "webpack-bundle-analyzer": "^3.9.0"
	 },
}

after

{
	"dependencies": {
	    "@emotion/react": "^11.11.3",
	    "@emotion/styled": "^11.11.0",
	    "@mui/base": "^5.0.0-alpha.69",
	    "@mui/icons-material": "^5.15.4",
	    "@mui/material": "^5.15.4",
	    "axios": "^1.6.5",
	    "date-fns": "^3.2.0",
	    "emotion-normalize": "^11.0.1",
	    "mobx": "^6.12.0",
	    "mobx-react-lite": "^4.0.5",
	    "quill-image-uploader": "^1.3.0",
	    "react": "^17.0.2",
	    "react-dom": "^17.0.2",
	    "react-hook-form": "^7.12.1",
	    "react-quill": "^2.0.0",
	    "react-responsive-carousel": "^3.2.23",
	    "react-router-dom": "^5.2.0",
	    "react-simple-image-viewer": "^1.2.1",
	    "react-swipeable-views": "^0.14.0",
	    "react-virtuoso": "^4.6.2",
	    "use-long-press": "^3.2.0"
	  },
	  "devDependencies": {
	    "@emotion/babel-plugin": "^11.11.0",
	    "@types/node": "^20.10.8",
	    "@types/react-dom": "^18.2.18",
	    "@types/react-router-dom": "^5.3.3",
	    "@types/react-swipeable-views": "^0.13.5",
	    "@typescript-eslint/eslint-plugin": "^6.14.0",
	    "@typescript-eslint/parser": "^6.14.0",
	    "@vitejs/plugin-react": "^4.2.1",
        "cross-env": "^7.0.3",
	    "eslint": "^8.55.0",
	    "eslint-config-prettier": "^9.1.0",
	    "eslint-import-resolver-typescript": "^3.6.1",
	    "eslint-plugin-import": "^2.29.1",
	    "eslint-plugin-prettier": "^5.1.3",
	    "eslint-plugin-react-hooks": "^4.6.0",
	    "eslint-plugin-react-refresh": "^0.4.5",
	    "prettier": "^3.1.1",
	    "speed-measure-webpack-plugin": "^1.5.0",
	    "typescript": "^5.2.2",
	    "vite": "^5.0.8",
	    "vite-plugin-babel": "^1.2.0",
	    "vite-plugin-svgr": "^4.2.0",
	    "webpack-bundle-analyzer": "^4.10.1"
	  }
}

🔍 번들 사이즈 비교

이제 CRA로 생성했던 프로젝트와 번들 사이즈를 비교해보려고 한다.

😇 삽질

// package.json
"scripts": {
  "build": "vite build && webpack-bundle-analyzer dist/webpack-stats.json -m static -r dist/webpack-stats.html -O",
    ...
}
npm run build

vite에는 번들링 파일을 json 으로 내보내는 옵션이 없나보다.

npm i --save-dev rollup-plugin-webpack-stats

vite.config.js

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { webpackStats } from 'rollup-plugin-webpack-stats';

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // Use a supported file pattern for Vite 5/Rollup 4
        assetFileNames: 'assets/[name].[hash][extname]',
        chunkFileNames: 'assets/[name].[hash].js',
        entryFileNames: 'assets/[name].[hash].js',
      },
    },
  },
  plugins: [
    react({
    webpackStats(),
	...
  ],
});

뭐지? 근데 알고보니 vite 에서 지원해주는 라이브러리가 따로 있었다.

😎 성공

npx vite-bundle-visualizer

rendered : 2.43MB -> 1.76MB (27.57% 감소)
gzip : 255.33KB -> 489.61KB (91.76% 증가)

빌드 시 불필요한 패키지를 devDependencies 로 분리하여 node_modules의 전체 크기는 약 27% 감소하였다. 버전업 영향인지 압축된 gzip 크기는 약 92% 증가하였다.

🔍 빌드 속도 비교

✅ dev build

개발서버의 경우 빌드 후 첫 화면 렌더링까지 약 20초 -> 약 5초 로 단축되어 UX와 DX 향상

✅ production build

1분 17초 -> 19초 로 75.32% 빌드 시간 단축
캐싱될 경우 5초 이내로 빌드 성공

💭 느낀점

기존 프로젝트를 처음 마이그레이션 해봤는데 이것만으로 좋은 경험이 되었다. 특히 번들 사이즈 크기 분석, 빌드 속도 분석은 성능 최적화 과정에서 많이 사용될 걸로 예상된다. 이번에 분석한 결과를 바탕으로 초기 렌더링에 불필요한 모듈을 code spliting을 통해 줄여볼 예정이다. 다음 장에서...

profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글