Figma 플러그인, 디자이너가 직접 만들어 보기

🧐 | 2024-07-09

안녕하세요, 넷마블 사업디자인팀 이동규입니다.

넷마블 영상·디자인실은 업무 효율 향상의 목적과 최신 디자인 트렌드에 맞추고자 웹 기반의 인터페이스 디자인 프로토타이핑 툴인 Figma를 사용하고 있습니다. Figma는 기존의 다른 툴과 비교했을 때 가볍고, 다양한 플러그인을 업무에 적용할 수 있다는 장점이 있습니다. 이러한 장점을 업무에 적용하려던 과정에서 직접 Figma 플러그인을 제작해 보았고, 이 글을 통해 경험을 공유해 보고자 합니다.

무겁고 깊이 있는 내용보다는 비전공자 입장에서 ChatGPT를 활용하여 실제 플러그인 제작까지 진행한 과정을 소개해 누구나 업무 효율을 높일 수 있다는 자신감을 가졌으면 하는 바람을 담았습니다.

편리한 툴이지만 그래도 불편한 부분이 있어서 만들기 시작함

전 세계 유저들에게 다양한 게임을 선보이는 넷마블의 디자인 업무 중에는 동일한 디자인 산출물을 다양한 이미지 사이즈와 언어로 변형(variation)해야 하는 일이 많습니다. Figma는 포토샵(Photoshop) 대비 단순한 UI 작업에는 가볍고 편하지만, 세밀하고 복잡한 그래픽 작업에는 무겁고, 세세한 타이포그래피 조정이 어렵습니다. Figma는 활용 가능한 공개 플러그인은 많지만 내가 원하는 작업에 완벽하게 적용하기가 쉽지 않아 추가 과정들을 거치며 업무를 진행해야만 했습니다. 예를 들면 다음 그림과 같은 작업입니다.

방금 소개한 것은 꼭 해야 하지만, 공수가 많이 드는 작업입니다. 그런데 TypeScript를 활용한 매크로 수준의 플러그인을 만들면 쉽게 해결할 부분으로 생각되었습니다. 그래서 업무에 보조적인 수단으로 활용할 수 있는 Figma 플러그인을 만들어 보기로 합니다.

사업디자인팀에서 대표 MZ를 맡고 있는 만큼, 무작정 시작하기보다 공식 가이드를 보고 차근차근 개발 환경을 설정했습니다. 개발 환경 설정에 참고한 자료는 다음과 같습니다.

Figma 플러그인과 Figma가 통신하는 구조는 다음 그림과 같습니다.

Figma 플러그인은 Figma의 동작을 직접적으로 제어하는 code.ts와 UI를 담당하는 ui.html라는 파일로 구성되어 있습니다. code.ts와 ui.html 사이는 직접적인 통신이 차단되어 있고 UI의 <iframe> 창(window)에 메시지를 전송하는 postMessage와 UI의 <iframe> 창(window)에 들어오는 메시지의 핸들러를 등록하는 onmessage라는 Figma의 API를 사용해 제한적으로 통신할 수 있습니다.

개발 환경도 설정했고 기본 구조도 알았으니 MVP(Minimum Viable Product, 최소 기능 제품)를 만들어 보기로 합니다.

첫 개발은 계획대로 되지 않아

ChatGPT를 활용해 첫 MVP까지

공통으로 사용하는 디자인 요소를 컴포넌트로 지정해도 언어와 UI 디자인을 구성하는 이미지 사이즈마다 필연적으로 일부 수정이 필요했고, 그때마다 컴포넌트화한 공통 디자인 요소들을 일일히 해제하여 작업하는 부분이 불편했습니다. 따라서 첫 번째 MVP로는 컴포넌트화하지 않고 선택한 객체와 동일한 이름을 가진 요소들의 스타일을 동기화하는 기능을 개발하기로 합니다.

원하는 기능이 개발 가능한지를 빠르게 알아보기 위해서 ChatGPT의 도움을 받아 코드 작성을 요청했습니다.

순조롭게 코드가 생성되고 컴파일하면 바로 실행될 줄 알았으나 오류가 발생합니다. 앞 프롬프트로 처음 생성된 코드는 대략 다음과 같습니다.

figma.ui.onmessage = msg => {
  // 중간 생략

  if (msg.type === 'paste-styles') {
    if (!copiedStyles) {
      figma.notify("No styles copied. Please copy styles first.");
      return;
    }

    const layers = figma.currentPage.findAll(node => node.name === figma.currentPage.selection[0].name);

    for (const layer of layers) {
      if (copiedStyles.fills) layer.fills = copiedStyles.fills;
      if (copiedStyles.strokes) layer.strokes = copiedStyles.strokes;
      if (copiedStyles.effects) layer.effects = copiedStyles.effects;
      if (layer.type === 'TEXT' && copiedStyles.textStyle) {
        layer.textStyleId = copiedStyles.textStyle;
        layer.fontSize = figma.getStyleById(copiedStyles.textStyle).fontSize;
        layer.textAlignHorizontal = figma.getStyleById(copiedStyles.textStyle).textAlignHorizontal;
        layer.textAlignVertical = figma.getStyleById(copiedStyles.textStyle).textAlignVertical;
      }
    }

    figma.notify("Styles pasted!");
  }
};

ChatGPT와 오류 메시지를 주고받으며 씨름해도 답이 없어 보였습니다. 직접 코드를 들여다보며 API 문서와 대조해 본 결과, ChatGPT의 Figma API 정보가 최신이 아니었고, 존재하지 않는 getStyleById라는 API를 존재하는 것처럼 코드를 생성해서 생긴 문제였습니다.

결국 시행착오의 반복이라는 고통의 시간 끝에 개발이 어려운 요소들은 ChatGPT의 도움을 받되, 생성된 코드를 부분적으로만 참고해야 하고 (당연하지만) 전체 구조에 대해서 잘 알고 직접 코드를 작성해야 한다는 것을 깨달았습니다.

이후 ChatGPT와 API 문서, 검색을 통해서 첫 MVP를 완성했습니다.

앞 그림을 보면 ‘타이틀 강조 문구’에 해당하는 객체와 동일한 이름을 가진 ‘제발 좀 ~ 가나다라마바사’ 요소가 ‘타이틀 강조 문구’ 스타일에 동기화됨을 확인할 수 있습니다.

플러그인 디자인

첫 기능이 동작하는 것에 신이 나 업무하면서 불편한 점을 보완할 수 있는 기능들을 생각하고 구현하기 시작했습니다. 그리고 플러그인 디자인을 함께 진행했습니다. 이때부터는 빠르게 플러그인을 제작하면서도 일정 부분 이상의 완성도를 보장하기 위한 목적으로 Figma Plugin DS 라이브러리를 사용해 Figma와 유사한 Look & Feel로 디자인했습니다.

이후 플러그인을 만들면서 여러 가지 난관에 부딪혔고 해결한 부분도, 해결하지 못한 채 넘어간 부분도 있었습니다. 주요 난관을 소개하면 다음과 같습니다.

난관 1: ui.html 내의 TypeScript 분리

Figma 플러그인에서는 상대 경로를 지원하지 않고 절대 경로만 지원하는 문제가 있습니다. 이 문제를 해결하려고 감자탕 먹고 Vue.js로 Figma 번역 플러그인 만든 이야기를 참고해 플러그인의 webpack.config.js 파일에 html-inline-css-webpack-plugin을 적용했습니다.

const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;

덕분에 code.ts에는 다음과 같이 상대 경로로 CSS 파일을 사용할 수 있었습니다.

/* Style Sheets */
import "../node_modules/figma-plugin-ds/dist/figma-plugin-ds.css";
import "./static/css/reset.css";
import "./static/css/fonts.css";

또한 ui.html 내에서 컨트롤하는 스크립트와 요소들도 별도의 파일로 분리하여 code.ts에 import하고 싶었지만 잘되지 않았고, html-inline-css-webpack-plugin처럼 스크립트 파일의 상대 경로를 지원하는 html-inline-script-webpack-plugin도 제대로 동작하지 않았습니다. 어쩔 수 없이 code.ts 내에서 TypeScript를 작성하되 함수와 주석으로 기능별 구분을 지었습니다.

난관 2: loadFontAsync

폰트와 관련된 기능이 동작하려면 항상 loadFontAsync로 로컬에 설치된 폰트를 동기화해야 합니다. 그런데 플러그인 실행 후 작업자가 폰트를 추가했을 경우에 실시간으로 반영되지 않아 이를 해결하고자 관련 기능이 동작할 때 실행하도록 했으나 플러그인 첫 실행에서만 동작하여 어쩔 수 없이 플러그인을 재실행하도록 안내하기로 했습니다.

난관 3: FontName의 style(weight) 타입과 StyledTextSegment의 fontweight 타입이 다름

텍스트의 스타일 속성을 복제할 때 읽어 오는 FontNamestyle(weight)은 Regular, Bold 등의 텍스트 형식으로 가져오나, 직접 굵기를 지정할 때 사용하는 StyledTextSegmentfontweight 데이터 타입은 fontweight: 400, fontweight: 700 등의 숫자 형식으로 되어 있습니다.

따라서 FontNamestyle: Regular에 해당하는 설정을 fontweight: 400, style: Bold에 해당하는 설정을 fontweight: 700으로 치환하여 처리하려 했으나, FontNamestyle은 폰트 제작사마다 다르게 표기(Bold, bold, BOLD 등)되어 있었습니다. 다른 폰트에 텍스트 스타일을 복제할 경우 굵기가 다르면 굵기를 제외한 나머지 스타일만 적용하도록 예외 처리했습니다.

글을 쓰는 지금 FontName 대신 TextNode에서 fontWeight를 가져오면 해결할 수 있는 것을 알게 되었습니다…. 😢 좀 더 개선해 볼 여지가 남은 셈입니다.

평소 하고 싶었던 것을 원 없이 다 해봄

각종 문제와 지식의 한계에 부딪혀 어려움은 있었지만 Figma 플러그인을 완성해 가면서 평소 업무를 진행할 때 일정과 의견 차이로 반영하지 못한 부분들을 마음껏 반영할 수 있었습니다. 그중 대표적인 사례 몇 가지를 소개합니다.

기능 동작과 입력 폼 검증

Figma는 내부 디자이너뿐만 아니라 다양한 부서 사람들과 함께 사용하기 때문에 플러그인을 사용할 때 발생하는 문제나 기능 동작에 대한 문의 사항에 모두 대응할 수 없겠다는 생각이 들었습니다.

따라서 ui.html 파일 안의 모든 기능은 특정 조건에 부합해야만 활성화가 되도록 했습니다.

if ((btn_apply.id === "btn--esc--apply" && selectedElementNum === 1 && 
  selectedElementName !== "#highlight") ||
  (btn_apply.id === "btn--lv--apply" && isRegexValid) && selectedElementNum > 0 ||
  (btn_apply.id === "btn--lv--align" && isRegexValid) && selectedElementNum > 0) {
  btn_apply.disabled = false;
} else {
  btn_apply.disabled = true;
}

입력 폼은 데이터를 검증하여 원하는 형식이 아닌 경우 오류 메시지가 나타나 무조건 안내된 가이드대로 해야만 동작하도록 설계했습니다.

또한 UI 부분인 ui.html뿐만 아니라 code.ts에서도 한 번 더 체크하도록 해두었습니다.

if(languages.length < 2) {
  figma.notify("⚠️ " + actionName + "할 언어 개수는 최소 2개 국어 이상이어야 합니다.");
  return false;
}

평소 프로젝트 진행 시 입력 단계가 아닌 폼 전송을 해야만 데이터 검증이 이루어지는 부분이 개인적으로 아쉬웠는데, 플러그인을 만들면서 입력 단계에서부터 검증할 수 있게 되었습니다.

웹 접근성 & 시맨틱 웹

프로젝트 일정이 촉박하거나 디자인이 자주 바뀌어 웹 접근성에 맞춰 UI를 디자인하기 어려운 경우가 많습니다. 단순 Figma 플러그인에 접근성이 필요할 일은 없지만 플러그인 제작을 기회 삼아 기능 전환, 토글 등의 단계에서 WAI-ARIA(Web Accessibility Initiative – Accessible Rich Internet Applications)를 적용했습니다.

// 메뉴(기능)를 전환할 때 aria-current를 제어하는 부분
btn_menu.forEach(menu => {
  menu.addEventListener("click", () => {
    btn_menu.forEach(btn => btn.removeAttribute("aria-current"));
    menu.setAttribute("aria-current", "page");
    panels.forEach(panel => panel.classList.remove("active"));
    panels[Array.from(btn_menu).indexOf(menu)].classList.add("active");
    selectedElement.style.display = (menu.id === "menu__info") ? "none" : "block";}
  );
});

단순하지만 있으면 좋은 UX

플러그인 첫 배포 이후에는 작업할 때 있으면 좋겠다 싶은 UX들을 플러그인에 반영했습니다.

플러그인을 실행할 때 마지막으로 열었던 메뉴가 활성화된 상태로 실행되거나, 이전에 입력한 입력 폼 데이터를 기억하되 최대 하루까지만 기억하는 등 각종 반복 작업을 하면서 동일한 메뉴로 이동하거나 데이터를 입력하는 수고를 덜 수 있도록 했습니다.

// clientStoreage에 저장된 시각과 현재 시각 차이 구분을 통한 입력 폼 저장 기간을 설정하는 부분
// 현재 날짜와 저장된 날짜 사이의 시간 차이 계산(밀리초 단위)
async function CheckLanguageListSavedTimeDiff() {
  const currentDate = new Date();
  let daysDiff = 0;
  try {
    const savedDate = await figma.clientStorage.getAsync("languageList-savedDate");
    const timeDiff = currentDate.getTime() - savedDate;
    daysDiff = timeDiff / (1000 * 3600 * 24); // 변수에 값 할당
  } catch (error) {
    console.error(error);
  }
  return daysDiff;
}

해당 기능을 개발하면서 사용자 컴퓨터에 데이터를 저장하는 clientStorage를 사용하고, 또 날짜 간의 차이를 계산하여 처리하는 방식 등을 생각해 볼 수 있어서 좋은 경험이 되었습니다.

배포 및 업무 적용

이후에도 여러 가지 테스트를 통해 사소한 버그들을 수정하면서 플러그인의 완성도를 높여갔습니다. 또한 플러그인 사용 가이드 작성과 함께 정식으로 Figma 플러그인을 사내에 소개했고 업무에 보조적으로 활용하기 시작했습니다.

Figma 플러그인을 만들 때는 그동안 업무를 진행하며 불편했던 부분들을 개선하고자 여러 가지 기능을 담았으나 막상 Figma 플러그인을 소개한 후 상황을 보면 특정 기능 위주로만 사용한다는 점이 흥미로웠습니다. 그래도 특정 기능을 사용하여 기존 대비 약 50% 이상 작업 속도와 효율이 향상될 수 있어 다행으로 생각합니다. 기능 사용에 대한 트래킹을 지속한다면 추후 유용한 핵심 기능들 위주로 플러그인을 구성할 수 있지 않을까 싶습니다.

다국어 프레임 복제 및 이름 변경(※ 이해를 돕기 위한 예시 영상입니다)
강조 구문 효과 적용(※ 이해를 돕기 위한 예시 영상입니다)

마치면서

평소 머릿속으로 “이렇게 하면 더 효율적으로 작업할 수 있지 않을까?” 혹은 “이런 건 자동화가 가능하면 좋을 텐데…”라는 생각을 자주 했었습니다. 하지만 직접 맨땅에 헤딩하기엔 엄두가 나질 않아 시도해 보지 못했던 것도 사실입니다.

그런데 ChatGPT가 등장하고, 이를 활용한 덕분에 그간 생각에 머물렀던 플러그인 제작을 구현해 낼 수 있었습니다. 저에게는 매우 뜻깊은 경험이었습니다. 특히 비전공자가 코드를 작성할 때는 이해도와 자료 검색의 한계가 분명히 있는데 이러한 부분을 ChatGPT가 해결해 준다는 점은 매우 유용했습니다. 단, ChatGPT가 잘못된 정보를 제공하는 경우도 많기 때문에 ChatGPT가 제공하는 정보를 기반으로 올바른 관련 지식도 함께 탐구해야 한다는 점은 많은 분이 꼭 주의해야 할 사실이라고 생각합니다.

이 글에서는 디자이너의 Figma 플러그인 제작 과정을 소개했지만, 꼭 Figma 플러그인이 아니더라도 평소 업무 환경에서 ChatGPT를 잘 활용한다면 Google Apps Script나 간단한 명령 프롬프트 스크립트를 통해 업무 효율성을 높일 수 있으리라 기대합니다.