figmascope의 기본 내보내기 대상은 Jetpack Compose입니다. 그것은 임의적이지 않습니다 — Compose의 레이아웃 모델(Column, Row, Box, Modifier)이 IR 노드 종류(stack, overlay, absolute, leaf)에 밀접하게 매핑됩니다. Figma의 수직 스택은 Compose의 Column입니다. 번역이 기계적이어서 에이전트 기반 코드 생성에 잘 맞습니다.

이 가이드는 IR-to-Compose 매핑을 상세하게 다루고, 해당 Composable이 있는 실제 JSON 조각을 보여주며, 토큰 매핑 레이어를 설명합니다.

Compose가 기본 대상인 이유

세 가지 구조적 이유:

  1. Auto-layout ↔ Column/Row. Figma의 auto-layout 프레임(현대 Figma 디자인의 대다수)은 kind: "stack" 노드로 내보내집니다. 스택 노드는 axis 속성을 가집니다 — verticalColumn으로, horizontalRow로 매핑됩니다. 이것은 해석 단계 없는 1:1 매핑입니다.
  2. 간격 토큰 ↔ dp 값. Compose는 모든 레이아웃 치수에 Dp를 사용합니다. tokens.json의 토큰 값은 단위 없는 정수(예: spacing.16 = 16)로, 16.dp에 직접 매핑됩니다. 변환도, 스케일링도 없습니다.
  3. 색상 토큰 ↔ Color composable. tokens.json의 Figma 16진수 값은 단일 변환으로 Color(0xFFrrggbb)에 매핑됩니다. 토큰 키는 테마의 의미론적 변수 이름이 됩니다.

IR 노드 종류와 Compose 매핑

IR 종류속성Compose 기본 요소
stackaxis: "vertical"Column
stackaxis: "horizontal"Row
overlay레이어드 자식Box
absolutex, y, width, heightBoxModifier.offset(x.dp, y.dp)
leaftype: "text"TextStyle이 있는 Text
leaffill이 있는 type: "rectangle"Box(Modifier.background(Color(...)))

모든 노드 종류는 spacing 속성(자식 사이의 간격)과 padding 객체(상/우/하/좌)를 가질 수 있습니다. 두 가지 모두 토큰 키를 참조합니다.

토큰 매핑 상세

tokens.json 파일은 다음과 같습니다:

{
  "spacing": {
    "4": 4, "8": 8, "12": 12, "16": 16,
    "20": 20, "24": 24, "32": 32, "48": 48
  },
  "radius": {
    "4": 4, "8": 8, "12": 12, "16": 16, "full": 9999
  },
  "color": {
    "7f5cfe": "#7F5CFE",
    "1a1a2e": "#1A1A2E",
    "f6f2ea": "#F6F2EA",
    "ffffff": "#FFFFFF",
    "e53935": "#E53935"
  },
  "typography": {
    "heading.24": { "size": 24, "weight": 700, "lineHeight": 1.2 },
    "body.14": { "size": 14, "weight": 400, "lineHeight": 1.5 },
    "label.12": { "size": 12, "weight": 500, "lineHeight": 1.4 }
  }
}

매핑 규칙:

토큰 키는 의도적으로 불투명합니다(색상은 16진수 문자열, 간격은 숫자 문자열). 모델이 특정 명명 규칙으로 편향되지 않도록 합니다. Compose 테마는 IR과 독립적으로 의미론적 이름으로 — colorPrimary, spacingMd — 별칭을 설정할 수 있습니다.

실제 예시: 홈 화면 JSON에서 Composable로

다음은 단순화된 홈 화면 IR입니다. 헤더 leaf와 카드 목록이 있는 수직 스택:

{
  "name": "home",
  "kind": "stack",
  "axis": "vertical",
  "spacing": "spacing.24",
  "padding": { "top": "spacing.16", "right": "spacing.16",
               "bottom": "spacing.16", "left": "spacing.16" },
  "fill": "color.f6f2ea",
  "children": [
    {
      "kind": "leaf",
      "type": "text",
      "stringRef": "home.title",
      "typography": "typography.heading.24",
      "fill": "color.1a1a2e"
    },
    {
      "kind": "stack",
      "axis": "vertical",
      "spacing": "spacing.12",
      "children": [
        {
          "kind": "overlay",
          "radius": "radius.12",
          "fill": "color.ffffff",
          "padding": { "top": "spacing.16", "right": "spacing.16",
                       "bottom": "spacing.16", "left": "spacing.16" },
          "children": [
            {
              "kind": "leaf",
              "type": "text",
              "stringRef": "home.card.label",
              "typography": "typography.label.12",
              "fill": "color.7f5cfe"
            }
          ]
        }
      ]
    }
  ]
}

해당 Composable — 에이전트가 이 IR에서 생성해야 하는 것:

@Composable
fun HomeScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFFF6F2EA))
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(24.dp)
    ) {
        Text(
            text = stringResource(R.string.home_title),
            style = TextStyle(
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold,
                lineHeight = 28.8.sp,
                color = Color(0xFF1A1A2E)
            )
        )
        Column(
            verticalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            Box(
                modifier = Modifier
                    .clip(RoundedCornerShape(12.dp))
                    .background(Color(0xFFFFFFFF))
                    .padding(16.dp)
            ) {
                Text(
                    text = stringResource(R.string.home_card_label),
                    style = TextStyle(
                        fontSize = 12.sp,
                        fontWeight = FontWeight.Medium,
                        lineHeight = 16.8.sp,
                        color = Color(0xFF7F5CFE)
                    )
                )
            }
        }
    }
}

Composable의 모든 값은 IR의 토큰 키로 추적됩니다. 하드코딩된 것이 없습니다 — 16.dpspacing.16에서, 24.dpspacing.24에서, Color(0xFF7F5CFE)color.7f5cfe에서 옵니다.

문자열 참조 — stringResource 매핑

IR의 모든 텍스트 노드는 점 표기법 키가 있는 stringRef를 가집니다. strings.json 파일은 키를 표시 값과 폴백에 매핑합니다:

{
  "home.title": { "value": "Good morning", "fallback": "Good morning" },
  "home.card.label": { "value": "Today's summary", "fallback": "Summary" }
}

점 표기법은 점을 밑줄로 대체하여 Android 문자열 리소스 ID에 매핑됩니다: home.titleR.string.home_title. fallback 필드는 strings.xml에 아직 리소스가 없을 때 리터럴 문자열로 하드코딩하는 것입니다:

text = stringResource(R.string.home_title, "Good morning")

strings.xml이 채워지기 전 개발 중에 화면이 읽을 수 있도록 에이전트에게 항상 빈 문자열이 아닌 fallback 필드를 사용하도록 지시하세요.

절대 위치 지정

kind: "absolute"가 있는 노드는 Figma 좌표를 직접 사용합니다. 겹치는 요소나 특정 위치에 고정된 요소가 있는 디자인에서 나타납니다. Compose 매핑은 부모로 Box를 사용하고 자식에 Modifier.offset을 사용합니다:

// IR: { "kind": "absolute", "x": 24, "y": 80, "width": 120, "height": 40 }

Box(modifier = Modifier.fillMaxSize()) {
    Box(
        modifier = Modifier
            .offset(x = 24.dp, y = 80.dp)
            .size(width = 120.dp, height = 40.dp)
    ) {
        // 자식들
    }
}

절대 위치 지정은 잘 구조화된 Figma 파일에서 흔하지 않습니다(대부분의 현대 파일은 auto-layout을 사용합니다). 볼 때는 디자인 의도가 진정으로 절대적인지 아니면 디자이너가 단지 auto-layout 제약을 적용하지 않은 것인지 확인하세요 — IR은 의도를 추론할 수 없습니다.

솔직한 한계

번들은 일반적인 경우를 잘 다룹니다. 다루지 않는 몇 가지:

이러한 한계는 명시적입니다 — _meta.json 경고와 CONTEXT.md에 나타납니다. 에이전트가 조용히 추측하지 않습니다.

figmascope 메인 앱에서 컨텍스트 번들을 내보내고, Claude CodeCursor와 함께 사용하여 IR에서 직접 Composable을 구현하세요. 스크린샷 추측도, 하드코딩된 값도 없습니다.