UIKit Transform의 순서가 결과를 바꾸는 이유

UIKit 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 순서에 따라 결과가 달라질까?

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
    

따라서 코드상에는 y 방향 이동이 없었지만, 회전 이후 좌표계가 변형된 상태에서 Scaling이 적용되어 결과적으로 오른쪽 아래 대각선 방향으로 이동하는 결과를 얻게 됩니다.

이처럼 transform 연산의 순서를 바꾸면 좌표계 자체가 변형된 상태에서 다음 연산이 적용되기 때문에 최종 결과도 크게 달라질 수 있습니다.

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

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

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

transform

이 그림은 translate, rotate, scale 연산이 모두 원점을 기준으로 수행되기 때문에,
적용 순서가 바뀌면 최종 결과도 달라지는 이유를 시각적으로 보여줍니다.


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

let transform = CGAffineTransform.identity
    .scaledBy(x: 2, y: 2)
    .rotated(by: .pi / 4)
    .translatedBy(x: 100, y: 0)
$$ x' = T \cdot R \cdot S \cdot x $$

즉, Scale -> Rotate -> Translate 순서로 연산을 적용하면 의도한 위치와 회전을 정확하게 표현할 수 있습니다.


실전 팁


마무리

UIKit에서 transform은 애니메이션 효과를 구현할 때 매우 자주 사용하는 기능입니다.
하지만 연산 순서에 대한 이해 없이 사용할 경우, 의도하지 않은 위치 이동이나 회전 결과가 발생할 수 있습니다.

transform 연산은 항상 원점을 기준으로 적용되며, 좌표계 자체가 연산 순서에 따라 바뀐다는 점을 이해하는 것이 중요합니다.

이 글을 통해 transform을 사용할 때 발생할 수 있는 실수를 미리 예방하고,
올바른 순서로 연산을 적용하는 데 도움이 되었기를 바랍니다.