Skip to content

May 9th, 2024

Vite Meetup SF

프로덕션 빌드

앱을 어느정도 완성하셨나요? 프로덕션으로 빌드하고자 한다면 vite build 명령을 실행해주세요. 빌드 시 기본적으로 <root>/index.html 파일이 빌드를 위한 진입점(Entry point)으로 사용되며, 정적 호스팅을 위한 형태로 진행됩니다. 추가적으로, GitHub Pages와 같은 정적 호스팅 서비스를 위한 빌드 방법을 알고싶다면 정적 웹 페이지로 배포하기 섹션을 참고해주세요.

브라우저 지원 현황

빌드된 프로덕션 번들은 모던 JavaScript를 지원하는 환경에서 동작한다고 가정합니다. 따라서 Vite는 기본적으로 네이티브 ES 모듈, 네이티브 ESM의 동적 Import, 그리고 import.meta를 지원하는 브라우저를 타깃으로 하고 있습니다:

  • Chrome >=87
  • Firefox >=78
  • Safari >=14
  • Edge >=88

만약 JavaScript 타깃을 지정하고자 한다면, build.target 설정을 이용해주세요. 다만 버전은 최소한 es2015 이상이어야 합니다.

위에서 언급되는 기본적으로 라는 말의 의미를 잠깐 설명하자면, Vite는 오로지 구분 변환만 진행할 뿐 기본적으로 폴리필을 다루지 않는다는 말 입니다. 따라서 만약 폴리필을 생각해야 할 경우, User Agent를 기반으로 자동으로 폴리필 번들을 생성해주는 Polyfill.io를 이용해주세요.

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

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로 치환되지 않는다는 것을 유의해주세요.

더욱 상세한 설정이 필요하다면 Base 옵션 상세 설정 섹션을 참고해주세요.

빌드 커스터마이즈하기

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

ts
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/configuration-options/
    }
  }
})

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

청크를 만드는 방식

build.rollupOptions.output.manualChunks를 사용해 청크를 분할하는 방식을 구성할 수 있습니다(Rollup 문서를 참고해 주세요). 프레임워크를 사용하는 경우, 청크 분할 방식 구성은 해당 프레임워크 문서를 참고해 주세요.

로드 에러 처리하기

Vite는 동적 임포트에 실패했을 때 vite:preloadError 이벤트를 발생시킵니다. event.payload에는 원본 임포트 에러가 포함되어 있으며, event.preventDefault()를 호출하면 에러가 발생하지 않습니다.

js
window
.
addEventListener
('vite:preloadError', (
event
) => {
window
.
location
.
reload
() // 예: 페이지 새로고침
})

새로운 배포가 시작되면 호스팅 서비스에서 이전에 배포된 에셋을 삭제할 가능성이 있습니다. 그 결과, 새로운 배포 이전에 사이트를 방문했던 사용자는 임포트 에러를 마주할 수 있습니다. 이 에러는 사용자의 기기에 존재하는 에셋이 만료되었음에도, 이에 대응하는 이전의 청크를 임포트하려고 하기 때문에 발생합니다. 위 이벤트는 이러한 상황을 해결하는 데 사용이 가능합니다.

파일 변경 시 다시 빌드하기

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

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

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

Multi-Page App

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

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

개발 시, /nested/ 디렉터리 아래에 있는 페이지는 간단히 /nested/로 참조가 가능합니다. 일반적인 정적 파일 서버와 다르지 않습니다.

빌드 시에는, 사용자가 접근할 수 있는 모든 .html 파일에 대해 아래와 같이 빌드 진입점이라 명시해줘야만 합니다.

js
// 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')
} } } })

참고로 루트를 변경한다 해도 __dirname은 여전히 vite.config.js 파일이 위치한 폴더를 가리키고 있다는 것을 유의하세요. 이를 방지하고자 한다면 resolve의 인자로 root 엔트리를 함께 전달해 줘야 합니다.

HTML 파일의 경우, Vite는 rollupOptions.input 객체에 명시된 엔트리의 이름을 무시하고, 대신 dist 폴더에 HTML 에셋을 생성할 때 확인할 수 있는 파일의 id를 사용합니다. 이는 개발 서버가 작동하는 방식과 일관성을 유지할 수 있도록 합니다.

라이브러리 모드

만약 브라우저 기반의 라이브러리를 개발하고 있다면, 라이브러리 갱신 시마다 테스트 페이지에서 이를 불러오는 데 많은 시간을 소모할 것입니다. Vite는 index.html을 이용해 좀 더 나은 개발 환경(경험)을 마련해줍니다.

라이브러리 배포 시점에서, build.lib 설정 옵션을 이용해보세요. 또한 라이브러리에 포함하지 않을 디펜던시를 명시할 수도 있습니다. vuereact 같이 말이죠.

js
// 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 구문이 포함되게 됩니다:

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

위와 같은 Rollup 설정과 함께 vite build 명령을 실행하게 되면, esumd 두 가지의 포맷으로 번들링 과정이 진행되게 됩니다(이에 대해 조금 더 자세히 알고 싶다면 build.lib 설정을 참고해주세요).

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

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

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"
    }
  }
}

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

json
{
  "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"' }을 사용하거나, 번들러와 런타임과의 호환성을 위해 esm-env을 사용할 수 있습니다.

심화 활용법

라이브러리 모드는 브라우저 지향의 JS 프레임워크 라이브러리를 위한 간단하고 명확한 설정들을 포함합니다. 제작 중이신 라이브러리가 브라우저 대상이 아니거나 고도의 빌드 플로우가 요구된다면 Rollup 또는 esbuild를 사용해 주세요.

Base 옵션 상세 설정

WARNING

실험적인 기능입니다. 이 곳에 피드백을 남겨주세요.

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

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

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

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

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

ts
experimental
: {
renderBuiltUrl
(
filename
, {
hostId
,
hostType
,
type
}) {
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
} }, },

Vite는 렌더링 시 자동으로 URL 인코딩을 수행합니다. 따라서 함수로 전달되는 filename은 모두 디코딩된 URL이며, 함수에서 반환하는 URL 문자열도 디코딩된 URL이어야 합니다. 다만 runtime을 포함한 객체를 반환하는 경우에는 명시된 코드가 그대로 렌더링 되기에 이를 사용하는 곳에서 인코딩을 직접 처리해 줘야 합니다.

Released under the MIT License.