SwiftUI는 무엇으로 이루어져 있는가
SwiftUI는 세 가지 Swift 언어 기능 위에 자리 잡고 있습니다. result builder, 불투명 반환 타입(opaque return type), 그리고 값 타입 view 트리입니다. 이 기반 구조가 눈에 들어오면, 개발자들을 놀라게 하는 SwiftUI의 부분들(AnyView, Group, ViewBuilder, @ViewBuilder 매개변수, 그리고 두려운 some View vs any View 오류)이 더 이상 신비롭지 않습니다.
SwiftUI view는 단 하나의 요구사항을 가진 단일 프로토콜을 준수하는 값 타입입니다. 프레임워크의 나머지 부분은 SwiftUI 외부에 존재하는 Swift 언어 기능들 위에 구축되어 있습니다. result builder, 불투명 타입, 제약을 가진 제네릭, 프로퍼티 래퍼가 그것입니다. 이러한 언어 기능들을 이해하면 프레임워크는 평범한 Swift API처럼 읽힙니다. 이해하지 못하면, 프레임워크는 가끔 물어뜯는 마법처럼 읽힙니다.
이 글에서는 그 기반 구조를 살펴봅니다. LiveActivityManager도 없고, Get Bananas 스크린샷도 없습니다. 핵심은 프로젝트가 아니라 프레임워크입니다. 프레임워크가 명확하게 읽히게 되면, 클러스터 내의 모든 출시 코드 게시물이 더 깔끔하게 읽힙니다.
TL;DR
- SwiftUI view는
View를 준수하는 Swift 값 타입입니다. 이 프로토콜에는 단 하나의 요구사항이 있습니다.var body: some View { get }. 그 외의 모든 것은 Swift 언어 기능 위에 구축되어 있습니다. @ViewBuilder는 result builder입니다. 모든View의 body가 result builder입니다. Result builder는 컴파일러가 합성한 호출을 통해 콤마로 구분된 표현식들을 단일 반환 값으로 변환합니다.some View는 불투명 반환 타입입니다. 컴파일러는 구체 타입을 알지만, 호출자는 알지 못합니다. 불투명 타입은 view body가 컴파일 타임과 런타임 모두에서 빠르게 동작하도록 만드는 핵심입니다.AnyView는 불투명성이 통하지 않는 경우를 위한 타입 소거 탈출구입니다.Group,EmptyView,TupleView,_ConditionalContent는 result builder가 합성하는 구현 타입들입니다. 문서화되어 있지만 직접 손으로 작성하는 일은 거의 없습니다.
모든 것을 시작하는 프로토콜
View 프로토콜에는 단 하나의 요구사항이 있습니다.1
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
이 프로토콜에서 SwiftUI의 나머지 부분을 이해하는 데 중요한 두 가지 부분이 있습니다.
연관 타입 Body : View. view의 body 자체가 view입니다. 이 재귀성이 프레임워크를 조합 가능하게 만듭니다. 모든 View는 자신의 body에서 또 다른 View를 반환하고, 이 과정은 프레임워크의 기본 view(Text, Color, Image, EmptyView 등) 중 하나에서 체인이 종료될 때까지 계속됩니다. 이러한 기본 view들의 Body는 Never입니다. 기본 view들은 트리의 잎이고, 여러분이 작성하는 view들은 가지입니다.
body의 @ViewBuilder 어트리뷰트. 모든 body는 result-builder 클로저입니다. Result builder는 SE-0289에 문서화된 Swift 언어 기능으로(Swift 5.4에서 @resultBuilder로 공식화됨), 일련의 표현식을 가진 클로저를 컴파일러가 합성된 메서드 호출을 통해 단일 반환 값으로 변환할 수 있게 해줍니다.2 이 변환이 SwiftUI body 내부의 콤마 없는, 문장 형태의 구문이 동작하게 만드는 핵심입니다.
이 프로토콜의 형태는 두 가지 이유로 특이합니다.
첫째, 요구사항이 메서드가 아닌 계산 프로퍼티입니다. View의 body는 SwiftUI가 view의 상태가 변경되었다고 판단할 때마다 매 렌더링 패스에서 재계산됩니다. 프레임워크는 body 호출이 저렴하다고 가정합니다. body 내부의 긴 계산은 매 렌더링마다 실행되기 때문에 안티패턴입니다.
둘째, Self.Body는 소거되지 않고 연관됩니다. View의 구체적인 body 타입은 컴파일 타임에 시그니처의 일부입니다. Text("Hello")의 body 타입은 Never입니다. 커스텀 view의 body 타입은 @ViewBuilder가 그 body에 대해 합성한 것입니다. 이 연관 타입 설계가 컴파일러로 하여금 런타임 타입 검사 없이 view 트리를 최적화할 수 있게 해줍니다. 또한 커스텀 view가 조건부 콘텐츠를 반환할 때 some View 요구사항을 만들어내는 원인이기도 합니다.
Result Builder: 콤마 없는 DSL
Result builder는 컴파일러가 합성한 메서드 호출을 삽입하여 클로저를 단일 반환 값으로 변환하는 Swift 언어 기능입니다. @ViewBuilder는 result builder입니다. 모든 SwiftUI view의 body가 그 클로저입니다.2
다음 view를 살펴봅시다.
struct ExampleView: View {
var body: some View {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
}
body에는 구분자 없이 세 개의 문장이 있습니다. 일반 Swift에서는 컴파일 오류입니다. 클로저는 단 하나의 값만 반환할 수 있습니다. Result builder는 컴파일 전에 클로저를 다시 작성합니다. @ViewBuilder의 확장 후 컴파일러가 실제로 보는 코드는 대략 다음과 같습니다.
struct ExampleView: View {
var body: some View {
ViewBuilder.buildBlock(
Text("Title"),
Text("Subtitle"),
Image(systemName: "star")
)
}
}
ViewBuilder.buildBlock(_:_:_:)은 세 개의 view를 받아 TupleView<(Text, Text, Image)>를 반환하는 정적 메서드입니다. body는 그 단일 tuple-view 값을 반환합니다. 이전 SwiftUI는 1, 2, 3, … 최대 10개의 자식을 위한 고정된 buildBlock 오버로드 세트를 제공했습니다. 현재 SwiftUI는 Swift의 가변 제네릭 지원(buildBlock<each Content>)을 사용하므로 11개 이상의 형제 view를 가진 body도 더 이상 특별한 경우가 아닙니다.
같은 패턴이 제어 흐름도 처리합니다. if 문이 있는 view body는 다음과 같이 보입니다.
struct ConditionalView: View {
let isActive: Bool
var body: some View {
if isActive {
Text("Active")
} else {
Text("Inactive")
}
}
}
@ViewBuilder는 이를 buildEither(first:) / buildEither(second:) 호출을 통해 다시 작성하여 _ConditionalContent<Text, Text>를 생성합니다. 어떤 렌더링에서든 한 분기만 실행되더라도 컴파일러는 컴파일 타임에 결과 타입을 알고 있습니다.
if let, switch, 옵셔널 언래핑, 그리고 몇 가지 다른 구문은 result builder의 다양한 buildXxx 정적 메서드에 의해 처리됩니다.3 반복되는 콘텐츠는 언어 기능이 buildArray를 통해 지원하지만 @ViewBuilder는 지원하지 않는 주목할 만한 한 가지 경우입니다. body 내부의 원시 for 루프는 “closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.” 오류로 실패합니다. SwiftUI식 답은 ForEach입니다. ForEach는 RandomAccessCollection과 콘텐츠 클로저를 받아 반복을 단일 값 타입 view로 합성합니다. 이 DSL은 별도로 만들어진 것이 아닙니다. View를 위해 구성된 Swift result builder입니다.
some View와 불투명성 문제
커스텀 view의 body는 보통 some View를 반환합니다. 이 키워드는 불투명 반환 타입이며 Swift 5.1에서 추가되었습니다.4
some View는 이렇게 말합니다. “나는 View를 준수하는 특정 타입을 반환하지만, 그것이 어떤 타입인지는 알려주지 않겠다.” 컴파일러는 최적화를 위해 내부적으로 구체 타입을 추적합니다. View의 호출자는 프로토콜 증인(protocol witness)만 봅니다. 이 패턴이 view의 body가 VStack<TupleView<(Text, Image, Spacer)>> 같은 복잡한 타입을 반환하면서도 소스 코드에 그 타입을 작성할 필요가 없게 만듭니다.
새로운 SwiftUI 개발자들을 혼란스럽게 만드는 some View의 두 가지 측면이 있습니다.
some View는 서로 다른 것을 반환할 때조차도 하나의 특정 타입입니다. if condition { Text("A") } else { Image("b") } 표현식은 @ViewBuilder body 내부에서 허용됩니다. 왜냐하면 result builder가 두 분기를 모두 _ConditionalContent로 감싸 단일 구체 타입을 만들어내기 때문입니다. 그러나 result builder 외부에서 if condition { return Text("A") } else { return Image("b") } 표현식은 컴파일 오류입니다. 두 분기는 서로 다른 구체 타입을 반환하지만 some View는 하나를 요구합니다. Result builder가 조건부 반환 형태를 동작하게 만드는 것입니다. 명시적 반환은 result-builder 변환을 잃습니다.
some View는 any View와 같지 않습니다. some View는 불투명합니다(하나의 특정 타입, 숨겨짐). any View는 존재형(existential)입니다(어떤 준수 타입이든 담을 수 있는 박스, 런타임 오버헤드 있음). 자유 함수나 프로퍼티는 합법적으로 any View를 반환할 수 있습니다. 그러나 View 프로토콜의 body는 그렇게 할 수 없습니다. 이 프로토콜은 associatedtype Body: View를 요구하며, any View 자체는 View를 준수하지 않으므로 var body: any View는 프로토콜을 만족시키지 못하고 컴파일러는 some View를 제안합니다. 실용적인 규칙은 다음과 같습니다. View body에는 some View를 사용하고, 런타임에 변하는 view 타입에는 AnyView(타입 소거 래퍼)를 사용하세요. “function declares an opaque return type but the return statements in its body do not have matching underlying types”라는 오류 메시지는 거의 항상 some View 함수에서 서로 다른 구체 타입을 반환하려 시도했다는 의미이며, result-builder 분기 또는 AnyView가 필요합니다.
AnyView: 탈출구
AnyView는 타입 소거된 view 래퍼입니다. 생성은 AnyView(myView)입니다. 이 래퍼는 어떤 준수 view든 담을 수 있으며, SwiftUI는 View가 예상되는 곳에서 이를 받아들입니다.5
탈출구로 사용하는 사례는 런타임 데이터에 따라 서로 다른 구체 타입을 반환하면서 result-builder 분기로 표현할 수 없는 함수입니다.
func viewForKind(_ kind: Kind) -> AnyView {
switch kind {
case .text: return AnyView(Text("hello"))
case .image: return AnyView(Image("photo"))
case .custom: return AnyView(MyCustomView())
}
}
AnyView의 비용은 내부 타입이 view의 정적 정체성의 일부가 되지 않는다는 점입니다. Apple 문서는 그 결과를 직접적으로 설명합니다. AnyView 내부에 감싸진 타입이 렌더링 간에 변경되면, 기존 view 계층 구조가 파괴되고 그 자리에 새로운 계층 구조가 생성됩니다. 이는 상태 손실, 애니메이션 재시작, 정체성 손실을 의미합니다. 동일한 구체 타입을 다시 감싸는 것은 그 파괴를 유발하지 않지만, 프레임워크가 선호하는 정적 타입 기반 diffing도 어느 쪽이든 더 이상 사용할 수 없습니다.
올바른 규칙은 다음과 같습니다. 조건부 view에는 @ViewBuilder result-builder 분기(if, switch, for)를 선호하고, 다양한 타입에는 매개변수화된 view를 선호하며, 둘 다 동작하지 않을 때만 AnyView에 손을 뻗으세요. AnyView를 반환하는 func viewForKind는 일반적으로 viewForKind가 some View를 반환하도록 만들고 result-builder 클로저 안에 switch를 넣어야 한다는 신호입니다.
Group, EmptyView, TupleView: 구현 타입들
Result builder는 특정 구체 view 타입들을 합성합니다. 그중 알아두면 유용한 세 가지가 있습니다.6
Group은 투명한 컨테이너입니다. 콘텐츠로 최대 10개의 view를 받아 부모 레이아웃에 형제로 표시합니다. 컨테이너 자체는 시각적 구조를 추가하지 않습니다. 콘텐츠는 개별적으로 렌더링되는 것과 정확히 동일하게 렌더링됩니다. 사용 사례는 단일 view를 기대하는 컨텍스트에서 여러 view를 감싸는 것입니다(.if 모디파이어, 조건부 반환, “하나의 view”를 생성하는 함수 등). Group { Text("A"); Text("B") }는 두 개를 포함하는 단일 view입니다. 이는 result builder가 암묵적으로 하는 일의 명시적 형태입니다.
EmptyView는 아무것도 렌더링하지 않는 view입니다. Result builder는 if에 else가 없을 때 조건부 거짓 분기로 이를 사용합니다. 자신의 코드에서 EmptyView()를 반환하는 것은 함수의 반환 타입을 변경하지 않고 렌더링을 거부하는 방법입니다.
TupleView는 body에 여러 형제 view가 있을 때 result builder가 생성하는 구체 타입입니다. 이 글 상단에서 세 개의 형제 view를 반환하는 표현식은 실제로 TupleView<(Text, Text, Image)>를 반환합니다. TupleView를 직접 작성하는 일은 거의 없습니다. 오류 메시지에서 읽게 됩니다.
_ConditionalContent(앞에 밑줄이 있음)는 if/else 분기를 처리하는 타입입니다. 이 타입은 ViewBuilder의 공개 표면에 나타나지만, 밑줄이 붙은 이름은 “이를 가볍게 직접 작성하지 말라”는 신호입니다. 직접 손으로 만들기보다는 if/else로부터 result builder가 합성하도록 두세요.
자신의 함수에 @ViewBuilder 적용하기
Result builder는 body만을 위한 것이 아닙니다. 어떤 함수나 클로저 매개변수든 @ViewBuilder로 어노테이션을 달 수 있으며, 동일한 DSL 구문이 그 안에서 합법적이 됩니다.2
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
content
}
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
// Usage: callers get the result-builder DSL inside the closure.
Card {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
이 패턴이 SwiftUI 자체의 VStack, HStack, ZStack, List, Form, Section, Group, NavigationStack이 여러 자식을 받아들이는 방식입니다. 이 모든 타입은 @ViewBuilder content: () -> Content 매개변수를 받습니다. 이 패턴을 인식하면 프레임워크와 동일한 사용성으로 자신만의 컨테이너 view를 작성할 수 있으며, 특별한 컴파일러 지원이 필요하지 않습니다.
init(content:)가 아닌 init(@ViewBuilder content:)를 작성하는 이유는 매개변수의 어트리뷰트가 호출자가 전달하는 클로저 본문 내부에서 result-builder 변환을 활성화하기 때문입니다. 이 어트리뷰트가 없으면 Card { Text("A"); Text("B") }는 컴파일 오류입니다. 클로저에 두 개의 문장이 있고 이를 변환할 @ViewBuilder가 없기 때문입니다.
상태, 바인딩, 그리고 프로퍼티 래퍼 계층
위의 모든 내용은 view 트리의 형태에 관한 것입니다. SwiftUI의 다른 절반은 상태이며, 그 절반은 Swift 프로퍼티 래퍼 위에 구축되어 있습니다.7
View 작성과 가장 관련된 프로퍼티 래퍼들은 다음과 같습니다.
@State는 단일 view 내부에서 값 타입 상태 한 조각을 소유합니다. 프로퍼티를 읽으면 기본 저장소를 읽고, 할당하면 view 재렌더링이 트리거됩니다. 이 래퍼는 단순한 view 로컬 상태(토글의 켜기/끄기, 텍스트 필드의 초안 문자열)에 적합합니다.
@Binding은 다른 view의 상태에 대한 양방향 참조입니다. 부모의 상태를 읽고 쓸 필요가 있는 자식 view는 Binding<T> 매개변수를 받습니다. 부모는 $state(@State의 달러 기호 프로젝션)를 통해 바인딩을 생성합니다.
@Observable(iOS 17+)은 이전의 ObservableObject 준수 패턴을 대체하는 매크로입니다. 클래스에 적용된 매크로는 클래스의 프로퍼티가 body 내부에서 읽히고 나중에 변경될 때 view 재렌더링을 트리거하도록 Observation 프레임워크 추적을 생성합니다. @Observable 클래스의 view 측 소유권은 @StateObject에서 일반 @State로 이동합니다. 양방향 핸들이 필요한 다운스트림 view는 @ObservedObject 대신 @Bindable을 사용합니다.
@Environment는 환경 체인에서 의존성 주입된 값을 읽습니다. SwiftUI는 내장 환경 키(locale, color scheme, dismiss action)를 제공합니다. 앱은 도메인별 의존성 주입을 위해 커스텀 키를 추가합니다.
프로퍼티 래퍼 계층이 상태가 변경될 때 view의 body가 재실행되도록 만듭니다. SwiftUI는 두 가지 별개의 메커니즘을 통해 body 내부의 읽기를 추적합니다. AttributeGraph(@State, @Binding, @Environment를 뒷받침하는 Apple의 비공개 의존성 그래프)는 이전 프로퍼티 래퍼 경로를 위한 것이고, 표준 라이브러리의 Observation 프레임워크(withObservationTracking, iOS 17+에서 공개)는 @Observable 타입을 위한 것입니다.8 추적된 프로퍼티가 변경되면 해당 body들이 재실행되고 diffing 메커니즘이 최소 view 트리 변경을 계산합니다.
두 절반(view 트리 계층과 상태 계층)은 느슨하게 결합되어 있습니다. View 트리는 값 타입이며 재계산이 빠릅니다. 상태 계층은 참조 타입이거나(@Observable의 경우) 저장소 포인터를 가진 값 타입(@State의 경우)이며 읽기를 추적합니다. 이 둘이 함께 프레임워크의 “상태의 함수로서 화면에 무엇이 있어야 하는지를 기술하면 프레임워크가 diff를 알아낸다” 모델을 만들어냅니다.
이제 오류 메시지에서 무엇을 알아볼 수 있는가
기반 구조가 보이는 상태에서 SwiftUI 컴파일러 오류를 읽으면 다음과 같습니다.
“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.” some View 함수에서 서로 다른 구체 타입을 가진 두 개의 return 문이 있습니다. 해결: @ViewBuilder를 사용해 result builder가 두 분기를 모두 _ConditionalContent로 감싸도록 하거나, 두 반환 모두를 AnyView로 감싸세요.
“The compiler is unable to type-check this expression in reasonable time.” 많은 모디파이어가 있는 긴 body 체인이 타입 체커를 소진시킵니다. 해결: body를 더 작은 계산 프로퍼티나 하위 view로 분해하세요. some View를 반환하는 각 조각이 추론 작업을 단순화합니다.
“Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.” 하나의 view를 기대하는 함수가 @ViewBuilder 없이 다중 문장 body의 결과를 받았습니다. 해결: 다중 문장 콘텐츠를 받는 클로저 매개변수에 @ViewBuilder를 추가하세요.
“Generic parameter ‘Content’ could not be inferred.” 커스텀 컨테이너가 @ViewBuilder content: () -> Content를 받지만 호출 사이트에 빈 클로저가 있습니다. 해결: result builder는 Content를 추론하기 위해 최소 하나의 표현식이 필요합니다. 빈 클로저는 호출 사이트가 명시적으로 제공하면 EmptyView()로 폴백됩니다.
오류 메시지가 불친절한 이유는 기반 구조가 보이지 않기 때문입니다. 기반 구조가 보이는 상태에서 이를 읽으면 대부분 “아, result builder가 이를 변환할 수 없구나” 또는 “아, 분기 또는 AnyView가 필요하구나”로 바뀝니다.
기반 구조 밖으로 손을 뻗어야 할 때
기반 구조가 깔끔하게 처리하지 못하는 몇 가지 패턴들입니다.
가변적인 구체 타입. 분기마다 다른 View 타입을 반환하면서 result-builder 분기로 감쌀 수 없는 함수는 AnyView가 필요합니다. 비용(diffing 손실, 애니메이션 없음)을 받아들이고 호출 사이트를 문서화하세요.
크로스 플랫폼 조건부 view. 컴파일 타임 #if os(iOS)는 @ViewBuilder body 내부에서 동작하지만 result builder의 분기 횟수를 제한합니다. 다중 OS 조건부 body는 때때로 “expression too complex” 한도에 부딪힙니다. 해결책은 플랫폼별 하위 view를 별도의 함수로 추출하고 각각이 some View를 반환하도록 만드는 것입니다.
명령형 view 구성. 프레임워크는 view가 표현식이기를 기대하며, 생성된 후 변경되는 객체가 아닙니다. UIKit 스타일의 “label을 생성하고, 텍스트를 설정하고, subview에 추가”는 변환되지 않습니다. SwiftUI의 등가물은 body에서 반환되는 값 타입 Text("...")입니다. 명령형 구성을 요구하는 패턴은 일반적으로 그 작업이 UIKit으로의 UIViewRepresentable 브리지에 속한다는 신호입니다.
이 패턴이 iOS 26+ 출시 앱에 의미하는 것
세 가지 요점입니다.
-
SwiftUI는 Swift이지, 마법이 아닙니다. Result builder, 불투명 반환 타입, 프로퍼티 래퍼는 모두 Swift 언어 참조 문서에 있습니다. 프레임워크를 특별한 DSL이 아닌 Swift 코드로 읽으면 놀라운 부분들이 예측 가능해집니다.
-
some View와AnyView는 서로 다른 문제를 해결합니다. 불투명 반환 타입이 기본이고, 타입 소거는 탈출구입니다.AnyView로 손을 뻗는 것은 드문 경우여야 하고,some View와 result-builder 분기로 손을 뻗는 것이 일반적인 경우여야 합니다. -
Result builder가 DSL 전체입니다. 함수나 매개변수가
@ViewBuilder인 곳이라면 어디에서나 콤마 없는, 문장 형태의 구문을 사용할 수 있습니다.VStack과 동일한 사용성으로 자신만의 컨테이너 view를 작성하는 것은 하나의 어트리뷰트와 하나의 클로저 매개변수입니다.
이 글을 클러스터의 출시 코드 시리즈와 함께 읽어보세요. 크로스 플랫폼 SwiftUI 출시(Return은 하나의 공유된 SwiftUI 코어로 다섯 개의 플랫폼에서 실행됨), Liquid Glass 시각 계층, iOS의 Live Activities 상태 머신, Apple Watch의 watchOS 런타임 계약을 다룹니다. 허브는 Apple Ecosystem Series에 있습니다. AI 에이전트가 포함된 더 넓은 iOS 컨텍스트는 iOS Agent Development guide를 참조하세요.
FAQ
SwiftUI의 View 프로토콜이란 무엇인가요?
View 프로토콜에는 단 하나의 요구사항이 있습니다. var body: some View { get }. 모든 SwiftUI view는 View를 준수하는 Swift 값 타입이며, 또 다른 view를 반환하는 body 계산 프로퍼티를 가집니다(또는 Text, Color, Image, EmptyView 같은 기본 view의 경우 Never를 반환합니다). body는 @ViewBuilder로 어노테이션이 달려 있어 SwiftUI의 콤마 없는 DSL 구문을 사용할 수 있습니다.
some View는 무엇을 의미하나요?
some View는 불투명 반환 타입(Swift 5.1+)입니다. 컴파일러는 구체 타입을 알지만, 호출자는 프로토콜 증인만 봅니다. 불투명 타입은 view body가 VStack<TupleView<(Text, Image, Spacer)>> 같은 복잡한 타입을 명시하지 않고 반환할 수 있게 하면서 컴파일 타임 최적화를 유지합니다. some View는 호출 사이트에서 그 타입이 보이지 않더라도 하나의 특정 타입입니다.
AnyView는 언제 사용해야 하나요?
@ViewBuilder result-builder 분기(if, switch, for)도, 매개변수화된 제네릭도 문제를 해결하지 못할 때만 AnyView를 사용하세요. 감싸진 구체 타입이 렌더링 간에 변경되면 기존 view 계층 구조가 파괴되고 그 자리에 새로운 것이 생성됩니다. 그 순간 애니메이션이 재시작되고 view 상태가 리셋됩니다. 동일한 구체 타입을 다시 감싸는 것은 그 파괴를 유발하지 않지만, 프레임워크가 선호하는 정적 타입 기반 diffing도 어느 쪽이든 더 이상 사용할 수 없습니다. AnyView에 자주 손을 뻗고 있다면, 변경되어야 할 패턴은 상류에 있습니다. 매개변수화된 view를 선호하거나 조건문을 result-builder body 안으로 밀어 넣으세요.
@ViewBuilder란 무엇이며 어디에서 사용할 수 있나요?
@ViewBuilder는 result builder(Swift 언어 기능)입니다. 컴파일러가 합성한 buildBlock, buildEither, buildOptional 등의 호출을 삽입하여 여러 표현식을 가진 클로저를 단일 반환 값으로 변환합니다. 모든 SwiftUI view의 body는 기본적으로 @ViewBuilder입니다. 어떤 함수나 클로저 매개변수에든 @ViewBuilder를 적용하여 호출자에게 동일한 DSL 구문을 제공할 수 있습니다. VStack, Card, Section은 동일한 패턴을 사용해 여러 자식을 받습니다.
예상하지 못한 상황에서 view body가 재렌더링되는 이유는 무엇인가요?
SwiftUI는 body가 읽는 어떤 상태 프로퍼티든 변경될 때마다 body를 재실행합니다. 프로퍼티 래퍼(@State, @Binding, @Observable, @Environment)는 읽기를 추적하고 쓰기 시 재렌더링을 트리거합니다. 예상치 못한 재렌더링은 보통 부모 view의 상태 변경, 환경 값 변경, 또는 @Observable 객체의 읽힌 프로퍼티가 수정된 것으로 추적됩니다. 그런 다음 프레임워크의 diffing이 최소 트리 변경을 계산합니다.
참고문헌
-
Apple Developer, “View” 및 “Configuring views”.
View프로토콜,Body연관 타입, 그리고body의@ViewBuilder어트리뷰트. ↩ -
Swift Evolution, “SE-0289: Result builders”. Result builder를 공식화한 언어 제안(5.1에서
_functionBuilder로 도입, 5.4에서@resultBuilder로 공식화).buildBlock,buildEither,buildOptional,buildArray,buildExpression,buildFinalResult등을 정의합니다. ↩↩↩ -
Apple Developer, “ViewBuilder” 및 “ForEach”. SwiftUI가 view body에 사용하는 result builder 타입(가변 제네릭
buildBlock,buildEither, 옵셔널 언래핑).ViewBuilder는buildArray를 노출하지 않으므로ForEach가 컬렉션에 걸쳐 view를 반복하기 위한 반복 기본 요소입니다. ↩ -
Swift Evolution, “SE-0244: Opaque result types”. Swift 5.1에 추가된 불투명 반환 타입을 위한
some키워드. ↩ -
Apple Developer, “AnyView”. 타입 소거된 view 래퍼, 생성, 그리고 diffing 트레이드오프. ↩
-
Apple Developer, “Group”, “EmptyView”, 그리고 “TupleView”. Result builder가 합성하는 구현 타입들. ↩
-
Apple Developer, “State and Data Flow”. 프로퍼티 래퍼 계층:
@State,@Binding,@Observable,@Environment. SwiftUI의 관찰 시스템과 iOS 17+의@Observable매크로. ↩ -
Apple Developer, “Observation” 및 “Migrating from the Observable Object protocol to the Observable macro”. 표준 라이브러리 Observation 프레임워크(
withObservationTracking(_:onChange:)포함), 그리고ObservableObject에서@Observable로의 iOS 17 마이그레이션 경로. ↩