Transform은 순서가 중요하다.

Transform은 순서가 중요하다.

UIKit에서 CGAffineTransform을 사용할 때는 transform의 적용 순서에 따라 결과가 완전히 달라진다.

아래 두 개의 코드 예시를 보자.
겉보기에는 결과가 같아 보이지만, 실제 동작은 전혀 다르다.

box1.transform = CGAffineTransform.identity
  .scaledBy(x: 2, y: 2)
  .rotated(by: .pi / 4)
  .translatedBy(x: 100, y: 0)

box2.transform = CGAffineTransform.identity
  .translatedBy(x: 100, y: 0)
  .rotated(by: .pi / 4)
  .scaledBy(x: 2, y: 2)

capture


왜 순서에 따라 결과가 달라질까?

transform

이 그림은 객체를 이동한 후 회전했을 때, 결과가 어떻게 달라지는지를 보여준다.
translate, rotate, scale 연산은 모두 원점을 기준으로 수행되기 때문에, 순서에 따라 시각적으로 완전히 다른 결과가 나타날 수 있다.

일반적으로는 다음 순서를 따르는 것이 안정적이다:

이 순서는 column-major 행렬 곱 표현으로 다음과 같이 나타낼 수 있다:

$$ x' = T \cdot R \cdot S \cdot x $$

왜 오른쪽 아래로 이동했을까?

아래 코드를 살펴보자. translate에는 y 값이 없지만, 결과적으로 y 방향으로도 이동한 것처럼 보인다.

let transform = CGAffineTransform.identity
    .translatedBy(x: 100, y: 0)
    .rotated(by: .pi / 4)
    .scaledBy(x: 2, y: 2)

이 코드를 적용하면 다음과 같은 과정이 진행된다.

  1. 먼저 (0, 0)(100, 0)으로 이동 (Translation)
  2. 이후 원점 기준으로 (100, 0)을 45도 회전
    결과:
    x' = cos(π/4) * 100 = 70.7
    y' = sin(π/4) * 100 = 70.7
    
  3. 마지막으로 Scale:
    x' * 2 = 141.4
    y' * 2 = 141.4
    

결과적으로 오른쪽 아래 대각선 방향으로 이동한 것처럼 보이게 된다.


올바른 순서로 적용하고 싶다면?

let transform = CGAffineTransform.identity
    .scaledBy(x: 2, y: 2)
    .rotated(by: .pi / 4)
    .translatedBy(x: 100, y: 0)

이렇게 작성하면:

즉, S → R → T 순서를 따르는 구조이다.


실전 팁


마무리

UIKit의 transform 역시 2D 그래픽스의 행렬 연산이다.
행렬의 곱셈 순서가 바뀌면 결과도 완전히 달라지기 때문에,
코드상의 순서 하나하나가 실제 화면의 위치와 회전에 큰 영향을 미친다.