SwiftUI Study – PreferenceKey와 AnchorPreference를 안전하게 사용하는 법 (레이아웃 역방향 데이터 전달)
Dev Study/SwiftUI 2025. 12. 9. 10:17SwiftUI Study – PreferenceKey와 AnchorPreference를 안전하게 사용하는 법 (레이아웃 역방향 데이터 전달)
1. 왜 중요한가 (문제 배경)
SwiftUI는 부모 → 자식 방향의 데이터 전달이 기본이다.
그러나 실제 UI 개발에서는 종종 다음과 같은 요구가 발생한다.
- 자식 뷰의 크기/위치를 부모가 알아야 함
- 특정 뷰 위치를 기준으로 Floating 버튼 배치
- 스크롤 위치에 따라 상단 UI 변화
- 하위 요소들이 모은 값을 부모가 필요로 함
이때 사용하는 도구가 PreferenceKey / AnchorPreference 이다.
하지만 이 기능은 다음과 같은 문제를 일으키기 쉽다.
- 값 병합 규칙을 잘못 설정해 예측 불가한 UI
- PreferenceKey 호출 타이밍 이해 부족
- Anchor 값이 잘못된 좌표계에서 해석됨
- 부모·자식 간 의존성이 꼬여 복잡한 구조 형성
따라서 올바른 설계 기준을 알고 사용하는 것이 매우 중요하다.
2. 잘못된 패턴 예시
❌ 예시 1: PreferenceKey 병합 로직을 무시하고 단순 대입
struct WrongKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value = nextValue() // ❌ accumulate 하지 않고 단순 덮어쓰기
}
}
문제점
- 여러 자식 뷰가 값을 전달하면 마지막 값만 남음
- 부모가 어떤 값을 받는지 예측 불가
- 복잡한 UI에서 심각한 버그 발생
❌ 예시 2: Anchor 값을 올바르지 않은 좌표계에서 사용
.anchorPreference(key: BoundsKey.self, value: .bounds) { $0 }
그리고 부모에서:
.overlayPreferenceValue(BoundsKey.self) { value in
GeometryReader { geo in
// ❌ geo 좌표계를 기준으로 value를 오해석
Circle()
.position(x: value.minX, y: value.minY)
}
}
문제점
- Anchor는 “현재 뷰의 좌표계” 기준
- GeometryReader는 “부모의 전체 좌표계” 기준
- 좌표계가 다르면 전혀 맞지 않는 위치에 UI가 표시됨
❌ 예시 3: PreferenceKey를 상태 업데이트 용도로 사용
.preference(key: SizeKey.self, value: proxy.size.width)
.onPreferenceChange(SizeKey.self) { new in
width = new // ❌ 빠른 렌더링 루프 발생 가능
}
문제점
- 상태 업데이트 → 렌더링 → size 업데이트 → 상태 업데이트 무한 루프 위험
- 렌더링이 지연되거나 애니메이션이 깨짐
3. 올바른 패턴 예시
✅ 예시 1: 병합 가능한 reduce 함수 구현
struct ChildSizesKey: PreferenceKey {
static var defaultValue: [CGSize] = []
static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) {
value.append(contentsOf: nextValue()) // 병합 규칙 명확
}
}
자식 뷰:
Text("Hello")
.background(
GeometryReader { geo in
Color.clear.preference(
key: ChildSizesKey.self,
value: [geo.size]
)
}
)
부모:
.onPreferenceChange(ChildSizesKey.self) { sizes in
print("모든 자식 크기:", sizes)
}
장점
- 어떤 값이 어떻게 전달되는지 완전히 예측 가능
- 여러 자식이 있어도 안정적으로 작동
✅ 예시 2: AnchorPreference는 GeometryProxy[anchor] 로 반드시 변환
struct BoundsKey: PreferenceKey {
static var defaultValue: Anchor<CGRect>? = nil
static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) {
value = value ?? nextValue()
}
}
사용:
.anchorPreference(key: BoundsKey.self, value: .bounds) { $0 }
.backgroundPreferenceValue(BoundsKey.self) { anchor in
GeometryReader { geo in
if let anchor {
let rect = geo[anchor] // 올바른 좌표 변환
Rectangle()
.stroke(Color.red)
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
}
}
}
장점
- Anchor의 좌표가 올바른 부모 좌표계에서 해석됨
- 복잡한 UI에서도 정확한 레이아웃 계산 가능
✅ 예시 3: 상태 업데이트는 throttle 또는 조건부 처리
.onPreferenceChange(SizeKey.self) { new in
guard width != new else { return }
width = new // 변경이 있을 때만 업데이트
}
장점
- 렌더링 루프 방지
- 성능 안정성 확보
4. 실전 적용 팁
✔ 팁 1 – PreferenceKey는 “뷰 → 부모로 데이터 전달”일 때만 사용
상태 저장·비즈니스 로직 전달로 사용하면 100% 구조가 꼬임.
✔ 팁 2 – reduce는 “값을 합칠 기준”으로 설계
덮어쓰기 금지.
✔ 팁 3 – AnchorPreference는 반드시 GeometryProxy로 좌표 변환
좌표계가 다른 상태 그대로 쓰면 위치가 엉망이 됨.
✔ 팁 4 – onPreferenceChange는 무한 루프가 되지 않도록 조건 필수
상태 갱신이 곧바로 다시 Preference 업데이트로 이어지기 때문.
✔ 팁 5 – PreferenceKey는 강력하지만 남용금지
필요한 경우:
- Floating UI 위치 결정
- Sticky Header
- Scroll 기반 UI 제어
- 자식 크기 취합
이외에는 다른 구조가 더 적합.
5. 정리
- PreferenceKey는 SwiftUI에서 보기 드문 “역방향 데이터 흐름”을 담당한다.
- reduce 규칙을 잘못 설계하거나 Anchor 좌표계를 잘못 해석하면 UI가 쉽게 깨진다.
- 올바른 사용법을 적용하면 레이아웃 제어, Sticky Header, Floating UI 같은 복잡한 구성도 안정적으로 구현할 수 있다.

