03 — Transform (좌표 변환)
한 줄 답:
transform은 요소의 렌더 박스를 좌표계상에서 이동·회전·확대·기울임한다. 결정적으로 layout box는 그대로 두고 그리는 위치만 바꾼다 — 그래서 형제 요소가 이동하지 않고, layout/paint를 재계산하지 않으며, 60fps에 가장 친화적이다. 2022년translate/rotate/scale개별 속성 도입으로 변환 합성이 훨씬 자연스러워졌다.
Why — transform이 왜 그렇게 특별 취급되나
CSS 속성 중 렌더 파이프라인의 마지막 단계(Composite)에서만 동작하는 것은 손에 꼽힌다 — transform과 opacity(그리고 filter 일부, will-change로 promote된 일부).
매 프레임 16ms(60fps)를 지키려면 가능한 한 적은 단계만 트리거해야 한다. transform은 Composite만 트리거 → 형제 요소도 영향 없음, layout flush 없음, 부드러움.
자세한 파이프라인은 04-rendering-perf.
How — 어떻게 동작하는가
1) 6개 기본 함수 (2D)
.box {
transform: translateX(50px); /* X 이동 */
transform: translateY(20px); /* Y 이동 */
transform: translate(50px, 20px); /* XY 이동 */
transform: rotate(45deg); /* 회전 */
transform: scale(1.2); /* 균등 확대 */
transform: scaleX(1.2); /* X 확대 */
transform: scale(1.2, 0.8); /* XY 확대 */
transform: skew(15deg, 0); /* 기울임 */
transform: matrix(1, 0, 0, 1, 50, 20); /* 모든 2D 변환의 일반형 */
}2) 변환의 합성: 여러 함수를 동시에
.box {
transform: translate(50px, 0) rotate(45deg) scale(1.2);
}⚠️ 순서가 결과를 바꾼다. 오른쪽부터 적용한다 (행렬 곱은 결합법칙).
*“이동 후 회전”*과 *“회전 후 이동”*은 다른 결과 — 회전은 원점 기준이라서.
3) 2022년 개별 속성 — translate / rotate / scale
기존 transform은 하나의 통합 속성이라 부분 변경이 까다로웠다.
.box {
transform: translate(50px, 0) rotate(45deg);
}
.box:hover {
transform: rotate(90deg); /* ❌ translate가 사라짐 */
}해결: 개별 속성.
.box {
translate: 50px 0;
rotate: 45deg;
scale: 1;
}
.box:hover {
rotate: 90deg; /* ✅ translate는 유지됨 */
}적용 순서는 명세에 고정: translate → rotate → scale → transform. 즉 transform은 항상 마지막에 적용된다.
이는 애니메이션 합성을 극단적으로 단순화한다 — 트랜잭션·호버·키프레임이 서로 다른 transform 요소를 다툴 수 있다.
4) transform-origin — 변환의 기준점
기본값: 50% 50% 0 (요소 중심). 즉 rotate(45deg)는 요소 중심을 축으로 회전.
.gear {
transform-origin: 0 0; /* 좌상단 기준 */
transform-origin: center top; /* 위쪽 중앙 */
transform-origin: 100% 100%; /* 우하단 */
}회전·확대 애니메이션 디자인의 핵심. 메뉴 드롭다운이 상단에서 펴지는 느낌은 transform-origin: top center + scaleY(0→1).
5) 3D Transform
.card {
transform-style: preserve-3d; /* 자식이 3D 컨텍스트에 참여 */
perspective: 1000px; /* 원근 깊이 (작을수록 강함) */
}
.card .front,
.card .back {
transform: rotateY(0deg);
backface-visibility: hidden; /* 뒷면 숨김 (카드 뒤집기) */
}
.card.flip .front { transform: rotateY(180deg); }
.card.flip .back { transform: rotateY(0deg); }3D 함수들:
translateZ(npx)— 화면 안/밖 방향rotateX·Y·Z(deg)— 각 축 회전scaleZ/scale3d— 3D 확대perspective(npx)— 함수형 원근 (속성형perspective와 다름)
6) perspective 속성 vs perspective() 함수 — 자주 헷갈리는 지점
여러 카드가 같은 카메라를 공유해야 하면 부모에 perspective. 카드 하나가 독립적 3D면 함수형.
What — 구체 스펙
변환에 쓰이는 단위들
| 함수 | 받는 단위 |
|---|---|
translate* | <length> (px, em, rem, %) or <percentage> |
rotate* | <angle> (deg, rad, turn, grad) |
scale* | <number> (1 = 100%) |
skew* | <angle> |
%는 자기 자신을 기준: translateX(50%)는 자기 너비의 50% 이동. flexbox·grid와 달리 부모 기준이 아님. 모달 정중앙 정렬:
.modal {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%); /* 자기 크기의 절반만큼 역방향 */
}will-change — Composite 레이어 promotion 가설 선언
.draggable {
will-change: transform;
}브라우저에게 *“이 요소는 곧 transform이 변할 거니까, 미리 별도 레이어로 promote해서 GPU에 올려둬라”*는 힌트다. 효과:
- Composite 레이어로 분리됨 → 변환 시 paint 안 일어남
- 부모/형제와 독립된 합성 평면에서 그려짐
⚠️ 남발 시 메모리 폭증. 모바일 GPU는 메모리가 제한적. 움직임 직전에 켜고, 끝나면 끄는 것이 BP.
el.addEventListener('mouseenter', () => el.style.willChange = 'transform');
el.addEventListener('mouseleave', () => el.style.willChange = 'auto');자세한 메커니즘은 04-rendering-perf.
backface-visibility: hidden
3D 회전 시 요소의 뒷면을 안 보이게. 카드 뒤집기 외에 GPU promotion 트릭으로도 사용됐다 (구식 hack — 지금은 will-change: transform이 정석).
transform-box
기본적으로 transform-origin은 border-box 기준. SVG에서는 fill-box가 기본. 통일하려면:
svg path {
transform-box: fill-box;
transform-origin: center;
}Composite-only 상태를 유지하는 5가지 속성
전체 CSS 중 Layout·Paint를 트리거하지 않는 변환 가능 속성:
transform(translate, rotate, scale, skew, matrix, perspective)translate,rotate,scale(2022 개별 속성)opacityfilter(요소가 자체 레이어일 때 — 조건부)backdrop-filter(조건부, iOS에서 비싸다)
→ 애니메이션은 가능한 한 위 5개만으로. 이게 모션 성능의 사실상 표준.
What-if — 잘못 쓰면
1) transform으로 position: fixed 자식의 좌표계가 깨짐
.parent {
transform: translateZ(0); /* GPU promotion */
}
.child {
position: fixed; /* ❌ viewport가 아니라 .parent 기준 */
}CSS Containing Block 규칙: transform이 적용된 요소는 자식 fixed의 containing block이 된다. 디자이너가 헤더에 작은 transform을 걸어둔 게 모달의 fixed 위치를 깬다. 디버깅 악몽.
대안: will-change: transform도 동일 문제 발생. 정말 필요한 곳에만.
2) width로 키프레임 → 30fps, scaleX로 → 60fps
@keyframes grow1 { to { width: 200px; } } /* ❌ 매 프레임 Layout */
@keyframes grow2 { to { transform: scaleX(2); } } /* ✅ Composite only */체감: 모바일 Chrome에서 전자는 눈에 보이게 끊김, 후자는 부드러움.
단, scale은 자식 텍스트까지 늘어진다. 자식이 텍스트라면 § FLIP 패턴으로 부모는 scale·자식은 inverse scale.
3) transform: translate(50%, -50%)가 흐릿함
서브픽셀 위치 (예: 0.5px)에서 텍스트가 블러. 해결: transform: translate3d(-50%, -50%, 0) 또는 짝수 픽셀로 정렬. iOS Safari에서 특히 두드러진다.
4) will-change: transform을 100개 카드에
.card { will-change: transform; } /* ❌ */각 카드가 별도 GPU 레이어 → 100개 레이어. 모바일 GPU 메모리 한계 초과 시 전체가 더 느려진다. 레이어는 자원. 호버 직전에 동적으로 켜는 게 BP.
5) 3D 변환에서 preserve-3d 누락
.cube {
transform: rotateY(45deg);
}
.cube .face {
transform: translateZ(100px); /* ❌ 부모에 preserve-3d 없으면 무시됨 */
}부모에 transform-style: preserve-3d가 있어야 자식의 3D 변환이 3D 공간에서 합성된다. 없으면 자식은 *평면화(flatten)*된다.
6) transform-origin이 SVG에서 안 먹힘
svg path {
transform-origin: center; /* ❌ fill-box가 기본, center가 의도와 다른 결과 */
}해결: transform-box: fill-box 명시.
Insight — 한 단락 이야기
“Transform은 CSS가 GPU와 화해한 첫 다리다.”
2007년 iPhone 발표 직후, Apple WebKit 팀은 *“모바일 Safari에서 60fps를 어떻게 보장할 것인가”*라는 문제에 직면했다. 답은 단순했다 — Layout과 Paint를 건드리지 않는 새 속성을 만들자. 그게
-webkit-transform이었다. 2009년 iPhone OS 3.0이 이를 H/W 가속하면서, “transform = GPU”라는 등식이 굳어졌다. 2012년 W3C가transform을 표준화할 때도 이 layout-non-impact 원칙은 그대로 유지됐다. 흥미로운 점은, 그 원칙이 2D 그래픽스 API의 affine transform에서 직접 따온 것이라는 점 — Quartz·Core Animation·OpenGL ES에 이미 있던 행렬 변환 모델을 CSS가 선언적으로 노출한 것이다. 2022년 개별 속성(translate·rotate·scale)은 그 행렬을 디자이너 친화적으로 분해했다. 30년 진화의 핵심은 *“GPU가 잘 하는 일을 CSS 문법으로 빼는 것”*이었다.
요약 + Mermaid
- transform은 Composite-only — Layout/Paint 안 거치고 60fps에 안전.
- 함수 합성은 오른쪽부터 적용. 순서가 결과를 바꿈.
- 2022년
translate/rotate/scale개별 속성으로 부분 갱신·합성이 깔끔해짐. - 3D는
transform-style: preserve-3d+perspective로 무대 설정. will-change는 가설 선언. 남발하면 메모리 폭발 → 동적 토글이 BP.- transform이 적용된 요소는 자식 fixed의 새 containing block이 된다 (디버깅 함정).
- 애니메이션은 가능한 한 transform·opacity·filter만.
다음: 04-rendering-perf — 왜 transform·opacity만 60fps인가의 메커니즘.