body 속성은 SwiftUI에서 View 프로토콜의 핵심 요소로, 화면에 표시될 콘텐츠를 결정합니다. Apple Developer Documentation, 2024에 따르면, body는 View 프로토콜의 유일한 필수 요구사항이며 동일한 프로토콜을 준수하는 일부 타입을 반환합니다. SwiftUI는 상태가 변경될 때마다 body를 호출하여 새로운 요소 트리를 구축하고 비교합니다.
핵심 포인트
body는 View 프로토콜의 유일한 필수 요구사항인 계산 속성입니다. View를 준수하는 모든 구조체는 body를 구현해야 합니다. 이 속성은 SwiftUI가 화면에 표시하는 콘텐츠를 반환합니다 — 텍스트, 이미지, 버튼, 중첩 요소가 있는 컨테이너 또는 View 프로토콜을 준수하는 다른 타입이 될 수 있습니다.
body의 시그니처는 항상 고정되어 있습니다: var body: some View { get }. 반환 타입은 some View(불투명 타입)이며, 구체적인 타입이 아닙니다. 이는 서로 다른 Views가 body에서 서로 다른 구체적 타입을 반환할 수 있지만, Swift 컴파일러가 각 구현의 구체적 타입을 컴파일 타임에 고정한다는 것을 의미합니다.
WWDC 2022에 따르면, body는 선언적 인터페이스 설명의 진입점입니다. UIView를 명령적으로 생성하고 구성하는 UIKit과 달리, SwiftUI에서는 무엇을 표시할지 선언적으로 설명하고 SwiftUI 자체가 구현 방법을 계산합니다.
body는 순수 함수처럼 동작해야 합니다 — 동일한 입력(구조체 속성 및 상태)으로 동일한 View 트리를 반환해야 합니다. body가 외부 가변 상태(전역 변수, @AppStorage 래퍼 없는 UserDefaults)에 의존하면 동작을 예측할 수 없게 되고 SwiftUI가 화면을 잘못 다시 그릴 수 있습니다.
계산 속성 body는 값을 저장하지 않습니다 — 접근할 때마다 계산됩니다. SwiftUI가 상태가 변경되었다고 판단하면 View 구조체를 다시 생성하고 표시할 현재 요소 트리를 얻기 위해 body의 새 값을 읽습니다.
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("카운터: \(count)")
.font(.largeTitle)
Button("증가") {
count += 1
}
.padding()
.background(.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
이 예제에서 body는 Text와 수식어가 있는 버튼을 포함하는 VStack을 반환합니다. 버튼을 누르면 @State 속성 count가 증가하고, SwiftUI는 CounterView 구조체를 다시 생성하고 새 Text 값으로 업데이트된 트리를 얻기 위해 body를 다시 호출합니다.
수식어(.font, .padding, .background, .foregroundColor, .cornerRadius)는 원래 View를 수정하지 않고 ModifiedContent — 수정을 추가하는 새 타입으로 래핑합니다. 각 수식어는 중첩의 또 다른 수준을 생성하며, 이는 성능을 위해 고려해야 할 중요한 사항입니다.
body의 반환 타입에서 some View는 단순한 관례가 아니라 컴파일러 요구사항입니다. Swift는 body의 모든 반환 경로가 동일한 구체적 타입을 가져야 합니다. @ViewBuilder 없이는 한 분기에서 Text를 반환하고 다른 분기에서 Button을 반환할 수 없습니다 — 컴파일러가 오류를 발생시킵니다.
struct ConditionalView: View {
var isReady: Bool
@ViewBuilder
var body: some View {
if isReady {
Text("준비")
.foregroundColor(.green)
} else {
ProgressView()
}
}
}
body의 @ViewBuilder는 컴파일 오류 없이 조건부 로직(if/else, switch)을 사용할 수 있게 합니다. ViewBuilder는 자동으로 다른 분기를 ConditionalContent — 구체적 타입의 차이를 숨기는 특수 타입으로 래핑합니다. 이는 동적 인터페이스를 구축하기 위한 핵심 기능입니다.
@ViewBuilder가 없으면 컴파일러는 모든 반환 경로에 대해 단일 타입을 추론하려고 시도합니다. 타입이 다른 경우 — 오류가 발생합니다. 이것이 SwiftUI가 View 선언에서 body에 암시적으로 @ViewBuilder를 적용하는 이유이지만, 사용자 코드에서는 여러 Views를 반환하는 사용자 정의 메서드와 속성에 명시적으로 어노테이션을 추가해야 합니다.
구체적 타입 대신 some View를 사용해도 성능이 저하되지 않습니다 — 컴파일러는 컴파일 타임에 정확한 타입을 알고 동적 디스패치 없이 직접 코드를 생성합니다. 반면 AnyView는 존재론적 컨테이너에 래핑하는 오버헤드와 함께 타입 이레이저를 사용합니다.
body는 SwiftUI에 의해 세 가지 주요 시나리오에서 호출됩니다: View가 처음 표시될 때, @State/@Binding/@ObservedObject/@StateObject가 변경될 때, 부모 View가 초기화자를 통해 새 값을 전달할 때입니다. SwiftUI는 환경 값(@Environment)이 변경될 때도 body를 호출할 수 있습니다.
body 호출 빈도에 대해 걱정할 필요가 없습니다 — SwiftUI는 식별성 메커니즘을 통해 다시 그리기를 최적화합니다. 계층 구조의 각 View에는 고유 식별자가 있습니다. 식별성과 입력 데이터가 변경되지 않은 경우 — 부모 View가 다시 그려져도 body가 호출되지 않습니다. 이는 Equatable 비교와 구조적 안정성을 통해 달성됩니다.
struct ParentView: View {
var body: some View {
ChildView(name: "Alice") // 안정적인 식별성
}
}
struct ChildView: View {
let name: String
var body: some View {
Text("안녕하세요, \(name)님!")
}
}
이 예제에서 ParentView가 다시 그려지지만 동일한 name 값을 전달하는 경우 — ChildView.body가 호출되지 않습니다. SwiftUI는 구조체의 입력 데이터를 비교하고, 변경되지 않은 경우 자식 컴포넌트의 다시 그리기를 건너뜁니다. 이것이 뷰 차별화 메커니즘입니다.
body의 예기치 않은 호출을 초래하는 몇 가지 함정이 있습니다: ObservableObject 없이 클래스 사용, body 내에서 생성된 클로저 전달(각 클로저 생성은 새 식별성을 생성), EquatableView의 잘못된 사용입니다. body가 너무 자주 호출되는 경우 — 모든 자식 컴포넌트의 식별성 안정성을 확인하세요.
첫 번째 규칙: body는 최소화해야 합니다. 복잡한 로직은 View를 반환하는 별도의 계산 속성이나 메서드로 이동하세요. 이렇게 하면 가독성이 향상되고 SwiftUI가 계층 구조의 어떤 부분이 변경되었는지 더 정확하게 결정할 수 있습니다. 큰 body는 명확한 책임 경계를 가진 하위 컴포넌트로 분할하세요.
두 번째 규칙: 작업을 수행하기 위해 body를 사용하지 마세요. 데이터 로딩, 네트워크 작업, 데이터베이스 쓰기 — 이 모든 것은 body 외부에서 task, onChange 수식어 또는 ObservableObject를 통해 이루어져야 합니다. body는 오직 인터페이스 선언만을 위한 것입니다.
세 번째 규칙: 표준 구조 비교가 충분하지 않은 경우 Views에 EquatableView 속성 또는 사용자 정의 Equatable 프로토콜을 사용하세요. 이를 통해 SwiftUI에 자식 View가 다시 그려져야 하는 시기를 명시적으로 알리고 불필요한 body 호출을 피할 수 있습니다.
네 번째 규칙: body에 복잡한 계산(서식 지정, 필터링, 정렬)이 포함된 경우 — 결과를 캐싱하기 위해 @State를 사용하거나 계산을 onChange에서 호출되는 별도 메서드로 이동하세요. 상태가 업데이트될 때마다 body에서 반복 계산을 수행하면 애니메이션이 느려지는 일반적인 원인입니다.
다섯 번째 규칙: 목록(List, ForEach)의 경우 id 매개변수를 통해 안정적인 식별자를 보장하세요. 안정적인 식별성이 없으면 ForEach는 변경이 있을 때마다 모든 요소를 다시 생성하고 각각에 대해 body를 호출합니다(단 하나의 요소만 변경된 경우에도).
자주 묻는 질문
body는 표시할 콘텐츠를 반환하는 View 프로토콜의 계산 속성입니다. 프로토콜의 유일한 필수 요구사항입니다. 반환 타입은 some View로, SwiftUI가 컴파일 타임에 계층 구조를 최적화할 수 있게 합니다.
네, SwiftUI는 상태(@State, @Binding, @ObservedObject) 또는 입력 데이터가 변경될 때마다 body를 호출합니다. 이는 선언적 프레임워크의 정상적인 동작입니다. SwiftUI는 식별성 메커니즘과 Equatable 비교를 통해 호출 빈도를 최적화합니다.
some View는 구체적 구현을 숨기는 불투명 타입입니다. 컴파일러는 컴파일 타임에 타입을 고정하여 직접 호출 성능을 보장합니다. 시그니처를 변경하지 않고 반환 타입을 변경할 수 있는 유연성을 제공합니다.
아니요, body는 옵셔널일 수 없습니다 — 반환 타입 some View는 nil을 허용하지 않습니다. 조건에 따라 요소를 숨겨야 하는 경우 @ViewBuilder 내에서 조건부 로직을 사용하거나 계층 구조에서 공간을 차지하지 않는 EmptyView를 반환하세요.
각 수식어는 새로운 ModifiedContent 계층을 생성하여 계층 깊이를 증가시킵니다. 대부분의 화면(최대 50개 수식어)에서 영향은 무시할 수 있습니다. 과도한 수식어 수(수백 개)는 diffing을 느리게 할 수 있습니다. 관련 수식어를 사용자 정의 확장으로 그룹화하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.