Figma Make의 비밀: 문서화되지 않은 바이너리 포맷 리버스 엔지니어링


최근 클라이언트가 Figma의 새로운 AI 프로토타이핑 도구인 Figma Make 로 만든 멋진 UI를 들고 찾아왔습니다. 디자인은 훌륭했고 애니메이션도 부드러웠죠. 문제는 단 하나, 코드를 추출할 방법이 없다는 것 이었습니다.

일반적인 Figma 파일이라면 API를 쓰거나 디자인 파일(.fig)을 내보내면 그만입니다. 하지만 Make 파일에 대해 Figma API를 호출하면 다음과 같은 친절한(?) 400 에러를 뱉어냅니다.

{"status":400,"err":"File type not supported by this endpoint"}

전형적인 “Corporate Lock-in(기업 벤더 잠금)” 전략입니다. 디자인 파일도 없고, API도 막혀있습니다. 여기서 포기해야 할까요? 엔지니어로서 그럴 순 없죠. 오늘은 Figma Make의 .make 파일 구조를 역분석(Reverse Engineering)하여 React 코드를 추출해낸 과정을 기술적으로 깊게 파헤쳐 보겠습니다.

1. 확장자는 거짓말을 한다: ZIP 파일의 발견

Figma 인터페이스를 뒤지다 보니 .make 파일을 다운로드할 수 있다는 걸 알았습니다. 하지만 이진 파일(Binary)이라 바로 읽을 수는 없었죠. 리버스 엔지니어링의 첫 번째 규칙은 “확장자를 믿지 마라” 입니다.

바이너리의 처음 4바이트를 확인해 봅니다.

xxd -l 4 "ClientApp.make"
# Output: 504b 0304

504b는 ASCII로 PK입니다. 네, 우리에게 익숙한 ZIP 파일 시그니처 죠. .make 파일은 그저 확장자만 바꾼 ZIP 아카이브였습니다. (Hacker News의 한 유저는 xxd 대신 리눅스의 file 커맨드를 쓰면 더 쉽게 알 수 있다고 조언하더군요. 맞는 말입니다.)

압축을 풀어보니 흥미로운 구조가 드러났습니다.

  • canvas.fig: 2.3MB 바이너리 (핵심 데이터)
  • meta.json: 프로젝트 메타데이터
  • ai_chat.json: 34MB 에 달하는 AI 대화 로그

ai_chat.json에는 클라이언트가 앱을 만들면서 입력한 모든 프롬프트와 수정 내역이 들어있었습니다. 꽤나 방대한 양이었지만, 제가 필요한 건 실제 코드였습니다. 정답은 canvas.fig에 있을 것 같았습니다.

2. 바이너리 디코딩: Kiwi와 압축 알고리즘의 혼종

canvas.fig의 헤더를 열어봤습니다.

xxd -l 20 canvas.fig
# Output: 6669 672d 6d61 6b65 65... (fig-makee)

표준 Figma 파일은 fig-kiwi라는 매직 헤더를 사용합니다. 반면 Make 파일은 fig-makee를 사용하더군요. 포맷은 다르지만 구조는 비슷할 것이라는 가설을 세웠습니다.

압축의 함정 (Deflate + Zstandard)

데이터는 두 개의 청크(Chunk)로 나뉘어 있었는데, 여기서 Figma 엔지니어들의 독특한(혹은 변태적인) 선택을 발견했습니다. 두 청크가 서로 다른 압축 알고리즘을 사용 하고 있었습니다.

  1. Chunk 1 (스키마): zlib/deflate 압축. (24KB → 59KB)
  2. Chunk 2 (데이터): Zstandard 압축. (매직 바이트 28 B5 2F FD)

처음엔 둘 다 zlib인 줄 알고 시도했다가 실패했습니다. LLM의 도움을 받아 매직 바이트를 분석한 끝에 두 번째 청크가 Facebook에서 만든 Zstandard 라는 것을 알아냈습니다. 왜 굳이 섞어 썼을까요? 아마도 스키마는 호환성을 위해 구형 방식을, 대용량 데이터는 성능을 위해 최신 방식을 택한 게 아닐까 추측해 봅니다.

Kiwi 스키마 파싱

Figma는 Kiwi 라는 자체 바이너리 포맷을 사용합니다. Protobuf와 비슷하지만 스키마 정의가 파일 내부에 포함되지 않는 경우가 많습니다. 다행히 Chunk 1에 스키마 정의가 포함되어 있었고, 이를 통해 Chunk 2의 데이터를 디코딩할 수 있었습니다.

디코딩 결과, 159개의 노드로 이루어진 트리 구조가 나타났고, 그중 CODE_FILE이라는 노드 타입을 발견했습니다.

{
  "type": "CODE_FILE",
  "name": "App.tsx",
  "sourceCode": "import React from 'react'..."
}

빙고! React 컴포넌트, 유틸리티 함수, CSS 파일들이 고스란히 텍스트 형태로 들어있었습니다.

3. 원시 코드(Raw Code)를 실행 가능한 앱으로

코드를 추출했다고 끝이 아닙니다. Figma Make가 내부적으로 사용하는 코드 스타일은 바로 실행하기엔 몇 가지 문제가 있었습니다.

  1. 버전 명시된 패키지 임포트: import { Dialog } from '@radix-ui/react-dialog@1.1.6' 처럼 버전을 하드코딩합니다. 이를 표준적인 패키지 명으로 변환해야 했습니다.
  2. 독자적인 에셋 경로: figma:asset/hash.png 같은 형태의 경로를 사용합니다. 이를 로컬 /images/ 경로로 치환하고, 해시로 된 파일명에 .png 확장자를 붙여주는 작업이 필요했습니다.
  3. Tailwind CSS v4 이슈: 최신 Tailwind v4를 사용하고 있어 PostCSS 설정이 기존과 달랐습니다.

이 모든 과정을 자동화하는 스크립트를 작성하여, 결과적으로 npm run dev 한 방으로 실행되는 완벽한 Vite + React 프로젝트를 복원해냈습니다.

4. 엔지니어로서의 고찰

이번 리버스 엔지니어링을 통해 몇 가지 흥미로운 점을 발견했습니다.

  • Figma Make의 정체: 마법처럼 보였지만, 내부적으로는 React + Radix UI + Tailwind CSS 라는 매우 표준적인 스택을 사용하고 있었습니다. AI가 생성한 코드치고는 구조가 꽤 깔끔했습니다.
  • 데이터의 투명성: 기업들은 독자 포맷으로 데이터를 가두려 하지만(Lock-in), 결국 효율성을 위해 표준 기술(ZIP, Zstd, Deflate)을 사용할 수밖에 없습니다. 이는 우리 같은 엔지니어들에게 항상 ‘뒷문’이 열려있음을 의미합니다.

결론

Figma Make는 훌륭한 도구입니다. 하지만 소스 코드 접근을 막아둔 것은 아쉬운 결정입니다. 이번 프로젝트를 통해 클라이언트는 원하던 코드를 얻었고, 저는 주말 동안 즐거운 퍼즐을 풀었습니다.

추출 스크립트는 오픈소스로 공개해 두었습니다. 혹시 비슷한 상황에 처한 분들이 있다면 도움이 되길 바랍니다.

GitHub: figma-make-extractor