(Input) -> Content를 호출자 제공 콘텐츠로 선언하고, Content가 View를 준수하도록 제한합니다. 이렇게 하면 macOS 구성 요소가 형식이 지정된 값을 제공하고 호출자는 그 표현 방식을 선택할 수 있습니다. 이 계약은 해당 입력을 받아야 한다고 요구하지만, 그 입력을 사용하도록 요구할 수는 없습니다.

이 튜토리얼에서는 간단한 데스크톱용 색상 견본 미리보기를 만듭니다. 재사용 가능한 구성 요소는 크기 제어 기능을 소유하고 호출자의 렌더링 클로저에 합성 구성을 제공합니다. 호출자는 구성 요소의 내부 상태를 관리하지 않고 색상 견본을 그립니다.

어떤 인터페이스가 요구 사항을 표현하나요?

반환되는 뷰에는 이름이 지정된 제네릭 매개변수를 사용하고, 구성 요소의 입력에는 구체적인 형식을 사용합니다. 아래 예시에서 핵심 시그니처는 @ViewBuilder content: @escaping (SwatchInput) -> Content이며, Content: View로 제한된 선언 안에 있습니다.

입력과 출력은 서로 다른 역할을 합니다. SwatchInput은 구성 요소가 제공하는 값을 정의합니다. Content는 각 호출자의 클로저가 생성하는 구체적인 뷰 형식을 나타냅니다. 이름이 지정된 제네릭 매개변수를 사용하면 Swift의 제네릭 장에 설명된 것처럼 선언에서 이러한 형식 관계를 표현하고 재사용할 수 있습니다.

Apple은 GeometryReader에서 이 인터페이스 패턴을 사용합니다. 이 초기화 메서드는 (GeometryProxy) -> Content 형식의 클로저를 받습니다. 컨테이너는 프록시를 제공하고 호출자는 이를 바탕으로 콘텐츠를 만듭니다. 자체 구성을 제공하는 사용자 정의 구성 요소를 만들 때 참고할 만한 선례입니다. Apple의 GeometryReader 및 init(content:) 문서를 참조하세요.

실제 작업에 맞는 계약을 선택하세요.

요구 사항 적절한 인터페이스
호출자가 구성 요소 입력 없이 콘텐츠를 제공함 () -> Content
구성 요소가 특정 구성을 제공함 (SwatchInput) -> Content
컨테이너가 사용할 때마다 형식이 달라지는 값을 받음 이름이 지정된 Input 제네릭 매개변수와 이에 대응하는 (Input) -> Content 클로저
컴파일러가 호출자가 구성을 사용한다고 증명해야 함 클로저 시그니처로는 이 요구 사항을 표현할 수 없음

이러한 선택은 Swift의 함수 형식에서 비롯됩니다. 함수 형식의 매개변수 형식과 반환 형식은 전달하고 반환할 수 있는 것을 설명합니다. 구현에서 인수를 어떻게 사용해야 하는지는 설명하지 않습니다. Swift 언어 레퍼런스의 함수 형식 절을 참조하세요.

macOS용 색상 견본 미리보기 전체 구현하기

SwiftUI를 사용하는 macOS 앱 타깃을 이용합니다. 이 예시는 기존 @ViewBuilder 표기를 사용하며 Swift 5.7 이상과 macOS 12 이상 배포 대상으로 작성되었습니다. 미리보기 매크로와 새로운 콘텐츠 빌더 이름은 사용하지 않습니다. 연구 환경에서는 Swift와 Apple SDK를 사용할 수 없어 이 프로그램을 검토만 했으며 여기서 컴파일하거나 실행하지는 않았습니다.

Xcode SwiftUI 앱 프로젝트에서 생성된 앱 선언을 아래 전체 프로그램으로 바꿉니다. 생성된 ContentView 선언이 중복된다면 제거합니다. @main으로 표시된 앱 진입점은 하나만 유지합니다. Apple의 Xcode 도움말에는 SwiftUI 프로젝트로 시작하거나 기존 프로젝트에 SwiftUI 파일을 추가하는 방법이 나와 있습니다.

import SwiftUI

struct SwatchInput {
    let title: String
    let side: CGFloat
    let color: Color
}

@MainActor
struct SwatchPanel<Content: View>: View {
    @State private var isLarge = false
    private let content: (SwatchInput) -> Content

    init(
        @ViewBuilder content: @escaping (SwatchInput) -> Content
    ) {
        self.content = content
    }

    private var input: SwatchInput {
        SwatchInput(
            title: "Sample A",
            side: isLarge ? 120 : 64,
            color: .blue
        )
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Toggle("Large sample", isOn: $isLarge)
            content(input)
        }
        .padding(24)
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        SwatchPanel { input in
            Text(input.title)
                .font(.headline)

            RoundedRectangle(cornerRadius: 12)
                .fill(input.color)
                .frame(width: input.side, height: input.side)

            if input.side == 120 {
                Text("Large preview")
                    .font(.caption)
            }
        }
        .frame(width: 300, height: 280, alignment: .topLeading)
    }
}

@main
@MainActor
struct TypedContentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

처음 화면에는 Large sample 토글, Sample A 제목, 한 변의 길이가 64포인트인 파란색 둥근 정사각형이 표시됩니다. 토글을 켜면 제공되는 한 변의 길이가 120포인트로 바뀌고 Large preview가 추가됩니다. 이 크기와 레이블은 예시를 위한 로컬 값이며 측정 결과가 아닙니다.

구성 요소는 isLarge을 소유합니다. 계산된 input는 현재 선택을 묶고, content(input)는 그 묶음을 호출자에게 전달합니다. Apple의 State 문서에 설명된 대로 SwiftUI는 상태 저장 공간과 변경된 값에 의존하는 뷰 계층의 부분을 갱신하는 작업을 관리합니다.

표현 방식은 호출자가 소유합니다. 제목, 도형, 선택적 캡션을 정할 수 있습니다. 같은 형식의 입력을 유지하면서 다른 배치로 바꿀 수도 있습니다. 패널과 호출자 모두 Apple이 문서화한 View 모델에 따라 body을 구현해 인터페이스를 구성합니다.

빌더와 이스케이핑 어노테이션이 필요한 이유는 무엇인가요?

@ViewBuilder를 사용하면 호출자는 하나의 클로저 안에 여러 뷰 표현식과 지원되는 조건부 분기를 작성할 수 있습니다. 이 예시에서 호출자는 조건 없는 뷰 두 개와 조건부 캡션 하나를 제공합니다. 빌더는 이 콘텐츠를 결합해 구성 요소의 반환 제약 조건을 충족하는 결과를 만듭니다. Apple은 ViewBuilder에서 여러 하위 뷰와 조건부 구성을 설명합니다.

빌더가 SwatchInput을 제공하지는 않습니다. 일반 클로저 매개변수가 입력을 선언하고 content(input)가 실제 인수를 제공합니다. 초기화 메서드가 () -> Content을 받는다면 빌더 어노테이션을 추가해도 구성 값을 받는 클로저로 바뀌지 않습니다.

초기화 메서드가 나중에 body에서 호출할 수 있도록 클로저를 속성에 저장하므로 @escaping이 필요합니다. Swift의 클로저 장에서는 클로저를 받는 함수가 반환된 뒤에도 호출할 수 있으면 해당 클로저가 이스케이프한다고 설명합니다.

초기화 시점에 구성이 이미 확정되는 구성 요소라면, 이스케이프하지 않는 빌더를 즉시 호출하고 그 결과인 Content 값을 저장하는 설계도 가능합니다. 그렇게 하면 구성이 적용되는 시점이 달라집니다. 여기서는 클로저를 보관하여 의도한 흐름을 명확히 합니다. 구성 요소 상태에서 현재 입력을 도출한 다음, 그 입력으로 콘텐츠를 만듭니다.

Xcode 빌더 경계 유지하기

Apple의 최신 문서는 Xcode 26 이하와 Xcode 27 이상을 구분합니다. 이전 도구에 대해서는 ViewBuilder가 뷰를 빌드한다고 설명합니다. Xcode 27 이상에서는 형식에 구애받지 않는 구성을 설명하고, 더 폭넓은 동작을 나타내도록 ContentBuilder 별칭을 권장합니다. ViewBuilder를 참조하세요.

Xcode 27 이상을 사용하는 코드베이스를 유지보수할 때는 초기화 메서드에 @ContentBuilder를 사용할 수 있습니다. Apple은 이를 ViewBuilder의 형식 별칭으로 문서화하며, 생성되는 콘텐츠 형식의 준수를 강제합니다. 이 구성 요소의 출력은 여전히 뷰 계층용이므로 Content: View를 유지합니다. 최신 문서에 이전 배포 대상이 나와 있다고 해서 오래된 SDK에도 새 표기가 있다고 가정하지 마세요. ContentBuilder를 참조하세요.

some View만으로 이 계약의 모든 부분을 해결할 수 없는 이유는 무엇인가요?

예시의 body 속성에는 some View가 적절합니다. 구체적인 결과 형식은 숨기면서 결과가 뷰라는 약속은 유지합니다. 저장된 클로저는 이름이 지정된 Content 매개변수를 사용하므로 구성 요소가 속성과 초기화 메서드에서 같은 결과 형식을 참조할 수 있습니다.

Swift 5.7에서는 SE-0341을 통해 불투명 매개변수 선언이 도입되었습니다. _ value: some View와 같은 직접 매개변수는 이름이 지정되지 않은 제네릭 매개변수를 도입하며, 그 구체적인 형식은 호출자에 의해 결정됩니다. 함수 구현이 임의의 뷰를 선택해 해당 이름 없는 형식이 필요한 위치에 전달할 수 있다는 뜻은 아닙니다. 불투명 매개변수 선언을 참조하세요.

특히 (some View) -> Content로 작성한 콜백 매개변수는 SE-0341이 문서화한 불투명 매개변수 사용 위치에서 지원되지 않습니다. 이 제안서는 콜백 함수 형식의 입력 위치에서 불투명 형식을 명시적으로 금지합니다. 해당 문법을 향후 지원할 가능성에 관한 논의는 구현된 동작이 아니라 향후 방향 섹션에 있습니다. 현재 형식 레퍼런스에도 이 제한이 유지되어 있습니다.

이 데스크톱 작업에서는 지원되는 방법이 간단합니다. 구성 요소가 제공하는 값을 구체적인 구성 형식으로 만듭니다. 다른 재사용 가능한 컨테이너에서 호출자가 제공한 입력 형식을 유지해야 한다면 제네릭 매개변수에 그 형식의 이름을 지정하고 저장된 값과 콜백 전체에 일관되게 사용합니다. 두 설계 모두 불투명 콜백 입력을 필요로 하지 않습니다.

컴파일러가 실제로 보장하는 것은 무엇인가요?

시그니처는 호출 가능한 형태를 보장합니다. SwatchInput 인수 하나를 받고 Content: View를 만족하는 결과를 반환합니다. 또한 클로저 매개변수에 추론된 형식을 지정하므로 호출자는 형식 어노테이션을 반복하지 않고 input.title, input.side, input.color에 접근할 수 있습니다. Swift의 클로저 문서에서는 클로저 컨텍스트에서 이루어지는 이러한 추론을 설명합니다.

이 속성들이 화면에 반영된다고 보장하지는 않습니다. 예를 들어 다음은 ContentView.body 안에서 유효한 대체 표현식이며, 의도적으로 구성을 받아 무시합니다.

SwatchPanel { _ in
    Text("Fixed preview")
}

요구 사항을 작성할 때 이 경계가 중요합니다. “호출자 콘텐츠가 SwatchInput을 받는다”는 형식 계약입니다. “호출자 콘텐츠가 제공된 크기를 표시한다”는 동작 요구 사항입니다. 해당 크기를 표시하는 것이 구성 요소에 필수라면 그 렌더링을 구성 요소 내부에 넣고 사용자 지정 영역을 더 작게 노출하세요.

통합 문제를 진단할 때 다음 점검 항목을 사용하세요. 컴파일러 테스트 결과를 보고하는 것이 아니라 인터페이스에서 도출한 확인 단계입니다.

증상 확인할 내용
content 호출 시 인수가 누락됨 구성을 전달합니다: content(input).
초기화 메서드에서 이스케이핑 클로저 진단 발생 콜백을 저장할 때 @escaping을 유지합니다.
호출자 코드에서 여러 형제 뷰를 사용할 때 오류 발생 초기화 매개변수에 적절한 빌더 어노테이션을 유지합니다.
명시적으로 형식을 지정한 콜백이 거부됨 입력이 String 같은 무관한 형식이 아니라 SwatchInput인지 확인합니다.
미리보기가 크기 토글을 반영하지 않음 렌더링이 input.side를 읽는지 확인합니다.
구성 요소가 초기 구성만 계속 표시함 콘텐츠를 한 번만 만들어 저장하고 현재 입력으로 콜백을 호출하지 않는지 확인합니다.

작업에 필요한 만큼만 계약을 유지하세요. 구체적인 구성과 제네릭 뷰 결과를 사용하면 색상 견본 패널의 경계가 명확해집니다. 구성 요소는 현재 값을 제공하고, 호출자는 완전한 형식 정보를 갖춰 값을 받으며, 선택한 표현 방식은 쉽게 바꿀 수 있습니다.

출처