개발해보자

팰리컨적 사고로 Figma Plugin 개발해보기 - 2. UI 화면 구성과 구현

Tech Tak 2025. 1. 18. 18:32

목차

     

     

     

     

    들어가며

    지난 포스팅에서는 피그마에서 플러그인 개발 환경 세팅과 예제 플러그인을 띄우는 것까지 다뤘다. 이번 글에서는 내가 만든 플러그인을 소개하고, 이를 개발하는 과정을 이야기해보겠다.

     

    플러그인 기획하기

    결과물

     

    이 플러그인은 순전히 내가 필요해서 만드는 것이다. 클론 디자인을 하다 보면, 특히 웹에서는 CSS 코드를 뜯어서 필요한 규칙은 확인할 수 있지만, 색상을 가져오는 작업은 은근히 번거롭다. 물론 클론 디자인의 목표에 따라 색상을 추측하며 진행하기도 하지만, 내 목표는 색상에 집중하는 게 아니었다. 반복 작업은 빠르게 처리할 수 있다면 처리하고, 다른 중요한 작업에 집중하는 것이 더 낫다고 생각했다.

     

    플러그인 구조

    GoodNotes에서 아이디어 초안을 작성하며 세운 기준은 크게 두 가지다.

    1. 내가 개발할 수 있을 만큼 간단한 구성  
    2. 내가 필요한 기능이 제대로 동작할 것

     

    사용자의 흐름


    플러그인 화면은 위와 같이 보이도록 기획했다. 사용자가 CSS code를 복사해서 input text field에 붙여넣은 다음 적용하기 버튼을 클릭하면, Color 코드가 Style이나 Variable로 등록된다. 그래서 사용자가 보는 화면에는 input text field와 버튼만 있으면 되는 것이다. 처음에 기획할 때는, style이나 variable 둘 중 하나의 형태를 고정해서 동작하도록 만들려고 했기 때문에 wire-frame에는 style이나 variable을 선택하는 버튼이 없다. 작업하면서 추가하게 된 것이다. 

     

     


    코드로 구현하기 위해 구조화를 간단하게 해보았다. 사용자가 CSS 코드 문자열을 입력하고 버튼을 누르면, 문자열과 style 또는 variable 선택 여부가 `msg`에 담겨서 `code.js`로 전달된다. `code.js`에서는 문자열을 다시 조립한 뒤, style이나 variable에 등록한다.

     

     

    화면만들기 (UI.html)

    이전 글에서 다룬 것처럼 사용자가 보는 화면에 대한 구현은 UI.html 에서 이루어진다. 이전 글에서 생성한 UI.html 을 열어보면, 다음과 같은 구조로 되어있는 것을 확인할 수 있다. 

     

    1. body에 해당

    2. script

    3. style

     

    Figma Plugin 에는 body의 개념은 없어보인다. 맨 상단부터 html 코드를 작성하면 된다. script와 style은 큰 차이가 없다. 작업에 앞서 UI.html 파일을 정리해보자. 1번은 모두 지우고, 2번과 3번은 상위 태그만 남기고 내용을 모두 지운다. 그러면 다음과 같이 UI.html 파일에는 script와 style 태그만 남는다.

     

    UI.html

    <script>
    </script>
    
    <style>
    </style>

     

    화면 요소 5가지

     

    화면을 구현하려면 다섯 가지 요소가 필요하다. 순서대로 Title, Description, Button(Slider), TextArea, Apply Button이고, 코드는 다음과 같다.

     

    UI.html

    <h1>CSS to Figma (Color)</h1>
    
    <p>Please paste css custom property code to textarea!</p>
    
    <div class="tabs">
      <div class="slider"></div>
      <button class="tab-button active" data-index="0">To Variables</button>
      <button class="tab-button" data-index="1">To Styles</button>
    </div>
    
    <textarea id="css-input"></textarea>
    
    <button class="apply-button">Apply</button>

     

     

    그 다음은 화면에 스타일을 입힌다. 아래 코드를 붙여넣고 플러그인을 열어보자. (Cmd + P → Plugin 이름 검색)

     

    UI.html 

    <style>
      body {
        font-family: sans-serif;
        margin: 24px;
      }
      h1 {
        margin-bottom: 0;
      }
      p {
        margin-top: 8px;
        color: #777777;
      }
      textarea {
        border-radius: 8px;
        border-color: #777777;
        resize: none;
        width: 100%;
        height: 400px;
        padding: 12px;
      }
    
      input {
        font-size: 14px;
        margin-top: 3px;
        border-style: none;
        outline: none;
        font-weight: 600;
      }
      .apply-button {
        display: block;
        margin-top: 10px;
        margin-left: auto;
        padding: 10px 20px;
        font-weight: 700;
        border-radius: 6px;
        border: none;
        cursor: pointer;
        transition: background-color 0.3s;
      }
      .apply-button:hover {
        background-color: #cccccc;
      }
      .apply-button:active {
        background-color: #777777;
      }
      .tabs {
        position: relative;
        display: inline-flex;
        background-color: #e5e5e5;
        border-radius: 8px;
        padding: 2px;
        width: 396px;
        height: auto;
        box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
        margin-bottom: 16px;
      }
    
      .tab-button {
        flex: 1;
        padding: 10px 20px;
        border: none;
        background-color: transparent;
        color: #999;
        font-size: 14px;
        font-weight: 500;
        border-radius: 6px;
        cursor: pointer;
        z-index: 1; /* slider 아래로 들어가지 않게 */
        transition: color 0.3s;
      }
    
      .tab-button:not(:last-child) {
        margin-right: 4px;
      }
    
      .tab-button:hover {
        background-color: #ffffff80;
      }
    
      .tab-button.active {
        color: black;
        font-weight: 700;
      }
    
      /* 슬라이더 */
      .slider {
        position: absolute;
        top: 2px;
        left: 2px;
        width: 196px; /* 버튼 크기만큼 조정 */
        height: 36px;
        background-color: white;
        border-radius: 6px;
        box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
        transition: transform 0.3s ease;
        z-index: 0; /* 버튼 뒤에 위치 */
      }
    </style>

     

     

    화면 크기를 조절하는 방법 + code.ts 건드리기 전에 알아둘 것

    화면 크기가 설정되어 있지 않은 상태

     

    스타일 코드를 작성하고 플러그인을 실행하면, 창 크기가 작게 표시되는 걸 볼 수 있다. 이는 창 크기를 설정하지 않아 기본값으로 적용되기 때문이다. 창 크기는 `code.ts` 또는 `code.js`에서 조절할 수 있다.

    `code.ts`를 건드리기 전에, 두 파일에 있는 예제 코드를 모두 지우고 빈 상태로 만들어야 한다. 그런 다음, Figma에 창을 띄우는 코드를 `code.ts`에 작성한다.

     

     

    code.ts

    figma.showUI(__html__);

     

     

    Figma에서는 TypeScript로 개발하기를 권장한다. 그래서 예제 파일을 열었을 때, code.ts 에 작성하고 code.js는 필요없는 파일인 줄 알고 지웠었는데, 그렇게 동작하는 것이 아니었다. code.ts에서 코드 작성을 완료하면, 이후 빌드 과정을 거쳐 code.js에 코드가 자동으로 변환되어 작성되며, 최종적으로는 code.js가 실행되는 것이다. code.ts에 작성한 코드를 code.js로 자동 빌드하기 위해 VS Code에서 설정할 것이 있다. 

    npm: watch

     

    VS Code에서 `cmd + shift + B`를 누르면 실행할 빌드 작업을 선택할 수 있다. 여기서 `npm: watch`를 검색해 실행하면, `code.ts` 파일을 저장할 때마다 `code.js`로 코드가 자동 변환된다. 변환된 코드를 확인하려면, 이제 창 크기를 조절하는 코드를 작성해 보자.

     

     

    code.ts

    figma.showUI(__html__);
    figma.ui.resize(448, 648);

     

    괄호 안 숫자의 순서대로 width 와 height 값이다. 나는 width 448px, height 648px 로 디자인 해서, 위와 같이 입력하였다.

     

    여기까지 작성한 다음, code.ts 파일을 저장하고 Figma 로 돌아가보면 아래와 같이 화면이 정상적으로 출력되는 것을 확인할 수 있다.

    정상적으로 화면이 출력되었다.

     

    code.ts 로 넘어가기 전에

    넘어가기 전에 추가적으로 해야할 작업이 있다. 바로 앞에 Slider 컴포넌트에 애니메이션을 적용하는 것과 사용자가 입력한 값을 code.js로 넘겨주는 작업이 필요하다. 

    <script>
      /* 슬라이드 버튼 애니메이션 동작을 위한 코드 */ 
      const cssInput = document.getElementById("css-input");
      const applyButton = document.querySelector(".apply-button");
    
      let currentTab = 0; // 기본은 탭 1 (변수 등록)
    
      const buttons = document.querySelectorAll(".tab-button");
      const slider = document.querySelector(".slider");
    
      buttons.forEach((button, index) => {
        button.addEventListener("click", () => {
          document.querySelector(".tab-button.active")?.classList.remove("active");
          button.classList.add("active");
    
          const sliderPosition = index * (button.offsetWidth + 4);
          slider.style.transform = `translateX(${sliderPosition}px)`;
          currentTab = index; // 현재 탭 업데이트
        });
      });
    
      /* 버튼 클릭했을 때, msg에 담아서 보내기 */
      applyButton.addEventListener("click", () => {
        const css = cssInput.value;
        parent.postMessage(
          {
            pluginMessage: { type: currentTab === 0 ? "variables" : "styles", css },
          },
          "*"
        );
      });
    </script>

     

     

    기능 구현 (code.ts)

    모든 기능을 한 번에 구현하기보다는, 단계별로 하나씩 구현하고 동작을 확인한 뒤 다음 기능을 추가하는 방식으로 진행한다. Style과 Variable에 등록하는 코드가 다르기 때문에, 먼저 Style에 등록하는 기능을 구현해 보겠다.

     

    1. 플러그인 실행 시, 화면을 띄움

    2. 플러그인 창 크기 조절

    3. UI.html에서 넘어온 msg를 받아서 처리

    4. 문자열을 가공하는 함수

    5. style로 저장하는 함수

    6. HEX 컬러를 RGB로 변환하는 함수

     

    1, 2는 앞서 다뤘으니, 3번부터 살펴보겠다.

     

    code.ts - 3

    figma.ui.onmessage = (msg) => {
      if (msg.type === "styles") {
        const cssString: string = msg.css;
    
        // CSS 문자열을 파싱하여 변수명과 값을 처리
        const parsedData = parseCss(cssString);
    
        // Styles로 저장
        saveAsStyles(parsedData);
      }

     

    3에서는 먼저 msg에서 type이 styles 인지 variables 인지 체크하는데, 여기서는 styles 인지만 다뤘다. type 이 styles 이면, css 값을 문자열을 가공하는 함수인 4로 처리한다음 parsedData에 저장한다. 그리고 저장한 값을 style로 저장한다. 이후 variable로 저장하는 경우에는 역시 type이 variables 인지 체크한 후 동일한 로직으로 변수를 저장시킬 것이다.

     

    입력 예시는 아래와 같다. (나이키 홈페이지 참고)

    --podium-cds-color-blue-50: #D6EEFF;
    --podium-cds-color-blue-100: #B9E2FF;
    --podium-cds-color-blue-200: #87CEFF;
    --podium-cds-color-blue-300: #4CABFF;
    --podium-cds-color-blue-400: #1190FF;
    --podium-cds-color-blue-500: #1151FF;
    --podium-cds-color-blue-600: #0034E3;
    --podium-cds-color-blue-700: #061DBB;
    --podium-cds-color-blue-800: #02068E;
    --podium-cds-color-blue-900: #020664;

     

    이런 문자열을 입력받으면 한 줄씩 변수명과 값으로 분리해서 저장하려고 한다. 이때, 변수명을 다음과 같이 처리해야 한다.

     

    1. "--" 지우기

    2. "-" 는 "/"로 치환하기

     

    이렇게 하면 podium/cds/color/blue/50 와 같은 형태로 저장이 되는데, 변수명에서 /는 Figma에서 폴더로 인식된다. 

     

    code.ts - 4

    function parseCss(css: string): { name: string; value: string }[] {
      const lines = css.split(";");
      const parsedData = lines
        .map((line) => line.trim()) // 앞뒤 공백 제거
        .filter((line) => line) // 빈 줄 제거
        .map((line) => {
          const [key, value] = line.split(":").map((part) => part.trim());
          const name = key.replace(/--/g, "").replace(/-/g, "/");
          return { name, value };
        });
      return parsedData;
    }

     

     

    가공한 문자열을 이제 하나씩 style로 등록해보자. 한 줄씩 style로 저장하는 데, Figma에서 실제로 사용자가 style에 등록하는 과정은 어떨까? 

     

    1. + 버튼을 누른다.

    2. Color 선택한다.

    3. 변수명과 컬러값을 입력한다.

    4. Create style을 눌러 저장한다.

     

    이렇게 하면 하나의 컬러 style을 등록하는 것이다. 또한, 컬러를 눌러보면, 4가지의 속성을 선택할 수 있다. 왼쪽부터 Solid, Gradient, Image, Video 이며, 주로 단색(Solid)를 등록하기 때문에 기본값으로 설정되어있다. 하지만, 코드를 작성할 때는 이부분도 같이 고려해서 작성해야한다.

     

    code.ts - 5

    function saveAsStyles(data: { name: string; value: string }[]) {
      data.forEach(({ name, value }) => {
        const paintStyle = figma.createPaintStyle();
        paintStyle.name = name;
        paintStyle.paints = [{ type: "SOLID", color: hexToRgb(value) }];
      });
    }

     

    code.ts - 5에서는 앞서 설명한 내용들이 고려되어있다. 먼저, 데이터의 양만큼 반복문을 실행한다. 그리고, 한 줄씩 style로 등록하는데, 변수명은 name으로 등록하고, 컬러값은 Solid에, rgb로 변환한 값으로 저장하게 된다.

     

     

    rgb로 변환하는 함수는 다음과 같다

     

    code.ts - 6

    function hexToRgb(hex: string): RGB {
      const r = parseInt(hex.slice(1, 3), 16) / 255;
      const g = parseInt(hex.slice(3, 5), 16) / 255;
      const b = parseInt(hex.slice(5, 7), 16) / 255;
      return { r, g, b };
    }

     

     

    실행해보기

    여기까지 완료했다면, 이제 플러그인을 테스트 해보자. 정상적으로 style이 등록되었다면 성공이다!

     

    Test

    --podium-cds-color-blue-50: #D6EEFF;
    --podium-cds-color-blue-100: #B9E2FF;
    --podium-cds-color-blue-200: #87CEFF;
    --podium-cds-color-blue-300: #4CABFF;
    --podium-cds-color-blue-400: #1190FF;
    --podium-cds-color-blue-500: #1151FF;
    --podium-cds-color-blue-600: #0034E3;
    --podium-cds-color-blue-700: #061DBB;
    --podium-cds-color-blue-800: #02068E;
    --podium-cds-color-blue-900: #020664;

     

     

     

    다음편에서 계속

    이번 글에서는 플러그인 기획을 간단히 설명하고, 디자인에 따라 화면을 구현했다. 또한, `UI.html`에서 전달된 `msg`를 `code.ts`에서 Style로 저장하는 과정까지 다뤘다.

    처음 개발할 때는 이 정도로 동작해도 괜찮다고 생각했지만, 커뮤니티 배포를 준비하면서 추가로 보완할 점이 생겼다. 다음 글에서는 어떤 점을 개선했는지, 그리고 커뮤니티에 배포하는 방법을 다룰 예정이다.