
Figma에서 아이콘을 svg 형태로 GitHub 저장소에 바로 입력하고 싶어서 방법을 찾기 위해 여러 플러그인을 조사해 보았지만 적절한 플러그인을 발견하지 못했다. 그래서 직접 플러그인 개발에 도전했고 그 과정을 정리했습니다.
플러그인은 커뮤니티에서 만든 프로그램 또는 애플리케이션으로, Figma의 기능을 확장합니다. 플러그인은 파일에서 실행되며 하나 이상의 작업을 수행합니다. 사용자와 조직은 플러그인을 활용하여 환경을 사용자 지정하고 보다 효율적인 워크플로를 만들 수 있습니다.
피그마 플러그인은 피그마 제품의 기능을 확장하는 경량 웹 애플리케이션입니다. 플러그인은 자바스크립트로 작성되며 UI는 HTML로 만들어집니다.
https://www.figma.com/plugin-docs/
피그마 plugin의 작동 원리를 설명해주는 그림이다.
sandbox에서 실행되는 code와 iframe에서 실행되는 UI 코드로 분리된다. sandbox 환경의 특징은 대부분의 javascript 코드를 사용할 수 있고 Figma 요소에 접근할 수 있지만 XMLHttpRequest와 DOM와 같은 browswerAPI를 사용할 수 없다. ifram에서는 HTML에서 작성된 UI가 작동된다. <script> 태그를 이용해서 js 코드를 사용할 수 있고sandbox에서 사용할 수 없었던 browserAPI를 사용할 수 있다 하지만 피그마 요소에는 접근할 수 없다. 서로 사용할 수 있는 기능이 다르기 때문에 다른쪽 기능이 필요하다면 postMessage 사용하면 된다.
plugin 개발은 Figma desktop에서 시작할 수 있다.
new plugin을 실행하면 위의 이미지가 나온다. 여기서는 plugin을 Figma에서만 사용할지, 아니면 Figma + Figjam에서 사용할 지 선택하고, plugin이름을 넣을 수 있다.
그 다음으로 plugin 폴더를 어떻게 세팅하고, code boilerplate를 어떻게 구성할지 선택하는 부분이다.
default 선택시 세팅되는 파일들과 코드이다. 아무것도 없이 manifest 와 plugin 실행시 돌아가는 code만 있다. 기본 세팅이 없어서 사용자가 모든 세팅을 해야 된다.
run once 선택시 세팅되는 코드와 파일들이다. 파일 중에 HTML 파일이 없기 때문에 UI가 없다. 이렇게 세팅되면 plugin 실행시 UI가 안나타나고 바로 코드가 실행된다. 이 플러그인 개발은 플러그인 실행시 코드로 작성된 로직을 한번 실행시키는 plugin을 개발할 수 있다.
custom ui를 선택시 세팅되는 코드와 파일들이다. plugin 실행시 iframe에 HTML로 작성한 UI가 렌더링되면서 사용자 input을 받을 수 있게 됩니다. 저는
위의 세팅으로도 개발을 진행 할 수 있지만 HTML, javascript, css로만 코드를 작성 해야 된다. UI는 React를 사용하는 것이 더 편하기 떄문에 React를 사용하자.
React boilerplate는 Figma repo에도 있고, 다른 분들이 만들어둔 boilerplate도 있다.
Figma Plugin Boilerplate: React + Vite Template 오픈소스
Figma제공 boilerplate
저는 Figma에서 제공해주는 react sample로 개발을 진행했습니다.
Plugin Quickstart Guide | Plugin API
피그마 공식 가이드로 API나 작동원리를 잘 설명해줍니다.👍
작성한 코드는 바로 Figma에서 실행할 수 있습니다. npm install 을 통해서 패키지를 설치해주고 npm run dev 또는 npm run build로 dist 폴더를 만들어줍니다. boilerplate에 작성된 script는 npm run dev 시에도 build가 실행되면서 dist 폴더가 생성됩니다.
Figma 에서 우측 상단 menu → Plugins → Development → import plugin from manifest로 플러그인 프로젝트의 manifest.json을 선택하면 된다. 그리고 해당 프로젝트가 build를 통해서 dist 폴더가 생성되야지 Figma에서 실행 할 수 있다.
plugin에서도 console을 사용할 수 있어서 개발 중 많이 사용하게 됩니다.
키보드 shortcut
ctrl+alt+i(window 기준), Cmd + Option + i (mac 기준),
menu
우측 상단 menu→ Plugins→Development → show/hide console
개발 중인 plugin에 대해서 hot reload를 적용할 수 있습니다. 이걸 적용하면 개발시 변경된 내용을 다시 build 하고 plugin은 다시 실행하는 것이 아닌 변경된 내용을 저장하면 바로 plugin에 적용할 수 있습니다. 우측 상단 menu → Plugins → Development → hot reload plugin 에 체크 해주면 된다. 그리고 개발 시 npm run dev 을 실행해서 변경 내용 저장 시 dist 폴더를 바로 업데이트 되도록 해준다.
플러그인은 sandbox와 iframe 환경으로 구분되어 작동됩니다. iframe은 UI를 구성하여 사용자와의 상호작용을 담당하며,sandbox는 현재 Figma의 요소에 접근할 수 있는 환경입니다. 두 환경 간의 데이터 교환은 postMessage를 통해 이루어지며, 메시지는 onmessage 이벤트를 통해 수신됩니다.
//iframe영역
//react에서 iframe 영역은 jsx, tsx 이다.
//App.tsx
...
function App(){
useEffect(()=>{
//onmessage를 통해서 이벤트 수신시 실행될 함수 등록
window.onmessage=(msg)=>{
//sandbox에서 postMessage()에서 매개변수로 전달되는 데이터
// msg.data.pluginMessage에 담겨서 전달된다.
const data = msg.data.pluginMessage
}
},[])
const handleClick=()=>{
//postMessage로 sandbox와 통신
//postMessage({pluginMessage: 어떤 형식이든 ok!}, '*')
parent.postMessage({pluginMessage:{
//이벤트를 구별하기 위해서 type을 지정해준다.
type:'click',
//sandbox에서 전달할 데이터
payload:'data'
//targetOrigin으로 '*'을 사용해서 별도로 지정하지 않습니. 필수에요
}}, '*')
}
return<>
<button onclick={handleClick}>click</button>
<>
}
//sandbox 영역
...
//ui에서 postMessage를 전송된 이벤트를 받을 수 있다.
//pluginMessage를 통해서 전달된 데이터가 매개변수로 전달된다.
figma.ui.onmessage = (msg) => {
console.log(msg);
if (msg.type === "test") {
console.log(msg.payload);
}
};
//postMessage로 전달한다. ui와 다르게 pluginMessage, targetOrigin 없이 사용할 수 있다.
// 원하는 데이터를 postMessage 인자로 전달
figma.ui.postMessage({type:'event', payload:'데이터'})
// 중복 할당시 마지막 할당 콜백만 실행된다.
// 이 callback은 실행x
figma.ui.onmessage=(msg)=>{
console.log('콜백1')
}
//이 callback만 실행된다. 마지막 할당된 함수
figma.ui.onmessage=(msg)=>{
console.log('콜백1')
}
postMessage 의 매개변수도 다르고 onmessage 를 통해 메세지 수신 시에도 다른 형태로 데이터를 받고 있다. figma.ui.onmessage=()⇒{} , window.onmessage=()⇒{} 와 같이 onmessage에 할당 방식으로 등록하고 있어서 하나의 callback 만 등록 할 수 있다.만약 여러 곳에서 이벤트를 등록하고 싶다면 iframe 쪽에서는 onmessage 함수에 할당하는 대신 window.addEventListener(’message’ , (msg)=>{}) 방식으로 여러 곳에서 등록할 수 있고, sandbox에서는figma.ui.on(’message’,(msg)⇒{}) 방식으로 여러 곳에서 등록할 수 있다.컴포넌트는 shadcn를 사용해서 간단하게 구현했습니다. 그리고 입력된 정보를 통해서 branch를 생성하거나, commit push와 같은 GItHub API가 필요한 부분은 octokit을 이용해서 통신했습니다.
manifest는 plugin에 대한 정보를 설정하는 파일입니다.
//manifest.json
{
"name": "React Sample",
"id": "react-sample",
"api": "1.0.0",
"editorType": ["figma"],
"permissions": [],
"main": "dist/code.js",
"ui": "dist/index.html",
"networkAccess": {
// 기본 설정은 'none'이다. GitHub도메인 등록으로 api 통신이 가능해진다.
"allowedDomains": ["https://api.github.com"]
},
"documentAccess": "dynamic-page"
}
import {useEffect, useState} from "react";
import {useForm} from "react-hook-form";
import {z} from "zod";
import {zodResolver} from "@hookform/resolvers/zod";
import {Input} from "@/components/ui/input.js";
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form.js";
import {Button} from "@/components/ui/button.js";
import "./index.css";
const formSchema = z.object({
userData: z.object({
repository: z.string(),
branch: z.string(),
gitHubToken: z.string(),
path: z.string(),
}),
});
type FormData = z.infer<typeof formSchema>;
export type UserData = FormData["userData"];
type Content = {
name: keyof UserData;
label: string;
placeholder: string;
inputType?: string;
description?: string;
};
//form 구성요소
const FORM_CONTENT: Content[] = [
{name: "repository", label: "repo owner/ repo name", placeholder: "GitHub repo owner / repo name"},
{
name: "branch",
label: "branch name",
placeholder: "branch name",
},
{
name: "path",
label: "path",
placeholder: "path",
description: "파일명까지 입력해주세요 ex. src/icons/icon.json",
},
{
name: "gitHubToken",
label: "GitHub token",
inputType: "password",
placeholder: "GitHub token",
description: "repo, workflow permission이 필요합니다.",
},
] as const;
const NULL_ICON_TEXT = "No icons available. Please add some.";
function App() {
const [outputText, setOutputText] = useState<null | string>(null);
const form = useForm<FormData>({resolver: zodResolver(formSchema)});
const refresh = () => {
//frame을 다시 검색한다.
parent.postMessage({pluginMessage: {type: "refresh"}}, "*");
};
useEffect(() => {
refresh();
//onmessage 등록
window.onmessage = async (msg) => {
if (msg.data && msg.data.pluginMessage) {
const {type, payload} = msg.data.pluginMessage;
if (type === "refresh-return") {
setOutputText(payload);
return;
}
if (type === "get-storage") {
form.setValue("userData", payload);
return;
}
}
};
}, []);
return (
<main className="p-4 ">
<Form {...form}>
<h2 className="text-4xl">setting</h2>
<form className="flex flex-col gap-4 mt-4">
//form 생성
{FORM_CONTENT.map((content) => (
<FormField
control={form.control}
name={`userData.${content.name}`}
render={({field}) => (
<FormItem>
<FormLabel>{content.label}</FormLabel>
<FormControl>
<Input placeholder={content.placeholder} {...field} required type={content.inputType ? content.inputType : "text"}></Input>
</FormControl>
{content.description && <FormDescription>{content.description}</FormDescription>}
</FormItem>
)}
></FormField>
))}
<Button>push</Button>
</form>
</Form>
<div>
<div className="flex justify-between">
<h2 className="text-4xl">output json</h2>
<Button onClick={refresh}>refresh</Button>
</div>
<caption className="text-sm text-red-400 w-full truncate mt-2">* frame 이름은 'icon-studio'</caption>
<div className="border border-zinc-300 p-4 rounded-lg w-full mt-4 relative whitespace-pre-wrap">
{outputText === null ? NULL_ICON_TEXT : outputText.replace(/\\n/g, "\n")}
</div>
</div>
</main>
);
}
export default App;
//code.ts
//plugin을 띄운다
figma.showUI(__html__, {themeColors: true, height: 1000, width: 500});
//onmessage 등록
figma.ui.onmessage = async (msg) => {
//storge에 저장
if (msg.type === "set-storage") {
const data = msg.payload as UserData;
await figma.clientStorage.setAsync(USER_DATA_STORAGE_KEY, data);
return;
}
//storage에 저장된 정보 꺼내오기
if (msg.type === "get-storage") {
const data = await figma.clientStorage.getAsync(USER_DATA_STORAGE_KEY);
if (data) {
figma.ui.postMessage({type: "get-storage", payload: data});
}
return;
}
if (msg.type === "refresh") {
//현재 Figma 페이지에서 'icon-studio'라는 frame을찾는다.
//findOneAPI로 현재 페이지에서 원하는 요소를 찾을 수 있다.
//요소 타입이 'FRAME'이고, 이름은'icon-studio'라는 요소를 찾는다
const target = figma.currentPage.findOne((node) => node.type === "FRAME" && node.name === TARGET_FRAME_NAME);
let returnValue=null;
if (target) {
//해당 frame에서 instance들을 모두 가져온다.
const iconComponentList = (target as FrameNode).findAll((node) => node.type === "INSTANCE");
const svgObj: {[key: string]: {svg: string}} = {};
for (let i = 0; i < iconComponentList.length; i++) {
const node = iconComponentList[i];
//exportAsync는 FigmaAPI로 format형태로 가져올 수 있다.
//해당 인스턴스를 SVG_STRING으로 변환해서 저장
await node.exportAsync({format: "SVG_STRING"}).then((res) => {
svgObj[node.name] = {svg: res};
});
}
returnValue = JSON.stringify(svgObj, null, 2).replace(/\\"/g, "'");
//icon 정보를 iframe에 념겨준다.
}
figma.ui.postMessage({type: "refresh-return", payload: returnValue});
return;
}
};
‘icon-studio’ frame에 있는 icon을 SVG 형태로 파싱한다
아이콘을 변경하고 사용자가 지정한 repo,branch, path에 직접 commit을 push 할 수 있다.
출처
[디자인 시스템] 아이콘 자동 추출 피그마 플러그인 만들기
[디자인 시스템] 리액트로 개발하는 피그마 플러그인 보일러 플레이트 만들기 (feat. vite plugin 만들어서 문제 해결하기)
피그마에 존재하는 아이콘을 개발단에 빠르게 적용하기 (피그마 플러그인, 아이콘 라이브러리) - 정현수 기술 블로그
[warrr-ui 디자인 시스템 개발기] 아이콘 자동화 피그마 플러그인 개발
Figma 플러그인, 디자이너가 직접 만들어 보기 - 넷마블 기술 블로그
여기어때 피그마 플러그인 제작기. 글. 한혜진(Pixie) / UX Designer | by 여기어때 UX Center | 여기어때 기술블로그
2주만에 피그마 플러그인 개발하기 - Flamel 3D icon AI generator - Flamel's blog
React 개발자를 위한 피그마 플러그인 개발(feat. 온보딩) | 우아한형제들 기술블로그
[Figma 플러그인] 플러그인 개발 시작하기(Dev Mode Codegen) ⇒ (run once 방식)