Vite 5 고급 가이드 — Rollup·플러그인·HMR·프로덕션 최적화

Vite 5 고급 가이드 — Rollup·플러그인·HMR·프로덕션 최적화

이 글의 핵심

Vite 5에서 프로덕션 번들러로 쓰이는 Rollup 4와의 연동 방식, 환경별 설정, 커스텀 플러그인 작성, HMR 병목 제거, 프로덕션 청크 전략, React·Vue·Svelte 공식 플러그인과의 조합을 한 번에 정리합니다.

이 글의 핵심

Vite 5는 개발 서버에서는 네이티브 ESM과 esbuild 기반의 사전 번들로 빠른 시작을 제공하고, 프로덕션 빌드에서는 Rollup 4를 통해 트리 쉐이킹·코드 스플리팅·에셋 파이프라인을 완성합니다. 본 글은 입문용 “설치와 기본 설정”을 넘어, 환경별 구성, 커스텀 플러그인, HMR 튜닝, 번들 최적화, React·Vue·Svelte 통합을 실무 관점에서 연결합니다.

선행 지식: vite.config.ts, ESM, import.meta, Node 18+ 환경에 익숙하다고 가정합니다. Vite 기초는 저장소 내 vite-5-complete-guide 등 입문 글을 먼저 보시면 흐름이 매끄럽습니다.


1. 아키텍처: 개발 서버와 프로덕션 번들

1.1 esbuild와 Rollup의 역할 분담

개발 모드에서 Vite는 의존성을 esbuild로 사전 번들(pre-bundle) 하고, 애플리케이션 소스는 변환만 거쳐 브라우저 네이티브 ESM으로 제공합니다. 반면 vite buildRollup을 통해 청크 그래프를 만들고, 압축·에셋 처리·CSS 분리 등을 수행합니다. 따라서 성능 이슈를 “개발”과 “빌드”로 나누어 진단하는 것이 첫 단계입니다.

1.2 Vite 4 대비: Rollup 3에서 Rollup 4로

Vite 5는 내부적으로 Rollup 4 계열을 사용합니다. 기존 Vite 4 + Rollup 3 시절의 커스텀 플러그인·설정을 이식할 때는 다음을 특히 점검합니다.

  • Import attributes 등 문법·플러그인 API 변화
  • this.resolve / this.parse 기본 동작 차이로 인한 해석 순서 변화
  • 순수 ESM 환경에서의 패키지 exports 필드 해석

공식 마이그레이션 문서의 브레이킹 체인지 목록과 함께, Lockfile과 peer dependency를 재설치하여 재현 가능한 빌드를 만드는 것이 안전합니다.


2. 환경별 설정: 모드, 디렉터리, loadEnv

2.1 defineConfig와 조건부 분기

운영·스테이징·로컬에서 동일한 저장소를 쓰되 엔드포인트·플래그만 바꾸려면, 설정 파일에서 modecommand를 기준으로 분기하는 패턴이 흔합니다.

// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd(), '');
  const isProd = mode === 'production';

  return {
    plugins: [react()],
    define: {
      __APP_VERSION__: JSON.stringify(env.VITE_APP_VERSION ?? '0.0.0'),
    },
    server: {
      port: Number(env.VITE_DEV_PORT) || 5173,
    },
    build: {
      sourcemap: !isProd,
    },
  };
});

loadEnv.env, .env.local, .env.[mode] 등을 모드에 맞게 병합합니다. 클라이언트에 노출할 변수만 VITE_ 접두사를 붙이는 관례를 지키면, 실수로 서버 전용 비밀을 번들에 싣는 사고를 줄일 수 있습니다.

2.2 NODE_ENVimport.meta.env의 관계

Vite는 import.meta.env.MODE, import.meta.env.PROD, import.meta.env.DEV 등을 주입합니다. 라이브러리 코드에서 process.env.NODE_ENV를 직접 참조하는 경우, define 치환 또는 호환 플러그인이 필요할 수 있으므로, 배포 파이프라인에서 프로덕션 빌드 한 번으로 재현되는지 꼭 확인합니다.


3. Rollup 옵션과 Vite 빌드 파이프라인

3.1 build.rollupOptions로 청크 전략 잡기

프로덕션 성능의 핵심은 초기 JS 페이로드캐시 무효화 범위의 균형입니다. manualChunks로 벤더·라우트 단위를 나누되, 너무 잘게 쪼개면 HTTP/2에서도 오버헤드가 커질 수 있습니다.

// vite.config.ts (발췌)
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react-dom')) return 'vendor-react-dom';
            if (id.includes('react')) return 'vendor-react';
            return 'vendor';
          }
        },
      },
    },
  },
});

실무에서는 번들 분석기(rollup-plugin-visualizer 등) 로 실제 청크 크기를 본 뒤, 라이브러리별로 그룹을 조정합니다. 팀 내에서 “벤더는 장기 캐시, 앱 코드는 자주 배포” 같은 정책을 맞춰 두면 운영이 단순해집니다.

3.2 resolve.alias와 모노레포

모노레포에서 패키지 경로를 workspace:*로 묶을 때, Vite의 resolve.alias개발 서버와 빌드 모두에 영향을 줍니다. 조건부 exports 가 있는 패키지는 main/module만 보고 착각하기 쉬우므로, 실제 해석 결과를 vite --debug 또는 플러그인 로그로 확인하는 것이 좋습니다.


4. 커스텀 플러그인 개발

4.1 Vite 플러그인의 최소 골격

Vite 플러그인은 Rollup 호환 훅에 더해 configureServer, transformIndexHtml 등 개발 서버 전용 훅을 제공합니다. 아래는 가상 모듈을 노출하는 작은 예입니다.

// plugins/virtual-config.ts
import type { Plugin } from 'vite';

export function virtualConfigPlugin(data: Record<string, unknown>): Plugin {
  const virtualId = '\0virtual:app-config';

  return {
    name: 'virtual-app-config',
    resolveId(id) {
      if (id === 'virtual:app-config') return virtualId;
    },
    load(id) {
      if (id === virtualId) {
        return `export default ${JSON.stringify(data)};`;
      }
    },
  };
}

resolveId에서 \0 접두사를 쓰면 실제 파일 시스템과 충돌을 줄일 수 있습니다. 가상 모듈은 HMR 경로를 설계할 때 모듈 그래프 상 식별자로 취급해야 합니다.

4.2 enforceapply로 실행 순서 제어

  • enforce: 'pre' | 'post': 동일 훅 내에서 다른 플러그인보다 먼저/나중에 실행
  • apply: 'serve' | 'build' | ((config, env) => boolean): 개발 전용·빌드 전용 분리

예를 들어 개발 서버에서만 목(mock) 데이터를 주입하고, 프로덕션에서는 완전히 제거하려면 apply: 'serve'로 비용을 없앨 수 있습니다.

4.3 Rollup 플러그인 호환성 점검 체크리스트

  1. buildStart/generateBundle에서 부수 효과(파일 쓰기)가 개발 모드에도 돌지 않는지
  2. this.getModuleInfo 등 Rollup 4 API 변화에 맞는지
  3. CSS/에셋을 다루는 경우 Vite의 자산 URL 처리와 충돌하지 않는지

5. HMR 최적화 기법

5.1 병목: 의존성 사전 번들과 감시 범위

HMR이 느릴 때 흔한 원인은 거대한 node_modules 의존성이 매번 재처리되거나, 불필요하게 넓은 파일 감시입니다. optimizeDeps명시적 포함/제외를 지정하면 cold start와 재시작 이후 안정성이 좋아집니다.

// vite.config.ts (발췌)
export default defineConfig({
  optimizeDeps: {
    include: ['react', 'react-dom', 'scheduler'],
    exclude: ['큰-로컬-패키지-이름'],
  },
  server: {
    watch: {
      ignored: ['**/coverage/**', '**/dist/**'],
    },
  },
});

모노레포 루트에 로그·빌드 산출물이 많다면 watch.ignoredOS 파일 감시 한도에 걸리는 문제를 완화할 수 있습니다.

5.2 server.hmr와 네트워크 레이어

Docker, WSL2, 원격 컨테이너, HTTPS 프록시 뒤에서 개발할 때는 WebSocket 경로·포트·프로토콜 불일치로 HMR이 끊깁니다. 이때 server.hmr클라이언트가 접속해야 할 호스트/포트/프로토콜을 명시적으로 맞춥니다.

server: {
  host: true,
  port: 5173,
  strictPort: true,
  hmr: {
    protocol: 'wss',
    host: 'localhost',
    clientPort: 443,
  },
},

환경마다 값이 달라지면 환경 변수로 주입하고, 팀 문서에 프록시(Nginx, Caddy)의 Upgrade 헤더 설정을 함께 적어 두는 것이 좋습니다.

5.3 경계 모듈과 “풀 리로드” 최소화

순수 부수 효과가 큰 모듈(전역 스토어 초기화, 사이드 이펙트 import)은 HMR 시 풀 리로드로 떨어지기 쉽습니다. 상태 관리 라이브러리·컨텍스트 설계 시 모듈 단위로 경계를 나누고, 가능하면 import.meta.hot.accept로 국소 갱신을 허용하는 패턴을 고려합니다.

5.4 오버레이와 로그 소음

server.hmr.overlay로 런타임 에러 오버레이를 끄거나 줄일 수 있습니다. 다만 CI나 스토리북 환경에서는 에러를 숨기기보다 근본 원인을 제거하는 편이 안전합니다.


6. 프로덕션 빌드 최적화

6.1 압축기 선택: esbuild vs terser

Vite는 기본적으로 esbuild로 minify합니다. 호이스팅·특정 라이브러리와의 호환 이슈로 더 보수적인 압축이 필요하면 build.minify: 'terser'terserOptions를 검토합니다. 대신 빌드 시간이 늘어날 수 있으므로 CI에서 측정합니다.

6.2 CSS 코드 스플리팅과 assetsInlineLimit

build.cssCodeSplit은 라우트 단위 CSS 분리에 유리합니다. 작은 에셋은 Data URL 인라인으로 HTTP 요청 수를 줄이는데, build.assetsInlineLimit으로 기준을 조정합니다. 디자인 시스템 아이콘 SVG가 많다면 스프라이트·심볼 스택과 함께 전략을 통일합니다.

6.3 build.target과 폴리필

build.target은 출력 문법 수준을 결정합니다. 구형 브라우저 지원 범위가 넓다면 트랜스파일 부담이 커지므로, Browserslist·실 사용자 데이터를 기준으로 타깃을 좁히는 것이 우선입니다. 폴리필은 필요한 API만 core-js 등으로 채우고, 불필요한 전역 주입을 피합니다.

6.4 소스맵과 배포

build.sourcemap을 켜면 디버깅은 쉬워지지만 산출물 크기·배포 파이프라인이 무거워집니다. Sentry 등에 숨은 소스맵을 올리는 경우, 공개 디렉터리에 노출되지 않게 CI 단계를 분리합니다.


7. React·Vue·Svelte 통합

7.1 React: @vitejs/plugin-react와 SWC 옵션

공식 React 플러그인은 Fast Refresh를 지원합니다. 대규모 코드베이스에서는 @vitejs/plugin-react-swc 로 컴파일러 레이어를 바꿔 리프레시·빌드 시간을 줄이는 사례가 많습니다. 둘 다 쓸 수 없으므로 하나만 선택합니다.

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

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-styled-components', { displayName: true, ssr: false }]],
      },
    }),
  ],
});

스타일드 컴포넌트·emotion 등 Babel 플러그인이 필수라면 SWC 대신 Babel 경로를 유지하는 결정이 자연스럽습니다.

7.2 Vue: @vitejs/plugin-vue<script setup>

Vue SFC는 Vite와 궁합이 좋습니다. 타입 기반 defineProps 등 최신 문법을 쓸 때는 vue-tsc빌드 전 타입 검증을 병행합니다. 라우트 기반 코드 스플리팅은 import() 동적 로딩과 함께 청크 이름을 관찰합니다.

7.3 Svelte: vite-plugin-svelte와 전처리

SvelteKit이 아닌 순수 Vite + Svelte를 쓸 때는 vite-plugin-svelte 설정에서 전처리기(sass, postcss)HMR 안정성 옵션을 맞춥니다. 스타일 전역 오염을 막기 위해 컴포넌트 스코프 CSS 전략을 팀 규칙으로 두면 HMR 경로도 단순해집니다.

7.4 프레임워크 공통: SSR·미들웨어

Vite 자체는 SSR 가이드를 제공하지만, Next·Nuxt·SvelteKit 같은 프레임워크가 통합 스택을 제공하는 경우가 많습니다. “Vite만” 도입할지 “풀스택 프레임워크”를 쓸지에 따라 라우팅·데이터 로딩·환경 분리 책임이 달라지므로, 팀 역량과 배포 환경에 맞춰 선택합니다.


8. 트러블슈팅 요약

증상우선 확인
cold start 느림optimizeDeps, 사전 번들 캐시, 불필요한 플러그인
HMR 미작동프록시·WSS·server.hmr·방화벽
빌드 OOM청크 분할, build.rollupOptions, Node 메모리 플래그
해석 오류resolve.dedupe, 패키지 exports, alias 충돌

9. 맺음말

Vite 5는 개발 경험과 프로덕션 번들을 명확히 분리된 두 레이어로 다루는 도구입니다. Rollup 4 기반 빌드, loadEnv를 통한 환경 주입, 플러그인의 apply/enforce, HMR 네트워크·사전 번들 튜닝, 프레임워크 플러그인 선택을 한 흐름으로 설계하면, 팀 전체의 빌드 시간과 디버깅 비용을 동시에 줄일 수 있습니다. 변경이 클 때는 마이그레이션 문서·번들 리포트·실제 배포 스테이징 세 가지를 항상 세트로 두시기 바랍니다.

배포 전에는 git add 후 커밋·푸시를 마친 뒤, 이 저장소 관례에 따라 npm run deploy를 실행하세요.