figmascope의 기본 내보내기 대상은 Jetpack Compose입니다. 그것은 임의적이지 않습니다 — Compose의 레이아웃 모델(Column, Row, Box, Modifier)이 IR 노드 종류(stack, overlay, absolute, leaf)에 밀접하게 매핑됩니다. Figma의 수직 스택은 Compose의 Column입니다. 번역이 기계적이어서 에이전트 기반 코드 생성에 잘 맞습니다.
이 가이드는 IR-to-Compose 매핑을 상세하게 다루고, 해당 Composable이 있는 실제 JSON 조각을 보여주며, 토큰 매핑 레이어를 설명합니다.
Compose가 기본 대상인 이유
세 가지 구조적 이유:
- Auto-layout ↔ Column/Row. Figma의 auto-layout 프레임(현대 Figma 디자인의 대다수)은
kind: "stack"노드로 내보내집니다. 스택 노드는axis속성을 가집니다 —vertical은Column으로,horizontal은Row로 매핑됩니다. 이것은 해석 단계 없는 1:1 매핑입니다. - 간격 토큰 ↔ dp 값. Compose는 모든 레이아웃 치수에
Dp를 사용합니다.tokens.json의 토큰 값은 단위 없는 정수(예:spacing.16 = 16)로,16.dp에 직접 매핑됩니다. 변환도, 스케일링도 없습니다. - 색상 토큰 ↔ Color composable.
tokens.json의 Figma 16진수 값은 단일 변환으로Color(0xFFrrggbb)에 매핑됩니다. 토큰 키는 테마의 의미론적 변수 이름이 됩니다.
IR 노드 종류와 Compose 매핑
| IR 종류 | 속성 | Compose 기본 요소 |
|---|---|---|
stack | axis: "vertical" | Column |
stack | axis: "horizontal" | Row |
overlay | 레이어드 자식 | Box |
absolute | x, y, width, height | Box와 Modifier.offset(x.dp, y.dp) |
leaf | type: "text" | TextStyle이 있는 Text |
leaf | fill이 있는 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 }
}
}
매핑 규칙:
spacing.16→16.dpradius.12→RoundedCornerShape(12.dp)color.7f5cfe→Color(0xFF7F5CFE)(전체 알파를 위해0xFF앞에 추가)typography.heading.24→TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.8.sp)
토큰 키는 의도적으로 불투명합니다(색상은 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.dp는 spacing.16에서, 24.dp는 spacing.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.title → R.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은 의도를 추론할 수 없습니다.
솔직한 한계
번들은 일반적인 경우를 잘 다룹니다. 다루지 않는 몇 가지:
- 그라디언트 fill. IR은 그라디언트를 만날 때 경고를 내보냅니다. 노드의 배경 fill이 생략됩니다.
// TODO: gradient를 남기고 수동으로 구현하세요. - 복잡한 효과. 드롭 섀도우와 블러는 IR에 표현되지 않습니다. 있을 경우
_meta.json경고에 나타납니다. - 벡터 아이콘. IR은 아이콘 노드에 대한 경로 데이터가 아닌 참조 ID를 저장합니다. 아이콘을 별도로 실제 drawable이나 compose 아이콘으로 해석해야 합니다.
- 중첩된 컴포넌트. IR은 인스턴스 노드에
componentId를 포함합니다.components/inventory.json은 ID를 이름에 매핑합니다. 컴포넌트를 별도로 구현하고 부모 Composable에서 이름으로 참조하세요.
이러한 한계는 명시적입니다 — _meta.json 경고와 CONTEXT.md에 나타납니다. 에이전트가 조용히 추측하지 않습니다.
figmascope 메인 앱에서 컨텍스트 번들을 내보내고, Claude Code나 Cursor와 함께 사용하여 IR에서 직접 Composable을 구현하세요. 스크린샷 추측도, 하드코딩된 값도 없습니다.