SwiftUI에서 ScrollView와 좌표 공간

발행: (2025년 12월 26일 오전 09:48 GMT+9)
9 min read
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please provide the content (excluding the source line you already shared)? Once I have it, I’ll keep the source link at the top and translate the rest into Korean while preserving the original formatting.

Introduction

  • collapsing headers
  • parallax effects
  • sticky toolbars
  • section pinning
  • scroll‑driven animations
  • pull‑to‑refresh logic
  • infinite lists

그럼에도 대부분의 SwiftUI 개발자는 ScrollView를 블랙 박스로 취급하여 다음과 같은 문제를 겪습니다:

  • jumpy animations
  • incorrect offsets
  • broken geometry math
  • magic numbers
  • fragile hacks

빠진 조각은 coordinate spaces 입니다.

이 글에서는 SwiftUI에서 스크롤이 실제로 어떻게 동작하는지, 좌표 공간이 어떻게 상호작용하는지, 그리고 신뢰할 수 있는 프로덕션 급 스크롤 기반 UI를 만드는 방법을 설명합니다.

🧠 정신 모델: 스크롤은 단지 기하학일 뿐

SwiftUI 스크롤링은 특별한 것이 아니라 시간에 따라 좌표 공간을 이동하는 뷰일 뿐입니다.
한 번 뷰가 자신을 어디에 있다고 생각하는지 이해하면, 스크롤 효과를 예측할 수 있습니다.

📐 1. 좌표 공간이란 무엇인가?

좌표 공간은 (0,0)이 어디인지 정의합니다. SwiftUI는 세 가지 주요 유형을 제공합니다:

1️⃣ Local

뷰 자체를 기준으로 합니다.

geo.frame(in: .local)

2️⃣ Global

전체 화면 / 창을 기준으로 합니다.

geo.frame(in: .global)

3️⃣ Named

사용자가 정의하는 커스텀 레퍼런스입니다.

// Define the space
.coordinateSpace(name: "scroll")

// Read from it
geo.frame(in: .named("scroll"))

대부분의 스크롤 버그는 잘못된 공간을 사용해서 발생합니다.

🧱 2. ScrollView 이동하는 좌표 시스템 생성

ScrollView 내부:

  • 콘텐츠가 움직이고,
  • 기하학 값이 변하고,
  • 좌표 원점이 이동합니다.
ScrollView {
    GeometryReader { geo in
        Text("Offset: \(geo.frame(in: .global).minY)")
    }
    .frame(height: 40)
}

스크롤할 때 minY가 지속적으로 업데이트됩니다 – 이것이 스크롤 오프셋입니다.

🧭 3. 왜 명명된 좌표 공간이 중요한가

.global은 작동합니다… 하지만 언제든지 멈출 수 있습니다. .global의 문제점은 다음과 같습니다:

  • 시트에서 깨짐,
  • 네비게이션 스택에서 깨짐,
  • 스플릿 뷰에서 깨짐,
  • macOS / iPad에서 깨짐.

올바른 패턴

ScrollView {
    // content
}
.coordinateSpace(name: "scroll")

그 공간을 기준으로 기하학을 읽기:

geo.frame(in: .named("scroll")).minY

이렇게 하면 모든 곳에서 수식이 안정적으로 유지됩니다.

📦 4. 클린 스크롤‑오프셋 패턴 (프로덕션‑급)

제로 높이 GeometryReader를 사용해 프리퍼런스 키를 통해 오프셋을 퍼블리시합니다.

ScrollView {
    GeometryReader { geo in
        Color.clear
            .preference(
                key: ScrollOffsetKey.self,
                value: geo.frame(in: .named("scroll")).minY
            )
    }
    .frame(height: 0)          // 레이아웃에서 제외

    // …스크롤 가능한 콘텐츠…
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetKey.self) { offset in
    scrollOffset = offset
}

프리퍼런스 키

struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

왜 이 패턴을 사용하나요?

  • 컨테이너 전반에 걸쳐 안정적
  • 재사용 가능
  • 애니메이션 친화적
  • 프로덕션 환경에 안전

🧩 5. 스크롤 기반 효과 만들기

scrollOffset을 얻으면 모든 것이 수학이 됩니다.

접히는 헤더

let progress = max(0, min(1, 1 - scrollOffset / 120))

패럴랙스

.offset(y: -scrollOffset * 0.3)

페이드 아웃

.opacity(1 - min(1, scrollOffset / 80))

스케일

.scaleEffect(max(0.8, 1 - scrollOffset / 400))

가이드라인

  • 값을 클램프하고,
  • 단조롭게 유지하고,
  • 예측 가능하게 만들기.

Source:

📌 6. 해킹 없이 고정 헤더 만들기

SwiftUI는 이미 고정 헤더를 제공합니다:

LazyVStack(pinnedViews: [.sectionHeaders]) {
    Section(header: HeaderView()) {
        rows
    }
}

헤더에 대해 기하학을 사용할 경우는 다음과 같은 경우에만:

  • 애니메이션이 있을 때,
  • 형태가 변할 때,
  • 페이드되거나 스케일될 때.

핀 로직을 새로 만들지 마세요.

⚠️ 7. GeometryReader Pitfalls in ScrollView

Common mistakes

  • 스크롤 전체 콘텐츠를 GeometryReader 로 감싸는 것,
  • 각 행마다 GeometryReader 를 배치하는 것,
  • 스크롤 중에 무거운 레이아웃 작업을 수행하는 것,
  • 여러 개의 GeometryReader 를 중첩하는 것.

Rules

  1. GeometryReader 를 가능한 한 작게 유지한다.
  2. 기하 정보를 한 번만 읽고 값을 위쪽으로 전달한다.
  3. 프레임당 비용이 많이 드는 작업을 절대 수행하지 않는다.

📱 8. ScrollView + 리스트 + 성능

List는 이미 행을 가상화합니다. 리스트 내부에서 기하 정보를 필요로 한다면:

  • 행마다 GeometryReader 사용을 피하세요,
  • 스크롤 오프셋을 한 번만 읽으세요(위 예시와 같이),
  • 그 전역 상태에서 행 효과를 파생시키세요.

성능은 다음에 달려 있습니다:

  • 레이아웃 안정성,
  • 최소한의 뷰 무효화,
  • 저비용 프레임당 연산.

🧠 9. 복잡한 레이아웃에서 좌표 공간

이름이 지정된 공간으로 구분하기:

.coordinateSpace(name: "root")
.coordinateSpace(name: "scroll")
.coordinateSpace(name: "card")

의도적으로 측정하기:

geo.frame(in: .named("card"))

이렇게 하면 취약한 가정을 없앨 수 있습니다.

🔁 10. 스크롤 복원 및 상태

스크롤 위치는 자동으로 보존되지 않습니다. 복원해야 할 경우:

  1. 오프셋을 앱 상태에 저장합니다.
  2. ScrollViewReader를 사용해 복원합니다.
ScrollViewReader { proxy in
    proxy.scrollTo(id, anchor: .top)
}
  • 진행 중인 애니메이션 동안 복원을 피하세요.
  • 최소한으로 사용하세요.

🧠 멘탈 모델 치트 시트

스스로에게 물어보세요:

  • 나는 어떤 좌표 공간에 있나요?
  • 그 공간에서 (0,0)은 어디에 있나요?
  • 안정적인 수학을 위해 이름이 있는 공간이 필요할까요?

이러한 질문을 이해하면 다음과 같은 스크롤 기반 UI를 만들 수 있습니다:

  • 예측 가능하고,
  • 재사용 가능하며,
  • 프로덕션에 바로 사용할 수 있습니다.
- What moves during scrolling?
- What stays fixed?
- Am I measuring the right thing?

If geometry feels “random”, one of these is wrong.

🚀 최종 생각

Scroll‑driven UI in SwiftUI는 마법이 아닙니다.

It’s:

  • 기하학
  • 좌표 공간
  • 안정적인 수학
  • 의도적인 측정

Once you understand this, you can build:

  • 축소 헤더
  • 패럴랙스 효과
  • 스크롤 기반 애니메이션
  • 적응형 레이아웃
  • 다듬어진 Apple 수준 UI
Back to Blog

관련 글

더 보기 »

SwiftUI 접근성 내부

접근성은 Parallel View Tree이다. SwiftUI는 두 개의 트리를 구축한다: - visual view tree - accessibility tree 이들은 관련이 있지만 동일하지 않다. 하나의…

SwiftUI 제스처 시스템 내부

!Sebastien Latohttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...