[디자인 시스템] 리액트로 개발하는 피그마 플러그인 보일러 플레이트 만들기 (feat. vite plugin 만들어서 문제 해결하기)

한낱·2024년 7월 4일
1

디자인 시스템

목록 보기
9/9

서론

지난 포스팅에서 아이콘 컴포넌트를 자동으로 추출해주는 피그마 플러그인을 개발해보았다.
당시, 리액트로 개발을 진행하기 위해 배민 개발자가 공개한 피그마 플러그인 보일러 플레이트를 사용했었는데, 이를 보며 왜 webpack으로 설정을 해놓았을까 궁금했었다.
드디어 warrr-ui의 자료 조사가 끝나고 본격적인 개발 단계가 시작되었는데, 위 이유를 알아볼 겸 피그마 플러그인 보일러 플레이트를 직접 만들어보았다.

번들러

이 피그마 플러그인 보일러 플레이트를 개발할 때에는 번들러가 핵심이다.

바닐라 JS 환경에서 동작하도록 만들어진 피그마 플러그인에 react를 설치만 한다면, 브라우저는 JS 문법을 확장하여 만든 JSX 문법을 이해하지 못한다.
따라서 바벨과 같은 트랜스파일러를 통해 브라우저가 이해할 수 있는 문법으로 변환해주는 작업이 필요하다.
번들러는 트랜스파일링이 가능할 뿐만 아니라, 웹을 개발할 때 필요한 여러 모듈들을 하나로 모아주기(번들링)을 제공한다.
따라서 리액트로 피그마 플러그인을 개발하려면 번들러를 사용해야 한다.

목표

번들러 종류가 많아서 우리 팀이 피그마 플러그인에 원하는 기준을 세워보았다.

HMR 모드 제공
HMR은 개발자가 새로고침하지 않아도 코드의 변경을 바로바로 확인해볼 수 있는 기능이다.
HMR을 제공하지 않는다면, 매번 개발자가 다시 빌드를 해서 사용해야 하기 때문에 필수로 제공할 기능으로 선정했다.

속도와 용량
번들링되는 데에 걸리는 시간이 짧을 수록 DX가 좋을 것이라고 생각했고,
피그마 플러그인 개발 자체가 부가적이 기능이다보니 자체적인 용량을 최대한 가볍게 가져가면 좋을 것으로 판단했다.

이 기준들을 토대로 HMR을 제공하는 번들러들 중,

  • vite: HMR에서 ESM을 사용하여 속도를 개선했다.
  • tsup: rollup과 같은 번들러와 다르게, 많은 플러그인을 필요로하지 않아 의존성을 적게 가져갈 수 있다.

을 선정해보았다.

(엄밀히 따지자면 vite가 번들러는 아니지만, 번들러를 포함하는 개념이므로 이렇게 작성했다.)

그리고 이 중 vite를 맡아 개발하였다.

개발

0. 세팅


plugins > development > new plugin을 통해 js로 동작하는 피그마 플러그인 템플릿을 받을 수 있다.
warrr-ui에서 개발하고자 하는 피그마 플러그인은 모두 UI를 포함하므로 UI 템플릿을 다운받으면 다음과 같이 구성되어 있다.

위 사진에서 manifest를 살펴보면,
ui를 위한 ui.html파일과 플러그인의 동작을 위한 code.js가 필요한 것을 알 수 있다.
code.ts를 빌드하여 code.js를 얻어내고, 피그마 앱에서 해당 manifest 파일을 피그마 플러그인으로 등록하면

다음과 같이 기본적인 UI가 있는 피그마 플러그인이 생기는 것을 확인할 수 있다.

1. 리액트 설치

vite 공식문서에서는 vite 환경에서 react를 사용하기 위한 기본 설정을 완료해줄 수 있는 create 명령어를 제공한다.

npx create vite

하지만, 애석하게도 지금처럼 이미 존재하는 프로젝트에 위 명령어를 사용하면 중복되는 파일이 덮어씌워지게 된다. (ex. package.json, tsconfig.json 등)

그래서 직접 세팅해주었다. (해당 커밋)

  1. vite와 react에 대한 의존성을 추가하고,
  2. 기본적인 파일(html, main.tsx, App.tsx)을 만들어준다.

추가로, tsconfig 관련해서 잡히는 에러가 발생할 때 마다 어떤 속성이 필요한지 검색해서 추가했다.

  • moduleResolution: 부재 시, cannot find module 'vite' 에러 발생
  • allowSyntheticDefaultImports: 부재 시, module can only be default-imported 에러 발생
  • jsx: 부재 시, cannot use jsx unless '--jsx' flag is provided 에러 발생
  • skipLibCheck: 부재 시, 빌드 후 node modules 내부 코드들로부터 오류 발생

2. 리액트와 피그마 플러그인 연결

manifest 파일에서 확인했듯이, 피그마 플러그인은 UI로 html 파일 하나만 받고 있다.
즉, react가 빌드되어 나온 결과물을 html에 삽입해주어야 한다.

vite-plugin-singlefile의 문제점

피그마에서 제공하는 vite로 리액트 세팅하는 예제를 확인해보면,
vite-plugin-singlefile이라는 라이브러리를 설치하고 있다.
이 라이브러리는 번들링 결과물을 하나의 파일로 합쳐주는 라이브러리이다.
해당 라이브러리를 사용하는 방법은 한 가지 단점이 있는데, html에 들어가지 않기를 원하는 파일이 있을 때 사용할 수 없다는 것이다.
즉, 리액트 파일과 함께 번들링되어 생성되었던 플러그인 코드까지 html에 집어넣으려고 한다는 것이다. (당연히 이러면 오류가 난다)
피그마 예제에서는 플러그인에 대한 빌드와 ui에 대한 빌드를 분리하여 이를 해결하였다.

(package.json 파일을 확인해보면, build:ui와 build:main이 나뉘어 있다.)

이렇게 구현을 하면, watch 모드를 제공하기 위해서는 ui와 plugin에 대한 빌드가 동시에 이루어져야 하기 때문에 여러 스크립트를 동시에 실행시켜줄 수 있는 별도의 라이브러리 설치가 필요하다.
(위 이미지에서는 concurrently가 이 역할을 담당한다.)

단순히 html에 번들링된 리액트 모듈을 집어넣기만 하면 되는 건데 빌드를 분리하고, 디펜덴시를 추가해서 해결해야할까라는 생각이 들었다.

그래서 직접 vite plugin을 개발하기로 마음 먹었다.

번들링이 완료된 시점에 index.html과 index.js(리액트 파일들이 번들링된 결과물이다)를 찾아 index.html에 index.js를 추가하도록 작성하였다.

// 단순히 기존 script 구문을 index.js가 추가된 script 구문으로 변경하면 js의 일부가 html에 노출되는 문제가 생겨서
// vite-plugin-singlefile 라이브러리의 코드를 참고하여
// replaceScript와 _removeViteModuleLoader를 작성했다.
export function replaceScript(html: string, scriptFilename: string, scriptCode: string): string {
	const reScript = new RegExp(`<script([^>]*?) src="[./]*${scriptFilename}"([^>]*)></script>`);
	const preloadMarker = /"?__VITE_PRELOAD__"?/g;
	const newCode = scriptCode.replace(preloadMarker, "void 0");
	const inlined = html.replace(
		reScript,
		(_, beforeSrc, afterSrc) => `<script${beforeSrc}${afterSrc}>${newCode}</script>`
	);
	return _removeViteModuleLoader(inlined);
}

const _removeViteModuleLoader = (html: string) =>
	html.replace(
		/(<script type="module" crossorigin>\s*)\(function(?: polyfill)?\(\)\s*\{[\s\S]*?\}\)\(\);/,
		'<script type="module">'
	);

function injectBundledJsIntoHTML(): Plugin {
	return {
		name: "bundle-inject",
		enforce: "post",
		generateBundle(_, bundle) {
          // 번들이 완료된 시점의 index.html에 index.js를 주입해준다.
			const htmlFile = bundle["index.html"] as OutputAsset;
			const jsFiles = bundle["index.js"] as OutputChunk;
			htmlFile.source = replaceScript(htmlFile.source as string, "index.js", jsFiles.code);
			delete bundle["index.js"];
		},
	};
}

export default defineConfig({
  // 생성한 플러그인을 추가해준다.
	plugins: [react(), injectBundledJsIntoHTML()],
	build: {
		rollupOptions: {
			input: [path.resolve("src/plugin/code.ts"), path.resolve("index.html")],
			output: {
				entryFileNames: "[name].js",
			},
		},
		outDir: path.resolve("dist"),
	},
});

생각보다 커스텀 플러그인 개발이 어렵지 않아서 쉽게 해결할 수 있었다.

3. HMR 지원

vite 자체의 watch모드도 잘 동작하고, 피그마 플러그인에 번들링된 리액트를 제공해주는 것도 잘 적용되었는데 HMR이 되지 않는 문제가 발생했다.
생성한 injectBundledJsIntoHTML에서 enforce 속성을 post로 주어 빌드 이후에 html에 js 코드가 삽입되도록 하였기 때문에,

  1. 코드 변경 및 저장
  2. 빌드
  3. 피그마 플러그인에서 변경 감지하고 HMR 시도
  4. injectBundledJsIntoHTML 호출

의 순으로 동작하여 HMR이 일어날 때에는 html에 변경이 반영되지 않은 상태일 것이라는 생각이 들었다.

하지만 enforce를 pre (빌드 전)으로 바꾸면

위와 같이 아직 번들링된 파일이 없는 오류가 발생한다.

그래서 vite plugin 중 vite-plugin-generate-file를 사용해서 해결해보았다.
빌드가 끝난 후 manifest 파일이 재생성되도록 하여 js가 성공적으로 추가된 index.html을 바라볼 수 있도록 했다.

export default defineConfig({
  plugins: [
    react(),
    injectBundledJsIntoHTML(),
    generateFile({
      type: "json",
      output: "manifest.json",
      data: manifest,
    }),
  ],
});

그 결과,

위처럼 코드의 변경사항을 바로 반영하는 피그마 플러그인 보일러 플레이트가 되었다!


마무리

사실 처음 목표는 vite로 생성된 플러그인과 tsup으로 생성된 플러그인을 개발하여 둘의 성능이나 크기 등을 비교하는 것이었다.
하지만, 아쉽게도 tsup 번들러의 경우 html loader가 존재하지 않아 html 파일을 변경할 수 없어 비교해보지 못했다.
대신, html loader가 존재하는 번들러로만 피그마 플러그인을 개발할 수 있다는 소중한 결론을 얻을 수 있었다.
(이 맥락에서 배민 개발자분께서 html loader가 존재하는 webpack으로 개발하셨던 것 같다.)


혹시 이 과정을 직접 겪어보고 싶으신 분이 계신다면 여기서 해당 pr 확인해보실 수 있습니다.

profile
제일 재밌는 개발 블로그(희망 사항)

2개의 댓글

항상 좋은 글 감사합니다^^
퍼가요~

1개의 답글