이미지 로딩 중...
AI Generated
2025. 11. 25. · 4 Views
Vite 커스텀 플러그인 개발 완벽 가이드
Vite 플러그인 시스템의 핵심 훅과 개념을 실무 예제와 함께 학습합니다. 초급 개발자도 쉽게 따라할 수 있도록 플러그인 기본 구조부터 가상 모듈까지 단계별로 안내합니다.
목차
- 플러그인 기본 구조 작성
- transform 훅으로 코드 변환
- resolveId와 load 훅 활용
- 가상 모듈(Virtual Module) 생성
- 플러그인 옵션 설계 패턴
- 실전 예제: 마크다운 플러그인
1. 플러그인 기본 구조 작성
시작하며
여러분이 Vite로 프로젝트를 개발할 때, "특정 파일 형식을 자동으로 변환하고 싶은데 어떻게 하지?"라는 고민을 해본 적 있나요? 예를 들어, 회사에서 사용하는 특별한 설정 파일이나 커스텀 데이터 포맷을 웹팩처럼 자동으로 처리하고 싶을 때가 있습니다.
이런 상황에서 매번 수동으로 파일을 변환하거나 별도의 빌드 스크립트를 작성하면 개발 속도가 느려지고 유지보수가 어려워집니다. 팀원들도 같은 작업을 반복해야 하죠.
바로 이럴 때 필요한 것이 Vite 플러그인입니다. 플러그인을 만들면 반복적인 작업을 자동화하고, 팀 전체가 같은 빌드 파이프라인을 공유할 수 있습니다.
개요
간단히 말해서, Vite 플러그인은 빌드 과정에 여러분만의 로직을 끼워넣을 수 있게 해주는 JavaScript 객체입니다. 실무에서 플러그인이 필요한 순간은 생각보다 많습니다.
예를 들어, SVG 파일을 React 컴포넌트로 자동 변환하거나, 환경변수를 특별한 방식으로 주입하거나, 특정 파일 타입에 대한 커스텀 처리가 필요할 때 매우 유용합니다. 기존에는 복잡한 웹팩 로더와 플러그인을 설정해야 했다면, Vite에서는 훨씬 간단한 객체 하나로 같은 일을 할 수 있습니다.
Vite 플러그인의 핵심 특징은 세 가지입니다. 첫째, Rollup 플러그인 시스템과 호환됩니다.
둘째, 개발 서버와 빌드 과정 모두에서 작동합니다. 셋째, 다양한 생명주기 훅을 통해 정확한 시점에 개입할 수 있습니다.
이러한 특징들이 여러분의 빌드 프로세스를 유연하게 만들어줍니다.
코드 예제
// my-first-plugin.js
// 가장 기본적인 Vite 플러그인 구조
export default function myFirstPlugin(options = {}) {
return {
// 플러그인 이름 (디버깅 시 유용)
name: 'my-first-plugin',
// 이 플러그인을 언제 실행할지 결정
apply: 'build', // 'serve' | 'build' | (config, { command }) => boolean
// Vite 설정을 수정하는 훅
config(config, env) {
console.log('빌드 설정이 준비되었습니다!');
return {}; // 수정할 설정 반환
},
// 빌드가 시작될 때 실행
buildStart() {
console.log('플러그인이 작동을 시작합니다!');
}
};
}
설명
이것이 하는 일: 플러그인은 빌드 파이프라인의 특정 시점에 여러분의 코드를 실행시켜줍니다. 마치 공장 조립 라인에 여러분만의 작업 공정을 추가하는 것과 같습니다.
첫 번째로, 플러그인을 함수로 감싸는 이유는 사용자로부터 옵션을 받기 위해서입니다. 예를 들어 myFirstPlugin({ debug: true })처럼 호출하면 options 객체에 설정이 담깁니다.
이렇게 하면 같은 플러그인을 다양한 설정으로 재사용할 수 있습니다. 두 번째로, name 속성은 필수는 아니지만 매우 중요합니다.
에러가 발생했을 때 어떤 플러그인에서 문제가 생겼는지 바로 알 수 있기 때문입니다. 실무에서는 항상 명확한 이름을 붙이는 습관을 들이세요.
세 번째로, apply 속성은 이 플러그인이 개발 서버(serve)에서만 작동할지, 프로덕션 빌드(build)에서만 작동할지 결정합니다. 함수로 만들어서 더 복잡한 조건을 설정할 수도 있습니다.
여러분이 이 코드를 사용하면 Vite 빌드 과정에 여러분만의 로직을 주입할 수 있는 기반을 마련하게 됩니다. 실무에서는 이 기본 구조 위에 transform, resolveId 같은 더 강력한 훅들을 추가해서 실제 파일 변환 작업을 수행합니다.
또한 여러 플러그인을 조합해서 복잡한 빌드 파이프라인을 구성할 수 있습니다.
실전 팁
💡 플러그인 이름은 'vite-plugin-' 접두사를 붙이는 것이 npm 생태계의 관례입니다. 예: 'vite-plugin-custom-loader'
💡 개발 중에는 console.log 대신 this.warn()이나 this.error()를 사용하면 Vite가 더 예쁘게 로그를 출력해줍니다
💡 플러그인 순서가 중요합니다. vite.config.js의 plugins 배열에서 앞쪽에 있을수록 먼저 실행됩니다
💡 디버깅할 때는 enforce: 'pre' 또는 'post' 옵션으로 실행 순서를 명시적으로 지정하세요
💡 TypeScript를 사용한다면 'vite' 패키지에서 Plugin 타입을 import해서 타입 안정성을 확보하세요
2. transform 훅으로 코드 변환
시작하며
여러분이 프로젝트에서 특별한 문법이나 파일 형식을 사용하고 있는데, 브라우저가 이해할 수 있는 JavaScript로 바꿔야 하는 상황을 만난 적 있나요? 예를 들어, 회사 내부에서 사용하는 템플릿 문법이나 특수한 주석 형식을 실제 코드로 변환해야 할 때가 있습니다.
이런 경우 매번 전처리 스크립트를 돌리거나 별도의 빌드 단계를 추가하면 개발 경험이 나빠집니다. 파일을 수정할 때마다 변환 스크립트를 실행해야 하고, HMR(Hot Module Replacement)도 제대로 작동하지 않죠.
바로 이럴 때 필요한 것이 transform 훅입니다. 이 훅을 사용하면 파일이 브라우저로 전달되기 직전에 자동으로 코드를 변환할 수 있습니다.
개요
간단히 말해서, transform 훅은 파일의 내용을 읽어서 다른 형태로 바꾸는 함수입니다. 마치 번역기처럼 한 언어를 다른 언어로 변환합니다.
실무에서 transform이 가장 많이 사용되는 경우는 코드 변환입니다. TypeScript를 JavaScript로 바꾸거나, JSX를 일반 함수 호출로 변환하거나, CSS Modules을 처리하는 것처럼요.
또한 코드에 자동으로 주석을 추가하거나, 특정 함수 호출을 제거하거나, 성능 측정 코드를 주입하는 등 다양하게 활용됩니다. 기존에는 Babel이나 Webpack 로더를 설정해서 이런 변환을 처리했다면, Vite에서는 transform 훅 하나로 같은 일을 더 빠르게 할 수 있습니다.
transform 훅의 핵심 특징은 세 가지입니다. 첫째, 파일 내용(code)과 파일 경로(id)를 인자로 받습니다.
둘째, 변환된 코드와 소스맵을 반환할 수 있습니다. 셋째, 필터 조건을 통해 특정 파일에만 적용할 수 있습니다.
이러한 특징들이 정확하고 효율적인 코드 변환을 가능하게 합니다.
코드 예제
// 특정 주석을 제거하는 플러그인
export default function removeDebugPlugin() {
return {
name: 'remove-debug-comments',
// code: 파일 내용, id: 파일 경로
transform(code, id) {
// .js, .ts 파일만 처리
if (!id.match(/\.(js|ts)$/)) {
return null; // null을 반환하면 변환하지 않음
}
// /* DEBUG */ ... /* END DEBUG */ 블록 제거
const transformed = code.replace(
/\/\* DEBUG \*\/[\s\S]*?\/\* END DEBUG \*\//g,
''
);
// 변환된 코드와 소스맵 반환
return {
code: transformed,
map: null // 소스맵이 필요하면 magic-string 라이브러리 사용
};
}
};
}
설명
이것이 하는 일: transform 훅은 모든 모듈이 로드될 때마다 실행되어 코드를 변환합니다. 파일 시스템에서 읽은 원본 코드를 브라우저가 실행할 수 있는 최종 코드로 바꿔주는 역할입니다.
첫 번째로, 파일 필터링이 매우 중요합니다. 위 코드에서 id.match(/\.(js|ts)$/)는 JavaScript와 TypeScript 파일만 처리하도록 합니다.
필터링 없이 모든 파일을 처리하면 이미지나 CSS 파일까지 변환을 시도해서 에러가 발생합니다. 필요 없는 파일은 빠르게 null을 반환해서 성능을 유지하세요.
두 번째로, 정규식을 사용한 코드 변환이 실행됩니다. replace 함수가 특정 패턴(/* DEBUG */ 블록)을 찾아서 빈 문자열로 교체합니다.
실무에서는 정규식 대신 AST(Abstract Syntax Tree) 파서를 사용하면 더 정확하고 안전하게 코드를 변환할 수 있습니다. 세 번째로, 반환값의 형태가 중요합니다.
단순히 문자열을 반환할 수도 있지만, { code, map } 객체를 반환하면 소스맵을 함께 제공할 수 있습니다. 소스맵이 있으면 디버깅할 때 변환 전의 원본 코드 위치를 볼 수 있어서 개발 경험이 훨씬 좋아집니다.
여러분이 이 코드를 사용하면 개발 환경에서만 필요한 디버그 코드를 자동으로 제거할 수 있습니다. 실무에서는 환경 변수를 체크해서 프로덕션 빌드에서만 제거하도록 조건을 추가할 수 있습니다.
또한 이 패턴을 응용해서 console.log 제거, 타입 변환, 코드 최적화 등 다양한 작업을 자동화할 수 있습니다.
실전 팁
💡 복잡한 변환은 @babel/parser와 @babel/traverse를 사용해서 AST 기반으로 처리하면 정규식보다 안전합니다
💡 소스맵이 필요하면 magic-string 라이브러리를 사용하세요. 간단한 API로 소스맵을 자동 생성해줍니다
💡 transform 훅은 매우 자주 호출되므로 성능이 중요합니다. 무거운 연산은 캐싱을 활용하세요
💡 id.includes('/node_modules/')로 체크해서 외부 라이브러리는 건너뛰면 빌드 속도가 빨라집니다
💡 변환 결과를 디버깅하려면 console.log 대신 파일로 저장해서 확인하는 것이 더 효과적입니다
3. resolveId와 load 훅 활용
시작하며
여러분이 코드에서 import something from 'custom:config'처럼 특별한 import 경로를 사용하고 싶은데, 실제로는 동적으로 생성된 데이터나 다른 곳의 파일을 가져오고 싶은 적이 있나요? 예를 들어, 설정 파일이나 환경에 따라 다른 모듈을 로드해야 하는 경우가 있습니다.
이런 상황에서 실제 파일 경로를 하드코딩하면 유연성이 떨어지고, 여러 환경에서 코드를 재사용하기 어려워집니다. 설정이 바뀔 때마다 import 문을 수정해야 하죠.
바로 이럴 때 필요한 것이 resolveId와 load 훅입니다. 이 두 훅을 조합하면 가상의 모듈 경로를 실제 파일이나 동적 코드로 연결할 수 있습니다.
개요
간단히 말해서, resolveId는 "이 import 경로를 내가 처리할게!"라고 선언하는 훅이고, load는 "그럼 이 내용을 사용해!"라고 실제 코드를 제공하는 훅입니다. 실무에서 이 훅들이 필요한 순간은 다양합니다.
예를 들어, 별칭(alias) 경로를 실제 파일로 변환하거나, 특정 프로토콜(data:, virtual:)을 처리하거나, 환경에 따라 다른 파일을 로드하거나, 외부 CDN에서 모듈을 가져오는 경우에 매우 유용합니다. 기존에는 webpack의 resolve 설정과 loader를 따로 설정해야 했다면, Vite에서는 resolveId와 load 훅을 함께 사용해서 더 직관적으로 모듈 해석을 제어할 수 있습니다.
이 훅들의 핵심 특징은 세 가지입니다. 첫째, resolveId는 import 경로를 고유한 ID로 변환합니다.
둘째, load는 그 ID에 해당하는 실제 코드를 반환합니다. 셋째, 둘을 분리함으로써 경로 해석과 내용 로딩을 독립적으로 처리할 수 있습니다.
이러한 특징들이 모듈 시스템을 유연하게 확장할 수 있게 합니다.
코드 예제
// 환경별로 다른 설정을 로드하는 플러그인
export default function envConfigPlugin(options) {
const { env = 'development' } = options;
return {
name: 'env-config-loader',
// import 경로를 해석하는 훅
resolveId(source, importer) {
// 'app:config' 경로를 처리
if (source === 'app:config') {
// 고유한 ID 반환 (보통 \0 접두사 사용)
return '\0app-config-' + env;
}
return null; // 다른 경로는 기본 처리
},
// 해석된 ID의 내용을 제공하는 훅
load(id) {
if (id.startsWith('\0app-config-')) {
// 환경에 따라 다른 설정 반환
const config = {
apiUrl: env === 'production'
? 'https://api.example.com'
: 'http://localhost:3000',
debug: env !== 'production'
};
// JavaScript 코드로 변환해서 반환
return `export default ${JSON.stringify(config, null, 2)}`;
}
return null;
}
};
}
설명
이것이 하는 일: resolveId와 load 훅은 Vite의 모듈 해석 파이프라인에 개입해서 여러분만의 import 로직을 구현할 수 있게 합니다. 실제 파일이 없어도 마치 있는 것처럼 동작하게 만들 수 있습니다.
첫 번째로, resolveId 훅이 실행됩니다. 코드에서 import config from 'app:config'를 만나면 Vite는 모든 플러그인의 resolveId를 순서대로 호출합니다.
위 코드는 'app:config' 경로를 발견하면 \0app-config-development 같은 고유한 ID로 변환합니다. \0 접두사는 관례적으로 "이건 가상 모듈이야"라고 표시하는 방법입니다.
두 번째로, load 훅이 호출됩니다. Vite는 resolveId가 반환한 ID(\0app-config-development)로 load 훅을 호출합니다.
이때 실제 파일을 읽는 대신, 우리가 동적으로 생성한 JavaScript 코드를 반환합니다. JSON.stringify를 사용해서 객체를 코드로 변환하는 것이 핵심 테크닉입니다.
세 번째로, 반환된 코드가 브라우저로 전달됩니다. Vite는 load가 반환한 코드를 마치 실제 파일인 것처럼 처리합니다.
다른 플러그인의 transform 훅도 이 코드를 처리할 수 있고, HMR도 정상적으로 작동합니다. 여러분이 이 코드를 사용하면 환경별 설정을 하드코딩 없이 자동으로 주입할 수 있습니다.
실무에서는 이 패턴을 확장해서 데이터베이스 스키마를 TypeScript 타입으로 변환하거나, API 명세를 클라이언트 코드로 생성하거나, 빌드 시점의 메타데이터를 런타임에 사용할 수 있게 만드는 등 강력한 기능을 구현할 수 있습니다.
실전 팁
💡 resolveId에서 반환하는 ID는 절대 경로나 \0 접두사를 사용해서 다른 모듈과 충돌하지 않도록 하세요
💡 load 훅에서는 반드시 유효한 JavaScript나 JSON을 반환해야 합니다. 문법 오류가 있으면 빌드가 실패합니다
💡 resolveId의 두 번째 인자 importer는 "누가 이 모듈을 import했는지" 알려줍니다. 상대 경로 해석에 유용합니다
💡 외부 라이브러리를 건너뛰려면 resolveId에서 { id, external: true }를 반환하세요
💡 복잡한 로직은 resolveId와 load를 분리해서 구현하면 테스트와 디버깅이 쉬워집니다
4. 가상 모듈(Virtual Module) 생성
시작하며
여러분이 빌드 시점의 정보를 런타임 코드에서 사용하고 싶은데, 어떻게 안전하게 전달할지 고민한 적 있나요? 예를 들어, Git 커밋 해시, 빌드 날짜, 패키지 버전 같은 정보를 코드에서 사용하고 싶을 때가 있습니다.
이런 경우 환경변수를 사용하면 타입 안정성이 없고, 실제 파일을 생성하면 버전 관리가 복잡해집니다. .gitignore에 추가해야 하고, 빌드할 때마다 파일이 변경되어 혼란스럽죠.
바로 이럴 때 필요한 것이 가상 모듈입니다. 가상 모듈은 파일 시스템에 실제로 존재하지 않지만, import해서 사용할 수 있는 마법 같은 기능입니다.
개요
간단히 말해서, 가상 모듈은 디스크에 파일이 없어도 import할 수 있는 모듈입니다. 마치 메모리에만 존재하는 파일처럼 동작합니다.
실무에서 가상 모듈이 빛을 발하는 순간은 많습니다. 빌드 메타데이터를 주입하거나, 자동 생성된 타입 정의를 제공하거나, 설정 파일을 코드로 변환하거나, 다른 플러그인이 생성한 데이터를 모듈로 제공하는 경우에 매우 유용합니다.
Next.js의 virtual:pwa-register 같은 유명 라이브러리들도 이 패턴을 사용합니다. 기존에는 빌드 스크립트로 실제 파일을 생성했다면, 가상 모듈을 사용하면 파일 시스템을 건드리지 않고도 같은 효과를 낼 수 있습니다.
가상 모듈의 핵심 특징은 네 가지입니다. 첫째, 파일이 없어도 import가 가능합니다.
둘째, TypeScript 타입을 제공할 수 있습니다. 셋째, HMR이 정상적으로 작동합니다.
넷째, 빌드 결과물에 번들링됩니다. 이러한 특징들이 빌드 시점과 런타임을 매끄럽게 연결해줍니다.
코드 예제
// 빌드 메타데이터를 제공하는 가상 모듈 플러그인
import { execSync } from 'child_process';
export default function buildMetaPlugin() {
// 빌드 시점에 한 번만 계산
const meta = {
buildTime: new Date().toISOString(),
gitCommit: execSync('git rev-parse HEAD').toString().trim(),
gitBranch: execSync('git branch --show-current').toString().trim(),
nodeVersion: process.version
};
const virtualModuleId = 'virtual:build-meta';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
return {
name: 'build-meta',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
// TypeScript 타입을 포함한 코드 생성
return `
// Auto-generated build metadata
export interface BuildMeta {
buildTime: string;
gitCommit: string;
gitBranch: string;
nodeVersion: string;
}
export const buildMeta: BuildMeta = ${JSON.stringify(meta, null, 2)};
export default buildMeta;
`.trim();
}
}
};
}
설명
이것이 하는 일: 가상 모듈 플러그인은 빌드 시점에 정보를 수집해서 런타임 코드가 import할 수 있는 모듈로 만듭니다. 실제 파일을 생성하지 않고도 마치 파일이 있는 것처럼 동작합니다.
첫 번째로, 빌드 메타데이터를 수집하는 과정이 실행됩니다. 플러그인이 로드될 때 Git 명령어를 실행해서 커밋 해시와 브랜치 이름을 가져옵니다.
이 정보는 플러그인 인스턴스가 생성될 때 한 번만 계산되고, 모든 모듈에서 같은 값을 공유합니다. 매번 계산하지 않으므로 성능에 영향을 주지 않습니다.
두 번째로, 가상 모듈의 이름 규칙이 중요합니다. virtual:build-meta는 개발자가 import할 때 사용하는 "공개 이름"이고, \0virtual:build-meta는 내부적으로 사용하는 "비공개 ID"입니다.
이렇게 분리하면 다른 플러그인이나 Vite의 기본 해석과 충돌하지 않습니다. virtual: 접두사는 "이건 가상 모듈이야"라는 명확한 신호입니다.
세 번째로, load 훅에서 TypeScript 타입을 함께 제공합니다. interface와 타입 주석을 포함한 코드를 반환하면, TypeScript 프로젝트에서 자동완성과 타입 체크가 정상적으로 작동합니다.
별도의 .d.ts 파일 없이도 완벽한 타입 지원을 제공할 수 있습니다. 여러분이 이 코드를 사용하면 import { buildMeta } from 'virtual:build-meta'처럼 간단하게 빌드 정보에 접근할 수 있습니다.
실무에서는 이 패턴을 확장해서 환경변수를 타입 안전하게 주입하거나, GraphQL 스키마를 TypeScript 타입으로 변환하거나, 라우팅 정보를 자동 생성하는 등 다양한 메타프로그래밍 기법을 구현할 수 있습니다. 또한 HMR이 지원되어 개발 중에도 즉시 반영됩니다.
실전 팁
💡 가상 모듈 이름은 'virtual:' 접두사를 사용하는 것이 Vite 생태계의 표준 관례입니다
💡 TypeScript 사용자를 위해 types/virtual-modules.d.ts 파일에 declare module 'virtual:*' 선언을 추가하세요
💡 개발 서버에서 메타데이터가 변경되면 handleHotUpdate 훅으로 HMR 이벤트를 발생시켜 자동 갱신할 수 있습니다
💡 무거운 연산(파일 읽기, 네트워크 요청)은 플러그인 초기화 시점에 한 번만 수행하고 캐싱하세요
💡 가상 모듈도 다른 모듈을 import할 수 있습니다. 복잡한 로직은 실제 파일로 분리하고 가상 모듈에서 re-export하세요
5. 플러그인 옵션 설계 패턴
시작하며
여러분이 만든 플러그인을 다른 프로젝트나 팀원들과 공유하려고 하는데, 사용자마다 필요한 설정이 다르다면 어떻게 유연하게 만들 수 있을까요? 예를 들어, 어떤 사용자는 디버그 모드가 필요하고, 다른 사용자는 특정 파일만 처리하고 싶을 수 있습니다.
이런 상황에서 옵션을 잘못 설계하면 사용자가 혼란스러워하고, 플러그인을 제대로 활용하지 못합니다. 필수 옵션과 선택 옵션이 불명확하거나, 타입 안정성이 없으면 런타임 에러가 발생하기 쉽죠.
바로 이럴 때 필요한 것이 체계적인 옵션 설계 패턴입니다. 좋은 옵션 설계는 플러그인을 사용하기 쉽고, 안전하고, 확장 가능하게 만듭니다.
개요
간단히 말해서, 플러그인 옵션 설계는 사용자가 플러그인의 동작을 커스터마이징할 수 있도록 인터페이스를 정의하는 작업입니다. 실무에서 옵션 설계가 중요한 이유는 명확합니다.
잘 설계된 옵션은 플러그인의 재사용성을 높이고, 유지보수를 쉽게 만들고, 사용자 경험을 개선합니다. 예를 들어, 파일 필터링 옵션을 제공하면 사용자가 특정 디렉토리만 처리할 수 있고, 디버그 옵션을 제공하면 문제가 생겼을 때 빠르게 원인을 파악할 수 있습니다.
기존에는 단순히 객체로 옵션을 받았다면, TypeScript와 검증 로직을 추가하면 훨씬 안전하고 명확한 API를 제공할 수 있습니다. 좋은 옵션 설계의 핵심 원칙은 다섯 가지입니다.
첫째, 합리적인 기본값을 제공합니다. 둘째, TypeScript 타입으로 명확하게 정의합니다.
셋째, 필수 옵션을 최소화합니다. 넷째, 검증 로직으로 잘못된 설정을 조기에 발견합니다.
다섯째, 옵션 간의 의존성을 명확히 합니다. 이러한 원칙들이 사용자 친화적인 플러그인을 만들어줍니다.
코드 예제
// 타입 안전한 옵션 설계 예제
interface PluginOptions {
// 필수 옵션 - 사용자가 반드시 제공해야 함
apiKey: string;
// 선택 옵션 - 합리적인 기본값 제공
debug?: boolean;
include?: string | string[];
exclude?: string | string[];
// 고급 옵션 - 대부분의 사용자는 건드리지 않음
transformOptions?: {
sourceMap?: boolean;
minify?: boolean;
};
}
export default function myPlugin(options: PluginOptions) {
// 옵션 검증 및 기본값 설정
if (!options.apiKey) {
throw new Error('myPlugin: apiKey is required');
}
const config = {
debug: options.debug ?? false,
include: Array.isArray(options.include)
? options.include
: options.include ? [options.include] : ['**/*.js'],
exclude: Array.isArray(options.exclude)
? options.exclude
: options.exclude ? [options.exclude] : ['**/node_modules/**'],
transformOptions: {
sourceMap: options.transformOptions?.sourceMap ?? true,
minify: options.transformOptions?.minify ?? false
}
};
return {
name: 'my-plugin',
transform(code, id) {
// 설정된 옵션 사용
if (config.debug) {
console.log(`Transforming: ${id}`);
}
// ... 변환 로직
}
};
}
설명
이것이 하는 일: 플러그인 옵션 설계 패턴은 사용자에게 명확한 API를 제공하고, 잘못된 설정을 방지하며, 플러그인의 동작을 유연하게 커스터마이징할 수 있게 합니다. 첫 번째로, TypeScript 인터페이스로 옵션을 정의합니다.
PluginOptions 인터페이스는 각 옵션의 타입을 명시해서 사용자가 IDE에서 자동완성을 받을 수 있게 합니다. ? 표시는 선택적 옵션임을 나타내고, 없으면 필수 옵션입니다.
이렇게 타입 레벨에서 명확히 구분하면 런타임 에러를 줄일 수 있습니다. 두 번째로, 옵션 검증과 기본값 설정이 실행됩니다.
플러그인 함수가 호출되는 즉시 필수 옵션이 있는지 확인하고, 없으면 명확한 에러 메시지와 함께 실패합니다. null 병합 연산자(??)를 사용해서 선택 옵션에 기본값을 설정합니다.
undefined일 때만 기본값을 사용하므로 false나 0 같은 falsy 값도 정상적으로 처리됩니다. 세 번째로, 배열과 문자열을 모두 받을 수 있는 유연한 API를 제공합니다.
include 옵션은 문자열이나 배열 모두 허용하지만, 내부적으로는 항상 배열로 정규화합니다. 이렇게 하면 사용자는 간단한 경우 include: '*.js'처럼 쓸 수 있고, 플러그인 내부에서는 항상 배열로 일관되게 처리할 수 있습니다.
여러분이 이 패턴을 사용하면 사용자가 설정 오류로 고생하지 않고, 명확한 타입 지원과 합리적인 기본값 덕분에 즉시 플러그인을 사용할 수 있습니다. 실무에서는 Zod나 Joi 같은 스키마 검증 라이브러리를 사용해서 더 복잡한 검증 규칙을 추가할 수 있습니다.
또한 옵션을 여러 레벨로 나누어(기본/고급) 복잡도를 관리하고, JSDoc 주석으로 각 옵션을 자세히 설명하면 더 좋은 개발 경험을 제공할 수 있습니다.
실전 팁
💡 옵션이 3개 이상이면 객체로 받고, 1-2개면 직접 매개변수로 받는 것이 더 간단합니다
💡 기본값은 플러그인 함수 매개변수에서 설정하지 말고, 함수 본문에서 명시적으로 설정하세요. 디버깅이 쉬워집니다
💡 복잡한 옵션은 별도의 validateOptions 함수로 분리하면 테스트하기 좋습니다
💡 환경변수로 옵션을 override할 수 있게 하면 CI/CD에서 유용합니다 (예: process.env.PLUGIN_DEBUG)
💡 breaking change를 피하려면 새 옵션을 추가할 때 항상 기본값을 제공하고, 기존 옵션은 deprecated로 표시하되 계속 작동하게 하세요
6. 실전 예제: 마크다운 플러그인
시작하며
여러분이 문서 사이트나 블로그를 만들면서 마크다운 파일을 React 컴포넌트로 직접 import하고 싶은데, 어떻게 해야 할지 막막했던 적 있나요? 예를 들어, import Readme from './README.md' 같은 코드가 실제로 작동하면 얼마나 편할까요?
이런 경우 별도의 빌드 스크립트로 마크다운을 HTML로 변환하거나, 런타임에 동적으로 파싱하면 성능이 떨어지고 개발 경험이 나빠집니다. 파일을 수정할 때마다 빌드를 다시 실행해야 하거나, 초기 로딩이 느려지죠.
바로 이럴 때 앞서 배운 모든 개념을 종합해서 마크다운 플러그인을 만들 수 있습니다. 이 실전 예제를 통해 transform, resolveId, load 훅을 실제로 어떻게 조합하는지 배워보겠습니다.
개요
간단히 말해서, 마크다운 플러그인은 .md 파일을 JavaScript 모듈로 변환해서 컴포넌트처럼 import할 수 있게 만드는 플러그인입니다. 실무에서 이런 플러그인이 필요한 상황은 자주 발생합니다.
문서 사이트, 기술 블로그, 스타일 가이드, API 문서 같은 콘텐츠 중심 웹사이트를 만들 때 마크다운을 직접 import할 수 있으면 개발 속도가 크게 향상됩니다. 또한 파일 기반 라우팅, 동적 메타데이터 생성, 코드 하이라이팅 같은 고급 기능도 쉽게 추가할 수 있습니다.
기존에는 gatsby-transformer-remark 같은 무거운 프레임워크를 사용했다면, Vite 플러그인으로 훨씬 가볍고 빠르게 같은 기능을 구현할 수 있습니다. 이 플러그인의 핵심 기능은 네 가지입니다.
첫째, .md 파일을 HTML로 변환합니다. 둘째, 변환된 HTML을 JavaScript 모듈로 export합니다.
셋째, frontmatter(메타데이터)를 파싱해서 함께 제공합니다. 넷째, HMR을 지원해서 마크다운 수정이 즉시 반영됩니다.
이러한 기능들이 모여 완전한 마크다운 솔루션을 만들어냅니다.
코드 예제
// vite-plugin-markdown.js
import { marked } from 'marked';
import matter from 'gray-matter';
export default function markdownPlugin(options = {}) {
return {
name: 'vite-plugin-markdown',
// .md 파일을 JavaScript 모듈로 변환
transform(code, id) {
if (!id.endsWith('.md')) {
return null;
}
// frontmatter와 본문 분리
const { data: frontmatter, content } = matter(code);
// 마크다운을 HTML로 변환
const html = marked.parse(content);
// React 컴포넌트로 export
return {
code: `
import React from 'react';
// frontmatter를 객체로 export
export const frontmatter = ${JSON.stringify(frontmatter)};
// HTML을 컴포넌트로 export
export default function MarkdownContent() {
return (
<div
className="markdown-content"
dangerouslySetInnerHTML={{ __html: ${JSON.stringify(html)} }}
/>
);
}
`.trim(),
map: null
};
},
// HMR 지원
handleHotUpdate({ file, server }) {
if (file.endsWith('.md')) {
// 마크다운 파일 변경 시 전체 페이지 리로드
server.ws.send({
type: 'full-reload',
path: '*'
});
}
}
};
}
설명
이것이 하는 일: 이 플러그인은 마크다운 파일을 읽어서 HTML로 변환한 다음, React 컴포넌트 코드를 생성해서 반환합니다. 사용자는 마크다운 파일을 일반 모듈처럼 import할 수 있습니다.
첫 번째로, 파일 필터링과 파싱이 실행됩니다. transform 훅이 모든 파일에 대해 호출되지만, .md로 끝나는 파일만 처리합니다.
gray-matter 라이브러리로 frontmatter(파일 상단의 YAML 메타데이터)를 추출하고, 본문 내용을 분리합니다. frontmatter에는 제목, 날짜, 태그 같은 메타데이터가 들어있어서 블로그나 문서 사이트에서 매우 유용합니다.
두 번째로, 마크다운을 HTML로 변환합니다. marked 라이브러리가 마크다운 문법(헤딩, 링크, 코드 블록 등)을 표준 HTML 태그로 변환합니다.
실무에서는 marked 대신 remark나 markdown-it 같은 더 강력한 파서를 사용할 수도 있고, 플러그인 옵션으로 파서를 선택하게 만들 수도 있습니다. 세 번째로, React 컴포넌트 코드를 생성합니다.
변환된 HTML을 dangerouslySetInnerHTML로 렌더링하는 함수형 컴포넌트를 문자열로 생성합니다. frontmatter는 별도로 export해서 import { frontmatter } from './post.md'처럼 접근할 수 있게 합니다.
JSON.stringify를 두 번 사용하는 이유는 HTML 문자열을 JavaScript 문자열 리터럴로 안전하게 escape하기 위해서입니다. 네 번째로, HMR 지원이 추가됩니다.
handleHotUpdate 훅은 파일이 변경될 때 호출됩니다. 마크다운 파일이 수정되면 전체 페이지를 리로드해서 변경사항을 즉시 반영합니다.
더 정교한 HMR을 원한다면 변경된 컴포넌트만 교체하도록 최적화할 수 있습니다. 여러분이 이 플러그인을 사용하면 마크다운 파일을 작성하는 즉시 웹사이트에 반영되는 매끄러운 개발 경험을 얻을 수 있습니다.
실무에서는 이 기본 구조를 확장해서 코드 하이라이팅(Prism.js, Shiki), 목차 자동 생성, 이미지 최적화, 내부 링크 검증, SEO 메타태그 생성 등 다양한 기능을 추가할 수 있습니다. 또한 Vue나 Svelte 같은 다른 프레임워크에서도 비슷한 패턴으로 마크다운 플러그인을 만들 수 있습니다.
실전 팁
💡 dangerouslySetInnerHTML 대신 react-markdown 같은 라이브러리를 사용하면 더 안전하고 커스터마이징하기 쉽습니다
💡 마크다운 파싱은 비용이 큰 작업이므로 결과를 캐싱하면 빌드 속도가 크게 향상됩니다
💡 frontmatter 스키마를 TypeScript 타입으로 생성하면 타입 안정성이 높아집니다
💡 코드 블록에 syntax highlighting을 추가하려면 marked의 renderer를 커스터마이징하거나 rehype 플러그인을 사용하세요
💡 대용량 마크다운 파일은 청크로 나누어 lazy loading하면 초기 로딩 성능이 개선됩니다
댓글 (0)
함께 보면 좋은 카드 뉴스
WebSocket과 Server-Sent Events 실시간 통신 완벽 가이드
웹 애플리케이션에서 실시간 데이터 통신을 구현하는 핵심 기술인 WebSocket과 Server-Sent Events를 다룹니다. 채팅, 알림, 실시간 업데이트 등 현대 웹 서비스의 필수 기능을 구현하는 방법을 배워봅니다.
API 테스트 전략과 자동화 완벽 가이드
API 개발에서 필수적인 테스트 전략을 단계별로 알아봅니다. 단위 테스트부터 부하 테스트까지, 실무에서 바로 적용할 수 있는 자동화 기법을 익혀보세요.
효과적인 API 문서 작성법 완벽 가이드
API 문서는 개발자와 개발자 사이의 가장 중요한 소통 수단입니다. 이 가이드에서는 좋은 API 문서가 갖춰야 할 조건부터 Getting Started, 엔드포인트 설명, 에러 코드 문서화, 인증 가이드, 변경 이력 관리까지 체계적으로 배워봅니다.
API 캐싱과 성능 최적화 완벽 가이드
웹 서비스의 응답 속도를 획기적으로 개선하는 캐싱 전략과 성능 최적화 기법을 다룹니다. HTTP 캐싱부터 Redis, 데이터베이스 최적화, CDN까지 실무에서 바로 적용할 수 있는 핵심 기술을 초급자 눈높이에서 설명합니다.
OAuth 2.0과 소셜 로그인 완벽 가이드
OAuth 2.0의 핵심 개념부터 구글, 카카오 소셜 로그인 구현까지 초급 개발자를 위해 쉽게 설명합니다. 인증과 인가의 차이점, 다양한 Flow의 특징, 그리고 보안 고려사항까지 실무에 바로 적용할 수 있는 내용을 다룹니다.