반응형

SwiftUI Study – 애니메이션 엔진 내부 구조: Transaction, Phase, animatableData, Rendering Pipeline

 

1. 왜 이 주제가 중요한가 (문제 배경)

SwiftUI의 애니메이션은 간단해 보이지만, 실제 동작 방식은 다음 요소들이 정교하게 결합된 구조이다.

  • Transaction
  • Animation Phase
  • animatableData
  • Timeline & Renderer
  • Layout → Display 변환 과정

이 구조를 이해하면 다음 문제가 해결된다.

  • .animation(.easeInOut) 을 붙였는데 일부만 애니메이트 되는 이유
  • withAnimation 블록 안에서도 애니메이션이 안 먹는 현상
  • Layout 변화는 애니메이션되는데, 특정 Modifier는 왜 즉시 반영되는지
  • GeometryReader 기반 뷰가 애니메이션 시 튀는 이유
  • animatableData를 커스텀해 정교한 애니메이션을 만드는 법

즉, 이번 문서는 SwiftUI 애니메이션의 엔진 내부 동작을 기준으로 한 고급 분석이다.

 

2. Transaction – 애니메이션의 운전석

SwiftUI에서 모든 애니메이션은 Transaction을 통해 전달된다.

withAnimation(.spring()) {
    isOn.toggle()
}

이 호출은 다음 순서로 처리된다:

  1. Animation을 포함한 새로운 Transaction 생성
  2. “State 변화”에 Transaction이 붙어서 전달
  3. SwiftUI는 이 Transaction을 기반으로 “무엇을 어떻게 애니메이트할지” 결정

Transaction이 하는 일

  • 애니메이션 여부 결정 (animated? true / false)
  • 사용하는 Animation 종류 저장
  • Animation Phase 결정
  • Layout 및 Rendering 모듈로 전달

결론:

Animation은 State 변화에 붙는 메타데이터이고,
Transaction은 그 메타데이터를 운반하는 컨테이너이다.

 

3. Animation Phase – SwiftUI가 계산하는 중간 프레임의 정체

SwiftUI는 애니메이션 동안 다음을 자동계산한다.

  • 현재 시간(t)
  • 전체 duration
  • easing function 적용
  • 보간(interpolation)된 중간 값

즉,

.opacity(isOn ? 1 : 0)

이 경우 SwiftUI는 내부적으로:

0.0 → 0.1 → 0.25 → 0.5 → 0.75 → 1.0 …

같은 값을 매 프레임 계산해 적용한다.

이를 위해 SwiftUI는 interpolatable value를 필요로 한다.

 

4. animatableData – SwiftUI가 “값 사이를 보간하는 방법”

SwiftUI는 애니메이션을 하기 위해 “값을 점진적으로 변화시킬 수 있어야” 한다.

예: Rectangle의 cornerRadius

struct RoundedBox: View {
    var radius: CGFloat

    var body: some View {
        RoundedRectangle(cornerRadius: radius)
    }
}

cornerRadius는 CGFloat이므로 SwiftUI는 쉽게 보간할 수 있다.
하지만 개발자가 만든 커스텀 타입은?

커스텀 애니메이션 예시

struct MyShape: Shape {
    var progress: CGFloat

    var animatableData: CGFloat {
        get { progress }
        set { progress = newValue }
    }

    func path(in rect: CGRect) -> Path {
        // progress를 기반으로 형상 변환
    }
}

animatableData를 정의하는 순간:

  • SwiftUI는 progress 사이 값을 보간
  • Shape는 보간된 상태로 다시 렌더링
  • 자연스러운 Shape 변형 효과를 구현 가능

다중 값 애니메이션

var animatableData: AnimatablePair<CGFloat, CGFloat>

또는 더 깊은 중첩도 가능:

AnimatablePair(AnimatablePair(a, b), AnimatablePair(c, d))

 커스텀 Path, Transition, Morphing 애니메이션에서 필수 요소

 

5. Modifier는 왜 어떤 건 애니메이션되고, 어떤 건 즉시 적용되는가?

SwiftUI Modifier는 크게 두 종류로 나뉜다.

1) Animatable Modifier

ex) opacity, frame, offset, scaleEffect, rotationEffect …

  • 값이 Animatable 프로토콜을 만족
  • SwiftUI가 값 보간을 처리할 수 있음

2) Non‑Animatable Modifier

ex) background(Color.red), overlay, shadow(일부), mask 등

  • “애니메이션 가능한 값”이 아님
  • Layout만 바뀌거나, 변경이 렌더 단계에서 즉시 반영됨
  • 실제로는 프레임(workload) 규칙이 다름

이 차이를 이해하면:

  • 애니메이션이 “반응하지 않는 것처럼 보이는 버그”
  • ZStack 이동이 튀는 문제
  • Color opacity는 애니메이션되는데 Color 변경은 안 되는 문제

모두 정확히 설명 가능하다.

 

6. Layout → Render 단계에서 애니메이션이 섞이는 방식

SwiftUI는 다음 단계를 거친다:

  1. State 변경
  2. Transaction 전달
  3. Layout 단계
  4. Animatable 값 보간
  5. Render Tree 업데이트

특히 Layout은 애니메이션의 영향을 받는 경우와 받지 않는 경우가 있다.

Layout에 영향 있는 애니메이션

  • frame(width:, height:)
  • offset
  • scaleEffect

Layout이 변경되면 전체 하위 트리에 영향이 생길 수 있다.

Layout에 영향 없는 애니메이션

  • opacity
  • blur
  • brightness

Render 단계에서 처리되므로 비용이 더 낮다고 볼 수 있다.

 

7. withAnimation vs .animation의 근본적 차이

.animation(_:) (Deprecated 계열)

.opacity(isOn ? 1 : 0)
.animation(.easeInOut, value: isOn)
  • View 변경 시 자동으로 애니메이션 적용
  • 트래킹 범위가 넓어 예측 어려움
  • SwiftUI 4~5에서 사실상 withAnimation 기반으로 이동

withAnimation { }

  • State 변화에 직접 Animation을 태워 보냄
  • 범위가 명확하고 예측 가능
  • 런타임에서 제어하기 편함
    (ex: 특정 조건에서만 애니메이션 적용)

 

8. Animation 성능 – 과도한 애니메이션의 비용

다음 경우 성능 문제가 발생할 수 있다.

  1. 리스트 셀에서 매번 애니메이션
  2. GeometryReader 안에서 반복적인 레이아웃 애니메이션
  3. Transition이 과도하게 중첩된 경우
  4. Path 기반 애니메이션을 고해상도에서 반복하는 경우

성능 팁:

  • Layout 변화를 최소화
  • Render-only 효과(opacity, scale) 활용
  • animatableData 최소화
  • TimelineView와 결합 시 프레임 수 줄이기 (60fps 강제 X)

 

9. 실전 팁 요약

✔ 팁 1 – 애니메이션이 적용되지 않는 이유는 대부분 “값이 animatable이 아니기 때문”

✔ 팁 2 – Layout 애니메이션은 비싸다

가능하면 Render-only 애니메이션 사용

✔ 팁 3 – Transaction을 명확히 제어하라

특히 withAnimation을 기본 패턴으로 사용할 것

✔ 팁 4 – 커스텀 애니메이션은 animatableData로 구현

Morphing, progress-driven animation은 이게 핵심

✔ 팁 5 – Transition은 실제로 뷰 삽입/제거 시에만 적용됨

상태변경만으로는 transition이 먹히지 않음 → .animation과 개념 다름

 

10. 정리

SwiftUI의 애니메이션은 단순히 “값이 바뀌면 부드럽게 움직인다”가 아니다.

실제로는:

  • Transaction이 상태 변화에 애니메이션 정보를 태워 전달하고
  • SwiftUI가 Animation Phase를 계산하며
  • animatableData를 기반으로 중간 값을 보간하고
  • Layout 단계와 Render 단계가 조합되어 최종 애니메이션이 구현된다.

이 내부 구조를 이해하면:

  • 애니메이션이 안 먹는 이유를 정확히 진단하고
  • 예측 가능한 부드러운 UI를 만들며
  • Shape/Path 기반의 고급 애니메이션까지 설계할 수 있다.

SwiftUI 애니메이션을 “마법”이 아닌 “엔진” 관점으로 다루는 고급 개발자용 문서이다.

반응형
Posted by 까칠코더
,