수학이 필요한 Figma to CSS 속성들
gradient 하나만 봐도 이미 머리가 살짝 뜨끈해졌습니다. 그런데 Figma to CSS에서 수학이 필요한 속성은 gradient만 있는 게 아닙니다.
Figma는 기본적으로 scene graph + affine transform + vector paint model 위에 올라가 있습니다. CSS는 DOM tree + box model + layout engine + paint/composite pipeline 위에 올라가 있고요. 둘 다 화면에 사각형과 텍스트를 그리지만, 값을 저장하는 방식이 꽤 다릅니다.
그래서 변환기는 단순히 이름만 바꾸는 번역기가 아닙니다.
Figma property
-> 중간 수학 모델
-> CSS로 정확히 표현 가능한가?
-> 안 되면 SVG, asset, runtime fallback을 쓸 것인가?
이 판단을 해야 합니다. 안 그러면 export 결과가 처음에는 그럴듯한데, 프레임 크기를 바꾸거나 회전된 그룹 안에 넣는 순간 슬쩍 다른 얼굴이 됩니다. 자, 범인을 잡으러 갑시다.
먼저 전체 지도부터 보기
Figma 속성을 CSS로 옮길 때 수학이 필요한 대표 항목은 이 정도입니다.
| Figma 쪽 값 | CSS 쪽 목표 | 필요한 수학 | 주의할 점 |
|---|---|---|---|
relativeTransform, absoluteTransform | transform: matrix(...), transform-origin: 0 0 | affine matrix, parent-child composition, inverse, pivot correction | Figma rotation은 top-left 기준, CSS 기본 origin은 center |
constraints | left, right, %, calc() | parent resize equation | Figma constraint와 CSS layout은 1:1 대응이 아님 |
| Auto Layout | display:flex, gap, padding, flex-grow | accumulated size, hug/fill/fixed sizing | Figma의 hug contents는 CSS intrinsic sizing과 비슷하지만 완전히 같지 않음 |
cornerRadius, rectangleCornerRadii, cornerSmoothing | border-radius, SVG path | radius normalization, superellipse approximation | corner smoothing은 CSS 기본 radius로 정확히 표현 불가 |
strokeWeight, strokeAlign, dashPattern | border, outline, box-shadow, SVG stroke | visual bounds expansion, path stroking | CSS border는 주로 box 안쪽 레이아웃에 영향을 줌 |
effects | box-shadow, filter, mix-blend-mode | blur spread, alpha compositing, bounds expansion | shadow/blur는 geometry 밖으로 퍼짐 |
| text metrics | font-*, line-height, letter-spacing | baseline, line box, glyph advance | 폰트 렌더링 엔진 차이로 픽셀 완전 일치가 어려움 |
| image fill | object-fit, background-size, background-position | cover/contain scale, UV transform | imageTransform이 있으면 배경 위치 계산이 필요함 |
vectorNetwork, vector, boolean, mask | SVG, clip-path, asset | graph traversal, path transform, fill rule, boolean operation | 복잡한 vector는 CSS box로 억지 변환하지 않는 편이 좋음 |
이 표가 오늘의 지도입니다. “CSS로 갈 수 있나?”만 보면 안 되고, “어떤 좌표계의 값을 어떤 좌표계로 보내는가?”를 같이 봐야 합니다.
1. Transform: 행렬은 거의 항상 출발점입니다
Figma의 transform은 2x3 affine transform입니다.
T = [
[a, c, e],
[b, d, f]
]
점 하나를 변환하면 이렇게 됩니다.
x' = a * x + c * y + e
y' = b * x + d * y + f
CSS의 matrix(a, b, c, d, e, f)도 같은 식으로 읽을 수 있습니다.
transform: matrix(a, b, c, d, e, f);
Figma 값을 CSS로 옮길 때는 거의 항상 origin을 같이 써야 합니다.
.figma-node {
position: absolute;
width: 120px;
height: 80px;
transform-origin: 0 0;
transform: matrix(a, b, c, d, e, f);
}
이유는 간단합니다.
Figma rotation 기준점 = local top-left
CSS 기본 transform-origin = center
Figma plugin API의 rotation은 top-left 기준입니다. CSS는 기본값이 50% 50%라서, 같은 matrix라도 origin 보정이 끼면 화면 위치가 달라질 수 있습니다. 그러니 Figma의 relativeTransform 또는 absoluteTransform을 CSS matrix로 직렬화할 때는 transform-origin: 0 0을 명시하는 편이 안전합니다.
단, 여기서 한 번 조심해야 합니다. Figma의 node는 parent frame 안에서 relativeTransform을 가지고, 화면 전체 기준으로는 absoluteTransform을 가집니다.
childWorld = parentWorld * childLocal
childLocal = inverse(parentWorld) * childWorld
이 공식은 editor 전체에서 계속 나옵니다. group 안의 child를 밖으로 빼거나, frame에서 group으로 옮기거나, 회전된 부모 안의 child를 드래그할 때 모두 같은 이야기입니다.
DOM으로 옮길 때도 마찬가지입니다.
Figma scene graph:
Frame
Group
Rect
CSS DOM:
div.frame
div.group
div.rect
이렇게 계층을 유지하면 local transform을 비교적 자연스럽게 쓸 수 있습니다. 반대로 모든 요소를 하나의 절대 좌표 평면에 펼치면 absoluteTransform을 CSS transform으로 만들어야 합니다. 둘 중 뭐가 더 좋냐고요? 상황마다 다릅니다. 변환기 입장에서는 둘 다 준비해야 마음이 덜 시끄럽습니다.
center 기준 회전은 translation 보정이다
그럼 Figma에서 center 기준으로 돌리고 싶으면 어떻게 할까요? CSS처럼 transform-origin: center 하나로 끝내는 게 아니라, pivot이 움직이지 않도록 matrix의 translation을 다시 계산합니다.
pivotLocal = (width / 2, height / 2)
pivotParent = oldMatrix * pivotLocal
newTranslation = pivotParent - newLinearPart * pivotLocal
좀 더 풀면 이렇습니다.
newE = pivotParent.x - (a * pivotLocal.x + c * pivotLocal.y)
newF = pivotParent.y - (b * pivotLocal.x + d * pivotLocal.y)
여기서 newLinearPart는 새 회전, 새 skew, 새 scale에 해당하는 matrix의 왼쪽 2x2 부분입니다.
newLinearPart = [
[a, c],
[b, d]
]
이 식의 의미는 하나입니다.
새 matrix를 적용해도 pivotLocal이 같은 parent 위치에 남아야 한다.
rotation handle, group resize, child reparenting이 모두 이 식의 친척입니다. 회전 중심, resize 반대편 corner, reparenting 전 world position은 이름만 다르고 전부 “고정해야 하는 점”입니다. 수학이 같은 집안입니다. 명절에 만나면 서로 알아봅니다.
2. Constraints: 반응형은 이름이 아니라 식입니다
Figma constraint는 부모 크기가 바뀔 때 child의 위치와 크기를 어떻게 다시 계산할지 말합니다.
왼쪽 고정이면 단순합니다.
left' = left
width' = width
오른쪽도 같이 고정이면 width가 바뀝니다.
right = parentWidth - left - width
left' = left
width' = parentWidth' - left - right
가운데 고정이면 중심점 비율이나 offset을 잡습니다.
centerOffset = childCenter - parentWidth / 2
childCenter' = parentWidth' / 2 + centerOffset
left' = childCenter' - width / 2
scale이면 비율입니다.
leftRatio = left / parentWidth
widthRatio = width / parentWidth
left' = parentWidth' * leftRatio
width' = parentWidth' * widthRatio
CSS로는 left, right, width, %, calc()를 섞어서 표현합니다.
.child {
position: absolute;
left: 24px;
right: 32px;
}
여기서 중요한 건 “Figma의 constraint 이름을 CSS 속성 하나로 바꾼다”가 아닙니다. 부모가 바뀔 때의 재계산 식을 CSS가 대신 풀 수 있게 만들어야 합니다.
3. Auto Layout: flex로 가지만, 완전히 flex는 아닙니다
Figma Auto Layout은 CSS flex와 많이 닮았습니다.
layoutMode: HORIZONTAL
paddingLeft / paddingRight / paddingTop / paddingBottom
itemSpacing
primaryAxisSizingMode
counterAxisSizingMode
가로 Auto Layout의 child 위치는 대략 이렇게 누적됩니다.
childX(i) = paddingLeft
+ sum(width(0..i-1))
+ itemSpacing * i
CSS로는 이렇게 갑니다.
.frame {
display: flex;
flex-direction: row;
gap: 12px;
padding: 16px 20px;
}
하지만 sizing에서 슬쩍 어려워집니다.
Figma FIXED -> width: Npx
Figma HUG -> width: fit-content 또는 auto
Figma FILL -> flex: 1 1 0
Figma의 HUG는 “내용에 맞춘다”이고, CSS의 intrinsic sizing도 비슷합니다. 하지만 text wrapping, min size, flex shrink가 끼면 결과가 달라질 수 있습니다. 이럴 때는 중간 모델에서 fixed, hug, fill을 유지한 뒤, CSS export와 runtime preview를 따로 검증하는 편이 좋습니다.
4. Radius와 smoothing: 둥근 모서리는 생각보다 성격이 있습니다
기본 cornerRadius는 CSS border-radius로 잘 갑니다.
border-radius: 12px;
네 모서리가 다르면 이렇게 됩니다.
border-radius: 8px 16px 24px 4px;
그런데 radius는 box보다 커질 수 있습니다. 브라우저는 인접한 radius 합이 edge 길이를 넘으면 자동으로 줄입니다.
scale = min(
width / (topLeftX + topRightX),
width / (bottomLeftX + bottomRightX),
height / (topLeftY + bottomLeftY),
height / (topRightY + bottomRightY),
1
)
normalizedRadius = radius * scale
Figma의 cornerSmoothing은 더 어렵습니다. CSS의 border-radius는 원호 기반이고, Figma smoothing은 squircle 또는 superellipse에 가까운 곡선으로 보입니다. 정확히 맞추려면 SVG path fallback이 필요합니다.
CSS 가능:
border-radius
정확도 필요:
SVG path로 rounded rectangle 생성
여기서 억지로 CSS 하나로 다 해결하려고 하면, 디자인 QA에서 “뭔가 모서리가 다른데요?”라는 아주 무서운 문장이 등장합니다.
5. Stroke: border와 stroke는 같은 척하지만 다릅니다
Figma stroke에는 strokeAlign이 있습니다.
INSIDE
CENTER
OUTSIDE
사각형의 원래 geometry가 w x h이고 stroke 두께가 s라면 visual bounds는 이렇게 달라집니다.
INSIDE:
visualWidth = w
visualHeight = h
CENTER:
visualWidth = w + s
visualHeight = h + s
OUTSIDE:
visualWidth = w + 2s
visualHeight = h + 2s
CSS border는 box model에서 기본적으로 요소 크기 계산에 영향을 줍니다. box-sizing: border-box를 쓰면 inside stroke처럼 다루기 좋습니다.
.rect {
box-sizing: border-box;
border: 4px solid #111;
}
하지만 center/outside stroke, dash, cap, join까지 정확히 가려면 SVG가 더 안전합니다.
<rect stroke="black" stroke-width="4" stroke-dasharray="8 4" />
즉 Figma stroke는 “border로 갈 수 있나?”보다 “이 stroke가 box stroke인가, path stroke인가?”를 먼저 판단해야 합니다.
6. Effects: shadow와 blur는 bounds를 키웁니다
Drop shadow는 CSS box-shadow로 꽤 잘 갑니다.
box-shadow: 0 12px 32px rgb(0 0 0 / 0.18);
하지만 bounds 계산은 별도입니다.
shadowLeft = x + offsetX - blur - spread
shadowTop = y + offsetY - blur - spread
shadowRight = x + width + offsetX + blur + spread
shadowBottom = y + height + offsetY + blur + spread
selection bounds를 geometry만 보고 잡으면 shadow가 잘립니다. export 이미지도 마찬가지입니다. blur나 shadow가 있으면 “보이는 영역”은 원래 도형보다 커집니다.
opacity와 blend mode는 색 합성입니다.
out = src * alpha + dst * (1 - alpha)
CSS로는 대체로 이렇게 갑니다.
opacity: 0.72;
mix-blend-mode: multiply;
filter: blur(8px);
단, Figma와 브라우저의 blending pipeline이 완전히 같지 않을 수 있습니다. 특히 isolation, stacking context, backdrop이 섞이면 변환기가 조용히 땀을 흘립니다.
7. Text: 폰트는 숫자보다 렌더러가 더 셉니다
텍스트는 CSS로 바로 옮길 수 있어 보입니다.
font-family: Inter, sans-serif;
font-size: 16px;
font-weight: 600;
line-height: 24px;
letter-spacing: -0.2px;
하지만 정확히 맞추려면 text metrics를 봐야 합니다.
lineBoxHeight = lineHeight
lineY(i) = top + i * lineHeight
glyphAdvance' = glyphAdvance + letterSpacing
Figma의 text auto resize도 중요합니다.
AUTO_WIDTH -> width: max-content 계열
AUTO_HEIGHT -> width fixed, height auto
FIXED_SIZE -> width/height fixed + overflow 처리
텍스트는 특히 운영체제, 브라우저, 폰트 파일, font feature에 따라 픽셀이 달라질 수 있습니다. 그래서 “완전한 픽셀 매칭”을 목표로 하면 CSS만으로는 피곤해질 수 있습니다. 웹으로 다시 편집할 텍스트는 CSS text로, 로고처럼 고정된 그래픽 텍스트는 SVG path나 asset fallback도 고려합니다.
8. Image fill: cover와 crop은 좌표 변환입니다
Figma image fill의 scaleMode는 CSS와 연결됩니다.
FILL -> object-fit: cover
FIT -> object-fit: contain
CROP -> background-size/position 또는 imageTransform
TILE -> background-repeat
cover와 contain은 scale 계산입니다.
coverScale = max(boxWidth / imageWidth, boxHeight / imageHeight)
containScale = min(boxWidth / imageWidth, boxHeight / imageHeight)
renderWidth = imageWidth * scale
renderHeight = imageHeight * scale
center crop이면 offset도 계산합니다.
offsetX = (boxWidth - renderWidth) / 2
offsetY = (boxHeight - renderHeight) / 2
Figma의 imageTransform이 들어오면 gradient와 비슷하게 normalized image 좌표계를 box 좌표계로 옮기는 문제가 됩니다. 단순히 background-size: cover로 끝나지 않습니다. 이때는 UV transform을 계산해서 background-position, background-size, 또는 wrapper transform으로 나눠야 합니다.
9. Vector, mask, boolean: CSS box로 억지 번역하지 않기
Figma vector는 path입니다. CSS box가 아닙니다.
path point -> transform -> fill rule -> stroke -> mask/clip
단순한 사각형, 원, rounded rectangle은 CSS box나 SVG primitive로 바꿀 수 있습니다.
.circle {
border-radius: 999px;
}
하지만 boolean operation, complex path, mask, winding rule이 들어오면 SVG가 더 자연스럽습니다.
<path d="..." fill-rule="evenodd" />
<clipPath id="clip">...</clipPath>
<mask id="mask">...</mask>
이건 포기라기보다 올바른 매체 선택입니다. CSS는 layout과 box paint에 강하고, SVG는 vector geometry에 강합니다. 친구끼리 역할을 나누면 됩니다. 싸움 붙이지 맙시다.
변환기의 세 가지 출력 전략
Figma to CSS 변환기는 모든 것을 CSS 한 줄로 만들려고 하면 금방 무리합니다. 대신 속성마다 전략을 나누는 게 좋습니다.
1. Exact CSS
CSS 속성으로 의미와 픽셀이 충분히 맞는 경우
2. CSS Approximation
거의 맞지만 일부 렌더링 차이가 허용되는 경우
3. SVG / Asset / Runtime fallback
vector, mask, smoothing, complex effect처럼 CSS box 모델로 무리인 경우
예를 들면 이렇습니다.
simple rectangle -> CSS div
rounded rectangle -> CSS border-radius
smoothed rounded rect -> SVG path fallback
linear gradient -> CSS linear-gradient with transform conversion
complex vector mask -> SVG
photo crop -> CSS background or img wrapper transform
좋은 변환기는 “CSS를 많이 쓰는 변환기”가 아닙니다. 어떤 값이 CSS의 수학 모델과 맞는지 알고, 안 맞는 값은 더 정확한 표현으로 보내는 변환기입니다.
다음에 깊게 파면 좋은 강의들
이 레슨은 지도입니다. 여기서 더 깊게 들어가면 다음 주제들이 따로 한 강의씩 필요합니다.
1. Figma transform tree -> CSS DOM transform tree
2. Constraints -> responsive absolute layout
3. Auto Layout -> flex/grid sizing resolver
4. Radius smoothing -> SVG path generation
5. Stroke align -> CSS/SVG stroke strategy
6. Text metrics -> browser text layout difference
7. Image fill and imageTransform -> CSS background math
8. Mask/vector/boolean -> SVG export pipeline
오늘 핵심은 하나입니다.
Figma to CSS는 속성 이름 바꾸기가 아니라, Figma의 수학 모델을 CSS/SVG/DOM의 수학 모델로 옮기는 일입니다. 이걸 인정하면 구현이 훨씬 차분해집니다. 물론 차분한 척하는 거죠. 내부에서는 여전히 행렬이 춤을 춥니다.