vite 플러그인 만들기 (with 간단한 예제 & vite-plugin-pages 분석) 😊

vite는 rollup을 기반으로 만들어졌고, 빌드시 rollup이 사용된다.

또한 vite의 플러그인은 rollup을 기반으로 vite의 특정 옵션이 추가된 형태로 개발되었기에,

vite에서 제작한 플러그인은 기본적으로 개발 환경과 빌드 환경에서 모두 호환이 된다. (물론, 각 환경에서만 실행되는 특화된 기능을 제공하는 플러그인도 있다)

 

실제로 vite 플러그인을 만들어보는 것도 재밌는 경험이 될 것 같아서, 어떻게 만들 수 있는지 간단한 예제를 만들어 알아보기로 했다 😙

 

🌟 일단 가장 간단한 예제로 일단 플러그인을 만들어보자.

// vite-plugin-hello.ts
import { Plugin } from 'vite';

export default function vitePluginHello(): Plugin {
  return {
    name: "vite-plugin-hello", // 플러그인 이름
    buildStart() {
      console.log("Hello from Vite Plugin!");
    },
  };
}

이렇게 플러그인 함수를 만든뒤, 연결한다.

// vite.config.ts
import { defineConfig } from "vite";
import vitePluginHello from "./vite-plugin-hello";

export default defineConfig({
  plugins: [vitePluginHello()],
});

 

이렇게 단순하게 로그를 남기는 플러그인을 만들 수 있지만, 좀 더 vite의 기능을 활용한 플러그인을 만들 수도 있다.

 

🌠 vite는 transform, resolveId, load 같은 여러 훅(hook)을 제공하여 빌드 과정에서 다양한 동작을 수행할 수 있다.

import { Plugin } from 'vite';

export default function vitePluginVirtualModule(): Plugin {
  return {
    name: "vite-plugin-virtual-module",
    resolveId(id: string) {
      if (id === "virtual:hello") {
        return id; // 가상 모듈로 처리
      }
      // return이 null 이나 void면 가상모듈 X
    },
    load(id: string) {
      if (id === "virtual:hello") {
        return `export default 'Hello from virtual module!';`;
      }
    },
  };
}

 

🥖  load와 resolveId 훅을 사용하면 특정 모듈의 경로를 변경할 수 있다.

🥖  load 훅은 resolveId에서 처리한 가상의 모듈에 대해 실제 내용을 제공한다.

위 예제에서 load virtual:hello 모듈이 요청될 때, export default 'Hello from virtual module!';을 반환하도록 하였다.

 

이렇게 하면 Vite는 해당 모듈이 실제 파일이 아니라도, 우리가 정의한 내용을 사용하여 코드를 빌드할 수 있다. 

예를 들면, API 호출을 가상으로 처리하거나, 동적 코드 생성을 할 수도 있다.

 

위의 예제 플러그인을 사용하면 virtual:hello를 import 할때, 실제 파일이 없어도 해당 코드가 반환된다 🫢

 

import message from 'virtual:hello';
console.log(message); // "Hello from virtual module!"

 

 

+ 근데, 타입스크립트라면 해당 모듈에 대한 타이핑이 필요하다

// global.d.ts
declare module 'virtual:hello' {
  const value: string;
  export default value;
}

 

// tsconfig.app.json

{
  "compilerOptions": {
  	...
    "types": ["./global.d.ts"],
 }

 

🍠 transform 훅을 사용하면 빌드 과정에서 특정 파일의 내용을 변경할 수 있다.

 

예를 들어, .env 변수를 자동으로 주입하는 기능을 플러그인으로 제공할 수 있다.

 

import { Plugin } from 'vite';

export default function vitePluginTransform(): Plugin {
  return {
    name: "vite-plugin-transform",
    transform(code: string, id: string) {
      if (id.endsWith(".ts")) {
        return code.replace("process.env.HELLO", "'Hello from transform' ");
      }
    },
  };
}

 

위의 플러그인 예제를 직접 사용하면,

console.log(process.env.HELLO); // "Hello from transform"

 

플러그인을 통해서 env.HELLO를 주입해줄 수 있다.

 

+ 기본적으로 transform의 인자로 code가 주어지기에, 코드가 처리되기 전에 `process.env.HELLO`라는 텍스트를 다른 문자열로 바꿔치기를 함으로써 마치 env를 주입하는 것 처럼 동작할 수 있게 한다.

 

 

+ 추가로 작성해본 vite plugins들 ( vite-plugin-trolling  & vite-plugin-code-stats.ts ) 🌺

 

https://github.com/citron03/practice-trpc/commit/4c2570f987d0b91eca47030ac5e799abf4dd381c

 

feat(vite): add custom vite-plugins · citron03/practice-trpc@4c2570f

+ plugins: [react(), vitePluginTrolling(), vitePluginCodeStats()],

github.com

 

 

vite 플러그인은 다양한 시점에서 코드를 변환할 수 있도록 훅을 제공하므로, 이를 참고하여 원하는 시점에서 원하는대로 코드를 변형하거나 추가로 기능을 제공하도록 할 수 있다 🫰

etc-image-0
티스토리 표가 자꾸 깨지네요

 

vite가 export하는 Plugin 타입을 확인하면, 더 많은 vite 플러그인의 기능을 확인할 수 있다. (물론, 타입도 함께)

+ 옵션이 있는 경우, ssr을 설정하는 boolean 값이 들어갈 수 있는데 vite에서 ssr에 대한 설정을 플러그인을 통해서도 제공할 수 있는 듯 하다 🌺

+ build 관련 훅 (buildStart, buildEnd 등)은 개발 서버에서 실행되지 않는다.

 

 

실제 사용하는 vite-plugin 간단 분석 (✨ vite-plugin-pages)

간단하게 vite 플러그인에 대해서 알아보고 예제도 작성해봤지만, 그래도 실제로 많이 사용하는 vite 플러그인을 분석해보면 해본 내용들이 와닿지 않을까, 싶어서 플러그인 하나의 코드를 열어보기로 했다.

 

열어볼 코드는 vite-plugin-pages로, 간단히 말하자면 next의 page router같은 page 폴더 기반 라우팅을 자동으로 vite 환경에서 구현해주는 착한 플러그인이다 ✈️

 

https://github.com/hannoeru/vite-plugin-pages

 

GitHub - hannoeru/vite-plugin-pages: File system based route generator for ⚡️Vite

File system based route generator for ⚡️Vite. Contribute to hannoeru/vite-plugin-pages development by creating an account on GitHub.

github.com

 

깃헙 레포지토리를 찾아 들어가서, 플러그인이 어떻게 구성되어 있는지 코드를 살펴보도록 했다.

 

packages.json의 main을 보고 index.ts를 찾아가면 익숙한 구조를 만날 수 있다.

 

간단하게 주석을 남긴 코드인데, 구조는 그렇게 어렵지 않다 😃

import type { Plugin } from 'vite'
import type { UserOptions } from './types'
import { MODULE_ID_VIRTUAL, ROUTE_BLOCK_ID_VIRTUAL, routeBlockQueryRE } from './constants'

import { PageContext } from './context'
import { parsePageRequest } from './utils'

function pagesPlugin(userOptions: UserOptions = {}): Plugin {
  let ctx: PageContext

  return {
    name: 'vite-plugin-pages',
    enforce: 'pre', // 플러그인이 다른 transform 훅보다 먼저 실행되도록 설정
    async configResolved(config) {
      // Vite 설정이 확정된 후 실행됨
      // 프로젝트가 React 또는 Solid인지 자동으로 감지하여 resolver 설정    
      // auto set resolver for react project
      if (
        !userOptions.resolver
        && config.plugins.find(i => i.name.includes('vite:react'))
      ) {
        userOptions.resolver = 'react'
      }

      // auto set resolver for solid project
      if (
        !userOptions.resolver
        && config.plugins.find(i => i.name.includes('solid'))
      ) {
        userOptions.resolver = 'solid'
      }

      ctx = new PageContext(userOptions, config.root)
      ctx.setLogger(config.logger)
      await ctx.searchGlob() // 페이지 파일을 검색하여 라우트 구성 ?
    },
    api: {
      getResolvedRoutes() {
        // 플러그인이 해석한 라우트 데이터를 제공하는 API ?      
        return ctx.options.resolver.getComputedRoutes(ctx)
      },
    },
    configureServer(server) {
      // 개발 서버 설정을 위한 훅    
      ctx.setupViteServer(server) // 핫 리로드 및 파일 감지 설정
    },
    resolveId(id) {
      // 특정 가상 모듈 ID를 처리하여 변환    
      if (ctx.options.moduleIds.includes(id))
        return `${MODULE_ID_VIRTUAL}?id=${id}`

      if (routeBlockQueryRE.test(id))
        return ROUTE_BLOCK_ID_VIRTUAL

      return null
    },
    async load(id) {
      // resolveId에서 처리한 가상 모듈의 실제 내용을 반환    
      const {
        moduleId,
        pageId,
      } = parsePageRequest(id)

      if (moduleId === MODULE_ID_VIRTUAL && pageId && ctx.options.moduleIds.includes(pageId))
        return ctx.resolveRoutes() // 라우트 정보를 변환하여 제공 ?

      if (id === ROUTE_BLOCK_ID_VIRTUAL) {
        return {
          code: 'export default {};',
          map: null,
        }
      }

      return null
    },
  }
}

export { syncIndexResolver } from './options'
export type {
  ReactRoute,
  SolidRoute,
  VueRoute,
} from './resolvers'

export {
  reactResolver,
  solidResolver,
  vueResolver,
} from './resolvers'
export * from './types'
export { PageContext }
export default pagesPlugin

 

물론, 훌륭한 라이브러리인만큼  기능이 분리되어 있고, 더 자세한 동작을 알기 위해서는 더 깊이 파고들어야 하겠지만.

일단, 우리가 vite 플러그인에 대해서 작성해보고 공부한 내용이 vite-plugin-pages에 포함되어 있음을 알 수 있다.

 

load, resolveId, configResolved(vite 전용 훅), configureServer(vite 전용 훅) 등이 vite에서 사용할 수 있는 훅임을 알고 이들이 어느때에 어떻게 동작하는지 파악할 수 있다. (이를 알기에, vite 공식문서를 참고할 수 있다)

 

그렇기 때문에, 어느 순서대로 이 플러그인을 분석해야될지 접근할 수 있으며, 크게 어떤 구조로 움직이는지 대강 상상할 수 있다 😎

 

😊 참고 자료 

https://ko.vite.dev/guide/api-plugin.html#authoring-a-plugin

 

Vite

Vite, 프런트엔드 개발의 새로운 기준

ko.vite.dev

https://www.npmjs.com/package/vite-plugin-pages

 

vite-plugin-pages

File system base vue-router plugin for Vite. Latest version: 0.32.5, last published: a month ago. Start using vite-plugin-pages in your project by running `npm i vite-plugin-pages`. There are 38 other projects in the npm registry using vite-plugin-pages.

www.npmjs.com