03 — Transform (좌표 변환)

한 줄 답: transform은 요소의 렌더 박스를 좌표계상에서 이동·회전·확대·기울임한다. 결정적으로 layout box는 그대로 두고 그리는 위치만 바꾼다 — 그래서 형제 요소가 이동하지 않고, layout/paint를 재계산하지 않으며, 60fps에 가장 친화적이다. 2022년 translate/rotate/scale 개별 속성 도입으로 변환 합성이 훨씬 자연스러워졌다.


Why — transform이 왜 그렇게 특별 취급되나

CSS 속성 중 렌더 파이프라인의 마지막 단계(Composite)에서만 동작하는 것은 손에 꼽힌다transformopacity(그리고 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-originborder-box 기준. SVG에서는 fill-box가 기본. 통일하려면:

svg path {
  transform-box: fill-box;
  transform-origin: center;
}

Composite-only 상태를 유지하는 5가지 속성

전체 CSS 중 Layout·Paint를 트리거하지 않는 변환 가능 속성:

  1. transform (translate, rotate, scale, skew, matrix, perspective)
  2. translate, rotate, scale (2022 개별 속성)
  3. opacity
  4. filter (요소가 자체 레이어일 때 — 조건부)
  5. 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인가의 메커니즘.