반응형

SwiftUI 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 같은 복잡한 구성도 안정적으로 구현할 수 있다.
반응형
Posted by 까칠코더
,