ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI의 View 재생성, ID 시스템, 그리고 View 계층 구조
    카테고리 없음 2025. 4. 11. 21:55
    구조체는 불변으로 유지하되, 상태는 별도로 관리한다.

     

    View 구조체의 불변성과 재생성

    SwiftUI에서 모든 View는 구조체(struct)로 구현됩니다. 구조체는 Swift의 값 타입(value type)이므로 한번 생성되면 내부 속성을 변경할 수 없는 불변(immutable) 객체입니다.

    struct CounterView: View {
        @State private var count = 0
        
        var body: some View {
            Button("Count: \(count)") {
                count += 1
            }
        }
    }
    

    상태가 변경될 때(예: count가 증가할 때) SwiftUI는 body 속성만 다시 계산하는 것이 아니라, 완전히 새로운 CounterView 구조체 인스턴스를 생성합니다. 이는 View 구조체가 불변이기 때문에 body 계산 속성의 결과만 변경하는 것이 불가능하기 때문입니다.

    대신 SwiftUI는 상태 변경을 감지하면

    1. 새로운 View 구조체 인스턴스를 생성합니다.
    2. 이 새 인스턴스의 body 속성을 계산합니다.
    3. 이전 뷰 트리와 새 뷰 트리를 비교(diffing)합니다.
    4. 실제로 변경된 부분만 화면에 업데이트합니다.

    이런 접근 방식은 일관성과 예측 가능성을 높여주지만, 한 가지 중요한 질문이 남습니다.

    뷰가 완전히 새로 생성된다면, 어떻게 사용자의 상태 정보(예: count 값)가 보존될 수 있을까요?

    @State와 상태 보존 메커니즘

    SwiftUI는 @State, @StateObject 같은 프로퍼티 래퍼를 통해 View 외부의 별도 저장소에 상태를 보관합니다. 이는 View가 재생성되어도 상태가 유지되도록 합니다.

    @State private var count = 0  // View 외부 저장소에 실제 값이 저장됨
    

    ID 시스템: 상태와 View 연결의 핵심

    SwiftUI는 각 View에 고유한 ID를 할당하고, 이 ID를 사용하여 상태 값과 View를 연결합니다.

    따라서 새로운 View 인스턴스가 생성되어도 같은 ID를 가지고 있으면 이전의 상태값을 유지할 수 있습니다.

    (View ID) -> (상태 저장소에 대한 참조)
    

    View의 ID는 다음 요소들로 결정됩니다.

    1. View의 타입
    2. View 계층 구조 내 위치
    3. View 생성자에 전달된 식별 가능한 값
    4. 명시적 .id() 수정자 값

    View 계층 구조와 그 구성

    SwiftUI에서 View 계층 구조는 뷰의 body 속성 내에 정의된 중첩 뷰들로 형성됩니다. 이 계층 구조는 트리 형태로 구성되며, 각 노드는 하나의 View입니다.

    계층 구조 형성 과정

    SwiftUI에서 View 계층 구조가 형성되는 과정은 다음과 같습니다.

    1. 루트 뷰 정의: 모든 SwiftUI 앱은 App 프로토콜을 준수하는 구조체로 시작하고, 이 구조체는 WindowGroup이나 Scene을 반환합니다.
    2. 중첩된 뷰 구성: 각 뷰의 body 속성은 하위 뷰들을 반환합니다. 이렇게 상위 뷰에서 하위 뷰로 이어지는 계층 구조가 형성됩니다.
    struct ContentView: View {
        var body: some View {
            VStack {  // 부모 뷰
                Text("Header")  // 자식 뷰
                HStack {  // 자식 뷰이자 그 하위 뷰의 부모
                    Image(systemName: "star")  // 손자 뷰
                    Text("Details")  // 손자 뷰
                }
            }
        }
    }
    
    • 수정자(Modifier) 적용: .padding(), .background() 같은 수정자를 적용하면, SwiftUI는 내부적으로 기존 뷰를 감싸는 새로운 수정된 뷰를 생성합니다. 이는 계층 구조를 더 깊게 만듭니다.
    Text("Hello")
        .padding()  // 이것은 내부적으로 _PaddingView라는 새 뷰 생성
        .background(Color.red)  // 이것은 _BackgroundView라는 새 뷰 생성
    

    이 코드는 실제로 다음과 같은 계층 구조를 형성합니다.

    _BackgroundView
      └─ _PaddingView
           └─ Text("Hello")
    

    View 계층 구조의 최적화

    SwiftUI는 선언된 뷰 계층 구조를 그대로 사용하는 것이 아니라, 렌더링 전에 여러 최적화 과정을 거칩니다.

    1. 중간 뷰 합성(View Flattening): SwiftUI는 불필요한 중간 컨테이너 뷰를 제거하여 계층 구조를 단순화합니다. 예를 들어, 중첩된 여러 VStack 또는 빈 뷰를 최적화할 수 있습니다.
    2. 레이아웃 최적화: 동일한 레이아웃 특성을 가진 여러 수정자를 단일 연산으로 병합합니다.
    3. 불필요한 래핑 제거: 일부 수정자는 내부적으로 새 뷰를 생성하지 않고 기존 뷰의 속성만 수정할 수 있도록 최적화됩니다.

    SwiftUI가 이러한 최적화를 수행한 후의 실제 뷰 계층 구조는 개발자가 작성한 코드보다 더 효율적일 수 있습니다.

    부분적 업데이트와 차이점 감지(Diffing)

    SwiftUI가 상태 변경에 따라 화면을 효율적으로 업데이트하는 방식은 차이점 감지(diffing) 알고리즘에 기반합니다. 이 프로세스는 다음과 같이 진행됩니다.

    1. 상태 변경 감지 및 뷰 재계산

    struct ProfileView: View {
        @State private var isExpanded = false
        
        var body: some View {
            VStack {
                Button("Toggle Details") {
                    isExpanded.toggle()  // 상태 변경 발생
                }
                
                if isExpanded {  // 조건부 뷰
                    Text("Additional details")
                }
            }
        }
    }
    

    isExpanded가 변경되면, SwiftUI는 ProfileView의 body를 재계산합니다. 이것은 새로운 뷰 트리를 생성합니다.

    2. 뷰 트리 차이점 감지

    SwiftUI는 이전 뷰 트리와 새 뷰 트리를 비교하여 실제로 변경된 부분을 식별합니다. 이 과정은 세 가지 수준에서 일어납니다.

    1. 구조 비교: 뷰 계층 구조의 형태 변경을 감지합니다. 예를 들어, 조건부 뷰가 추가되거나 제거된 경우.
    2. 타입 비교: 같은 위치에 있는 뷰의 타입이 변경되었는지 확인합니다.
    3. 속성 비교: 같은 타입의 뷰라면, 텍스트 내용이나 색상 같은 속성이 변경되었는지 확인합니다.

    이 과정에서 ID 시스템이 중요한 역할을 합니다. SwiftUI는 뷰의 ID를 사용하여 이전 뷰와 새 뷰가 "동일한 뷰의 다른 버전"인지 아니면 "완전히 다른 뷰"인지 판단합니다.

    3. 최소한의 업데이트 적용

    차이점 감지 후, SwiftUI는 변경된 부분만 효율적으로 업데이트합니다.

    1. 제거된 뷰 처리: 더 이상 존재하지 않는 뷰를 UI에서 제거합니다.
    2. 추가된 뷰 처리: 새로 추가된 뷰를 삽입합니다.
    3. 업데이트된 뷰 처리: 속성만 변경된 뷰의 경우, 전체를 다시 그리지 않고 해당 속성만 업데이트합니다.
    4. 이동된 뷰 처리: 위치가 변경된 뷰를 적절히 이동시킵니다.

    예를 들어, 위 ProfileView 예제에서 isExpanded가 false에서 true로 변경될 경우

    1. SwiftUI는 VStack과 Button은 그대로 유지합니다(변경 없음).
    2. if isExpanded 조건에 따라 Text("Additional details")가 새로 추가되었음을 감지합니다.
    3. 이 Text 뷰만 UI에 새로 추가합니다.

    이런 방식으로 SwiftUI는 전체 UI를 다시 그리지 않고, 변경된 부분만 효율적으로 업데이트합니다.

    ID 시스템과 계층 구조의 상호작용

    SwiftUI의 ID 시스템과 뷰 계층 구조는 밀접하게 상호작용합니다.

    1. 계층적 ID 형성: 뷰의 ID는 부분적으로 계층 구조 내 위치에 의해 결정됩니다. 같은 타입과 속성을 가진 뷰라도 계층 구조 내 다른 위치에 있으면 다른 ID를 가집니다.
    2. 컨텍스트 의존성: 특정 뷰의 ID는 그 상위 계층 구조에 의해 영향을 받습니다. 이는 SwiftUI가 뷰를 식별하는 방식의 핵심 부분입니다.
    3. 계층적 업데이트 최적화: SwiftUI는 상태 변경이 영향을 미치는 계층 구조의 최소 하위 트리만 업데이트합니다. 상위 뷰가 변경되지 않으면, 그 모든 자식 뷰를 다시 계산할 필요가 없습니다.

    댓글

Designed by Tistory.