Apple🍎/Swift

DSL과 SwiftUI: 도메인 특화 언어의 이해와 활용

생각 깎는 아이 2025. 5. 24. 23:02

들어가며

우리가 일상적으로 사용하는 프로그래밍 언어들은 대부분 범용적인 목적으로 설계되었습니다. Swift, Java, Python 같은 언어들은 웹 개발부터 모바일 앱, 데이터 분석까지 다양한 분야에서 활용할 수 있죠. 하지만 때로는 특정 분야에서 더 직관적이고 효율적으로 문제를 해결할 수 있는 전용 언어가 필요합니다. 이것이 바로 DSL(Domain Specific Language)의 개념입니다.

SwiftUI는 이러한 DSL의 훌륭한 예시입니다. UI 구성이라는 특정 도메인에 특화되어 설계된 언어로, 개발자가 더 직관적이고 선언적인 방식으로 사용자 인터페이스를 구성할 수 있게 해줍니다. 이 글에서는 DSL의 기본 개념부터 시작해서 SwiftUI가 어떻게 DSL로 작동하는지, 그리고 이를 가능하게 하는 @resultBuilder와 @ViewBuilder의 역할까지 깊이 있게 살펴보겠습니다.

DSL이란 무엇인가?

DSL의 정의와 특징

DSL(Domain Specific Language)은 특정 응용 분야나 도메인에 특화된 컴퓨터 언어입니다. 범용 프로그래밍 언어(GPL: General Purpose Language)와 달리, DSL은 특정 문제 영역을 효과적으로 해결하기 위해 설계됩니다.

우리는 이미 일상에서 많은 DSL을 사용하고 있습니다. SQL은 데이터베이스 조작에 특화된 언어이고, CSS는 웹 스타일링에, HTML은 문서 구조 표현에 특화되어 있습니다. 이들 각각은 해당 도메인에서 매우 직관적이고 표현력이 뛰어납니다.

-- SQL: 데이터베이스 도메인 DSL
SELECT name, age 
FROM users 
WHERE age > 18 
ORDER BY name ASC

이 SQL 문장을 보면, 데이터베이스에 익숙한 사람이라면 "18세 이상 사용자의 이름과 나이를 이름 순으로 정렬해서 가져와라"라는 의미를 즉시 이해할 수 있습니다. 만약 이를 일반적인 프로그래밍 언어로 표현한다면 훨씬 복잡해질 것입니다.

DSL의 장점

DSL이 제공하는 주요 장점들을 살펴보겠습니다.

표현력(Expressiveness): DSL은 해당 도메인의 개념을 직접적으로 표현할 수 있어 코드가 매우 간결해집니다. 복잡한 개념을 몇 줄의 코드로 표현할 수 있죠.

가독성(Readability): 도메인 전문가가 보기에 자연스럽고 이해하기 쉽습니다. 비개발자도 어느 정도 이해할 수 있는 경우가 많습니다.

생산성(Productivity): 해당 도메인의 문제 해결에 집중할 수 있어 개발 속도가 빨라집니다. 반복적인 코드를 줄이고 핵심 로직에 집중할 수 있습니다.

오류 감소(Error Reduction): 도메인에 특화된 제약 조건을 언어 차원에서 제공하므로 실수를 줄일 수 있습니다.

External DSL vs Internal DSL

DSL은 구현 방식에 따라 두 가지로 나눌 수 있습니다.

External DSL은 완전히 새로운 문법을 가진 독립적인 언어입니다. JSON, XML, SQL 등이 이에 해당합니다. 이들은 자체적인 파서와 인터프리터를 가지고 있습니다.

{
    "name": "홍길동",
    "age": 30,
    "skills": ["Swift", "iOS", "SwiftUI"]
}

Internal DSL은 기존 호스트 언어(여기서는 Swift) 안에서 특정 도메인을 위한 API나 문법을 제공하는 방식입니다. SwiftUI가 바로 이런 Internal DSL의 좋은 예시입니다.

GPL vs DSL: Swift와 SwiftUI 비교

Swift: 범용 프로그래밍 언어

Swift는 범용 프로그래밍 언어입니다. iOS 앱 개발부터 서버 사이드 개발, 시스템 프로그래밍까지 다양한 분야에서 활용할 수 있죠. 이런 유연성이 GPL의 큰 장점입니다.

// Swift로 계산기 클래스 구현
class Calculator {
    private var result: Double = 0
    
    func add(_ value: Double) -> Calculator {
        result += value
        return self
    }
    
    func multiply(_ value: Double) -> Calculator {
        result *= value
        return self
    }
    
    func getResult() -> Double {
        return result
    }
}

// 사용 예시
let calculator = Calculator()
    .add(10)
    .multiply(2)
    .add(5)
print(calculator.getResult()) // 25.0

이 코드는 Swift의 범용적 특성을 잘 보여줍니다. 객체 지향 프로그래밍, 함수형 프로그래밍 등 다양한 패러다임을 지원하며, 어떤 종류의 애플리케이션이든 만들 수 있습니다.

SwiftUI: UI 도메인 특화 언어

반면 SwiftUI는 UI 구성이라는 특정 도메인에 특화된 DSL입니다. UI를 구성하는 데 필요한 개념들이 언어의 핵심 요소로 내장되어 있습니다.

// SwiftUI로 동일한 계산기 UI 구현
struct CalculatorView: View {
    @State private var result: Double = 0
    @State private var currentInput: String = ""
    
    var body: some View {
        VStack(spacing: 20) {
            // 결과 표시 영역
            Text(String(result))
                .font(.largeTitle)
                .foregroundColor(.primary)
                .frame(maxWidth: .infinity, alignment: .trailing)
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(8)
            
            // 입력 영역
            TextField("숫자를 입력하세요", text: $currentInput)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .keyboardType(.decimalPad)
            
            // 버튼 영역
            HStack(spacing: 15) {
                Button("더하기") {
                    addToResult()
                }
                .buttonStyle(.borderedProminent)
                
                Button("곱하기") {
                    multiplyResult()
                }
                .buttonStyle(.borderedProminent)
                
                Button("초기화") {
                    resetCalculator()
                }
                .buttonStyle(.bordered)
            }
        }
        .padding()
    }
    
    private func addToResult() {
        if let value = Double(currentInput) {
            result += value
            currentInput = ""
        }
    }
    
    private func multiplyResult() {
        if let value = Double(currentInput) {
            result *= value
            currentInput = ""
        }
    }
    
    private func resetCalculator() {
        result = 0
        currentInput = ""
    }
}

이 SwiftUI 코드를 보면 UI 구성에 필요한 개념들이 언어의 핵심 요소로 제공되는 것을 알 수 있습니다. VStack, HStack, Text, Button 등은 모두 UI 도메인에 특화된 구성 요소들입니다.

선언적 vs 명령적 프로그래밍

SwiftUI의 가장 큰 특징 중 하나는 선언적 프로그래밍 패러다임을 따른다는 것입니다. 이는 DSL의 일반적인 특성이기도 합니다.

명령적 프로그래밍은 "어떻게(How)" 할 것인지에 초점을 맞춥니다. 단계별로 무엇을 해야 하는지 명시합니다.

// UIKit (명령적 방식)
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 20
stackView.distribution = .fill

let titleLabel = UILabel()
titleLabel.text = "계산기"
titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
titleLabel.textAlignment = .center
stackView.addArrangedSubview(titleLabel)

let resultLabel = UILabel()
resultLabel.text = "0"
resultLabel.font = UIFont.systemFont(ofSize: 36)
resultLabel.textAlignment = .right
resultLabel.backgroundColor = UIColor.systemGray6
stackView.addArrangedSubview(resultLabel)

let addButton = UIButton(type: .system)
addButton.setTitle("더하기", for: .normal)
addButton.backgroundColor = UIColor.systemBlue
addButton.setTitleColor(.white, for: .normal)
addButton.layer.cornerRadius = 8
stackView.addArrangedSubview(addButton)

view.addSubview(stackView)

선언적 프로그래밍은 "무엇을(What)" 원하는지에 초점을 맞춥니다. 최종 결과가 어떤 모습이어야 하는지 기술합니다.

// SwiftUI (선언적 방식)
VStack(spacing: 20) {
    Text("계산기")
        .font(.title)
        .fontWeight(.bold)
    
    Text("0")
        .font(.largeTitle)
        .frame(maxWidth: .infinity, alignment: .trailing)
        .padding()
        .background(Color.gray.opacity(0.1))
    
    Button("더하기") {
        // 액션
    }
    .foregroundColor(.white)
    .background(Color.blue)
    .cornerRadius(8)
}

선언적 방식이 훨씬 간결하고 의도가 명확하게 드러나는 것을 알 수 있습니다. 이는 UI라는 도메인의 특성을 잘 활용한 결과입니다.

@resultBuilder: DSL 제작의 핵심 도구

@resultBuilder의 등장 배경

Swift 5.4에서 도입된 @resultBuilder는 개발자가 Swift 언어 내에서 자신만의 DSL을 만들 수 있게 해주는 강력한 도구입니다. 이전에는 Function Builder라고 불렸지만, 더 명확한 의미를 전달하기 위해 이름이 변경되었습니다.

@resultBuilder가 없다면 SwiftUI와 같은 선언적 문법을 구현하기 매우 어려웠을 것입니다. 여러 개의 View를 자연스럽게 나열하고, 조건문과 반복문을 섞어서 사용하는 것이 불가능했겠죠.

@resultBuilder의 동작 원리

@resultBuilder는 컴파일 타임에 특별한 문법 변환을 수행합니다. 개발자가 작성한 코드를 분석해서 미리 정의된 메서드 호출로 변환하는 것이죠.

// 간단한 문자열 빌더 예제
@resultBuilder
struct StringBuilder {
    // 여러 문자열을 하나로 합치기
    static func buildBlock(_ components: String...) -> String {
        return components.joined(separator: " ")
    }
    
    // 조건부 문자열 처리
    static func buildOptional(_ component: String?) -> String {
        return component ?? ""
    }
    
    // if-else 문 처리
    static func buildEither(first component: String) -> String {
        return component
    }
    
    static func buildEither(second component: String) -> String {
        return component
    }
    
    // 배열(반복문) 처리
    static func buildArray(_ components: [String]) -> String {
        return components.joined(separator: ", ")
    }
}

이제 이 StringBuilder를 사용해서 문자열 DSL을 만들 수 있습니다.

@StringBuilder
func createGreeting(name: String, showTime: Bool) -> String {
    "안녕하세요"
    name + "님"
    
    if showTime {
        "현재 시간은"
        DateFormatter().string(from: Date())
    }
    
    "좋은 하루 되세요!"
}

let greeting = createGreeting(name: "홍길동", showTime: true)
// 결과: "안녕하세요 홍길동님 현재 시간은 2025-05-24 좋은 하루 되세요!"

@resultBuilder의 변환 과정

위의 코드가 컴파일될 때 실제로는 다음과 같이 변환됩니다:

func createGreeting(name: String, showTime: Bool) -> String {
    return StringBuilder.buildBlock(
        "안녕하세요",
        name + "님",
        StringBuilder.buildOptional(showTime ? 
            StringBuilder.buildBlock(
                "현재 시간은",
                DateFormatter().string(from: Date())
            ) : nil
        ),
        "좋은 하루 되세요!"
    )
}

이렇게 @resultBuilder는 자연스러운 문법을 복잡한 메서드 호출로 변환해주는 역할을 합니다.

 

변환과정 자세히 알아보기 

더보기

컴파일러의 시각에서 보는 변환 과정

Swift 컴파일러가 @resultBuilder를 만나면 마치 번역가처럼 작동합니다. 우리가 쓴 "자연어 같은 코드"를 "기계가 이해할 수 있는 메서드 호출"로 번역하는 것이죠.

1단계: 구문 분석 (Syntax Analysis)

컴파일러는 먼저 우리 코드의 구조를 파악합니다. 예제를 다시 살펴보면서 컴파일러가 어떻게 "읽어내는지" 보겠습니다.

@StringBuilder
func createGreeting(name: String, showTime: Bool) -> String {
    "안녕하세요"           // 1. 문자열 리터럴
    name + "님"            // 2. 표현식 결과 (String)  
    
    if showTime {          // 3. 조건문 시작
        "현재 시간은"       // 4. 조건부 문자열 1
        DateFormatter().string(from: Date())  // 5. 조건부 문자열 2
    }                      // 6. 조건문 종료
    
    "좋은 하루 되세요!"    // 7. 문자열 리터럴
}

컴파일러는 이 코드를 보고 다음과 같이 구조를 인식합니다: "StringBuilder라는 result builder를 사용하는 함수가 있고, 그 안에 여러 개의 문자열 표현식과 하나의 if문이 있구나"

2단계: 구문 요소 식별과 매핑

이제 컴파일러는 각 구문 요소를 적절한 builder 메서드에 매핑합니다. 이때 미리 정의된 규칙을 따릅니다:

// StringBuilder에 정의된 메서드들을 다시 보면
@resultBuilder
struct StringBuilder {
    // 여러 요소를 순서대로 나열 → buildBlock 사용
    static func buildBlock(_ components: String...) -> String
    
    // if문으로 조건부 요소 → buildOptional 사용  
    static func buildOptional(_ component: String?) -> String
    
    // if-else문 → buildEither 사용
    static func buildEither(first component: String) -> String
    static func buildEither(second component: String) -> String
}

컴파일러는 코드의 각 부분을 다음과 같이 해석합니다.

순차적 나열 (1, 2, 7번)

"안녕하세요"
name + "님"  
"좋은 하루 되세요!"

→ "이건 여러 요소를 순서대로 나열한 거니까 buildBlock을 써야겠다"

조건문 (3-6번)

if showTime {
    "현재 시간은"
    DateFormatter().string(from: Date())
}

→ "이건 조건부 코드니까 buildOptional을 써야겠다. 그리고 조건문 안의 여러 요소는 또 다른 buildBlock으로 묶어야겠다"

3단계: 중첩 구조 처리 (Nested Structure Processing)

여기서 흥미로운 점은 컴파일러가 중첩된 구조를 어떻게 처리하는지입니다. 조건문 안의 두 문자열을 먼저 처리하고, 그 결과를 조건문 전체에 적용합니다.

// 먼저 조건문 내부 처리
if showTime {
    "현재 시간은"                           // 내부 요소 1
    DateFormatter().string(from: Date())    // 내부 요소 2
}

// 컴파일러가 변환: 내부 먼저 buildBlock으로 묶기
if showTime {
    StringBuilder.buildBlock(
        "현재 시간은",
        DateFormatter().string(from: Date())
    )
}

// 그 다음 조건문 전체를 buildOptional로 감싸기
StringBuilder.buildOptional(
    showTime ? StringBuilder.buildBlock(
        "현재 시간은",
        DateFormatter().string(from: Date())
    ) : nil
)

4단계: 최종 변환 (Final Transformation)

모든 부분을 분석한 후, 컴파일러는 전체를 하나의 큰 buildBlock 호출로 묶습니다:

// 최종 변환된 코드
func createGreeting(name: String, showTime: Bool) -> String {
    return StringBuilder.buildBlock(
        "안녕하세요",                    // 첫 번째 요소
        name + "님",                    // 두 번째 요소
        StringBuilder.buildOptional(     // 세 번째 요소 (조건부)
            showTime ? StringBuilder.buildBlock(
                "현재 시간은",
                DateFormatter().string(from: Date())
            ) : nil
        ),
        "좋은 하루 되세요!"             // 네 번째 요소
    )
}

컴파일러가 사용하는 변환 규칙들

컴파일러는 미리 정의된 패턴 매칭 규칙을 사용해서 이런 변환을 수행합니다. 각 구문 패턴이 어떤 메서드로 변환되는지 살펴보겠습니다.

규칙 1: 순차적 나열 → buildBlock

// 이런 패턴을 보면
expression1
expression2  
expression3

// 이렇게 변환
BuilderType.buildBlock(expression1, expression2, expression3)

규칙 2: 조건문 → buildOptional 또는 buildEither

// if 문 (else 없음)
if condition {
    content
}
// 변환 →
BuilderType.buildOptional(condition ? content : nil)

// if-else 문
if condition {
    trueContent
} else {
    falseContent  
}
// 변환 →
BuilderType.buildEither(first: condition ? trueContent : falseContent)
// 또는
BuilderType.buildEither(second: !condition ? falseContent : trueContent)

규칙 3: 반복문 → buildArray

// for 문이나 배열 처리
for item in items {
    processItem(item)
}
// 변환 →
BuilderType.buildArray(items.map { item in processItem(item) })

실제 SwiftUI에서의 복잡한 변환 예제

이제 더 복잡한 SwiftUI 예제로 이 과정을 확인해보겠습니다.

// 우리가 작성한 복잡한 SwiftUI 코드
@ViewBuilder
var complexView: some View {
    Text("제목")
    
    if user.isLoggedIn {
        Text("환영합니다, \(user.name)님!")
        
        if user.hasNotifications {
            Button("알림 확인") { }
        }
    } else {
        Text("로그인이 필요합니다")
        Button("로그인") { }
    }
    
    ForEach(items) { item in
        Text(item.title)
    }
}

컴파일러가 이를 어떻게 변환하는지 단계별로 봅시다:

1단계: 가장 안쪽 조건문부터 처리

// user.hasNotifications 조건문
ViewBuilder.buildOptional(
    user.hasNotifications ? Button("알림 확인") { } : nil
)

2단계: 로그인 상태 조건문의 각 분기 처리

// if user.isLoggedIn 분기 (true case)
ViewBuilder.buildBlock(
    Text("환영합니다, \(user.name)님!"),
    ViewBuilder.buildOptional(
        user.hasNotifications ? Button("알림 확인") { } : nil
    )
)

// else 분기 (false case)  
ViewBuilder.buildBlock(
    Text("로그인이 필요합니다"),
    Button("로그인") { }
)

3단계: 전체 if-else를 buildEither로 감싸기

ViewBuilder.buildEither(
    first: user.isLoggedIn ? ViewBuilder.buildBlock(
        Text("환영합니다, \(user.name)님!"),
        ViewBuilder.buildOptional(
            user.hasNotifications ? Button("알림 확인") { } : nil
        )
    ) : nil,
    second: !user.isLoggedIn ? ViewBuilder.buildBlock(
        Text("로그인이 필요합니다"),
        Button("로그인") { }
    ) : nil
)

4단계: 최종적으로 모든 것을 buildBlock으로 묶기

// 최종 변환된 코드 (의사 코드)
var complexView: some View {
    return ViewBuilder.buildBlock(
        Text("제목"),                           // 첫 번째 요소
        ViewBuilder.buildEither(                // 두 번째 요소 (복잡한 조건문)
            first: user.isLoggedIn ? ViewBuilder.buildBlock(
                Text("환영합니다, \(user.name)님!"),
                ViewBuilder.buildOptional(
                    user.hasNotifications ? Button("알림 확인") { } : nil
                )
            ) : nil,
            second: !user.isLoggedIn ? ViewBuilder.buildBlock(
                Text("로그인이 필요합니다"),
                Button("로그인") { }
            ) : nil
        ),
        ForEach(items) { item in               // 세 번째 요소 (반복문)
            Text(item.title)
        }
    )
}

컴파일러가 이 모든 걸 어떻게 알까?

여기서 정말 흥미로운 점은 "컴파일러가 어떻게 이런 복잡한 변환 규칙을 알고 있느냐"는 것입니다. 답은 Swift 언어 명세에 이런 규칙들이 명시되어 있기 때문입니다.

Swift 컴파일러에는 @resultBuilder를 처리하는 전용 모듈이 있고, 이 모듈이 다음과 같은 작업을 수행합니다.

  1. 구문 트리 분석: 코드를 추상 구문 트리(AST)로 파싱
  2. 패턴 매칭: 각 구문 패턴을 미리 정의된 규칙과 매칭
  3. 메서드 검색: 해당 @resultBuilder 타입에서 적절한 메서드 찾기
  4. 코드 생성: 원본 코드를 메서드 호출로 대체하는 새로운 AST 생성

이 모든 과정이 컴파일 타임에 일어나므로, 런타임에는 변환된 코드만 실행됩니다. 그래서 성능 손실 없이 자연스러운 DSL 문법을 사용할 수 있는 것이죠.

 

여기서 하나 주의할 점은 @resultBuilder가 하는 일은 구조 변환이지 값 평가가 아니다라는 점입니다. 컴파일 과정에서 변환이 일어난다는 말 때문에 헷갈릴 수 있는데 실제 값 평가는 런타임에 이루어지며 어떤 구조로 조합할지에 대해서만 사전에 변환 작업이 이루어집니다. 

@ViewBuilder: SwiftUI DSL의 핵심 엔진

@ViewBuilder의 역할

@ViewBuilder는 SwiftUI에서 제공하는 특별한 @resultBuilder입니다. SwiftUI의 선언적 UI 구성을 가능하게 하는 핵심 요소죠. 여러 개의 View를 자연스럽게 조합하고, 조건부 렌더링과 반복 렌더링을 직관적으로 표현할 수 있게 해줍니다.

@ViewBuilder 없이는 불가능한 것들

@ViewBuilder가 없다면 SwiftUI에서 다음과 같은 코드를 작성할 수 없을 것입니다.

// 이런 자연스러운 코드가 불가능했을 것
VStack {
    Text("제목")
    Text("부제목")
    
    if user.isLoggedIn {
        Text("환영합니다, \(user.name)님!")
        Button("로그아웃") {
            logout()
        }
    } else {
        Button("로그인") {
            showLoginSheet = true
        }
    }
    
    ForEach(items) { item in
        ItemRow(item: item)
    }
}

대신 모든 것을 명시적으로 컨테이너에 감싸야 했을 것입니다.

// @ViewBuilder 없이 작성해야 했을 코드
VStack {
    TupleView((
        Text("제목"),
        Text("부제목"),
        Group {
            if user.isLoggedIn {
                VStack {
                    Text("환영합니다, \(user.name)님!")
                    Button("로그아웃") {
                        logout()
                    }
                }
            } else {
                Button("로그인") {
                    showLoginSheet = true
                }
            }
        },
        ForEach(items) { item in
            ItemRow(item: item)
        }
    ))
}

@ViewBuilder의 내부 동작

@ViewBuilder가 어떻게 작동하는지 좀 더 자세히 살펴보겠습니다.

// 우리가 작성하는 코드
VStack {
    Text("첫 번째")
    Text("두 번째")
    if showThird {
        Text("세 번째")
    }
}

이 코드는 컴파일 타임에 다음과 같이 변환됩니다:

// 실제 변환된 코드
VStack {
    ViewBuilder.buildBlock(
        Text("첫 번째"),
        Text("두 번째"),
        ViewBuilder.buildOptional(showThird ? Text("세 번째") : nil)
    )
}

ViewBuilder의 핵심 메서드들

SwiftUI의 ViewBuilder는 다양한 상황을 처리하기 위한 메서드들을 제공합니다.

@resultBuilder
public struct ViewBuilder {
    // 단일 View 처리
    public static func buildBlock<Content>(_ content: Content) -> Content 
        where Content : View
    
    // 두 개 View 조합
    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> 
        where C0 : View, C1 : View
    
    // 세 개 View 조합 (최대 10개까지 오버로드됨)
    public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> 
        where C0 : View, C1 : View, C2 : View
    
    // 조건부 View (옵셔널)
    public static func buildOptional<Content>(_ content: Content?) -> Content? 
        where Content : View
    
    // if-else 문 처리
    public static func buildEither<TrueContent, FalseContent>(first content: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> 
        where TrueContent : View, FalseContent : View
    
    public static func buildEither<TrueContent, FalseContent>(second content: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> 
        where TrueContent : View, FalseContent : View
    
    // 배열/반복문 처리
    public static func buildArray<Content>(_ components: [Content]) -> ForEach<Range<Int>, Int, Content> 
        where Content : View
}

 

결론

DSL(Domain Specific Language)은 특정 도메인의 문제를 그 도메인의 언어로 자연스럽게 표현할 수 있게 해주는 강력한 도구입니다. SwiftUI는 UI 구성이라는 도메인에 특화된 훌륭한 DSL의 예시이며, 이를 통해 개발자는 더 직관적이고 선언적인 방식으로 사용자 인터페이스를 구성할 수 있습니다.

Swift의 @resultBuilder는 이러한 DSL을 언어 내부에서 구현할 수 있게 해주는 메타 프로그래밍 도구입니다. 컴파일 타임에 자연스러운 문법을 복잡한 메서드 호출로 변환하여, 마치 새로운 언어를 사용하는 것 같은 경험을 제공합니다.

@ViewBuilder는 SwiftUI DSL의 핵심 엔진으로, 여러 View의 조합, 조건부 렌더링, 반복 렌더링 등을 자연스럽게 처리할 수 있게 해줍니다. 이를 통해 복잡한 UI 로직도 매우 직관적이고 읽기 쉬운 코드로 표현할 수 있습니다.

실제 프로젝트에서는 @resultBuilder를 활용하여 다양한 도메인별 DSL을 만들 수 있습니다. 레이아웃 구성, 폼 검증, 네트워크 요청 체이닝, 상태 관리 등 각각의 도메인에 특화된 DSL을 만들면 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다.

DSL의 핵심은 "해당 도메인의 전문가가 자연스럽게 이해할 수 있는 언어"를 만드는 것입니다. 이를 통해 코드는 단순한 명령어의 나열이 아닌, 도메인 지식을 표현하는 언어가 됩니다. SwiftUI와 @resultBuilder는 이러한 철학을 Swift 생태계에서 실현할 수 있게 해주는 훌륭한 도구들입니다.

앞으로 iOS 개발을 할 때, 단순히 SwiftUI의 기능을 사용하는 것을 넘어서 DSL의 관점에서 접근해보시기 바랍니다. 여러분만의 도메인별 DSL을 만들어 보고, 팀 내에서 공통으로 사용할 수 있는 선언적 API를 설계해보세요. 이를 통해 더 표현력 있고 유지보수하기 쉬운 코드를 작성할 수 있을 것입니다.