Vite 플러그인 개발 가이드: 커스텀 빌드 도구 만들기

프론트엔드

Vite플러그인빌드 도구Rollup번들러

이 글은 누구를 위한 것인가

  • Vite 플러그인을 직접 만들어 빌드 파이프라인을 확장하려는 팀
  • 코드 변환, 가상 모듈, HMR 커스터마이징이 필요한 개발자
  • Rollup 플러그인을 Vite에서 활용하거나 마이그레이션하려는 팀

들어가며

Vite 플러그인은 Rollup 플러그인 인터페이스를 확장한다. transform, resolveId, load 같은 Rollup 훅에 Vite 전용 훅(configureServer, handleHotUpdate)이 추가된다. 플러그인 하나로 개발 서버와 프로덕션 빌드 모두를 처리한다.

이 글은 bluefoxdev.kr의 Vite 플러그인 개발 가이드 를 참고하여 작성했습니다.


1. Vite 플러그인 훅 개요

[Rollup 공용 훅 (빌드 단계)]
  options        → Rollup 설정 수정
  buildStart     → 빌드 시작
  resolveId      → 모듈 경로 해석 (가상 모듈용)
  load           → 모듈 소스 로드
  transform      → 모듈 소스 변환
  buildEnd       → 빌드 완료
  generateBundle → 번들 생성 직전
  writeBundle    → 번들 디스크 기록 후

[Vite 전용 훅]
  config           → Vite 설정 수정/반환
  configResolved   → 최종 설정 읽기
  configureServer  → 개발 서버 미들웨어 추가
  configurePreviewServer → 프리뷰 서버 미들웨어
  transformIndexHtml → index.html 변환
  handleHotUpdate  → HMR 커스터마이징

[훅 실행 순서 (개발 서버)]
  config → configResolved → configureServer
  요청마다: resolveId → load → transform

[플러그인 적용 순서 제어]
  enforce: 'pre'   → 내장 플러그인 전에 실행
  enforce: 'post'  → 내장 플러그인 후에 실행
  (없음)           → 내장 플러그인 사이에 실행
  
  apply: 'build' | 'serve'  → 빌드 또는 개발만 적용

2. Vite 플러그인 구현

// 1. 기본 플러그인 구조
import type { Plugin } from 'vite';

function myPlugin(): Plugin {
  return {
    name: 'vite-plugin-my',  // 필수, 에러 메시지에 표시
    enforce: 'pre',
    apply: 'build',  // 빌드 시에만 적용
    
    // Vite 설정 수정
    config(config) {
      return {
        define: {
          __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
        },
      };
    },
    
    // 모듈 변환
    transform(code, id) {
      if (!id.endsWith('.vue')) return;
      
      // Vue SFC 처리 예시
      return {
        code: code.replace(/console\.log\([^)]*\)/g, ''),  // console.log 제거
        map: null,
      };
    },
  };
}
// 2. 가상 모듈 패턴 (빌드 시 데이터 주입)
import type { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';

interface RouteInfo {
  path: string;
  component: string;
}

function autoRoutesPlugin(dir: string): Plugin {
  const VIRTUAL_MODULE_ID = 'virtual:auto-routes';
  const RESOLVED_ID = '\0virtual:auto-routes';
  
  function generateRoutes(): RouteInfo[] {
    const pagesDir = path.resolve(process.cwd(), dir);
    const files = fs.readdirSync(pagesDir, { recursive: true }) as string[];
    
    return files
      .filter(f => f.endsWith('.tsx') || f.endsWith('.vue'))
      .map(file => ({
        path: '/' + file.replace(/\.(tsx|vue)$/, '').replace(/index$/, ''),
        component: path.join(pagesDir, file),
      }));
  }
  
  return {
    name: 'vite-plugin-auto-routes',
    
    resolveId(id) {
      if (id === VIRTUAL_MODULE_ID) {
        return RESOLVED_ID;  // 가상 모듈 ID 반환
      }
    },
    
    load(id) {
      if (id === RESOLVED_ID) {
        const routes = generateRoutes();
        
        // 동적 import로 코드 분할
        const code = `
export const routes = [
  ${routes.map(r => `
  {
    path: '${r.path}',
    component: () => import('${r.component}'),
  }`).join(',\n')}
];
        `;
        
        return { code };
      }
    },
    
    // 파일 변경 시 HMR
    configureServer(server) {
      server.watcher.on('add', (file) => {
        if (file.startsWith(path.resolve(process.cwd(), dir))) {
          const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
          if (mod) {
            server.moduleGraph.invalidateModule(mod);
            server.hot.send({ type: 'full-reload' });
          }
        }
      });
    },
  };
}

// 사용: import { routes } from 'virtual:auto-routes';
// vite.config.ts: plugins: [autoRoutesPlugin('src/pages')]
// 3. SVG → React 컴포넌트 변환 플러그인
import type { Plugin, TransformResult } from 'vite';
import { optimize } from 'svgo';

function svgToReact(): Plugin {
  return {
    name: 'vite-plugin-svg-to-react',
    enforce: 'pre',
    
    async transform(code, id): Promise<TransformResult | null> {
      if (!id.endsWith('.svg?react') && !id.includes('.svg?react&')) {
        return null;
      }
      
      const filePath = id.split('?')[0];
      const svgContent = await import('fs').then(
        fs => fs.promises.readFile(filePath, 'utf-8')
      );
      
      // SVGO로 최적화
      const optimized = optimize(svgContent, {
        plugins: ['preset-default', 'removeDimensions'],
      });
      
      const optimizedSvg = optimized.data;
      
      // SVG → React 컴포넌트
      const component = `
import React from 'react';

export default function SvgIcon(props) {
  return (
    ${optimizedSvg.replace('<svg', '<svg {...props}').replace(/class=/g, 'className=')}
  );
}
      `;
      
      return { code: component };
    },
  };
}

// 4. 환경변수 검증 플러그인
function envValidatorPlugin(schema: Record<string, string>): Plugin {
  return {
    name: 'vite-plugin-env-validator',
    enforce: 'pre',
    
    configResolved(config) {
      const missing = Object.keys(schema).filter(
        key => !config.env[key] && !process.env[key]
      );
      
      if (missing.length > 0) {
        throw new Error(
          `Missing required environment variables:\n${missing.map(k => `  - ${k}: ${schema[k]}`).join('\n')}`
        );
      }
    },
  };
}
// 5. index.html 변환 플러그인 + 개발 서버 미들웨어
import type { Plugin } from 'vite';

function htmlTransformPlugin(): Plugin {
  return {
    name: 'vite-plugin-html-transform',
    
    // index.html에 nonce, CSP 메타태그 주입
    transformIndexHtml: {
      order: 'pre',
      handler(html, ctx) {
        const nonce = crypto.randomUUID().replace(/-/g, '');
        
        return html.replace(
          '<head>',
          `<head>
  <meta http-equiv="Content-Security-Policy" 
    content="default-src 'self'; script-src 'nonce-${nonce}'">`,
        );
      },
    },
    
    // 개발 서버에 API 프록시 미들웨어 추가
    configureServer(server) {
      server.middlewares.use('/api/mock', (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({ mocked: true, path: req.url }));
      });
    },
  };
}

// vite.config.ts 최종 설정
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react(),
    autoRoutesPlugin('src/pages'),
    svgToReact(),
    envValidatorPlugin({
      VITE_API_URL: 'API 서버 URL',
      VITE_APP_NAME: '앱 이름',
    }),
    htmlTransformPlugin(),
  ],
});

마무리

Vite 플러그인의 핵심 패턴은 세 가지다. 첫째, resolveId + load로 가상 모듈을 만들어 빌드 시 생성된 데이터를 JavaScript처럼 import한다. 둘째, transform으로 특정 파일 형식을 원하는 코드로 변환한다. 셋째, configureServer로 개발 서버에 미들웨어를 추가한다. enforce: 'pre'로 내장 플러그인보다 먼저 실행되고, apply: 'serve' | 'build'로 환경을 분리한다.