React 개발자를 위한 피그마 플러그인 개발(feat. 온보딩)

Jul.14.2022 황윤서

Web Frontend

안녕하세요, 우아한테크캠프 4기를 마치고 2022년 1월에 회사 생활을 시작한 신입 개발자 황윤서입니다.

긴장했던 첫 회사 생활은 온보딩 프로젝트와 함께 시작되었습니다. ‘쌩신입’에서 신입으로 레벨 업을 하기 위한 필수 단계죠. 같은 팀 입사 동기 분과 함께 진행한 온보딩 프로젝트는 피그마 플러그인에 대한 리서치 그 자체였습니다. 추후 팀에서 필요로 할 것 같은 지식을 미리 확보해두는 것이 목표인, 나름 실무와 완전히 동떨어지지 않은 온보딩 프로젝트였죠.

이 글을 모두 읽으시면 피그마 플러그인에 대한 배경지식이 없으셨더라도 이것을 개발할 수 있는 기반 지식을 얻게 되실 겁니다. 피그마 플러그인에 대한 리서치 결과를 정리해서 튜토리얼 형태로 소개해 드릴 테니까요. 피그마와 피그마 플러그인의 기초지식부터 시작해서 작은 플러그인까지 만들어보면서, 피그마 플러그인 개발에 대한 을 잡을 수 있으실 거라 믿습니다.

예제 플러그인 실행 모습

< 피그마 플러그인 예제 실행 모습 >

피그마와 피그마 플러그인

피그마를 사용하는 회사들

< 피그마를 사용하는 회사들 / 출처: SetProduct Blog – Figma 10 Tech Companies 이미지 참고 >

피그마는 UI 작업, 버전 히스토리 기록 등의 작업을 단일 프로그램에서 할 수 있도록 하고 작업자들 사이의 실시간 공유 기능을 제공합니다. 강력한 기능을 제공하고 있어 전 세계의 많은 회사에서 사용하고, 우아한형제들 사내에서도 디자인 툴로 채택하고 있습니다.

피그마가 제공하는 기능들은 매우 다양하지만 때로는 피그마가 제공하지 않는 기능들을 필요로 할 때가 있습니다. 한 가지 예시로 Unsplash에 있는 이미지를 가지고 와 삽입하고 싶다고 했을 때, 피그마에서 제공하는 기능에는 Unsplash와의 연동은 없기 때문에 Unsplash 사이트와 피그마를 오가며 삽입해 줘야 합니다. 외부의 시스템/데이터를 필요로 하거나 특정 상황에 더 특화된 기능을 필요로 할 때, 사용자가 직접 본인에게 필요한 기능을 정의할 수 있도록 피그마는 플러그인이라는 시스템을 제공합니다. 이 플러그인 시스템 위에서 현재는 많은 플러그인들이 다양한 사용자에 의해 만들어져왔고, 이러한 플러그인 덕분에 피그마를 사용하는 디자이너의 생산성은 더 높아졌습니다. 유용한 플러그인들이 많아지면 많아질수록 피그마를 사용하는 디자이너들의 생산성은 향상되게 됩니다.

플러그인… 혹시 뭘 할 수 있니?

iconify 플러그인 예시

< Iconify 플러그인 실행 캡처 >

피그마 플러그인은 캔버스 상의 요소를 생성하거나 삭제할 수 있고, 요소에 대해 피그마의 레이어 패널과 속성 패널로 조작할 수 있는 속성들을 편집하는 것이 가능합니다. 그리고 이러한 기능들을 위 이미지(Iconify 플러그인 실행 캡처)와 같이 UI 형태로 제공하는 것 역시 가능하죠.
더 구체적으로 플러그인과 해당 플러그인이 제공하는 기능들을 살펴보겠습니다.

  • Unsplash – Unsplash에 있는 이미지 삽입 기능 제공
  • Iconify – Material Design Icons, FontAwesome 등 여러 아이콘 셋으로부터 아이콘 삽입 기능 제공
  • Lorem ipsum – 더미 텍스트 생성 기능 제공
  • Remove BG – 이미지에서 배경 삭제 기능 제공
  • Autoflow – 두 요소 사이의 연결선 쉽게 그릴 수 있도록 기능 제공

플러그인은 네트워크 상의 리소스를 요청하는 것도 가능하고 캔버스 상의 요소를 제어하는 것도 할 수 있습니다. 이 덕분에 생각보다 훨씬 다양한 기능들을 플러그인 사용자에게 제공하는 것이 가능합니다. 그렇지만 피그마에서 제공하는 플러그인 시스템 구조상 불가능한 부분, 제약사항들도 존재합니다. 따라서 개발할 때는 아래 제약사항과 한계점에 대해서 이해하고 접근해야 할 필요성이 있습니다.

피그마에서 제공하는 플러그인 API는 아래와 같은 특징, 제약사항을 가지고 있습니다.

  • 한 번에 하나의 플러그인만 실행 가능합니다. 다른 플러그인이 실행되고 있는 상황에서 새로운 플러그인을 실행하면 이전 플러그인이 종료됩니다.
  • 백그라운드에서 실행되는 플러그인을 만드는 것은 불가능합니다.
  • 팀/조직 라이브러리 내 스타일/컴포넌트에 접근할 수 없습니다. 플러그인이 실행되는 파일에 로컬로 존재하는 스타일/컴포넌트/인스턴스에만 접근 가능합니다.
  • 파일의 메타 데이터(파일이 속한 팀 혹은 위치, 권한들, 사용자, Comment, Version History 등)에는 접근할 수 없습니다. 파일의 메타 데이터를 가지고 오기 위해서는 피그마에서 제공하는 REST API를 사용해야 합니다.

틈새 배경지식: 피그마에서는 팀/조직 단위로 스타일과 컴포넌트를 공유할 수 있는 팀 라이브러리/조직 라이브러리 기능을 제공합니다. 모든 팀 파일과 프로젝트에서 해당 라이브러리의 스타일/컴포넌트를 사용할 수 있어 효율적이고 일관적인 디자인을 하는 데 도움을 줍니다. 반면 파일 내에서 정의된 스타일/컴포넌트/인스턴스는 해당 파일 내에서만 사용 가능합니다.

개발자라면 플러그인 파악에 있어서 코드가 더 쉬우려나요? GitHub에 올라와 있는 오픈소스 피그마 플러그인들을 정리한 Repo를 공유합니다.

원리를 알면 개발이 보인다: 피그마 플러그인 아키텍처

피그마 플러그인 아키텍처

< 피그마 플러그인 아키텍처 / 출처: figma.com/plugin-docs – How Plugins Run >

틈새 배경지식: 피그마는 웹을 기반으로 웹과 데스크톱 버전을 제공하고 있습니다. 웹을 기반으로 구축되어 있기 때문에 피그마 플러그인 역시 웹 기술(HTML, CSS, JS)로 개발합니다.

본격적인 피그마 플러그인 개발에 앞서서 피그마 플러그인이 어떻게 동작하는지, 그 아키텍처에 대해서 살펴보도록 하겠습니다.

플러그인 아키텍처의 핵심은 기존 시스템과의 격리입니다. 시스템의 핵심적인 로직과는 격리되어 있어야만 악의적인 코드가 플러그인에 있어도 기존 시스템에 영향을 주지 않을 수 있고 애초에 그런 코드가 실행되는 것 자체를 방지할 수도 있겠죠. 피그마 플러그인 역시도 마찬가지로 한정적인 API에만 접근 가능한 별도의 Sandbox 내에서 실행됩니다. 그리고 UI는 iframe 위에서 실행되어 웹으로 구축된 피그마와는 분리돼서 실행됩니다.

피그마 플러그인 Sandbox에서는 표준 타입들, JSON & Promise API들, 바이너리 타입 등이 포함된 표준 ES6 자바스크립트 라이브러리를 비롯하여 minimal 한 console API에도 접근 가능합니다. 가장 중요한 점은 이 Sandbox 내에서 피그마의 캔버스에 접근할 수 있는 플러그인 API를 사용할 수 있다는 것입니다. 하지만 위의 아키텍처 그림을 보면 알 수 있다시피 플러그인 API를 사용할 수 있는 Sandbox와 UI는 분리되어 있습니다. 따라서 플러그인에서 사용자의 입력을 받기 위해 HTML, CSS, JS로 UI를 구현하여도 UI 내 JS 코드에서는 플러그인 API에 접근할 수 없습니다. 반대로 플러그인 API를 사용할 수 있는 Sandbox 내에서는 네트워크 접근 등의 브라우저 API를 사용할 수 없습니다.

그렇기 때문에 UI를 제공하는 피그마 플러그인을 구현할 때는 이 Sandbox와 UI 상의 통신을 하게 되는 데 Window 객체 사이에서 메시지를 전달할 때 사용하는 postMessage를 Sandbox와 UI 간의 통신할 때 사용하게 됩니다. UI를 제공하는 피그마 플러그인은 UI 내 플러그인 API 접근 제한으로 인해 UI에서 사용자의 입력을 받고 postMessage로 Sandbox 쪽에 메시지를 전송하여 플러그인 API에 접근하고, 그 결과를 다시 UI에 전달하여 사용자에게 피드백을 전달하는 흐름으로 플러그인을 구현하게 됩니다.

더 구체적인 플러그인의 동작 원리를 보려면 공식 문서의 How Plugins Run에서!

빠른 개발엔 역시 보일러 플레이트

UI가 있는 피그마 플러그인을 개발할 때 순수한 HTML, CSS, JS만 사용할 수도 있지만 webpack 등의 번들러 설정을 추가하게 되면 웹서비스 개발과 마찬가지로 React + TypeScript 스펙을 피그마 플러그인 개발에서도 사용할 수 있습니다.

이러한 번들링 설정은 꽤나 직접 하기 번거로운데 본 글의 나머지 내용을 따라오기 위해서는 반드시 직접 해야 합니다. 그래서 미리 보일러 플레이트를 생성하여 GitHub에 올려두었습니다(Repo). 아래 절차대로 보일러 플레이트를 설치한 후 각자의 추가적인 번들링 설정을 추가하시면 됩니다.

$ npx degit https://github.com/hseoy/figma-plugin-react-boilerplate <project name>
$ cd <project name>
$ yarn install
$ yarn build

피그마에서 플러그인을 실행하기 위해서는 피그마 데스크톱 버전이 필요합니다. 보일러 플레이트를 빌드한 후 아래 절차를 따라 주세요.

  1. 피그마 데스크톱 버전을 열어 주세요.
  2. 오른쪽 위의 프로필 아이콘 드롭 다운을 클릭한 후 Plugins 선택합니다.
  3. In development 섹션에서 + 버튼을 클릭합니다.
  4. Import plugin from manifest...를 선택하신 후 다운로드한 보일러 플레이트 내 manifest.json을 선택합니다.
  5. 아무 디자인 파일이나 들어간 후 왼쪽 위 피그마 아이콘을 선택합니다.
  6. Plugins에서 Development 선택 후 보일러 플레이트 플러그인인 sample plugin을 클릭합니다.

그러면 플러그인이 실행되면서 Hello World UI가 나타납니다.

보일러 플레이트 플러그인 실행 모습

< 보일러 플레이트 실행 캡처 >

플러그인 실행도 해봤으니 이제는 보일러 플레이트 코드 구조를 살펴보도록 하겠습니다. 핵심 부분은 src 디렉터리가 pluginui로 나뉘어 있는데, 원리를 알면 개발이 보인다: 피그마 플러그인 아키텍처 섹션을 읽으셨다면 왜 나뉘어 있는지 이해하실 수 있을 것입니다. UI가 있는 피그마 플러그인일 때 Sandbox에서 실행되는 플러그인 코드 부분과 UI 부분이 분리되어 있기 때문에, 코드상으로도 분리해 주기 위해 Webpack의 번들링 결과에 대한 설정을 한 것이죠. src 디렉터리가 왜 분리되어 있고 각각의 디렉터리가 무슨 역할을 하는지만 정확히 이해하셨다면 보일러 플레이트를 사용한 피그마 플러그인을 개발할 준비가 되신 겁니다!

→ package.json
→ yarn.lock
→ ... 생략
→ manifest.json : 플러그인 메타 정보
→ webpack-configs : 웹팩 설정 - develop/product 모드 구분
  → paths.js
  → webpack.common.js
  → webpack.dev.js
  → webpack.prod.js
→ src
  → plugin : 별도 Figma Sandbox에서 실행되며 Plugin API에 접근 가능
    → index.ts
  → shared : plugin & ui 모두 사용하는 공통 코드
  → ui : 사용자 인터페이스 부분으로 iframe 기반에서 실행되어 브라우저 API에 접근 가능
    → index.tsx
    → index.html
    → ... 생략

피그마에서 개발자 도구 여는 방법

Mac 환경 기준으로 Option + Cmd + i 단축키를 눌러도 열립니다.

Plugins에서 Development를 선택하면 Open console이라는 버튼이 보입니다.

Open console을 클릭하면 브라우저 환경과 동일한 개발자 도구가 열립니다. Elements 탭에서는 UI의 HTML 요소를 확인할 수 있고 Console, Sources, Network 등 브라우저와 완전히 동일하게 도구를 제공해 주고 있습니다. 한 가지 여기서 유용한 것은 Console 창에서 플러그인 API를 테스트하고 실행해 볼 수 있다는 점입니다. 아래와 같이 Console 창에 입력하면 API의 버전이 출력됩니다.

> figma.apiVersion;
'1.0.0'

여기서 console.log가 주요 디버깅 방법이 아닌 분들은(ex-debugger 사용) 개발자 도구를 열긴 열었지만 제대로 디버깅할 수 없을 겁니다. 플러그인이 실행되는 공간은 브라우저의 자바스크립트 엔진이 아니라 별도의 Sandbox 내에서 동작하기 때문에 개발자 도구를 완전히 활용하기에는 어려움이 있습니다. 그래서 피그마에서는 플러그인을 Sandbox가 아니라 브라우저의 자바스크립트 엔진에서 실행시키는 옵션을 제공하는 데, 그것의 위치는 마찬가지로 Plugins > Development 내에 있습니다. Use developer VM을 활성화하면 플러그인이 브라우저의 자바스크립트 엔진에서 실행되어 debugger를 사용한 중단점 설정 등의 개발자 도구 기능을 온전히 사용할 수 있게 됩니다.

백문이 불여일타: 예제로 살펴보는 피그마 플러그인

예제 플러그인 실행 모습

< 피그마 플러그인 예제 실행 모습 >

백문이 불여일타라 하죠? 이번 섹션에서는 본격적으로 이전 섹션에서 소개한 보일러 플레이트를 기반으로 임의의 인용문을 피그마상에서 선택한 텍스트 요소에 삽입하는 작은 피그마 플러그인을 개발해 보도록 하겠습니다.

$ npx degit https://github.com/hseoy/figma-plugin-react-boilerplate random-quote-figma-plugin
$ cd random-quote-figma-plugin
$ yarn install
$ yarn build
# ... 이외 초기 세팅(package 정보 수정, 기타 라이브러리 설치 등)은 설명 생략하도록 하겠습니다.

빠르게 실행만 해보고 싶다면 이 Repo를 가지고 와서 보일러 플레이트를 실행했던 것과 동일하게 실행하시면 됩니다!

플러그인 메타 정보는 manifest.json에

피그마 플러그인을 실행하기 위해서는 플러그인의 이름, 실행할 경로 등 메타 정보를 manifest.json 파일에 기술해야 합니다. manifest.json은 피그마가 플러그인을 인식하는 데 사용하는 중요한 파일이기 때문에 가장 먼저 살펴보도록 하겠습니다.

{
  "name": "sample plugin",
  "id": "00000000",
  "api": "1.0.0",
  "main": "dist/plugin.js", // 중요: Sandbox에서 실행될 플러그인 코드의 Entry Point 파일 경로
  "editorType": ["figma", "figjam"],
  "ui": "dist/ui.html" // 중요: UI 코드 Entry Point 파일 경로
}

여기서 중요하게 살펴봐야 할 필드는 mainui입니다. 각 필드는 Sandbox에서 실행될 플러그인 코드와 UI 코드의 Entry Point 파일 경로를 명시해 주기 위한 필드로, 플러그인을 피그마상에서 실행하면 피그마는 해당 필드를 읽어 코드를 실행하게 됩니다.

두 번째로 살펴볼 부분은 editorType입니다. 피그마 플러그인은 엄밀히 말하면 피그마/피그잼 플러그인이라 할 수 있습니다. 피그마와 피그잼 플러그인 모두가 접근 가능한 API가 있고 각각에서만 접근 가능한 API가 있는데 이것에 대한 구분을 위한 필드가 editorType입니다. 본 글에서는 피그마 플러그인에 초점을 맞추고 있기 때문에 editorType으로 인한 차이는 언급하지 않고 기본적으로 피그마에서 사용 가능한 API만 다루도록 하겠습니다.

manifest.json에서 ui 필드에 .html 파일의 경로를 명시해 주는 데 html 파일 내에서 웹 서비스와는 다르게 별도의 로컬 리소스를 사용할 수 없습니다. CSS, JS 파일의 경우 번들러를 사용해서 html 파일 내에 번들링해야 한다는 점 기억해 주시길 바랍니다.

틈새 배경지식: 외부 리소스 제약 – 외부 리소스는 iframe으로 실행되는 플러그인 UI에서만 불러올 수 있으며 http:// 혹은 https://로 시작하는 절대 URL을 사용해야 합니다. Sandbox 내에서는 외부 리소스를 불러올 수 없습니다. CSS, JS 파일을 번들링 해야만 하는 이유도 이와 같은 이유입니다.

manifest.json에 명시된 main 필드의 파일에서는 플러그인 API에 접근할 수 있는 데 figma라는 전역 객체를 통해서 API들을 사용할 수 있습니다. 흔한 예로 UI를 사용자에게 보여주는 API는 아래와 같습니다.

figma.showUI(__html__);

피그마 플러그인 실행은 언제나 Sandbox에서부터

보일러 플레이트를 가지고 온 random-quote-figma-plugin 디렉터리를 열어 src/plugin/index.ts 코드를 보면 단 한 줄의 코드만 존재할 것입니다. figma.showUI(html: string, options?: ShowUIOptions):void는 첫 번째로 받은 HTML 콘텐츠를 UI로 보여주는 API입니다. 모든 플러그인은 manifest.json의 Main Entry Point 파일로부터 시작되며 아무리 화려한 UI를 가지고 있다고 하더라도 Sandbox 위의 플러그인 코드에서 실행됩니다. 따라서 플러그인 코드를 살펴볼 때는 UI가 아니라 manifest.jsonmain에 명시된 파일 코드를 가장 먼저 살펴보는 것이 좋습니다.

figma.showUI(__html__);

__html__manifest.json에서 명시한 UI HTML 파일의 콘텐츠를 담고 있습니다.

네트워크 요청은 UI 쪽에서

원리를 알면 개발이 보인다: 피그마 플러그인 아키텍처 섹션에서 Sandbox 내에서 실행되는 플러그인 코드는 네트워크 요청 등을 비롯한 브라우저 API에 접근할 수 없다고 설명했습니다. 임의의 인용문을 가지고 오기 위해서는 UI 단에서 API 요청을 통해 인용문 데이터들을 가지고 와야만 합니다. UI를 보여주는 플러그인 코드는 이미 완성되어 있으니 UI 단에서 API 요청을 통해 인용문 데이터를 가지고 오는 부분에 대해서 작성해 보도록 하겠습니다.

// src/ui/App.tsx
function App() {
  return (
    <Container>
      <Text>Select Text Node and Click</Text>
      <Button>Random Quote</Button>
    </Container>
  );
}

export default App;

우선은 styled components 등을 통해 App 컴포넌트의 UI를 구현해 줍니다. 그런 다음 API를 요청해서 임의의 인용문을 가지고 오는 로직을 useRandomQuote라는 hook에 구현하도록 하겠습니다. 가장 먼저 인용문 데이터를 가지고 오는 API 요청 함수를 분리해서 구현해 줍니다.

// src/shared/index.ts
export type Quote = {
  author: string | null;
  text: string;
};

// src/ui/api/index.ts
// [{ text: 'quotes', author: 'yunseo' }, { text: 'quotes2', author: null }] 과 같은 포맷으로 응답하는 인용문 목록 API
const apiUrl = "https://type.fit/api/quotes";

export async function requestQuotes() {
  const response = await fetch(apiUrl);
  const data = await response.json();
  return data as Quote[];
}

그다음 API 요청 함수를 사용해서 임의의 인용문을 응답하는 함수를 제공하는 hook인 useRandomQuote를 구현합니다.

// src/ui/hooks/useRandomQuotes.ts
function useRandomQuotes() {
  const [quotesData, setQuotesData] = useState<Quote[] | null>(null);

  // 인용문 목록 API 응답값이 항상 같으므로 처음 요청하고서는 캐싱. 이후에는 캐싱 데이터 반환.
  const getQuotes = async () => {
    if (quotesData) {
      return quotesData;
    }
    const apiQuotes = await requestQuotes();
    setQuotesData(apiQuotes);
    return apiQuotes;
  };

  // 인용문 목록 중 임의의 아이템을 선택하여 반환.
  const getRandomQuote = async () => {
    const quotes = await getQuotes();
    const quote = quotes[Math.floor(Math.random() * quotes.length)];
    return quote;
  };

  return getRandomQuote;
}

마지막으로 이렇게 구현한 hook을 App 컴포넌트에서 사용하도록 하면 네트워크 요청 부분 구현이 끝납니다.

// src/ui/App.tsx
function App() {
  const [isLoading, setIsLoading] = useState(false);
  const getRandomQuote = useRandomQuotes();

  const generateRandomQuote = async () => {
    setIsLoading(true);
    const randomQuote = await getRandomQuote();
    console.log(randomQuote); // 수정 예정
    setIsLoading(false);
  };

  return (
    <Container>
      <Text>Select Text Node and Click</Text>
      <Button onClick={generateRandomQuote}>
        {isLoading ? "Loading..." : "Random Quote"}
      </Button>
    </Container>
  );
}

여기까지 구현하고 피그마 데스크톱 버전에서 실행해서 콘솔 창을 확인해 보면 아래와 같이 임의의 인용문 데이터가 출력되는 것을 확인하실 수 있습니다.

네트워크 요청 구현 후 실행 모습

< 네트워크 요청까지 구현한 이후 실행 캡처 >

UI와 플러그인 코드 사이의 통신

Sandbox에서 실행되는 플러그인 코드에서 브라우저 API에 접근 못한 것처럼 UI에서는 플러그인 API에 대해 접근할 수 없습니다. UI에서 가지고 온 임의의 인용문을 텍스트에 삽입하기 위해서는 이 데이터를 플러그인 코드 쪽으로 넘겨줘야만 합니다. UI와 플러그인 코드 사이의 통신은 postMessage를 통해 메시지를 전송하고 onmessage를 통해 메시지를 수신함으로써 이뤄집니다.

UI에서 postMessageonmessage는 Window 객체를 통해 접근할 수 있고 플러그인 코드에서는 플러그인 API를 통해서 접근할 수 있습니다. 플러그인 API에서 제공하는 postMessageonmessage 메서드는 아래와 같이 선언되었습니다.

type MessageEventHandler = (
  pluginMessage: any,
  props: OnMessageProperties
) => void;

// figma.ui로 접근하여 사용하는 API. ex) figma.ui.onmessage
interface UIAPI {
  postMessage(pluginMessage: any, options?: UIPostMessageOptions): void;
  onmessage: MessageEventHandler | undefined;
  // ... 이외 API는 생략
}

UI 쪽에서 postMessage를 통해 메시지를 보낼 땐 pluginMessage 필드를 지닌 객체로 전송해야만 합니다. 해당 필드가 Plugin API의 onmessage 첫 번째 인자인 pluginMessage로 넘어오게 됩니다.

// 플러그인 UI는 웹으로 구현된 피그마 위에서 iframe으로 띄워지기 때문에 parent window 객체를 사용하여 postMessage를 호출함
window.parent.postMessage({ pluginMessage: { message: string } }, "*");

여기까지가 기초적인 UI와 플러그인 코드 사이의 통신에 대한 기초 API 설명이었고 본격적으로 임의의 인용문 데이터를 플러그인 코드에서 수신하기 위한 코드를 작성해 보도록 하겠습니다. 가장 먼저 주고받을 데이터에 대한 타입 선언을 해줍니다.

// src/shared/index.ts
export type PluginAction = "generateRandomQuote";

export type PluginMessagePayload = {
  type: PluginAction;
  randomQuote: Quote;
};

type 필드는 UI에서 여러 플러그인 API를 사용해야 할 때를 구분해 주기 위한 필드입니다. 우선은 임의의 인용문을 텍스트 요소에 삽입하는 generateRandomQuote 타입만을 추가해 줍니다. 그다음 인용문 데이터를 받기 위한 randomQuote 필드를 추가해 줬습니다.

이렇게 정의한 타입을 사용하여 UI 쪽에서 postMessage로 데이터를 전송하는 함수를 구현합니다.

// src/ui/lib/figma.ts
import { PluginMessagePayload, Quote } from "../../shared";

export function requestToPlugin<T>(payload: T) {
  parent.postMessage({ pluginMessage: payload }, "*");
}

export function requestGenerateRandomQuoteToPlugin(randomQuote: Quote) {
  requestToPlugin<PluginMessagePayload>({
    type: "generateRandomQuote",
    randomQuote,
  });
}

그런 다음 App 컴포넌트에서 버튼을 클릭했을 때 임의의 인용문을 가지고 오는 부분에서 콘솔에 출력하는 것이 아니라 requestGenerateRandomQuoteToPlugin 함수를 통해 플러그인 코드로 전송하도록 수정해 줍니다.

// src/ui/App.tsx
const generateRandomQuote = async () => {
  setIsLoading(true);
  const randomQuote = await getRandomQuote();
  // console.log(randomQuote); // 수정 예정
  requestGenerateRandomQuoteToPlugin(randomQuote);
  setIsLoading(false);
};

전송하는 부분을 구현하였으면 받는 부분을 구현을 해야겠죠. 플러그인 코드 부분에서 onmessage API를 사용하는 부분을 추가하여 type 필드와 동일한 이름의 함수를 호출하도록 구현하겠습니다.

// src/shared/index.ts
export type PluginCallbackFunction<T = void> = (
  payload: PluginMessagePayload
) => T;

// src/plugin/index.ts
function isPayload(payload: unknown): payload is PluginMessagePayload {
  return (
    typeof payload === "object" &&
    Object.prototype.hasOwnProperty.call(payload, "type") &&
    Object.prototype.hasOwnProperty.call(payload, "randomQuote")
  );
}

function generateRandomQuote({ randomQuote }: PluginMessagePayload) {
  // 임의의 인용문 데이터 출력
  console.log("PLUGIN: ", randomQuote);
}

figma.ui.onmessage = (payload: unknown) => {
  const callbackMap: Record<PluginAction, PluginCallbackFunction> = {
    generateRandomQuote,
  };

  if (isPayload(payload) && callbackMap[payload.type]) {
    callbackMap[payload.type](payload);
  }
};

generateRandomQuote 타입의 메시지를 수신하면 해당하는 함수를 호출하고 그 함수에서는 받은 데이터를 출력하도록 구현해 줬습니다. 여기까지 작성하고 플러그인을 실행한 뒤 UI의 버튼을 클릭하게 되면 UI에서 가지고 온 임의의 인용문이 플러그인 코드에 전달되어 콘솔에 출력되는 것을 확인할 수 있습니다.

postMessage쪽 구현 후 실행 모습

< UI와 플러그인 코드 사이 통신까지 구현한 이후 실행 캡처 >

Document에 접근하는 방법

임의의 인용문 데이터를 플러그인 코드에서 받아왔으니 이제 그 데이터를 사용자가 선택한 텍스트 노드의 내용으로 삽입하기만 하면 됩니다. 이 작업을 하기 전에 먼저 피그마의 내부 문서 구조와 접근 방법에 대해서 설명을 드리려고 합니다.

피그마 Canvas 위의 요소들은 아래와 같이 트리 형태의 데이터로 저장되고 구성됩니다. 그리고 트리를 구성하는 하나하나의 요소들을 노드(Node)라고 부릅니다. 피그마 디자인 문서를 생성해 보면 페이지 단위로 문서가 구성되는 것을 보실 수 있을 겁니다. 최상위에는 Document Node, 각각의 페이지는 Page Node로, 페이지 아래의 모든 요소들은 각 타입의 노드들로 피그마의 문서는 구성됩니다.

→ Document Node # figma.root
  → Page Node # figma.currentPage
    → Frame Node
      → Text Node # figma.currentPage.selection[0]
      → Text Node # figma.currentPage.selection[1]
    → Rectangle Node
    → Text Node
  → Page Node
    → ... 생략
  → Page Node
    → ... 생략

플러그인 API에서는 이 노드 트리에 접근하기 위한 API들을 제공하고 있습니다만 대표적으로는 아래와 같은 API를 사용합니다.

figma.root : Document Node 참조
figma.currentPage : 현재 페이지 Node 객체 참조
figma.currentPage.selection : 사용자가 선택하고 있는 노드들의 배열 참조

만약에 지금 만들려고 하는 플러그인처럼 사용자가 현재 선택하고 있는 노드를 가지고 오려고 한다면 figma.currentPage.selection API를 사용해야 합니다. 그렇다면 해당 노드가 Text Node인지 Frame Node인지를 확인하려면 어떻게 해야 할까요? 그러기 위해서는 위 API로 가지고 온 노드들의 속성 중 type 속성을 확인하면 됩니다. 노드 객체들은 각각이 어떤 유형의 노드인지에 대한 정보를 담고 있는 type 속성을 가지고 있고 이 속성의 값이 텍스트 노드의 타입인 TEXT와 같다면 이 노드는 텍스트 노드라고 볼 수 있습니다.

const currentSelectionNode = figma.currentPage.selection[0];
if (currentSelectionNode?.type === "TEXT") {
  console.log("현재 텍스트 노드가 선택되었습니다");
} else {
  console.log("텍스트 노드가 선택되지 않았습니다");
}

이번 섹션에 대한 자세한 내용이 궁금하다면 공식 문서의 Accessing the Document를 참고해 주세요!

텍스트 요소의 내용을 바꾸려면

이제 사용자가 선택한 텍스트 요소의 내용을 UI로부터 받아온 임의의 인용문으로 변경하는 것을 구현하여 플러그인 개발을 마무리 지어보도록 하겠습니다. 사용자가 현재 선택한 노드가 텍스트 노드라면 그 텍스트 노드의 내용을 인용문으로 변경하고 아니라면 에러를 던지는 단순한 로직입니다. 텍스트 노드의 내용은 characters 속성의 값으로 문자열을 대입함으로써 변경할 수 있습니다.

// src/plugin/index.ts
function generateRandomQuote({ randomQuote }: PluginMessagePayload) {
  // 1. 현재 사용자가 선택한 노드를 가지고 와서
  const currentSelectionNode = figma.currentPage.selection[0];

  // 2. 사용자가 선택한 노드가 텍스트 노드인지 확인하고
  if (currentSelectionNode?.type === "TEXT") {
    // 2-1. 텍스트 노드라면 내용을 인용문으로 대체합니다.
    currentSelectionNode.characters = `${randomQuote.text} - ${
      randomQuote.author || "Unknown"
    }`;
  } else {
    // 2-2. 텍스트 노드가 아니라면 에러를 던집니다.
    throw new Error("No text node is selected");
  }
}

여기서 텍스트를 다룰 때 주의해야 할 점은 사용할 폰트를 미리 불러와야 한다는 점입니다. 폰트를 불러오지 않고 텍스트 노드의 속성에 접근하게 되면 에러가 발생하게 됩니다. 폰트를 불러오기 위해서는 loadFontAsync API를 사용할 수 있습니다.

interface FontName {
  readonly family: string
  readonly style: string
}

loadFontAsync(fontName: FontName): Promise<void>

이 플러그인에서는 Robert Regular를 인용문 폰트로 사용하기 위해서 플러그인을 실행하기 전에 불러오고 사용자가 선택한 텍스트 노드의 폰트를 Robert Regular로 변경해 주도록 하겠습니다.

// src/plugin/index.ts
async function loadFonts() {
  await figma.loadFontAsync({
    family: "Roboto",
    style: "Regular",
  });
}

function generateRandomQuote({ randomQuote }: PluginMessagePayload) {
  const currentSelectionNode = figma.currentPage.selection[0];
  if (currentSelectionNode?.type === "TEXT") {
    currentSelectionNode.fontName = {
      family: "Roboto",
      style: "Regular",
    };
    currentSelectionNode.characters = `${randomQuote.text} - ${
      randomQuote.author || "Unknown"
    }`;
  } else {
    throw new Error("No text node is selected");
  }
}

loadFonts().then(() => {
  figma.ui.onmessage = (payload: unknown) => {
    // ... 생략
  };
});

플러그인 구현 완료!!

예능 프로그램 무한도전의 '무한상사' 코너 - 오늘은 여기까지

< 예능 프로그램 무한도전의 ‘무한상사’ 코너 / 출처: MBC ‘무한도전’ 캡처 >

텍스트 노드의 내용을 변경하는 것까지 살펴보면서 UI에서 네트워크 요청을 통해 사용자가 버튼을 클릭했을 때 임의의 인용문을 가지고 오고 그 데이터를 플러그인 코드에 전달하여 사용자가 선택한 텍스트 노드의 내용으로 넣는 플러그인 동작을 모두 구현하였습니다.

  1. 사용자가 버튼을 클릭하면 네트워크 요청을 통해 전체 인용문 데이터를 가져옴
  2. 가지고 온 데이터 중 임의의 인용문을 선택해서 UI에서 플러그인 코드로 전달
  3. 플러그인 코드에서 데이터를 받으면 사용자가 선택한 텍스트 노드의 내용으로 삽입

여기까지 구현된 전체 코드를 살펴보시려면 이 Repo를 참고해 주세요.

지금까지의 과정을 통해서 플러그인에서 네트워크 요청을 하는 방법, UI와 플러그인 코드 사이의 통신, 노드 접근, 텍스트 노드 속성 변경 등에 대해서 살펴봤습니다. 만약 더 자세하고 다양한 플러그인 API를 다뤄보고 싶다면 공식 문서를 참고해 주세요.

모든 끝은 언제나 새로운 시작이다

플러그인 목록

< 피그마 플러그인 목록 캡처 / 출처: figma.com/community/plugins >

지금까지 피그마 플러그인이 무엇인지, 무엇을 할 수 있는지, 그 원리는 무엇인지와 같은 배경지식과 함께 보일러 플레이트 기반의 작은 플러그인을 구현하는 실습을 진행하였습니다. 지금까지 구현해 본 것은 단순히 학습을 위해 구상한 플러그인 예시일 뿐이었지만 피그마가 제공하는 플러그인 API는 다양하고, 피그마 캔버스의 많은 것들에 접근하고 제어할 수 있는 기능을 제공하기 때문에 실무에 필요한 기능들을 구현할 수 있습니다.

많은 사용자들이 더 확장된 기능을 위해 피그마 플러그인들을 개발해서 배포해왔고 그 플러그인들은 수많은 사람들이 다운로드하여 사용되고 있습니다. 특히나 피그마 플러그인은 특별하게 많은 지식을 알아야 할 필요도 없고 웹 기술에 대해 이해하고 있다면 API를 하나씩 살펴보면서 충분히 개발이 가능하기 때문에 진입 장벽이 낮은 편입니다.

신입 개발자라도 쉽게 개발할 수 있고 그렇게 개발한 결과물이 디자이너의 생산성과도 연관되니 피그마의 생태계에 쉽게 기여할 수 있는 방법이 피그마 플러그인 개발이지 않을까 싶습니다. 이 글을 읽으면서 조금의 관심이라도 생기셨다면 작은 것이라도 개발해서 저 플러그인 목록에 하나를 추가해 보는 것이 어떨까요?