본문 바로가기
> 레벨업의 테크노트

[프론트엔드]Vue build time 최적화 해보기 feat.Vite - 2탄

by 알 수 없는 사용자 2022. 1. 11.

📌 목차

  • 요약
  • 서론
  • 레벨업 나라의 역사
  • Vite 무기를 찾아서
  • Vite 무기 휘둘러 보기. (회사 프로젝트 적용하기)
  • 결론

📌 요약

  기존 vite 적용후
build time 34,897ms 548ms
re-build time 3,310ms 100ms 이하

📌 서론

안녕하세요! 빅픽처에서 종선버를 맡고 있는 최종선이라고 합니다. 🙇🏻‍♂️

저번 1탄을 쓴지 시간이 많이 흘렀네요. 그동안 레벨업 프론트 프로젝트에는 많은 변화가 있었고 빌드 타임또한 최대한 줄이려는 노력들이 있었습니다. (조금의 코드 수정후 매번 기다리는 시간이 아깝고 금붕어 집중을 가진 종선버는 쉽게 집중도 깨지기 때문입니다.)

그럼 이제 어떻게 3,4897ms에서 548ms까지 줄일 수 있었는지에 대해서 알아보겠습니다. 그럼 출발! 🚀

📌 레벨업 나라의 역사

132,882ms -> 106,634ms -> 34,897ms -> ???ms

1탄을 작성할 당시만 해도 레벨업 프로젝트의 build 타임은 까지 build 타임이 증가 하였습니다. 그리고 최적화를 통해 39,913ms까지 감소 하였으나 이후 추가 되는 코드들과 의존성들에 의해 다시 106,634ms까지 올라가게 되었습니다. 그 이후 그동안 빠르게 개발을 하면서 쌓여왔었던 기술 부채들과 build 타임을 해결하기 위해 대대적인 프론트엔드 리뉴얼이 시작이 되었고 그 결과 34,897ms까지 줄였습니다. re-build는 3,310ms까지 도달 했으나 개발시 드는 비용을 최대한 줄이고 싶었습니다.

📌 Vite 무기를 찾아서

먼저 들어가기 전에 vite가 생소하실거 같아 간략하게 vite에 대해 설명해보겠습니다.

vite는 바이트 라고 읽지 않고 비트라고 읽어야 한답니다. (빠르다는 뜻의 불어라서 이렇게 읽어야합니다.)
그리고 vite는 기존 webpack, rollup, parcel 과 같은 bundler 라고 보시면 됩니다.
다른 툴과는 다르게 훨씬 더 빠르게 server를 시작하고 hot-module-replacement가 빠릅니다. 왜그럴까요?

Vite가 여타 다른 툴보다 빠른 이유:

vite가 빠른 가장 큰 이유는 브라우저가 ES modules를 지원함에 따라서 일반적인 번들링 과정을 생략하게 되어 그렇습니다.

기존:

Es module을 활용한 방법:

그림을 보시면 기존에는 모든 파일을 번들링을 한 후에나 서버가 시작이 됩니다. 하지만 변경된 방법은 서버만 시작한 뒤에나 파일이 떨어집니다. module을 이용하여 각각의 파일을 그대로 모듈 형태로 브라우저에서 인식하게 합니다. 그래서 훨씬 빠른 dev-server가 가능하게 됩니다.

📌 Vite 무기 휘둘러 보기. (회사 프로젝트 적용하기)

회사 프로젝트에 적용하기에 앞서 회사 프로젝트에 대해 설명을 하자면 일반적인 vue framework 나 mono-repo와는 조금 다르게 환경이 설정이 되어있습니다.

회사 프로젝트 구성

  1. vue를 사용하긴 하지만 vue를 통하여 ssr이 가능하게 되는 형태를 사용하고 있습니다.
  2. mono-repo의 형식을 가지고 있지만 실제로는 분리 되어있는 shared 폴더를 복사해서 가져온 뒤 혹은 alias로 프로젝트가 실행되게 됩니다.

Vite에서 ssr 지원은 현재 Experimetal 단계이기 때문에 아직 적용을 할수 가 없었습니다. 따라서 다음과 같이 개발환경을 구축합니다.

  1. 개발 환경에서만(dev-server) Vite 적용하기.
  2. Production build에서는 그대로 webpack을 사용하기.
  3. Vite와 Webpack을 공존하여 사용하되 서로 충돌이 나지 않게 하기.

이에 따라 어려웠던 점들은 다음과 같습니다.

  1. Webpack과 Vite 서로 충돌나는 부분 없게 하기
  2. .env.local 파일관리
  3. router dynamic import
  4. .vue 익스텐션 처리
  5. import.meta 처리
  6. HMR 처리

1. Webpack과 Vite 서로 충돌나는 부분 없게 하기

먼저 Webpack에서 기본적으로 define이 되어있는 부분은 Vite엔 없기에 해당 부분을 처리 해주었습니다.

// vite.config.js

export default defineConfig(({ mode, command }) => {
  return {
    define: {
      // 로컬에서만 vite를 사용하기 때문에 ssr을 할필요가 없음에 따라 해당 변수를 false로 해줌
      'TARGET_NODE': false,
      // webpack의 hot-reloaded를 사용하고 있기때문에 해당 변수를 빈 object로 설정
      'module': {}
    },
  };
});

또한 위에서 언급했던 특이한 형태의 monorepo를 사용하고 있기에 Webpack에서는 CopyWebpackPlugin을 사용하여 shared를 복사하여 가져오고 있었습니다.

// vue.config.js
module.exports = {
  configureWebpack: 
    {
      // shared를 복사하여 가져오고 있음
      plugins: [new CopyWebpackPlugin([{ from: path.resolve(__dirname, '../../shared/public/'), to: 'shared/' }])]
    }
  }
};

이에 따라 vite도 똑같은 처리를 해주었습니다. (rollup-plugin-copy 사용)

// vite.config.js
import copy from "rollup-plugin-copy";

export default defineConfig(({ mode, command }) => {
  return {
    plugins: [
      {
        ...copy({
          targets: [
            {
              src: join(process.cwd(), "../../shared/public/img"),
              dest: "shared/",
            },
          ],
          copyOnce: true, // 실행하고 한번만 copy 하게 만들어줌
          hook: "config", // hook으로 config의 실행 시점으로 일치 시켜줌
        }),
        enforce: 'pre', // build 단계일때 pre로 사용하도록 변경
        apply: 'serve', // serve 명령어 일때만 실행하도록 강제
      },
    ],
  };
});

2. .env.local 파일관리

vite의 default mode는 development 입니다. 그리고 local은 막혀져 있습니다.(+ 자동적으로 .env.local 파일이 추가 됩니다.) 레벨업 프로젝트를 로컬에서 개발할때는 .env.local을 사용해야하지만 --mode local로 해주어야 합니다.(.env.development도 따로 존재함) 이문제를 해결하기 위해 생각한 방법은 다음과 같습니다.

  1. vite 전용 .env.local 같은 파일을 만듬(ex> .env.viteLocal, --mode=viteLocal)
  2. mode를 이상하게 주어서 .env.local 파일만 자동 임포트를 하게 함

1번 방법은 이미 .env 파일이 5개(.env, .env.local, .env.development, .env.stage, .env.production)나 있는 상황 이었고 .env.local을 변경한다면 .env.viteLocal도 변경을 해주어야하는 번거로움이 있어서 2번 방법으로 해결을 하였습니다.
따라서 최종적으로 이런 명령어가 탄생하게 됩니다.

vite --mode eddie_is_the_best

3. router dynamic import

보통 router에 dynamic import를 할때 이런식의 factory 함수로 많이 씁니다.

const view = path => () => import(/* webpackChunkName: "arena" */ `@/views/pages/arena/${path}`);

하지만 이경우 alias가 이미 붙어 있기 전이라 @를 읽을수가 없습니다. 이에 따라 이렇게 변경을 해주었습니다.

const view = path => () => import(/* webpackChunkName: "arena" */ `../views/pages/arena/${path}.vue`);

4. .vue 익스텐션 처리

이부분이 가장 걸렸던거 같아요. vite에서는 이제 extension을 직접 입력하는 방식으로 변경이 됩니다. .js외의 모든 파일들을 import 할때 이제 extension을 붙여 줘야합니다. (ex> .vue)
안붙여 주면 이렇게 못찾는 다는 에러가 뜹니다.

그래서 처음에는 그냥 자동적으로 해결하는 방법을 택했었습니다. 하지만 이내 이런글을 보게 됩니다.

그렇습니다. 자동으로 extension을 붙여주는게 legacy로 남게 될것이고 누군가는 이 legacy를 해결해야 하는 문제가 있었습니다.
그래서 그냥 붙여줘야한다고 판단이 들었습니다.

네 그렇습니다. 노가다를 했습니다.(약 2000개의 파일들 모두 찾아다니면서 붙여줬습니다. 😭) 처음엔 인텔리제이의 힘으로 정규식으로 한번에 붙여보려고 했지만 .js 인지 다른 extension인지 판단을 할수가 없어 직접 붙여줬습니다.

5. import.meta 처리

vite는 import.meta에 관한 처리가 가능하지만 webpack은 가능하지 않습니다. 때문에 webpack loader를 추가 시켜주었습니다.

import.meta 처리가 production 모드 일 때 빌드가 실패하여 다음과 같이 코드를 수정하였습니다.

<!DOCTYPE html>
<html lang="">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="/favicon.ico">
</head>
<body>
<div id="app"></div>
  <!-- built files will be auto injected -->
  <script type="module" src="/src/vite-hmr.js"></script> // 삽입!
</body>
</html>

🚧 해당 html파일은 vite 전용이기 때문에 webpack 일때는 접근을 하지 않습니다.

vite-hmr.js는 아래에서 설명하겠습니다.😎

6. HMR 처리

ssr처리를 위해 vue에서 asyncData는 webpack.module.hot에서 신호를 받아 처리를 해왔습니다.

if (process.env.NODE_ENV === 'development' && module && module['hot']) {
    /** @type {{ addStatusHandler: (callback) => void }} */
    const hot = module['hot'];
    hot.addStatusHandler(status => {
      if (status === 'idle') {
        Vue.nextTick(async () => {
         // ... 여기서 asyncData 처리를 해주고 있음
          }
        });
      }
    });
  }

동일하게 vite의 hmr도 처리를 해주어야 했습니다.
하지만 해당 처리는 vite:beforeUpdate 이후여야 하기 때문에 plugin을 하나 만들어 주었습니다.

import { HmrContext } from 'vite';

const customHmrHandler = () => ({
  name: 'customHmrHandler',
  /**
   * @param {HmrContext} obj
   */
  handleHotUpdate({ server }) {
    setTimeout(() => {
      server.ws.send({
        type: 'custom',
        event: 'deferred-updated',
        data: {}
      });
    }, 250);
  },
});

export default customHmrHandler;

이제 이걸 vite.config.js에 연결 시켜놓았습니다.

import customHmrHandler from '../../shared/vitePlugins/customHmrHandler';

export default defineConfig(({ mode, command }) => {
  return {
    plugins: [customHmrHandler()]
  }
}

그리고 이제 신호만 받으면 됩니다.

// vite-hmr.js
if (import.meta.hot) {
    import.meta.hot.on('deferred-updated', () => {
        Vue.nextTick(async () => {
          // ... 여기서 asyncData 처리
          }
        });
    });
  }

이부분을 추후에 다시 고민을 해봐야 할거 같습니다. 일단 계속 찾아보고 코드도 열심히 찾아봤는데 after updated를 받을 수 있는 event가 없었습니다. 그래서 일단 vite.js에 issue로 해당 기능을 구현요청을 해놓은 상태입니다. (해당 ISSUE 바로가기)

📌 결론

이제 훨씬 빠르게 개발을 할수 있게 되었습니다.

  기존 vite 적용후
build time 34,897ms 548ms
re-build time 3,310ms 100ms 이하

기존 코드도 리뉴얼을 한번 해서 많이 빌드 타임을 낮춘 상태였지만 이것으로 만족을 할수 없었습니다.
저는 조금의 기다림이 없는 그런 개발 환경을 추구합니다.
이 문제는 생산성과 직접적인 연관이 있는 문제라 꼭 해결을 하고 싶었고 Vite로 해결을 해보았습니다.

그럼 긴글 읽어주셔서 감사합니다. 🙇🏻‍♂️

댓글