Figma의 노드 트리는 풍부하지만 노이즈가 많습니다. Frame, Group, Component, Instance, Text, Rectangle, Ellipse, Vector — 각각은 수십 개의 선택적 필드를 가지고 있으며, 일부는 서로 충돌합니다. 원시 Figma API 응답을 직접 작업하는 에이전트는 코드를 작성하는 대신 스키마를 이해하는 데 인지 예산을 소비합니다.

figmascope는 트리를 네 가지 노드 종류로 정규화합니다: stack, overlay, absolute, leaf. 모든 screens/*.json 파일의 모든 노드는 이 네 가지 중 하나입니다. 다른 것은 없습니다.

네 가지 종류

stack

스택은 Figma Auto Layout이 있는 노드 — layoutMode: "VERTICAL" 또는 layoutMode: "HORIZONTAL". 자식들은 축을 따라 흐름 위치에 있습니다. 간격과 패딩이 명시적입니다.

{
  "kind": "stack",
  "id": "123:456",
  "name": "ContentColumn",
  "axis": "vertical",
  "gap": 16,
  "paddingTop": 24,
  "paddingBottom": 24,
  "paddingLeft": 20,
  "paddingRight": 20,
  "primaryAxisAlignItems": "SPACE_BETWEEN",
  "counterAxisAlignItems": "CENTER",
  "width": 390,
  "height": 844,
  "fills": [{ "type": "SOLID", "color": "#1a1a2e" }],
  "children": [ ... ]
}

Jetpack Compose에서 Column(수직 축) 또는 Row(수평 축)로 매핑됩니다. primaryAxisAlignItemscounterAxisAlignItems 필드는 각각 ArrangementAlignment에 매핑됩니다. gapArrangement.spacedBy()가 됩니다.

overlay

오버레이는 자식들이 내부에서 절대 위치를 가지는 노드입니다. Figma는 이것을 자식들이 absoluteBoundingBox 좌표를 가지는 layoutMode: "NONE" Frame으로 표현합니다. overlay 종류는 에이전트가 구조를 이해하기 위해 원시 좌표를 추론할 필요 없이 이것을 캡처합니다.

{
  "kind": "overlay",
  "id": "123:789",
  "name": "CardOverlay",
  "width": 358,
  "height": 200,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "children": [
    {
      "kind": "absolute",
      "offset": { "x": 16, "y": 16 },
      "child": { ... }
    }
  ]
}

Compose에서 Box로 매핑됩니다. 자식들은 부모 경계와의 위치 관계에 따라 Modifier.offset() 또는 Modifier.align()을 사용하여 위치가 지정됩니다.

absolute

absolute 노드는 부모 오버레이 내의 특정 (x, y) 오프셋에 단일 자식을 가지는 래퍼입니다. 얇은 구조적 노드입니다 — 위치를 콘텐츠와 혼합하지 않고 Figma에서 공간적 관계를 보존하기 위해 존재합니다.

{
  "kind": "absolute",
  "offset": { "x": 24, "y": 140 },
  "child": {
    "kind": "leaf",
    "name": "BadgeLabel",
    "text": "NEW",
    "stringRef": "badge.new",
    ...
  }
}

Compose에서 이것은 일반적으로 Modifier.offset(x.dp, y.dp)가 있는 Box의 자식이 됩니다. 오프셋 값은 간격 토큰이 범위 내에 있는 경우 토큰 대체의 후보입니다.

leaf

leaf는 터미널 노드입니다 — 자식 없음. 스타일링(fill, stroke, corner radius)을 가지며 선택적으로 텍스트 콘텐츠를 가집니다. 텍스트 leaf는 text 필드(리터럴 문자열)와 stringRef 필드(strings.json의 i18n 리소스 키)를 가집니다.

{
  "kind": "leaf",
  "id": "123:101",
  "name": "SpeedLabel",
  "text": "Speed Test",
  "stringRef": "speed.test",
  "fontSize": 18,
  "fontWeight": 600,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "width": 160,
  "height": 28
}

Compose에서 텍스트 콘텐츠의 경우 Text() composable로, fill 유형에 따라 비텍스트 leaf의 경우 Surface, Box, 또는 Image로 매핑됩니다.

네 가지 종류는 모든 UI 프레임워크의 합성 모델에 1:1로 매핑됩니다. 스택은 흐름 레이아웃입니다. 오버레이는 z-위치 컨테이너입니다. absolute는 공간적 메타데이터를 가집니다. leaf는 콘텐츠입니다. 그것이 전부입니다. Figma 노드 분류법은 12개 이상의 노드 유형을 가집니다 — IR은 그것을 네 가지로 줄입니다.

종류가 결정되는 방법

분류 로직:

  1. layoutMode"VERTICAL" 또는 "HORIZONTAL"이면 → stack
  2. layoutMode"NONE"이고 자식이 있으면 → overlay (자식들은 absolute로 래핑됨)
  3. 노드가 overlay의 직접 자식이고 위치를 가지면 → absolute
  4. 노드에 자식이 없고 absolute 래퍼가 아니면 → leaf

_meta.jsonlayout-mode-none-inferred 경고는 layoutMode: "NONE"인 Frame이 비자명하게 겹치는 경계 박스를 가진 자식을 가질 때 발생합니다. figmascope는 그것을 overlay로 처리하지만, 실제로 일부 layoutMode: "NONE" frame은 단순히 단일 자식을 위한 컨테이너(겹침 없음)이고 단순화될 수 있기 때문에 추론을 기록합니다. 경고는 에이전트가 그것을 어떻게 처리할지 결정하게 합니다.

absoluteBoundingBox — 보존되는 이유

IR의 모든 노드는 Figma의 absoluteBoundingBox를 유지합니다. 부모가 stack인 경우에도 (절대 위치가 이론적으로 레이아웃과 무관한 경우에도):

{
  "kind": "stack",
  "absoluteBoundingBox": { "x": 0, "y": 88, "width": 390, "height": 756 },
  ...
}

이것은 직접적인 부모-자식 관계를 공유하지 않는 노드 간의 공간적 관계를 추론해야 하는 에이전트를 위해 있습니다. 트리의 다른 가지에서 두 요소를 겹치려면 상대적 오프셋만이 아닌 절대 위치를 알아야 합니다. 좌표 시스템은 Figma의 캔버스 — 왼쪽 상단 원점, y가 아래로 증가합니다.

PNG 교차 참조에도 도움이 됩니다. _meta.json에는 pngCount 필드가 포함되며 내보낸 PNG는 화면 슬러그로 이름이 지정됩니다. IR을 스크린샷과 비교하는 경우, absoluteBoundingBox를 사용하면 이미지 내에서 특정 노드를 찾을 수 있습니다. 자신의 디자인에서 이것을 보려면 figmascope.dev에서 번들을 내보내세요.

컨테이너 종류의 Fill과 Stroke

혼란스러운 공통 지점: stack과 overlay 노드는 leaf만이 아니라 fill을 가질 수 있습니다. 배경색이 있는 column은 여전히 stack입니다 — 그냥 fills 배열도 가집니다. figmascope는 이것을 보존합니다:

{
  "kind": "stack",
  "axis": "vertical",
  "fills": [{ "type": "SOLID", "color": "#f6f2ea" }],
  "cornerRadius": 12,
  ...
}

Compose에서 이것은 일반적으로 fill 색상과 corner radius가 컨테이너에 적용된 Surface 또는 Card 안의 Column이 됩니다. IR은 fill을 자식으로 축소하지 않습니다 — Figma가 설정한 노드에 유지합니다.

컨테이너 종류의 그라디언트 fill은 background-gradient-not-supported:<name> 경고를 발생시킵니다. 노드는 여전히 다른 필드가 온전한 상태로 IR에 있습니다. 에이전트는 fill을 TODO로 처리하고 솔리드 색상으로 근사하거나 CONTEXT.md 범위 주석에 따라 커스텀 드로잉 호출을 생성해야 합니다.

stringRef + text 관계

텍스트 leaf 노드는 두 가지 모두를 가집니다:

텍스트 노드의 콘텐츠가 충돌이나 필터(숫자만, 비어 있음, 너무 짧음)로 인해 strings.json에서 제외된 경우, leaf는 여전히 text를 가지지만 stringRef가 없을 것입니다. 에이전트는 그 경우 리터럴로 폴백해야 합니다. 전체 충돌 처리 로직은 strings.json을 참조하세요.

둘 다 있을 때, CONTEXT.md 제약은 stringRef를 사용하라고 합니다. text 필드는 에이전트 추론(strings.json을 열지 않고도 문자열이 무엇인지 알 수 있게)과 폴백을 위해 있습니다.

IR의 컴포넌트 인스턴스

Figma 노드가 컴포넌트의 INSTANCE인 경우, IR 노드는 두 가지 추가 필드를 가집니다:

{
  "kind": "stack",
  "componentId": "789:012",
  "componentName": "PrimaryButton",
  ...
}

이것은 components/inventory.json과 연결됩니다. 에이전트는 이 노드가 맞춤형 레이아웃이 아닌 PrimaryButton의 인스턴스임을 알고, 중복 코드를 생성하는 대신 기존 컴포넌트를 참조해야 합니다. 이것의 전체 내용은 컴포넌트 인벤토리에 있습니다.

실제 화면 예시

헤더, 콘텐츠 column, 플로팅 버튼이 있는 화면의 단순화하되 구조적으로 정확한 예시:

{
  "screen": "home",
  "root": {
    "kind": "overlay",
    "name": "HomeScreen",
    "width": 390,
    "height": 844,
    "fills": [{ "type": "SOLID", "color": "#0d0d1a" }],
    "children": [
      {
        "kind": "absolute",
        "offset": { "x": 0, "y": 0 },
        "child": {
          "kind": "stack",
          "name": "ContentArea",
          "axis": "vertical",
          "gap": 24,
          "paddingTop": 56,
          "paddingLeft": 20,
          "paddingRight": 20,
          "children": [
            {
              "kind": "leaf",
              "name": "Title",
              "text": "Speed Test",
              "stringRef": "speed.test",
              "fontSize": 28,
              "fontWeight": 700
            }
          ]
        }
      },
      {
        "kind": "absolute",
        "offset": { "x": 111, "y": 752 },
        "child": {
          "kind": "stack",
          "name": "StartButton",
          "componentId": "321:654",
          "componentName": "PrimaryButton",
          "axis": "horizontal",
          "gap": 8,
          "paddingTop": 16,
          "paddingBottom": 16,
          "paddingLeft": 32,
          "paddingRight": 32,
          "cornerRadius": 24,
          "fills": [{ "type": "SOLID", "color": "#7f5cfe" }]
        }
      }
    ]
  }
}

이것으로 에이전트는 콘텐츠를 위한 절대 위치의 Column과 하단에 절대 위치의 PrimaryButton 컴포넌트가 있는 Compose Box 화면을 올바르게 생성할 수 있습니다. 모든 레이아웃 결정이 추측 없이 IR에서 도출 가능합니다.

이 구조의 간격과 색상 값에 토큰이 어떻게 적용되는지는 tokens.json 해설을 참조하세요. Cursor나 Claude Code와 함께 이 IR을 사용하는 전체 에이전트 워크플로는 Figma로 Jetpack Compose 만들기를 참조하세요. figmascope에 Figma URL을 붙여넣어 직접 시도해보세요.