본문으로 건너뛰기 Vite 완벽 가이드: 차세대 프론트엔드 빌드 도구

Vite 완벽 가이드: 차세대 프론트엔드 빌드 도구

Vite 완벽 가이드: 차세대 프론트엔드 빌드 도구

이 글의 핵심

Vite는 esbuild 기반의 초고속 개발 서버와 Rollup 기반의 최적화된 프로덕션 빌드를 제공하는 차세대 빌드 도구입니다. 네이티브 ESM을 활용하여 번들링 없이 즉시 서버를 시작하고, HMR로 빠른 피드백 루프를 제공합니다. 이 글에서는 Vite가 빠른 이유가 되는 내부 동작 원리와, 실무에서 바로 쓸 수 있는 설정 예제를 함께 다룹니다.

들어가며

Vite(프랑스어로 ‘빠른’이라는 뜻)는 Vue.js 창시자인 Evan You가 만든 프론트엔드 빌드 도구입니다. Webpack이 오랫동안 지배해 온 자리를 빠르게 대체하고 있는데, 그 이유는 단순히 “더 빠르다”는 마케팅 문구 때문이 아니라 개발 서버와 프로덕션 빌드를 근본적으로 다른 방식으로 처리하기 때문입니다. 이 글에서는 Vite가 왜 빠른지에 대한 내부 동작 원리를 먼저 짚어보고, 이어서 실무에서 바로 활용할 수 있는 vite.config 설정 예제와 Webpack에서 넘어올 때 자주 겪는 문제들을 정리합니다. Vite 자체의 소개나 프레임워크별 기능 비교는 다른 글(vite-complete-guide)에서 폭넓게 다루고 있으므로, 이 글은 빌드 도구가 실제로 어떻게 동작하는지와 설정 파일을 어떻게 짜야 하는지에 초점을 맞춥니다.

Vite의 동작 원리: 왜 이렇게 빠른가

개발 서버 — 번들링을 생략하는 전략

Webpack은 개발 서버를 시작하기 전에 애플리케이션 전체를 한 번 번들링합니다. 파일이 수백 개, 수천 개로 늘어나면 이 초기 번들링 시간이 30초에서 60초까지 늘어나는 경우가 흔합니다. 코드를 한 줄만 수정해도 HMR(Hot Module Replacement)이 관련된 그래프를 다시 계산해야 하므로, 프로젝트가 커질수록 저장 후 반영까지 걸리는 시간도 함께 길어집니다.

Vite는 이 문제를 다른 방식으로 해결합니다. 최신 브라우저가 네이티브 ES 모듈(<script type="module">)을 직접 로드할 수 있다는 점을 활용해서, 개발 서버는 애플리케이션을 사전에 번들링하지 않습니다. 브라우저가 import 구문을 만나면 해당 모듈을 브라우저가 직접 요청하고, Vite 서버는 요청이 들어온 파일만 그 순간에 변환해서 응답합니다. 즉 “필요한 것만, 요청받은 순간에” 처리하는 온디맨드(on-demand) 방식입니다. 이 방식 덕분에 프로젝트 규모와 무관하게 개발 서버는 12초 안에 뜨는 경우가 대부분이며, 파일을 수정했을 때도 변경된 모듈 하나만 다시 컴파일해서 브라우저로 전송하기 때문에 HMR이 보통 50200ms 안에 끝납니다.

다만 사용자가 설치한 node_modules의 의존성 패키지들은 예외입니다. lodash-es처럼 수백 개의 내부 파일로 구성된 패키지를 브라우저가 ESM 요청으로 그대로 가져오면 요청 수가 폭발적으로 늘어나 오히려 느려질 수 있습니다. 그래서 Vite는 이런 의존성들을 서버 시작 시점에 esbuild로 한 번 사전 번들링(pre-bundling)합니다. esbuild는 Go 언어로 작성된 번들러라서 JavaScript로 작성된 번들러보다 수십 배 빠른 속도를 냅니다. 이 사전 번들링 결과는 node_modules/.vite에 캐시되며, 의존성이 바뀌지 않는 한 재사용됩니다.

프로덕션 빌드 — Rollup으로 최적화

개발 시의 온디맨드 방식은 파일 요청이 잦아도 로컬 네트워크에서는 문제가 없지만, 실제 서비스 환경에서 모듈 단위로 파일을 쪼개어 배포하면 HTTP 요청 수가 지나치게 많아집니다. 그래서 Vite는 프로덕션 빌드 단계에서는 완전히 다른 도구인 Rollup을 사용합니다. Rollup은 트리 셰이킹(사용하지 않는 코드 제거)과 정교한 코드 스플리팅을 지원하는 성숙한 번들러로, 결과적으로 몇 개의 최적화된 청크 파일로 묶어냅니다.

정리하면 Vite의 핵심 아이디어는 “개발 환경과 프로덕션 환경에 맞는 서로 다른 전략을 각각 최적의 도구로 처리한다”는 것입니다. 개발할 때는 번들링을 생략해 속도를 얻고, 배포할 때는 Rollup으로 실제 서비스에 적합한 번들을 만드는 구조입니다.

프로젝트 시작하기

CLI로 프로젝트 생성

새 프로젝트를 시작할 때는 공식 CLI를 사용하는 것이 가장 간단합니다. 대화형으로 프레임워크와 언어(JavaScript/TypeScript)를 선택할 수도 있고, CI 스크립트에서는 템플릿을 직접 지정할 수도 있습니다.

npm create vite@latest
npm create vite@latest my-app -- --template react-ts

React, Vue, Svelte, Solid, Preact, Lit 등 주요 프레임워크의 템플릿을 기본으로 제공하므로, 프레임워크를 바꾸더라도 학습 곡선이 크지 않습니다. 이미 존재하는 프로젝트에 Vite를 수동으로 붙이고 싶다면 npm install -D vite를 실행한 뒤, React를 쓰는 경우 @vitejs/plugin-react를 함께 설치하면 됩니다.

디렉터리 구조 이해하기

Vite 프로젝트는 Webpack 프로젝트와 구조가 다소 다릅니다. 가장 눈에 띄는 차이는 index.htmlpublic 폴더 안이 아니라 프로젝트 루트에 위치한다는 점입니다. Webpack에서는 index.html이 단순한 템플릿이었지만, Vite에서는 이 HTML 파일 자체가 애플리케이션의 진입점(entry point) 역할을 합니다. 마이그레이션할 때 반드시 익혀야 하는 부분입니다.

my-vite-app/
├── index.html         # 애플리케이션의 진입점
├── src/
│   └── main.tsx        # index.html에서 <script type="module">로 로드
├── public/              # 그대로 복사되는 정적 자산
├── vite.config.ts
└── dist/                # 빌드 결과물

vite.config 실전 설정

기본 설정 (React 예시)

vite.config는 서버 옵션, 빌드 옵션, 경로 별칭(alias), 플러그인을 한 곳에서 관리합니다. 아래는 React 프로젝트에서 자주 쓰는 조합으로, 개발 서버 포트와 자동 브라우저 열기, 프로덕션 빌드의 출력 폴더와 소스맵, 벤더 청크 분리, @ 별칭, VITE_ 환경 변수 접두사를 한 번에 설정한 예시입니다.

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

export default defineConfig({
  plugins: [react()],
  server: { port: 3000, open: true, host: true },
  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'esbuild',
    rollupOptions: {
      output: {
        // React, ReactDOM처럼 자주 바뀌지 않는 라이브러리를
        // 별도 청크로 분리해 캐시 효율을 높입니다.
        manualChunks: { vendor: ['react', 'react-dom'] },
      },
    },
  },
  resolve: {
    alias: { '@': '/src', '@components': '/src/components' },
  },
  envPrefix: 'VITE_',
});

server.host: true는 로컬 네트워크의 다른 기기(모바일 디바이스 등)에서 개발 서버에 접속할 수 있도록 열어주는 옵션입니다. build.rollupOptions.output.manualChunks는 자주 변경되지 않는 라이브러리 코드를 애플리케이션 코드와 분리해서, 배포 시 사용자가 애플리케이션 코드만 새로 내려받도록 만드는 캐싱 전략입니다.

TypeScript 별칭과 API 프록시

TypeScript 프로젝트에서는 path.resolve(__dirname, ...)로 별칭 경로를 더 명확하게 지정하는 방식을 흔히 사용합니다. 또한 백엔드 API 서버가 다른 포트에서 돌아가는 경우, server.proxy에 규칙을 등록하면 로컬 개발 중 CORS 문제를 신경 쓰지 않고 상대 경로로 API를 호출할 수 있습니다.

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

export default defineConfig({
  plugins: [react()],
  resolve: { alias: { '@': path.resolve(__dirname, './src') } },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true,
        rewrite: (p) => p.replace(/^\/api/, ''),
      },
    },
  },
});

이 설정은 프런트엔드 코드에서 /api/users로 요청을 보내면 Vite 개발 서버가 이를 http://localhost:5000/users로 그대로 전달해 주는 역할을 합니다. 프로덕션 환경에서는 보통 Nginx나 클라우드 인프라의 리버스 프록시가 같은 역할을 대신하므로, server.proxy는 로컬 개발 환경에서만 의미가 있는 설정이라는 점을 기억해 두면 좋습니다.

Webpack에서 넘어올 때 반드시 알아야 할 것

환경 변수 접근 방식의 변화

Webpack 기반 프로젝트(특히 Create React App)에서는 process.env.REACT_APP_* 형태로 환경 변수에 접근했습니다. Vite에서는 이 방식이 그대로 동작하지 않습니다. .env 파일에 정의한 변수는 반드시 VITE_ 접두사를 붙여야 클라이언트 코드에 노출되며, 접근할 때는 import.meta.env.VITE_API_URL처럼 import.meta.env 객체를 사용합니다. 현재 모드가 개발인지 프로덕션인지는 import.meta.env.DEV, import.meta.env.PROD, import.meta.env.MODE로 확인할 수 있습니다.

브라우저 코드에서 Node.js의 process 전역 객체를 직접 참조하는 패턴이 남아 있다면, 이 부분을 import.meta.env로 바꿔주는 작업이 마이그레이션에서 가장 먼저 손봐야 할 지점입니다. 또한 require를 사용하는 CommonJS 스타일의 코드는 대부분 import 문법으로 바꿔야 하며, 반드시 CommonJS 형태로만 배포되는 패키지가 있다면 optimizeDeps.include에 등록해 두면 사전 번들링 단계에서 ESM으로 변환됩니다.

정적 자산과 특수 임포트 쿼리

이미지나 CSS를 다루는 방식은 Webpack과 크게 다르지 않습니다. import logo from './logo.svg'처럼 자산을 그대로 임포트할 수 있고, public 폴더에 넣은 파일은 루트 경로(/)로 그대로 서빙됩니다. 다만 Vite는 ?url(URL 문자열로 가져오기), ?raw(파일 내용을 문자열로 가져오기), ?worker(Web Worker로 등록하기) 같은 특수 쿼리 파라미터를 지원하므로, 상황에 맞게 활용하면 별도 로더 설정 없이도 원하는 형태로 자산을 불러올 수 있습니다.

프로덕션 최적화 전략

코드 스플리팅과 번들 분석

개발 서버가 빠르다고 해서 프로덕션 번들도 자동으로 가벼워지는 것은 아닙니다. 이 부분은 Vite만의 문제가 아니라 프런트엔드 빌드 전반에 공통적으로 적용되는 원칙입니다. React.lazySuspense를 조합해 라우트 단위로 코드를 나누는 패턴은 Webpack에서 쓰던 것과 동일하게 Vite에서도 유효합니다. 번들 크기가 예상보다 크게 느껴진다면, rollup-plugin-visualizer로 어떤 모듈이 번들을 부풀리고 있는지 시각적으로 확인하는 것이 첫 단계입니다.

사전 번들 캐시 문제 다루기

개발 서버가 갑자기 오래된 코드를 보여주거나 의존성 관련 오류가 발생한다면, node_modules/.vite에 저장된 사전 번들링 캐시가 최신 상태와 어긋난 경우가 많습니다. vite --force 옵션이나 optimizeDeps.force: true 설정으로 캐시를 강제로 다시 생성하면 해결되는 경우가 대부분입니다.

자주 발생하는 문제와 해결 방법

Webpack 프로젝트를 옮길 때 실제로 자주 마주치는 문제들은 다음과 같습니다.

  • Node.js 코어 모듈 참조 오류: fs, path 같은 Node.js 전용 모듈을 브라우저에서 실행되는 코드가 직접 임포트하려고 하면 빌드가 깨집니다. 해당 로직은 서버 사이드로 옮기거나, 브라우저에서 동작하는 대체 라이브러리로 바꿔야 합니다.
  • CJS 전용 패키지 오류: 일부 오래된 패키지는 CommonJS 형식으로만 배포되어 있어 ESM 환경에서 바로 동작하지 않습니다. 이 경우 optimizeDeps.include에 등록하거나, 최신 버전(ESM을 지원하는)으로 교체하는 것이 근본적인 해결책입니다.
  • 경로 별칭 불일치로 인한 Module not found: vite.configresolve.aliastsconfig.jsonpaths 설정이 서로 일치하지 않으면 에디터에서는 타입 에러가 없어도 빌드 시 모듈을 찾지 못하는 오류가 발생합니다. 두 설정 파일의 별칭을 항상 함께 관리해야 합니다.
  • SSR 경계 문제: 서버 사이드 렌더링을 함께 사용하는 프레임워크(Nuxt, SvelteKit 등)에서는 브라우저 전용 API(window, document)를 서버 코드에서 참조하면 빌드나 런타임에서 오류가 발생합니다. import.meta.env.SSR 플래그로 실행 환경을 분기하는 것이 일반적인 대응 방법입니다.

이러한 문제들은 Vite라는 도구 자체의 결함이 아니라, 도구를 바꾸는 과정에서 기존 코드에 숨어 있던 암묵적인 가정(Node 전용 모듈에 대한 의존, CJS 패키지, 별칭 불일치 등)이 드러나는 경우입니다. 도구를 바꾼다고 해서 설계상의 문제가 저절로 해결되지는 않으므로, 마이그레이션 과정을 기존 코드를 점검하는 기회로 활용하는 것이 바람직합니다.

마무리

Vite로 옮기는 작업은 단순히 설정 파일의 줄 수를 줄이는 것이 목적이 아닙니다. index.html의 위치, 환경 변수 접근 방식, ESM 기반 임포트 모델을 새로 익히는 비용이 분명히 존재합니다. 하지만 그 비용을 지불하고 나면, 개발 서버의 콜드 스타트와 HMR 속도에서 체감할 수 있는 차이를 얻게 됩니다. 프로덕션 빌드는 성숙한 번들러인 Rollup이 담당하기 때문에, “개발 환경에서는 번들링을 생략하고 배포 환경에서는 최적화된 번들을 만든다”는 Vite의 기본 방정식은 실제 서비스 운영에도 잘 맞아떨어집니다. Vite 생태계 전반의 기능이나 프레임워크별 활용 사례가 더 필요하다면, 함께 참고할 수 있도록 관련 글들을 아래에 정리해 두었습니다.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.