← 모든 글

SwiftUI는 무엇으로 이루어져 있는가

장르: 프레임워크 해설. 이 글은 SwiftUI가 자리 잡고 있는 기반 구조를 설명합니다. result builder, 불투명 반환 타입, 그리고 값 타입 view 트리가 그것입니다. 기반 구조가 보이기 시작하면 개발자들을 놀라게 하는 SwiftUI의 부분들(AnyView, Group, ViewBuilder, @ViewBuilder 매개변수, 그리고 그 악명 높은 some View vs any View 오류)이 더 이상 미스터리가 아닙니다.

SwiftUI view는 단 하나의 요구사항을 가진 단 하나의 프로토콜을 채택하는 값 타입입니다. 프레임워크의 나머지 부분은 SwiftUI 외부에 존재하는 Swift 언어 기능들 위에 구축되어 있습니다. result builder, 불투명 타입, 제약 조건이 있는 제네릭, property wrapper가 그것입니다. 언어 기능을 이해하면 프레임워크는 평범한 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처럼 BodyNever인 view) 중 하나에서 종료될 때까지 이어집니다. 기본 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는 result builder의 클로저입니다.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는 이 단일 튜플 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입니다. ForEachRandomAccessCollection과 콘텐츠 클로저를 받아 반복을 단일 값 타입 view로 합성합니다. DSL은 맞춤 제작이 아닙니다. 그것은 view를 위해 구성된 Swift result builder입니다.

some View와 불투명성 문제

커스텀 view의 body는 보통 some View를 반환합니다. 이 키워드는 불투명 반환 타입(opaque return type)이며 Swift 5.1에서 추가되었습니다.4

some View는 이렇게 말합니다. “나는 View를 채택하는 특정 타입을 반환하지만, 어떤 타입인지는 알려주지 않을게요.” 컴파일러는 최적화를 위해 내부적으로 구체적인 타입을 추적합니다. view의 호출자는 프로토콜 위트니스만 봅니다. 이 패턴이 view의 body가 VStack<TupleView<(Text, Image, Spacer)>> 같은 복잡한 타입을 소스 코드에 작성하지 않고도 반환할 수 있게 해주는 것입니다.

some View에 대해 새로운 SwiftUI 개발자들을 혼란스럽게 하는 두 가지가 있습니다.

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 Viewany View와 같지 않습니다. some View는 불투명입니다(하나의 특정 타입, 숨겨짐). any View는 존재형입니다(어떤 채택 타입이든 담을 수 있는 박스, 런타임 오버헤드 있음). 자유 함수나 프로퍼티는 합법적으로 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 계층이 파괴되고 그 자리에 새로운 계층이 생성됩니다. 이는 상태 손실, 애니메이션 재시작, 정체성 손실을 의미합니다. 같은 구체 타입을 다시 감싸는 것은 그러한 파괴를 일으키지 않지만, 어느 쪽이든 프레임워크가 선호하는 정적 타입 기반 디핑은 더 이상 사용할 수 없습니다.

올바른 규칙은 이렇습니다. 조건부 view에는 @ViewBuilder result-builder 분기(if, switch, for)를 우선하고, 다양한 타입에는 매개변수화된 view를 우선하며, 둘 다 작동하지 않을 때만 AnyView에 손을 뻗으세요. AnyView를 반환하는 func viewForKind는 보통 viewForKindsome View를 반환하도록 만들고 switch를 result-builder 클로저 안에 넣어야 한다는 신호입니다.

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는 ifelse가 없을 때 조건부 false 분기로 사용합니다. 자신의 코드에서 EmptyView()를 반환하는 것은 함수의 반환 타입을 변경하지 않고 렌더링을 옵트아웃하는 방법입니다.

TupleView는 body에 여러 형제 view가 있을 때 result builder가 생성하는 구체 타입입니다. 이 글 상단의 세 형제 view를 반환하는 표현식은 실제로 TupleView<(Text, Text, Image)>를 반환합니다. TupleView를 직접 작성하는 일은 거의 없습니다. 오류 메시지에서 읽게 됩니다.

_ConditionalContent(앞에 밑줄이 있음)는 if/else 분기를 처리하는 타입입니다. 이 타입은 ViewBuilder의 공개 표면에 등장하지만, 밑줄이 붙은 이름은 “이걸 가볍게 직접 사용하지 마라”는 신호입니다. 손으로 만들어내지 말고 result builder가 if/else로부터 합성하도록 하세요.

직접 작성한 함수에 적용하는 @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와 같은 사용성을 가진 자신만의 컨테이너 view를 작성할 수 있다는 의미입니다.

init(content:)가 아니라 init(@ViewBuilder content:)를 작성하는 이유는, 매개변수의 그 어트리뷰트가 호출자가 전달하는 클로저 body 안에서 result-builder 변환을 활성화하기 때문입니다. 어트리뷰트 없이는 Card { Text("A"); Text("B") }가 컴파일 오류가 됩니다. 클로저에 두 개의 문장이 있고 그것을 변환할 @ViewBuilder가 없기 때문입니다.

상태, 바인딩, Property Wrapper 계층

위의 모든 것은 view 트리의 형태에 관한 것입니다. SwiftUI의 다른 절반은 상태이고, 그 절반은 Swift property wrapper 위에 구축되어 있습니다.7

view 작성과 가장 관련 있는 property wrapper들입니다.

@State는 단일 view 안에 값 타입 상태 한 조각을 소유합니다. 프로퍼티를 읽으면 기저 저장소를 읽고, 할당하면 view 재렌더링을 유발합니다. 이 wrapper는 단순한 view 지역 상태(토글의 켜짐/꺼짐, 텍스트 필드의 초안 문자열)에 적합합니다.

@Binding은 다른 view의 상태에 대한 양방향 참조입니다. 부모의 상태를 읽고 쓸 필요가 있는 자식 view는 Binding<T> 매개변수를 받습니다. 부모는 $state(@State의 달러 기호 투영)를 통해 바인딩을 구성합니다.

@Observable(iOS 17+)은 이전의 ObservableObject 채택 패턴을 대체하는 매크로입니다. 클래스에 적용된 매크로는 Observation 프레임워크 추적을 생성하여, body 안에서 읽힌 후 나중에 변경되는 클래스의 프로퍼티가 view 재렌더링을 유발하도록 합니다. @Observable 클래스의 view 측 소유권은 @StateObject에서 일반 @State로 이동합니다. 양방향 핸들이 필요한 다운스트림 view는 @ObservedObject 대신 @Bindable을 사용합니다.

@Environment는 environment 체인에서 의존성 주입된 값을 읽습니다. SwiftUI는 내장 environment 키(로케일, 컬러 스킴, dismiss 액션)를 제공하고, 앱은 도메인 특화 의존성 주입을 위한 커스텀 키를 추가합니다.

Property wrapper 계층이 상태가 변경될 때 view의 body가 다시 실행되도록 하는 것입니다. SwiftUI는 두 가지 별개의 메커니즘을 통해 body 내부의 읽기를 추적합니다. 이전 property-wrapper 경로를 위한 AttributeGraph(Apple의 비공개 의존성 그래프로 @State, @Binding, @Environment를 뒷받침함), 그리고 @Observable 타입을 위한 표준 라이브러리의 Observation 프레임워크(withObservationTracking, iOS 17+에서 공개)입니다.8 추적되는 프로퍼티가 변경되면 해당 body가 다시 실행되고 디핑 머신이 최소 view 트리 변경을 계산합니다.

두 절반(view 트리 계층과 상태 계층)은 느슨하게 결합되어 있습니다. view 트리는 값 타입이고 다시 계산하는 것이 빠릅니다. 상태 계층은 참조 타입(@Observable의 경우)이거나 저장소 포인터가 있는 값 타입(@State의 경우)이며 읽기를 추적합니다. 이 둘이 함께 프레임워크의 “상태의 함수로 화면에 무엇이 있어야 하는지를 기술하면 프레임워크가 디프를 계산한다”는 모델을 만들어냅니다.

이제 오류 메시지에서 알아볼 수 있는 것들

기반 구조가 보이는 상태에서 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가 필요합니다. 비용(디핑 손실, 애니메이션 없음)을 받아들이고 호출 측에 문서화하세요.

크로스 플랫폼 조건부 view. 컴파일 타임 #if os(iOS)@ViewBuilder body 안에서 작동하지만 result builder의 분기 수를 제한합니다. 다중 OS 조건부 body는 때때로 “expression too complex” 한계에 부딪힙니다. 해결책은 플랫폼별 하위 view를 별도의 함수로 추출하고 각각 some View를 반환하도록 하는 것입니다.

명령형 view 구성. 프레임워크는 view가 표현식이지, 생성된 후 변경되는 객체가 아니길 기대합니다. UIKit 스타일의 “레이블 생성, 텍스트 설정, subview에 추가”는 번역되지 않습니다. SwiftUI 등가물은 body에서 반환되는 값 타입 Text("...")입니다. 명령형 구성을 요구하는 패턴은 보통 작업이 UIKit으로의 UIViewRepresentable 브리지에 속한다는 신호입니다.

iOS 26+에서 출시되는 앱에 이 패턴이 의미하는 것

세 가지 핵심.

  1. SwiftUI는 Swift이지, 마법이 아닙니다. Result builder, 불투명 반환 타입, property wrapper는 모두 Swift 언어 레퍼런스에 있습니다. 프레임워크를 특수 DSL이 아니라 Swift 코드로 읽으면 놀라운 부분들이 예측 가능해집니다.

  2. some ViewAnyView는 다른 문제를 해결합니다. 불투명 반환 타입이 기본이고, 타입 소거는 탈출구입니다. AnyView에 손을 뻗는 것은 드문 경우여야 하고, some View 더하기 result-builder 분기에 손을 뻗는 것이 일반적인 경우여야 합니다.

  3. 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 값 타입이며, body 계산 프로퍼티는 또 다른 view를 반환합니다(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 상태가 초기화되는 순간입니다. 같은 구체 타입을 다시 감싸는 것은 그러한 파괴를 일으키지 않지만, 어느 쪽이든 프레임워크가 선호하는 정적 타입 기반 디핑은 더 이상 사용할 수 없습니다. 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를 다시 실행합니다. Property wrapper(@State, @Binding, @Observable, @Environment)는 읽기를 추적하고 쓰기 시 재렌더링을 유발합니다. 예상치 못한 재렌더링은 보통 부모 view의 상태 변경, environment 값 변경, 또는 @Observable 객체의 읽기 프로퍼티 변경으로 추적됩니다. 그러면 프레임워크의 디핑이 최소 트리 변경을 계산합니다.

참고 문헌


  1. Apple Developer, “View”“Configuring views”. View 프로토콜, Body 연관 타입, 그리고 body에 적용된 @ViewBuilder 어트리뷰트. 

  2. Swift Evolution, “SE-0289: Result builders”. result builder를 공식화한 언어 제안(5.1에서 _functionBuilder로 도입, 5.4에서 @resultBuilder로 공식화). buildBlock, buildEither, buildOptional, buildArray, buildExpression, buildFinalResult 등을 정의함. 

  3. Apple Developer, “ViewBuilder”“ForEach”. SwiftUI가 view body에 사용하는 result builder 타입(가변 제네릭 buildBlock, buildEither, 옵셔널 언래핑). ViewBuilderbuildArray를 노출하지 않으므로, 컬렉션에 대해 view를 반복하기 위한 반복 기본 요소는 ForEach입니다. 

  4. Swift Evolution, “SE-0244: Opaque result types”. Swift 5.1에서 추가된 불투명 반환 타입을 위한 some 키워드. 

  5. Apple Developer, “AnyView”. 타입 소거 view 래퍼, 생성, 그리고 디핑 트레이드오프. 

  6. Apple Developer, “Group”, “EmptyView”, 및 “TupleView”. result builder가 합성하는 구현 타입들. 

  7. Apple Developer, “State and Data Flow”. property wrapper 계층: @State, @Binding, @Observable, @Environment. SwiftUI의 옵저베이션 시스템과 iOS 17+ @Observable 매크로. 

  8. Apple Developer, “Observation”“Migrating from the Observable Object protocol to the Observable macro”. 표준 라이브러리 Observation 프레임워크(withObservationTracking(_:onChange:) 포함), 그리고 ObservableObject에서 @Observable로의 iOS 17 마이그레이션 경로. 

관련 게시물

watchOS Runtime Is a Contract, Not a Background Task

watchOS does not have iOS's background. WKExtendedRuntimeSession is a contract you sign with the system, broken on wrist…

15 분 소요

RealityKit And The Spatial Mental Model

RealityKit is an entity-component-system, not SwiftUI in 3D. Anchors place entities in real space. Five ways the model d…

16 분 소요

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 분 소요