이 글은 누구를 위한 것인가
- 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'로 환경을 분리한다.