SwiftUI 제스처 시스템 내부

발행: (2025년 12월 25일 오전 03:46 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

Sebastien Lato

SwiftUI 제스처는 겉보기에는 간단해 보입니다:

.onTapGesture { }

하지만 내부적으로 SwiftUI는 강력하고 계층화된 제스처 시스템을 가지고 있어 다음을 결정합니다:

  • 어떤 제스처가 승리하는지
  • 어떤 제스처가 실패하는지
  • 어떤 제스처가 동시에 실행되는지
  • 제스처가 서로 언제 취소되는지
  • 제스처가 뷰 트리를 통해 어떻게 전파되는지

대부분의 제스처 버그는 개발자가 제스처 우선순위와 해결 방식을 이해하지 못해서 발생합니다.

이 글에서는 SwiftUI 제스처가 실제로 어떻게 동작하는지, 엔진 수준에서부터 자세히 살펴봅니다 — 이를 통해 신뢰할 수 있고 예측 가능한 복잡한 인터랙션을 만들 수 있습니다.

🧠 핵심 제스처 모델

SwiftUI 제스처는 다음 파이프라인을 따릅니다:

Touch input

Hit‑testing

Gesture recognition

Gesture competition

Resolution (win / fail / simultaneous)

State updates

여러 제스처가 동일한 터치를 관찰할 수 있지만, 모두가 승리하는 것은 아닙니다.

🖐 1. SwiftUI의 제스처 유형

SwiftUI는 여러 기본 제스처를 제공합니다:

  • TapGesture (탭 제스처)
  • LongPressGesture (길게 누르기 제스처)
  • DragGesture (드래그 제스처)
  • MagnificationGesture (확대 제스처)
  • RotationGesture (회전 제스처)

이들은 값 타입이며, 뷰 트리 안에 구성됩니다.

🧩 2. 제스처는 화면이 아니라 뷰에 연결됩니다

Text("Hello")
    .onTapGesture { print("Tapped") }

이 제스처는 뷰가 히트‑테스트되는 영역에서만 작동합니다.

핵심 규칙:
📌 뷰에 크기가 없거나 히트‑테스트에 투명한 경우, 해당 제스처는 작동하지 않습니다.

탭 가능한 영역을 정의하려면 content shape를 사용하세요:

.contentShape(Rectangle())

⚔️ 3. Gesture Competition: Who Wins?

여러 제스처가 동일한 터치를 감지하면 SwiftUI는 다음 순서대로 해결합니다:

  1. Exclusive gestures (default)
  2. High‑priority gestures
  3. Simultaneous gestures
  4. Parent gestures

기본적으로 가장 깊은 하위 제스처가 승리합니다.

🥇 4. highPriorityGesture

자식 제스처의 우선순위를 무시합니다.

.view
    .highPriorityGesture(
        TapGesture().onEnded { print("Parent tap") }
    )

사용 시기:

  • 부모가 터치를 가로채야 할 때.
  • 자식 인터랙션이 부모 로직을 방해하지 않아야 할 때.

경고: 과도하게 사용하면 기대되는 UX가 깨질 수 있습니다.

🤝 5. simultaneousGesture

제스처를 동시에 발생시킬 수 있습니다.

.view
    .simultaneousGesture(
        TapGesture().onEnded { print("Also tapped") }
    )

Typical uses:

  • 분석
  • 햅틱
  • 부가 효과
  • 상호작용 로깅

This does not block other gestures.

🔗 6. 제스처 구성

SwiftUI는 제스처를 명시적으로 결합할 수 있게 해줍니다.

순차 (하나씩 차례로)

LongPressGesture()
    .sequenced(before: DragGesture())

동시에

TapGesture()
    .simultaneously(with: LongPressGesture())

배타적으로

TapGesture()
    .exclusively(before: DragGesture())

이러한 결합자는 제스처 흐름을 완전히 제어할 수 있게 합니다.

📏 7. 제스처 상태 vs. 뷰 상태

임시 제스처 값을 위해 @GestureState를 사용하세요.

@GestureState private var dragOffset = CGSize.zero

핵심 속성:

  • 제스처가 끝날 때 자동으로 리셋됩니다.
  • 영구적인 상태 업데이트를 트리거하지 않습니다.
  • 애니메이션 구동에 최적입니다.

예시:

DragGesture()
    .updating($dragOffset) { value, state, _ in
        state = value.translation
    }

📌 가이드라인: 움직임에는 @GestureState를, 결과에는 @State를 사용하세요.

🔄 8. Gesture Lifecycle

Every gesture has phases:

  • .onChanged
  • .onEnded
  • .updating

Internally, gestures can:

  • Fail
  • Cancel
  • Restart

This is why gestures sometimes feel “jumpy” when their identity changes.

🧱 9. 제스처 전파 및 뷰 아이덴티티

뷰가 재생성되면:

  • 제스처가 재생성됩니다.
  • 제스처 상태가 초기화됩니다.
  • 진행 중인 제스처가 취소됩니다.

일반적인 원인:

  • id() 변경
  • 조건부 뷰
  • 리스트 아이덴티티 문제
  • 부모 무효화

📌 안정적인 아이덴티티 = 안정적인 제스처 동작.

📜 10. ScrollView vs. Gestures

ScrollView는 높은 우선순위의 드래그 제스처를 가지고 있어, 이는 다음을 의미합니다:

  • 자식 드래그 제스처가 때때로 작동하지 않을 수 있습니다.
  • 커스텀 스와이프 제스처가 제대로 동작하지 않는 것처럼 느껴질 수 있습니다.

해결책:

  • simultaneousGesture를 사용합니다.
  • 제스처를 오버레이에 연결합니다.
  • 스크롤을 일시적으로 비활성화합니다.
  • gesture(_:including:)를 사용하여 서브뷰를 포함합니다.
.gesture(drag, including: .subviews)

⚠️ 11. 일반적인 제스처 버그 (및 해결 방법)

증상일반적인 원인해결 방법
제스처가 작동하지 않음View has zero sizeEnsure the view has a size or add a contentShape.
뷰에 크기가 있도록 하거나 contentShape을 추가하세요.
Hit‑testing disabledProvide a contentShape or make the view opaque to hits.
contentShape을 제공하거나 뷰를 히트에 대해 불투명하게 만드세요.
제스처가 무작위로 취소됨View identity changed (e.g., id() changes)Keep view identity stable.
뷰 식별자를 안정적으로 유지하세요.
Parent re‑renderedMinimize unnecessary parent updates.
불필요한 부모 업데이트를 최소화하세요.
Navigation transitionUse .transaction or keep gesture state outside the transitioning view.
.transaction을 사용하거나 전환 중인 뷰 외부에 제스처 상태를 유지하세요.
스크롤 충돌Competing drag gesturesAdjust priority (highPriorityGesture) or use simultaneousGesture.
우선순위를 조정(highPriorityGesture)하거나 simultaneousGesture를 사용하세요.

시스템을 이해하면 이 모든 문제를 해결할 수 있습니다.

🧠 Mental Model Cheat Sheet

  • 제스처는 views 위에서 동작합니다.
  • Identity가 중요합니다 – 안정적인 ID가 제스처를 안정적으로 유지합니다.
  • 기본적으로 Children win합니다.
  • Priority 수정자(highPriorityGesture, simultaneousGesture)가 동작을 변경합니다.
  • 동시에 발생하는 제스처는 서로 don’t block합니다.
  • @GestureStateephemeral이며, @State는 지속적입니다.
  • ScrollView는 자체 드래그 제스처를 aggressive하게 사용합니다.
  • 레이아웃이 hit‑testing에 영향을 줍니다.

🚀 최종 생각

SwiftUI 제스처는 마법이 아니라 결정론적인 시스템입니다.
다음 개념을 이해하면:

  • Competition
  • Priority
  • Identity
  • Propagation

신뢰할 수 있고 복잡한 상호작용을 자신 있게 구축할 수 있습니다.

ld:

  • 스와이프 동작
  • 맞춤 슬라이더
  • 드래그로 해제
  • 멀티터치 상호작용
  • 고급 애니메이션

…SwiftUI와 싸우지 않고.

Back to Blog

관련 글

더 보기 »

SwiftUI View Diffing 및 Reconciliation

SwiftUI는 화면을 “다시 그리”는 것이 아닙니다. 뷰 트리를 차이(diff)합니다. SwiftUI가 무엇이 변경되고 무엇이 동일하게 유지되는지를 어떻게 결정하는지 이해하지 못한다면, 불필요한 …