SwiftUI View 구조: var body·struct·상태 객체의 역할

반응형

SwiftUI View 구조var, struct, class 가운데 하나를 고르는 문제로 이해하면 각 keyword의 역할이 섞인다. struct는 custom view type을 선언하는 방식이고, var body: some View는 그 type이 View protocol을 만족하기 위해 제공하는 computed property다. class는 대개 view 자체보다 identity를 공유하는 model에 사용한다.

var body는 별도의 View 선언 방식이 아니다

Apple의 View protocol 문서는 custom view type이 body computed property를 구현해 content를 제공하도록 설명한다.

import SwiftUI

struct GreetingView: View {
    let name: String

    var body: some View {
        Text("Hello, \(name)")
    }
}

여기서 역할은 세 층으로 나뉜다.

  • struct GreetingView: 하나의 custom view type
  • : View: View protocol conformance
  • var body: some View: 현재 view의 content description을 반환하는 required computed property

some View는 “아무 View나 매번 바꿔 반환한다”는 뜻이 아니다. Swift opaque type 문서가 설명하듯 concrete underlying type을 caller에게 숨기되 compiler는 그 type identity를 안다. @ViewBuilder와 conditional content가 이를 조합하지만, 일반 opaque return은 하나의 underlying type 규칙을 가진다.

custom View는 왜 보통 struct인가

SwiftUI view value는 UI element를 직접 붙잡고 있는 mutable object라기보다 현재 UI가 어떻게 보여야 하는지에 대한 description에 가깝다. input과 state가 바뀌면 SwiftUI가 필요한 body를 평가하고 기존 view identity와 state를 조정한다.

따라서 body가 다시 평가된다는 사실을 “무거운 화면 object가 매번 통째로 새로 만들어진다”로 해석하면 안 된다. 반대로 body 안에서 network request나 database write를 바로 시작하면 평가 횟수와 side effect 횟수를 혼동할 수 있다.

struct ProfileHeader: View {
    let displayName: String
    let subtitle: String

    var body: some View {
        VStack(alignment: .leading) {
            Text(displayName).font(.headline)
            Text(subtitle).font(.subheadline)
        }
    }
}

이처럼 화면 input은 plain property로 받고, view 자체는 struct로 두는 것이 출발점이다. struct가 언제나 물리적 memory copy를 만들기 때문에 빠르다거나, class가 무조건 느리다는 식의 성능 설명은 근거가 되지 않는다. 선택 기준은 value description과 shared identity 중 무엇이 필요한가다.

class는 shared model의 identity에 쓴다

여러 view가 같은 mutable model을 봐야 한다면 reference identity가 유용할 수 있다. 현재 Observation 방식에서는 @Observable model을 만들고, owner view가 @State로 lifetime을 관리하며, child가 binding이 필요할 때 @Bindable을 사용할 수 있다. Apple의 model data guide는 view의 body가 읽은 observable property를 dependency로 추적하는 방식을 설명한다.

import Observation
import SwiftUI

@Observable
final class CounterModel {
    var count = 0
}

struct CounterScreen: View {
    @State private var model = CounterModel()

    var body: some View {
        CounterEditor(model: model)
    }
}

struct CounterEditor: View {
    @Bindable var model: CounterModel

    var body: some View {
        Stepper("Count: \(model.count)", value: $model.count)
    }
}

Apple State 문서는 local source of truth를 가장 높은 owner view에 private state로 두라고 안내한다. model instance를 어디서 생성하고 누가 lifetime을 소유하는지를 정한 뒤 전달해야 한다.

legacy ObservableObject도 ownership부터 본다

이전 deployment target이나 기존 Combine code에서는 ObservableObject, @Published, @StateObject, @ObservedObject를 계속 사용한다.

final class LegacyCounter: ObservableObject {
    @Published var count = 0
}

struct LegacyOwner: View {
    @StateObject private var model = LegacyCounter()

    var body: some View {
        LegacyChild(model: model)
    }
}

struct LegacyChild: View {
    @ObservedObject var model: LegacyCounter

    var body: some View {
        Text("Count: \(model.count)")
    }
}

@StateObject는 owner, @ObservedObject는 injected object를 관찰하는 borrower라는 구분이 핵심이다. 최신 Observation code와 legacy wrapper를 이유 없이 한 model에 섞지 말고 deployment target과 migration 범위를 먼저 정한다.

선택표

질문 우선 선택
화면의 declarative description인가 struct SomeView: View
View가 제공할 content인가 var body: some View
view가 소유하는 작은 local state인가 @State private var
공유 identity가 필요한 observable model인가 @Observable final class
observable model property의 binding이 필요한가 @Bindable
legacy ObservableObject를 owner view가 생성하는가 @StateObject
legacy object를 외부에서 받는가 @ObservedObject

struct와 class의 value·identity 기준은 SwiftUI에서 struct와 class 고르기, wrapper 전체 관계는 SwiftUI 상태 관리 기준에서 이어서 볼 수 있다.

정리

var, struct, class는 경쟁하는 세 선언법이 아니다. custom UI는 보통 struct로 만들고 body computed property로 content를 설명한다. class는 화면 자체보다 shared model identity가 필요할 때 검토한다. 마지막 선택은 성능에 대한 막연한 인상이 아니라 state의 owner와 lifetime을 code에서 예측할 수 있는지로 판단한다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글