@ViewBuilder는 선언적 View 계층 구조 구축을 위해 설계된 SwiftUI의 result builder 어노테이션입니다. Apple Developer Documentation, 2024에 따르면, @ViewBuilder는 여러 표현식과 조건 로직이 포함된 코드 블록을 Swift 컴파일러가 이해할 수 있는 단일 View 타입으로 변환합니다. 이 어노테이션이 없으면 if/else 및 body의 여러 요소와 함께 익숙한 선언적 SwiftUI 구문을 사용하는 것이 불가능합니다.
핵심 요점
@ViewBuilder는 result builder 패턴(SE-0289)을 구현하는 어노테이션으로, 선언적 구문을 사용하여 여러 View를 하나의 구성으로 조합할 수 있게 합니다. 여러 표현식, 조건부 구문 및 옵셔널 값을 해당 타입(TupleView, ConditionalContent, OptionalContent)으로 자동 래핑합니다.
result builder가 등장하기 전에는 개발자가 수동으로 요소를 VStack이나 HStack으로 래핑하고, 조건 로직에 삼항 연산자나 팩토리 메서드를 사용해야 했습니다. @ViewBuilder는 SwiftUI 구문을 간결하고 읽기 쉽게 만들어 if/else 및 루프가 포함된 일반 Swift처럼 보이는 코드를 작성할 수 있게 했습니다.
Swift Evolution SE-0289에 따르면, result builder는 SwiftUI에 국한되지 않는 일반적인 메커니즘입니다. @ViewBuilder는 이 메커니즘의 구현 중 하나이며, 문자열 구성을 위한 @StringBuilder 및 다른 DSL을 위한 라이브러리 구현도 있습니다. SwiftUI에서 @ViewBuilder는 body뿐만 아니라 컨테이너(VStack, HStack, ZStack, List)의 클로저 매개변수에도 사용됩니다.
명령형 UIKit에서는 UIView를 명시적으로 생성하고, 속성을 구성한 다음 addSubview를 통해 계층 구조에 추가합니다. @ViewBuilder를 사용하는 SwiftUI에서는 표시할 View를 선언적으로 설명하고, SwiftUI가 상태 변경에 따라 요소의 생성, 업데이트 및 제거를 관리합니다.
Result builder는 정적 메서드 buildBlock, buildOptional, buildEither 등을 통해 표현식 시퀀스를 단일 복합 값으로 변환하는 Swift 메커니즘입니다. 컴파일러가 @ViewBuilder 어노테이션을 발견하면 컴파일 중에 자동으로 이러한 메서드를 코드 블록에 적용합니다.
@resultBuilder
struct ViewBuilder {
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)>
static func buildIf<C>(_ c: C?) -> C?
static func buildEither<T, F>(first: T) -> ConditionalContent<T, F>
static func buildEither<T, F>(second: F) -> ConditionalContent<T, F>
}
buildBlock은 1~10개의 표현식을 받아 TupleView를 반환합니다. 각 인자 수(표현식의 개수)에는 자체 buildBlock 오버로드가 있습니다: buildBlock
buildEither(first/second)는 if/else 구문을 처리합니다. 각 분기는 해당 메서드에 전달되고 결과는 ConditionalContent에 래핑됩니다. 이는 특정 분기 타입을 숨기고 SwiftUI에 통합 인터페이스를 제공하는 타입입니다.
SwiftUI에서 body 프로퍼티는 이미 암시적으로 @ViewBuilder로 어노테이션되어 있습니다. 코드에서 해당 어노테이션을 볼 수는 없지만 컴파일러가 자동으로 적용합니다. 그러나 여러 View를 반환하는 사용자 정의 프로퍼티나 클로저 매개변수의 경우 어노테이션을 명시적으로 지정해야 합니다.
제한 사항 1 — 블록당 10개 요소. 이것은 @ViewBuilder의 가장 잘 알려진 제한 사항입니다. 같은 레벨에 10개 이상의 요소를 표시해야 하는 경우 컴파일러가 오류를 발생시킵니다. 해결 방법으로는 Group, ForEach, List 또는 하위 컴포넌트로 분할이 있습니다. Group은 시각적 중첩을 추가하지 않지만 각 Group은 하나의 요소로 계산됩니다.
struct ManyElementsView: View {
var body: some View {
Group {
Text("1"); Text("2"); Text("3")
Text("4"); Text("5"); Text("6")
Text("7"); Text("8"); Text("9")
}
Group {
Text("10"); Text("11"); Text("12")
}
}
}
제한 사항 2 — 특정 구문 지원 부족. @ViewBuilder는 do/catch, guard, for-in(ForEach 없음) 및 기타 제어 흐름 구문을 지원하지 않습니다. 루프에는 식별 가능한 데이터와 함께 ForEach를 사용하세요. 오류 처리에는 Result나 옵셔널 값을 받는 별도의 View를 사용합니다.
제한 사항 3 — 디버깅 복잡성. @ViewBuilder에서 오류가 발생하면 컴파일러가 장황한 메시지를 생성하여 근본 원인을 찾기 어렵습니다. 일반적인 문제: if/else 분기의 타입 불일치, 10개 요소 제한 초과, 필요한 buildBlock 오버로드 부재 등이 있습니다.
패턴 1: if/else를 통한 조건부 표시. @ViewBuilder의 가장 일반적인 사용 사례입니다. 삼항 연산자나 팩토리 메서드 없이 상태에 따라 다른 View를 표시할 수 있습니다.
struct StatusView: View {
var status: LoadStatus
@ViewBuilder
var body: some View {
switch status {
case .loading:
ProgressView("Loading...")
case .loaded(let data):
DataView(data: data)
case .error(let message):
ErrorView(message: message)
}
}
}
패턴 2: 함수 및 초기화 매개변수의 @ViewBuilder. 클로저를 통해 자식 View를 받는 재사용 가능한 컨테이너를 만드는 데 사용됩니다. 라이브러리 및 UI 컴포넌트의 표준 패턴입니다.
struct SectionCard<Content: View>: View {
let title: String
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading) {
Text(title).font(.headline)
content
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
}
}
패턴 3: ForEach와의 구성. @ViewBuilder는 ForEach와 올바르게 작동하여 데이터 배열에서 요소를 동적으로 생성할 수 있습니다. ForEach의 각 요소는 @ViewBuilder 컨텍스트에서 하나의 표현식으로 계산됩니다.
사용자 정의 ViewBuilder는 @ViewBuilder로 어노테이션되고 some View를 반환하는 사용자 정의 함수 또는 프로퍼티입니다. 이러한 함수를 통해 복잡한 표시 로직을 캡슐화하고 애플리케이션의 여러 부분에서 재사용할 수 있습니다.
struct FormRow<Content: View>: View {
let label: String
@ViewBuilder let content: Content
var body: some View {
HStack {
Text(label)
.frame(width: 120, alignment: .trailing)
content
}
}
}
// 사용 방법:
FormRow(label: "Name") {
TextField("Enter name", text: $name)
}
FormRow(label: "Gender") {
Picker("Select", selection: $gender) {
Text("남성").tag(Gender.male)
Text("여성").tag(Gender.female)
}
}
중요한 규칙: @ViewBuilder를 사용하는 사용자 정의 함수는 구체적인 타입이나 View 프로토콜이 아닌 some View를 반환해야 합니다. 불투명 타입만이 구성의 유연성을 유지하면서 구체적인 구현을 숨길 수 있습니다.
성능: 사용자 정의 @ViewBuilder 함수는 직접 body 코드와 비교하여 오버헤드를 추가하지 않습니다. 컴파일러는 호출을 인라인화하고 결과 코드를 최적화합니다. body를 @ViewBuilder 함수로 분할하면 성능 저하 없이 가독성이 향상됩니다.
자주 묻는 질문
@ViewBuilder는 여러 표현식과 조건이 포함된 코드 블록을 단일 View 타입으로 변환하는 result builder 어노테이션입니다. SwiftUI의 선언적 UI 내에서 익숙한 Swift 구문(if/else, switch, 옵셔널 표현식)을 사용할 수 있습니다.
제한은 buildBlock의 구현에서 비롯됩니다. 1부터 10까지 각 인자 수에 대해 메서드의 별도 오버로드가 존재합니다. Swift는 가변 제네릭을 지원하지 않으므로 오버로드 수가 고정되어 있습니다. 이를 해결하려면 Group, ForEach 또는 하위 컴포넌트를 사용하세요.
아니요, View 프로토콜은 body 프로퍼티에 @ViewBuilder를 암시적으로 적용합니다. 그러나 여러 View를 반환하는 사용자 정의 프로퍼티, 메서드 및 클로저 매개변수의 경우 어노테이션을 명시적으로 지정해야 합니다. 지정하지 않으면 컴파일러가 여러 표현식을 처리할 수 없습니다.
옵셔널 표현식에는 buildIf 메서드가 사용되며, 옵셔널 View를 받아 값이 있으면 반환합니다. 값이 nil이면 buildIf는 nil을 반환하고 요소는 표시되지 않습니다. 이를 통해 body 내에서 if let을 사용할 수 있습니다.
네, Swift 5.9부터 @ViewBuilder는 buildExpression 메서드를 통해 switch를 지원합니다. 컴파일러는 각 case 분기를 해당 buildEither 호출로 변환합니다. switch 지원으로 중첩된 if/else 구문보다 코드를 더 읽기 쉽게 만듭니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.