FrontEnd/React

React에서 CRA보다 Vite가 좋다고? - 프로덕션 버전으로 빌드하기

Grace 2022. 12. 27. 12:43

프로덕션 버전으로 빌드하고자 한다면 vite build 명령을 실행해주세요. 빌드 시 기본적으로 <root>/index.html 파일이 빌드를 위한 엔드 포인트로 사용되며, 정적 호스팅을 위한 형태로 진행됩니다.


브라우저 지원 현황

빌드된 프로덕션 버전의 경우 모던 Javascript를 지원하는 브라우저에서 동작한다고 가정합니다.
빌드된 프로덕션 버전은 모던 Javascript를 지원하는 환경에서 동작한다고 가정합니다. 따라서 Vite는 기본적으로 네이티브 ES 모듈, 네이티브 ESM의 동적 Import, 그리고 import.meta를 지원하는 브라우저를 타깃으로 하고 있습니다.
만약 자바스크립트 타깃을 지정하고자 한다면, build.target 설정을 이용해주세요. 다만 버전은 최소한 es2015 이상이어야 합니다.
위에서 언급되는 기본적으로 라는 말의 의미를 잠깐 설명하자면, Vite는 오로지 구분 변환만 진행할 뿐 폴리필을 다루지 않는다는 말입니다. 따라서 만약 폴리필을 생각해야 할 경우 User Agent를 기반으로 자동으로 폴리필 번들을 생성해주는 polyfill.io를 이용해주세요.

💡 폴리필 (Polyfill)
명세서엔 새로운 문법이나 기존에 없던 내장 함수에 대한 정의가 추가되곤 합니다. 새로운 문법을 사용해 코드를 작성하면 트랜스파일러는 이를 구 표준을 준수하는 코드로 변경해줍니다. 반면, 새롭게 표준에 추가된 함수는 명세서 내 정의를 읽고 이에 맞게 직접 함수를 구현해야 사용할 수 있습니다. 자바스크립트는 매우 동적인 언어라서 원하기만 하면 어떤 함수라도 스크립트에 추가할 수 있습니다. 물론 기존 함수를 수정하는 것도 가능합니다. 개발자는 스크립트에 새로운 함수를 추가하거나 수정해서 스크립트가 최신 표준을 준수할 수 있게 작업할 수 있습니다.
이렇게 변경된 표준을 준수할 수 있게 기존 함수의 동작 방식을 수정하거나, 새롭게 구현한 함수의 스크립트를 폴리필이라 부릅니다. 말 그대로 구현이 누락된 새로운 기능을 메꿔주는 역할을 합니다.

레거시 브라우저의 경우 @vitejs/plugin-legacy 플러그인을 이용할 수 있습니다. 이 플러그인을 사용하면 자동으로 레거시 버전에 대한 청크를 생성하게 되고, 이를 통해 레거시 브라우저 또한 Vite로 필드된 앱을 이용할 수 있게 됩니다. 참고로, 생성된 레거시 청크는 브라우저가 ESM을 지원하지 않는 경우에만 불러오게 됩니다.

💡 청크
PNG, IFF, MP3 및 AVI 와 같은 많은 멀디미디어 파일 형식에서 사용되는 정보 조각입니다.
각 청크는 일부 매개변수(예: 청크 유형, 주석, 크기 등)를 나타내는 헤더가 포함되어 있습니다. 헤더 다음에는 헤더의 매개변수에서 프로그램에 의해 디코딩되는 데이터를 포함하는 변수 영역이 있습니다. 청크는 P2P 프로그램에 의해 다운로드되거나 관리되는 정보의 조각일 수도 있습니다.
분산 컴퓨팅에서 청크는 처리를 위해 프로세서 또는 컴퓨터 부품 중 하나로 전송되는 데이터 세트입니다.


Public Base Path

만약 배포하고자 하는 디렉터리가 루트 디렉터리가 아닌가요? 간단히 base 설정을 이용해 프로젝트의 루트가 될 디렉터리를 명시해 줄 수 있습니다. 또는 vite build --base=/my/public/path 명령과 같이 커맨드 라인에서도 지정이 가능합니다.
JS(import), CSS(url()), 그리고 .html파일에서 참조되는 에셋 파일의 URL들은 빌드 시 이 Base Path를 기준으로 가져올 수 있도록 자동으로 맞춰지게 됩니다.
만약 동적으로 에셋의 URL을 생성해야 하는 경우라면, import.meta.env.BASE_URL을 이용해주세요. 이 상수는 빌드 시 Public Base Path로 변환되어 이를 이ㅛㅇ해 동적으로 가져오려는 에셋에 대한 URL을 생성할 수 있습니다. 다만 정확히 import.meta.env.BASE_URL과 동일한 문자열에 대해 치환하는 방식이며, import.meta.env['BASE_URL']과 같은 경우 Public Base Path로 치환되지 않는다는 것을 유의해주세요.


빌드 커스터마이즈하기

빌드와 관련된 커스터마이즈는 build 설정을 통해 가능합니다. 특별히 알아두어야할 것이 하나 있는데, Rollup 옵션을 build.rollupOptions에 명시해 사용이 가능합니다.

// vite.config.js
export default defineConfig({
    build: {
      rollupOptions: {
        // https://rollupjs.org/guide/en/#big-list-of-options
      }
    }
})

예를 들어, 여러 Rollup 빌드 결과를 위해 빌드 플러그인을 등록할 수도 있습니다.


청크를 만드는 방식

build.rollupOptions.output.manualChunks를 사용해 청크를 분할하는 방식을 구성할 수 있습니다. Vite 2.8까지는 청크를 만들 때 기본적으로 indexvender를 기준으로 분할했습니다. 이 방식은 일부 SPA를 대상으로는 잘 동작했으나 Vite가 지원하고자 하는 모든 사례에 대해서 범용적으로 적용하기는 어려웠습니다. 따라서 Vite 2.9부터 manualChunks는 더 이상 기본적으로 수정하지 않습니다. 만약 계속 manualChunks를 수정하기 원한다면 splitVendorChunkPlugin을 사용해주세요.

// vite.config.js
import { splitVendorChunkPlugin } from 'vite'
export default defineConfig({
  plugins: [splitVendorChunkPlugin()]
})

이러한 방식은 사용자 정의 로직을 사용한 구성이 필요한 경우를 대비해 splitVendorChunk({ cache: SplitVendorChunkCache }) 팩토리 함수로도 제공됩니다. 이 때, 빌드 감시 모드가 정상적으로 작동하기 위해서는 cache.reset()buildStart 훅에서 호출해야 합니다.


파일 변경 시 다시 빌드하기

vite build --watch 명령을 통해 Roullup Watcher를 활성화 할 수 있습니다. 또는, build.watch 옵션에서 WatcherOptions를 직접 명시할 수도 있습니다.

// vite.config.js
export default defineConfig({
  build: {
    watch: {
      // https://rollupjs.org/guide/en/#watch-options
    }
  }
})

--watch 플래그가 활성화된 상태에서 vite.config.js 또는 번들링 된 파일을 변경하게 되면 다시 빌드가 시작됩니다.


Multi-Page App

아래와 같은 구조의 소스 코드를 갖고 있다고 가정합니다.

├── package.json
├── vite.config.js
├── index.html
├── main.js
└── nested
    ├── index.html
    └── nested.js

개발 시, /nested/ 디렉터리 아래에 있는 페이지는 간단히 /nested/로 참조가 가능합니다. 일반적인 정적 파일 서버와 다르지 않습니다.
빌드 시에는, 사용자가 접근할 수 있는 모든 .html 파일에 대해 아래와 같이 빌드 진입점이라 명시해줘야만 합니다.

// vite.config.js
import { resolve } from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        nested: resolve(__dirname, 'nested/index.html')
      }
    }
  }
})

라이브러리 모드

만약 브라우저 기반의 라이브러리를 개발하고 있다면, 라이브러리 갱신 시마다 테스트 페이지에서 이를 불러오는 데 많은 시간을 소모할 것입니다. Vite는 index.html을 이용해 좀 더 나은 개발 환경(경험)을 마련해줍니다.
라이브러리 배포 시점에서 build.lib설정 옵션을 이용해보세요. 또한 라이브러리에 포함하지 않을 디펜던시를 명시할 수도 있습니다 vuereact처럼 사용할 수 있습니다.

// vite.config.js
import { resolve } from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'lib/main.js'),
      name: 'MyLib',
      // 적절한 확장자가 추가됩니다.
      fileName: 'my-lib'
    },
    rollupOptions: {
      // 라이브러리에 포함하지 않을 디펜던시를 명시해주세요
      external: ['vue'],
      output: {
        // 라이브러리 외부에 존재하는 디펜던시를 위해
        // UMD(Universal Module Definition) 번들링 시 사용될 전역 변수를 명시할 수도 있습니다.
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
})

패키지의 진입점이 되는 파일에는 패키지의 사용자가 import 할 수 있도록 export 구문이 포함되게 됩니다.

// lib/main.js
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export { Foo, Bar }

위와 같은 Rollup 설정과 함께 vite build 명령을 실행하게 되면, esumd 두 가지의 포맷으로 번들링 과정이 진행되게 됩니다.

$ vite build
building for production...
dist/my-lib.js      0.08 KiB / gzip: 0.07 KiB
dist/my-lib.umd.cjs 0.30 KiB / gzip: 0.16 KiB

package.json에는 아래와 같이 사용할 라이브러리를 명시해주세요.

{
  "name": "my-lib",
  "type": "module",
  "files": ["dist"],
  "main": "./dist/my-lib.umd.cjs",
  "module": "./dist/my-lib.js",
  "exports": {
    ".": {
      "import": "./dist/my-lib.js",
      "require": "./dist/my-lib.umd.cjs"
    }
  }
}

여러 진입점을 노출하는 경우는 아래와 같습니다.

{
  "name": "my-lib",
  "type": "module",
  "files": ["dist"],
  "main": "./dist/my-lib.cjs",
  "module": "./dist/my-lib.js",
  "exports": {
    ".": {
      "import": "./dist/my-lib.js",
      "require": "./dist/my-lib.cjs"
    },
    "./secondary": {
      "import": "./dist/secondary.js",
      "require": "./dist/secondary.cjs"
    }
  }
}

❗️ 참고
package.json에 "type":"module"이 명시되어 있지 않으면 Vite는 Node.js 호환성을 위해 다른 파일 확장자를 생성합니다. 즉, .js는 .mjs가 되고, .cjs는 .js가 됩니다.

❗️ 환경 변수
라이브러리 모드에서 모든 import.meta.env.*는 프로덕션용으로 빌드 시 정적으로 대체됩니다. 그러나 process.env.*는 그렇지 않기에 라이브러리를 사용하는 측에서 이를 동적으로 변경할 수 있습니다. 만약 이 역시 정적으로 대체되길 원한다면 define: { 'process.env.NODE_ENV': '"production"' }와 같이 설정해주세요.


Base 옵션 상세 설정

❗️ WARNING
이 기능은 실험적이며, 시멘틱 버전을 따르지 않고 향후 마이너 버전에서 API가 변경될 수 있습니다. 사용 시 Vite를 마이너 버전으로 고정해주세요.

이미 배포된 에셋과 Public 디렉터리에 위치한 파일이 서로 다른 경로에 있을 수 있습니다. 이 경우 각각에 대해 다른 캐시 전략을 사용하고자 할 수 있는데, 이 때 Base 옵션에 대한 상세 설정이 필요합니다. 사용자는 세 가지 다른 경로로 배포하도록 선택할 수 있습니다

  • 생성된 HTML 진입점(Entry) 파일 (SSR을 사용하는 동안 처리될 수 있음)
  • 해시화 되어 생성된 에셋 (JS, CSS, 및 이미지와 같은 여러 파일들)
  • 복사된 Public 디렉터리 파일

이런 상황에서는 하나의 정적인 base 만으로는 충분하지 않습니다. Vite는 실험적으로 experimental.renderBuiltUrl를 통해 빌드하는 동안 Base에 대한 상세 설정을 제공하고 있습니다.

experimental: {
renderBuiltUrl(filename: string, { hostType }: { hostType: 'js' | 'css' | 'html' }) {
    if (hostType === 'js') {
      return { runtime: `window.__toCdnUrl(${JSON.stringify(filename)})` }
    } else {
      return { relative: true }
    }
  }
}

해시된 에셋과 Public 디렉터리 내 파일이 함께 배포되지 않은 경우, 함수에 전달된 두 번째 context 매개변수에 포함된 에셋의 type 프로퍼티를 이용해 각 그룹에 대한 동작을 독립적으로 정의할 수 있습니다.

experimental: {
renderBuiltUrl(filename: string, { hostId, hostType, type }: { hostId: string, hostType: 'js' | 'css' | 'html', type: 'public' | 'asset' }) {
    if (type === 'public') {
      return 'https://www.domain.com/' + filename
    }
    else if (path.extname(hostId) === '.js') {
      return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` }
    }
    else {
      return 'https://cdn.domain.com/assets/' + filename
    }
  }
}