SwiftUI에서 ScrollView와 좌표 공간
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
GeometryReader를 가능한 한 작게 유지한다.- 기하 정보를 한 번만 읽고 값을 위쪽으로 전달한다.
- 프레임당 비용이 많이 드는 작업을 절대 수행하지 않는다.
📱 8. ScrollView + 리스트 + 성능
List는 이미 행을 가상화합니다. 리스트 내부에서 기하 정보를 필요로 한다면:
- 행마다
GeometryReader사용을 피하세요, - 스크롤 오프셋을 한 번만 읽으세요(위 예시와 같이),
- 그 전역 상태에서 행 효과를 파생시키세요.
성능은 다음에 달려 있습니다:
- 레이아웃 안정성,
- 최소한의 뷰 무효화,
- 저비용 프레임당 연산.
🧠 9. 복잡한 레이아웃에서 좌표 공간
이름이 지정된 공간으로 구분하기:
.coordinateSpace(name: "root")
.coordinateSpace(name: "scroll")
.coordinateSpace(name: "card")
의도적으로 측정하기:
geo.frame(in: .named("card"))
이렇게 하면 취약한 가정을 없앨 수 있습니다.
🔁 10. 스크롤 복원 및 상태
스크롤 위치는 자동으로 보존되지 않습니다. 복원해야 할 경우:
- 오프셋을 앱 상태에 저장합니다.
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