Figma Variables을 추출하는 커스텀 플러그인 제작기

안녕하세요. 원스토어 iOS Platform팀 강석원입니다.

이번 포스팅에서는 Figma Plugin API를 활용해 Variables를 JSON 파일로 추출할 수 있는 커스텀 플러그인을 만들어본 경험을 공유하려 합니다.
원스토어에서 디자인 시스템을 개발 환경에 통합하여 업무 프로세스를 개선한 내용이 궁금하시다면 디자인 시스템 개발 환경에 통합하기에서 확인하실 수 있습니다.

Figma 플러그인이란?

Figma 플러그인은 Figma의 기능을 확장하고 사용자의 워크플로우를 개선해주는 애플리케이션입니다.
피그마 커뮤니티에서 다양한 플러그인을 찾아 설치할 수 있으며, JavaScript와 HTML을 사용해 제작됩니다. 플러그인은 Plugin API를 통해 Figma와 상호작용할 수 있습니다.

새로운 플러그인을 생성하는 자세한 방법은 공식 문서를 참고하세요.

플러그인 만들기

0. 요구사항 정리

원스토어 디자인 시스템의 Style에는 Color, Typography, Spacing 등의 디자인 토큰이 Variables에 정의되어 있습니다.
이 Variables 데이터를 가공해 추출하고, 버전 관리를 위해 원스토어에서 사용 중인 Bitbucket에 메시지와 함께 업데이트할 수 있어야 합니다.

요구사항은 다음과 같습니다:

  1. Figma Variables를 추출하여 JSON 파일로 생성
  2. Bitbucket(Git)과 연동
  3. 변경사항을 입력할 수 있는 창 추가

1. Variables를 추출하여 JSON 파일로 생성

Variables는 디자인 시스템에서 일관성을 유지하고 효율성을 높이기 위해 다양한 속성을 저장하고 관리하는 기능입니다.

Figma Variables의 구조는 다음과 같이 4가지 주요 요소로 구성됩니다:
이미지 출처: Figma site이미지 출처: Figma site

  • A. Variable Collections: Variables를 체계적으로 관리하기 위한 그룹화된 컨테이너
  • B. Variables: 색상, 숫자, 텍스트, 상태 등 다양한 속성 값을 저장하는 개별 데이터
  • C. Modes for Variables: 다크 모드, 라이트 모드 등 다양한 디자인 컨텍스트에 맞는 Variables 정의
  • D. Variable Groups: 관련된 Variables를 계층 구조로 정리하여 가독성과 관리성을 향상

이 구조를 기반으로 Variables 데이터를 JSON 파일로 추출하는 방법은 아래와 같습니다:

async function exportToJSON() {
  const collections = await figma.variables.getLocalVariableCollectionsAsync();
  const files = [];

  for (const collection of collections) {
    const processedFiles = await processCollection(collection);
    processedFiles.forEach((file) => {
      files.push(file);
    });
  }

figma.ui.postMessage({ type: "EXPORT_RESULT", files });
}

exportToJSON() 함수는 getLocalVariableCollectionsAsync()를 사용해 현재 파일의 모든 Variable Collections를 가져옵니다. 이후, 각 Collection 내부의 Variables를 가공하여 files 배열에 저장합니다.

Style Dictionary란 디자인 토큰을 코드로 변환하여 다양한 플랫폼에서 사용할 수 있게 해주는 도구입니다. 원스토어의 적용 사례는 링크에서 확인할 수 있습니다.

processCollection() 함수는 Collection의 Variables를 Style Dictionary에서 사용하기 적합한 포맷으로 변환합니다.

async function processCollection({ name, modes, variableIds }) {
  const files = []; // 1. 생성된 파일 정보를 저장할 배열 초기화

  for (const mode of modes) { // 2. 주어진 모드(modes)를 순회
    const file = { fileName: `${name}.${mode.name}.tokens.json`, body: {} }; // 3. 각 모드에 대한 JSON 파일 구조 초기화
    const collectionData = {};
    collectionData[name] = {};

    for (const variableId of variableIds) { // 4. 변수 ID 리스트를 순회하며 각 변수를 처리
      const {
        name: varName,
        resolvedType,
        valuesByMode,
        description,
        codeSyntax,
      } = await figma.variables.getVariableByIdAsync(variableId); // 5. 변수 ID로부터 변수 정보를 가져옴
      
      const value = valuesByMode[mode.modeId]; // 6. 현재 모드에 해당하는 변수 값을 추출
      if (
        value !== undefined &&
        ["COLOR", "FLOAT", "STRING", "NUMBER", "BOOLEAN"].includes(resolvedType)
      ) { // 7. 변수 값과 타입이 유효한 경우에만 처리
        let obj = collectionData[name];

        const arr = varName.split("/"); // 8. 변수 이름을 "/" 기준으로 그룹화
        for (let index = 0; index < arr.length; index++) {
          const groupName = arr[index];

          if (!obj[groupName]) obj[groupName] = {}; // 9. 계층 구조를 생성
          if (index === arr.length - 1) {
            obj[groupName].$type =
              resolvedType === "COLOR" ? "color" : resolvedType.toLowerCase();
            if (value.type === "VARIABLE_ALIAS") { // 10. 변수 값이 참조(alias)일 경우 처리
              const referencedVariable =
                await figma.variables.getVariableByIdAsync(value.id);
              const collection =
                await figma.variables.getVariableCollectionByIdAsync(
                  referencedVariable.variableCollectionId
                );

              obj[groupName].$value = `{${sanitizeString(
                collection.name
              )}.${sanitizeString(
                referencedVariable.name.replace(/\//g, ".")
              )}}`; // 11. 참조된 변수의 경로를 `$value`로 추가
            } else {
              obj[groupName].$value =
                resolvedType === "COLOR" ? rgbToHex(value) : value; // 12. 직접적인 값을 `$value`로 추가
            }
            if (description !== "") { // 13. 변수에 설명이 있으면 `comment`로 추가
              obj[groupName].comment = description;
            }
          } else {
            obj = obj[groupName]; // 14. 계층 구조를 따라 이동
          }
        }
      }
    }
    file.body = collectionData; // 16. 모드별 변수 데이터를 JSON 파일에 추가
    files.push(file); // 17. 생성된 파일 정보를 배열에 저장
  }

  return files; // 18. 모든 파일 정보를 반환
}

(1~3) 파일 구조 초기화

모든 mode(다크 모드, 라이트 모드 등)에 대해 각각 별도의 JSON 파일 구조를 생성합니다. 파일명은 {컬렉션 이름}.{모드 이름}.tokens.json 형태로 설정되며, 이는 데이터 관리의 가독성을 높이는 데 도움을 줍니다.

(4) 변수 순회 및 데이터 추출

각 모드에 대해 해당 컬렉션에 포함된 모든 변수(variableIds)를 순회하면서, 다음과 같은 데이터를 추출합니다:

  • varName: 변수의 이름 (예: Color/Primary/Background)
  • resolvedType: 변수의 타입 (예: COLOR, FLOAT, STRING 등)
  • valuesByMode: 각 모드에 따른 변수 값
  • description: 변수에 추가된 설명
  • codeSyntax: 변수를 참조할 때 사용하는 코드 문법(필요시 활용 가능)

이 데이터를 기반으로 변수를 계층 구조로 정리하여 JSON 구조를 생성합니다.

(5~9) 계층적 데이터 구조 생성

변수 이름(varName)을 /로 구분하여 그룹화하고, 이를 계층적으로 구성합니다. 예를 들어, Sementic-Color.Bg.Default라는 변수는 다음과 같은 JSON 구조로 변환됩니다:

{
  "Sementic-Color": {
    "Bg": {
      "Default": {
        "$type": "color",
        "$value": "#FFFFFF",
        "comment": "Default background color"
      }
    }
  }
}

(10) 참조 변수 처리

변수가 다른 변수(VARIABLE_ALIAS)를 참조하는 경우, 참조된 변수의 전체 경로를 $value에 저장합니다. 이 작업은 다음 단계를 포함합니다:

  1. 참조된 변수의 ID를 사용해 해당 변수 정보를 조회합니다.
  2. 참조된 변수가 속한 컬렉션 이름과 변수를 계층적으로 표시한 이름을 조합하여 경로를 생성합니다.
  3. 생성된 경로를 {컬렉션 이름.변수 이름} 형식으로 저장합니다.

예를 들어, Sementic-Color.Bg.DefaultColor.White.W100을 참조한다면 다음과 같이 변환됩니다:

{
  "Sementic-Color": {
    "Bg": {
      "Default": {
        "$type": "color",
        "$value": "{Color.White.W100}",
        "comment": "Default background color"
      }
    }
  }
}

(11~12) 변수 값 처리

변수 값은 타입에 따라 다르게 처리됩니다:

  • COLOR: RGB 값을 HEX 값으로 변환하여 저장합니다.
  • NUMBER, FLOAT, STRING, BOOLEAN: 타입에 맞게 그대로 저장합니다.
  • 참조(VARIABLE_ALIAS): 참조 경로를 $value로 저장합니다.

(13) 변수 설명 추가

변수에 설명(description)이 포함되어 있다면, 이를 comment 필드로 추가합니다.

(14~18) 최종 JSON 파일 저장

각 모드에 대한 변수 데이터를 JSON 파일 구조로 완성한 후, 이를 배열(files)에 저장합니다. 최종적으로 이 배열은 함수가 반환하며, 각 파일은 이후 저장 및 처리 단계에서 활용됩니다.

2. Bitbucket과 연동

이제 추출한 Variable들을 JSON 파일로 만들었으니, 변경사항을 추적하고 버전을 관리할 수 있도록 Git과 연결하는 작업을 진행해야 합니다.

원스토어에서는 형상관리 도구로 Bitbucket을 사용하고 있습니다. 따라서 추출한 데이터는 Bitbucket repository로 push해야 합니다.
Bitbucket REST API v2 문서에서 add&push 작업을 지원하는 API를 확인할 수 있었고, 이를 사용하면 쉽게 연동할 수 있을 것이라 생각했습니다.

그런데 여기에서 문제가 발생했습니다.

원스토어에서는 Bitbucket Data Center를 사용하고 있습니다. 하지만 위에서 언급한 REST API v2는 Bitbucket Cloud에서만 사용이 가능합니다.
따라서 Bitbucket Data Center와의 연동을 위해 이전 버전인 Bitbucket REST API v1을 사용해야 했습니다. 하지만 REST API v1은 CORS(Cross-Origin Resource Sharing) 정책이 적용되어 있지 않아 브라우저에서 API를 호출할 때 에러가 발생했습니다.

CORS(Cross-Origin Resource Sharing)
브라우저에서 다른 출처의 리소스에 접근하려 할 때 발생하는 보안 이슈로, 이를 해결하지 않으면 클라이언트에서 특정 API를 호출할 수 없습니다.

Bitbucket 버전을 변경할 수 없는 상황에서, 아래와 같은 해결책을 생각해냈습니다.

JSON 파일을 생성한 후 바로 Bitbucket REST API를 호출하는 대신, 중간에 다리 역할을 해주는 서버를 추가했습니다.
이 서버에서 API를 호출하도록 설정하면 CORS 정책이 적용되지 않은 REST API v1을 사용하더라도 데이터를 성공적으로 Bitbucket repository에 add&push할 수 있습니다.

3. 동작 확인


플러그인을 실행하면 정상적으로 Figma Variables를 추출할 수 있습니다. 그리고 변경사항을 입력한 후 Upload 버튼을 누르면 Bitbucket에 JSON 파일이 push됩니다.

마치며

MVP로 제작한 Variables Exporter 플러그인은 다음과 같은 성과를 가져왔습니다:

  • Figma Variables에 정의된 디자인 토큰을 효율적으로 관리
  • 변경사항 추적 및 버전 관리 개선

다음 포스팅에서는 Style Dictionary를 사용해 추출한 데이터를 개발 환경에서 활용하는 방법을 소개해 보겠습니다.
앞으로의 포스팅에 많은 관심 부탁드립니다. 감사합니다!