SwiftUI 전문가은(는) 무엇을 하나요?
SwiftUI 화면은 상태 소유권에 의해 성패가 결정됩니다. 상태를 잘못된 위치에 두면 모든 키 입력마다 전체 트리가 재평가되고, 목록이 버벅거리며, 애니메이션이 끊어집니다. 이 스킬은 SwiftUI 뷰를 구축하고 검토하여 무효화가 실제로 변경된 데이터를 읽는 뷰로만 범위가 제한되고, 스크롤 중 프레임 예산을 유지하며, 결과물이 사후에 추가되는 것이 아니라 기본적으로 접근성을 갖추도록 합니다.
운영 절차
상태 설계가 이후 모든 것을 결정하기 때문에 이 순서대로 작업하십시오. 뷰 계층 구조를 구축한 후 상태를 수정하려면 계층 구조를 다시 작성해야 합니다.
1단계: 입력 수집
뷰 코드를 작성하기 전에 수집합니다. 사용자가 모르는 경우 기본값을 사용하고 추측이라고 표시합니다.
- 대상 기기 및 재생률 (기본값: ProMotion 120Hz iPhone 및 60Hz 기준 기기 가정).
- 데이터 형태: 가장 큰 목록의 항목 수, 데이터 변경 빈도, 스트리밍 여부.
- 소유권 맵: 각 상태 조각에 대해 어떤 뷰가 생성하고, 어떤 뷰가 읽고, 어떤 뷰가 변경하는지.
- 기본 이상의 접근성 요구사항 (기본값: 전체 Dynamic Type + VoiceOver 지원).
2단계: 값별 상태 소유권 선택
작동하는 가장 좁은 래퍼를 선택합니다.
@State- 로컬, 뷰 소유, 값 유형 상태. 또한 뷰가 생성한@Observable모델 인스턴스의 올바른 소유자입니다.@Binding- 다른 곳에서 소유한 상태에 대한 양방향 참조. 자식이 변경할 때만 바인딩을 전달하고, 읽기만 할 때는 일반 값을 전달합니다.@Observable(Observation 프레임워크) - 모델 객체; 뷰는 읽은 속성만 관찰하므로 무효화는 객체별이 아닌 속성별로 발생합니다.@Environment- 서비스 및 공유 모델에 대한 의존성 주입을 트리 아래로 전달합니다.
규칙: 상태는 해당 상태를 읽거나 쓰는 모든 뷰의 가장 가까운 공통 조상에 위치해야 합니다. 그보다 낮으면 (뷰가 사라질 때 재설정됨) 안 되고, 더 높으면 (무효화 범위가 넓어짐) 안 됩니다.
3단계: 무효화 범위를 좁히기 위해 뷰 구성
body를 작게 유지하고, 서브뷰를 추출하여 변경이 화면 전체가 아닌 하나의 행에만 영향을 미치도록 합니다. 약 50줄 또는 세 단계 중첩을 초과하는body는 분할 대상입니다.- 서브뷰에 최소한의 데이터만 전달합니다 - 전체 저장소가 아닌
item.title을 전달하여 영향받는 뷰만 다시 렌더링되도록 합니다. - 거대한 중첩 클로저 대신
@ViewBuilder헬퍼를 사용합니다. body에서 무거운 작업(형식 지정, 정렬, 필터링, 디코딩)을 절대 수행하지 마십시오. 모델에서 계산하고 완성된 값을 뷰에 전달하십시오.body는 마이크로초 범위를 유지해야 하며, 애니메이션 중에는 매 프레임 실행될 수 있습니다.
4단계: 목록 및 프레임 예산
예산은 120Hz에서 프레임당 8.3ms, 60Hz에서 16.7ms입니다. 이는 해당 프레임의 메인 스레드에 있는 모든 작업(행만이 아님)을 포함합니다. 실용적인 규칙:
- 약 50행 이상인 경우
List또는LazyVStack을 사용하여 행이 필요할 때 렌더링되도록 합니다.ScrollView안의 일반VStack은 모든 행을 미리 생성합니다. - 데이터의 안정적인
id(item.id)를 항목에 제공하고, 배열 인덱스나body에서 계산된UUID()를 절대 사용하지 마십시오. 불안정한 id는 행 식별성을 파괴하여 깜박임, 스크롤 위치 손실, 행 상태 손실을 유발합니다. - 행은 일정한 비용이 들어야 합니다: 행별 날짜 포맷터나 이미지 디코딩이 없어야 합니다. 포맷터를 캐시하고,
AsyncImage또는 캐싱 이미지 파이프라인을 사용하십시오. - Instruments의 SwiftUI 템플릿으로 확인: 스크롤하는 동안 뷰 본문 평가 횟수를 관찰하십시오. 관련 없는 상태가 변경될 때 본문이 평가되는 행 유형은 상태 소유권 버그입니다 - 2단계를 수정하고 주변에 메모이제이션하지 마십시오.
5단계: 상태로부터 애니메이션
- 상태로부터 애니메이션을 구동합니다:
withAnimation { model.expanded.toggle() }. - 암시적 전역 애니메이션(의도하지 않은 것들을 애니메이션함)보다 특정 값에 범위가 지정된
.animation(_:value:)를 선호합니다. - 공유 요소 전환에는
matchedGeometryEffect를, 삽입/제거에는transition을 사용합니다. accessibilityReduceMotion을 존중합니다: 설정된 경우 이동을 불투명도 페이드로 대체합니다.
6단계: 접근성 검토
accessibilityLabel/accessibilityValue를 추가하고, 관련 요소를accessibilityElement(children: .combine)으로 그룹화하여 행이 다섯 개의 조각이 아닌 하나의 문장으로 읽히도록 합니다.- Dynamic Type을 존중합니다 - 시스템 텍스트 스타일을 사용하고, 텍스트를 자르는 고정 프레임을 피하며, 가장 큰 접근성 크기에서 확인합니다.
- 대비를 확인하고 다크 모드, 큰 Dynamic Type, 빈/로딩/오류 상태에 대한
#Preview변형을 샘플 데이터와 함께 추가합니다.
작업 예시: 상태 소유권, 나쁨 vs 좋음
나쁨 - 검색 텍스트가 전체 트리가 관찰하는 모델에 있고, 행이 전체 저장소를 수신하기 때문에 모든 키 입력에 전체 화면이 무효화됩니다:
@Observable final class ScreenStore {
var searchText = ""
var items: [Item] = []
}
struct ContactList: View {
@State private var store = ScreenStore()
var body: some View {
VStack {
TextField("Search", text: $store.searchText)
ForEach(store.items) { item in
ContactRow(store: store, item: item) // 행이 모든 것을 관찰
}
}
}
}
좋음 - 검색 텍스트는 뷰 로컬이고, 행은 읽는 값만 받으며, 목록은 지연 로딩되고 안정적인 id를 사용합니다:
struct ContactList: View {
@State private var searchText = ""
@Environment(ContactModel.self) private var model
var body: some View {
List(model.filtered(searchText)) { item in
ContactRow(name: item.name, isOnline: item.isOnline)
}
.searchable(text: $searchText)
}
}
struct ContactRow: View {
let name: String
let isOnline: Bool
var body: some View {
Label(name, systemImage: isOnline ? "circle.fill" : "circle")
}
}
좋은 버전에서 입력하면 List 내용 클로저와 필터된 멤버십이 변경된 행만 무효화됩니다 - 그 외에는 아무것도 무효화되지 않습니다.
인도물
작동하는 SwiftUI 뷰 코드와 함께 각 상태 조각에 대해 소유자, 래퍼, 읽기/쓰기를 수행하는 뷰를 명시한 한 문단 분량의 상태 맵을 생성합니다. 검토의 경우 프레임 예산 영향 순서대로 정렬된 결과 목록을 각각의 수정 사항과 함께 생성합니다.
하지 말아야 할 것
body평가 중에는 절대 상태를 변경하지 마십시오 - 루프가 발생하거나 정의되지 않은 동작 경고가 트리거됩니다.onAppear에서 원시Task로 비동기 작업을 수행하지 마십시오 - 대신.task {}를 사용하면 사라질 때 자동으로 취소됩니다.- 모델에 뷰 유형(
Color,Image,AnyView)을 넣지 마십시오. 모델은 UI 없이도 테스트 가능해야 합니다. - "단순함을 위해" 모든 상태를 하나의 화면 수준 저장소로 끌어올리지 마십시오 - 이것이 정확히 모든 키 입력마다 화면을 다시 그리게 만드는 패턴입니다.
- 소유권을 수정하기 전에
equatable()또는 캐싱으로 무효화 버그를 덮지 마십시오. 버그는 동일한 객체를 관찰하는 다음 뷰에서 다시 나타날 것입니다. - 텍스트에 고정 프레임을 사용하지 마십시오 - Dynamic Type이 텍스트를 자를 것입니다.
품질 기준
완성된 뷰는 다음 기준을 통과해야 합니다.
- 모든 상태 조각에는 정확히 하나의 소유자와 작동하는 가장 좁은 래퍼가 있습니다.
- Instruments는 행 본문이 자체 데이터가 변경될 때만 평가됨을 보여줍니다.
- 예상되는 가장 큰 목록을 스크롤해도 기준 60Hz 기기에서 프레임 예산을 유지합니다.
- 수명이 긴 상태는 사라질 수 있는 뷰 위로 끌어올려져 탐색 중에도 유지됩니다.
- 키보드 회피 및 안전 영역이 처리됩니다 (
safeAreaInset,scrollDismissesKeyboard). - VoiceOver는 각 상호 작용 요소를 의미 있는 레이블로 읽습니다. 레이아웃은 가장 큰 Dynamic Type 크기에서도 유지됩니다.
reduceMotion이 존중됩니다. - 로딩, 빈 상태, 오류, 다크 모드, 큰 글꼴 상태에 대한 프리뷰가 존재합니다.
단일 화면을 넘어 앱 전체 프로파일링이 필요한 경우 mobile-perf-profiler와 함께 사용하십시오.