🔥 Result Builder로 선언형 코드 작성하기
Swift에서 Result Builder
를 사용하면 리스트나 트리와 같은 중첩된 데이터를 자연스럽고 선언적인 방식으로 생성할 수 있습니다. Result Builder를 사용하면 if
문이나 for
문과 같은 일반적인 Swift 문법으로 조건부 또는 반복적인 데이터 조각을 처리할 수 있어요.
아래 코드는 별표와 텍스트를 사용하여 한 줄에 그리는 몇 가지 타입을 정의합니다.
protocol Drawable { func draw() -> String } struct Line: Drawable { var elements: [Drawable] func draw() -> String { elements.map { $0.draw() }.joined(separator: "") } } struct Text: Drawable { var content: String init(_ content: String) { self.content = content } func draw() -> String { content } } struct Space: Drawable { func draw() -> String { " " } } struct Stars: Drawable { var length: Int func draw() -> String { String(repeating: "*", count: length) } } struct AllCaps: Drawable { var content: Drawable func draw() -> String { content.draw().uppercased() } }
swift
Drawable
프로토콜은 선이나 도형과 같이 그릴 수 있는 것에 대한 요구사항을 정의해요. 해당 타입은 반드시draw()
메서드를 구현해야 합니다.Line
구조체는 한 줄 그리기를 나타내며, 대부분의 그림에서 최상위 컨테이너 역할을 맡아요.Line
은 선의 각 구성 요소에서draw()
를 호출하고, 결과 문자열을 하나의 문자열로 연결합니다.Text
구조체는 문자열을 감싸 그림의 일부로 만들어요.AllCaps
구조체는 다른 그림을 감싸고 수정하여 그림의 모든 텍스트를 대문자로 변환해요.
우리는 이러한 타입의 이니셜라이저를 호출하여 그림을 만들 수 있어요:
let name: String? = "Ravi Patel" let manualDrawing = Line(elements: [ Stars(length: 3), Text("Hello"), Space(), AllCaps(content: Text((name ?? "World") + "!")), Stars(length: 2), ]) print(manualDrawing.draw()) // "***Hello RAVI PATEL!**" 출력
swift
- 이 코드는 동작하지만 약간 어색해 보여요.
AllCaps
다음에 깊게 중첩된 괄호는 읽기 어렵죠. name
이nil
일 때 "World"를 사용하는 대체 로직은??
연산자를 사용하여 인라인으로 처리해야 해서 더 복잡한 것은 어려워요.- 그림의 일부를 작성하기 위해
switch
문이나for
루프를 포함해야 하는 경우에는 수행할 방법이 없어 보여요.
Result Builder를 사용하면 우리는 이 코드를 일반 Swift 코드처럼 보이도록 다시 작성할 수 있어요.
Result Builder를 정의하려면 타입 선언에 @resultBuilder
속성을 작성하세요. 예를 들어, 아래 코드는 선언적인 구문을 사용하여 그림을 설명할 수 있는 DrawingBuilder
라는 Result Builder를 정의합니다:
@resultBuilder struct DrawingBuilder { static func buildBlock(_ components: Drawable...) -> Drawable { Line(elements: components) } static func buildEither(first: Drawable) -> Drawable { first } static func buildEither(second: Drawable) -> Drawable { second } }
swift
DrawingBuilder
구조체는 Result Builder 구문의 일부를 구현하는 세 가지 메서드를 정의해요.buildBlock(_:)
메서드는 코드 블록에서 일련의 줄을 작성할 수 있도록 지원해요. 이 메서드는 해당 블록의 구성 요소를Line
으로 결합합니다.buildEither(first:)
와buildEither(second:)
메서드는if``else
를 지원해요.
우리는 함수의 매개변수에 @DrawingBuilder
속성을 적용하여 해당 함수에 전달된 클로저를 Result Builder가 해당 클로저에서 생성한 값으로 변환할 수 있어요. 예를 들면:
func draw(@DrawingBuilder content: () -> Drawable) -> Drawable { content() } func caps(@DrawingBuilder content: () -> Drawable) -> Drawable { AllCaps(content: content()) } func makeGreeting(for name: String? = nil) -> Drawable { let greeting = draw { Stars(length: 3) Text("Hello") Space() caps { if let name = name { Text(name + "!") } else { Text("World!") } } Stars(length: 2) } return greeting } let genericGreeting = makeGreeting() print(genericGreeting.draw()) // "***Hello WORLD!**" 출력 let personalGreeting = makeGreeting(for: "Ravi Patel") print(personalGreeting.draw()) // "***Hello RAVI PATEL!**" 출력
swift
makeGreeting(for:)
함수는name
매개변수를 받아 개인화된 인사말을 그려요.draw(_:)
와caps(_:)
함수는 모두@DrawingBuilder
속성으로 표시된 단일 클로저를 인자로 받아요.- 우리는 이러한 함수를 호출할 때
DrawingBuilder
에서 정의한 특수 구문을 사용해요. - Swift는 그림에 대한 선언적 설명을
DrawingBuilder
의 메서드에 대한 일련의 호출로 변환하여 함수 인자로 전달되는 값을 작성합니다.
예를 들어, Swift는 해당 예제에서 caps(_:)
에 대한 호출을 다음과 같은 코드로 변환해요:
let capsDrawing = caps { let partialDrawing: Drawable if let name = name { let text = Text(name + "!") partialDrawing = DrawingBuilder.buildEither(first: text) } else { let text = Text("World!") partialDrawing = DrawingBuilder.buildEither(second: text) } return partialDrawing }
swift
- Swift는
if``else
블록을buildEither(first:)
와buildEither(second:)
메서드에 대한 호출로 변환해요. - 우리는 자신의 코드에서 이러한 메서드를 직접 호출하지는 않지만, 변환 결과를 보면
DrawingBuilder
구문을 사용할 때 Swift가 코드를 어떻게 변환하는지 더 쉽게 확인할 수 있어요.
특수한 그리기 구문에서 for
루프를 작성할 수 있도록 지원을 추가하려면 buildArray(_:)
메서드를 추가하세요.
extension DrawingBuilder { static func buildArray(_ components: [Drawable]) -> Drawable { Line(elements: components) } } let manyStars = draw { Text("Stars:") for length in 1...3 { Space() Stars(length: length) } }
swift
- 위의 코드에서
for
루프는 그림의 배열을 생성하고,buildArray(_:)
메서드는 해당 배열을Line
으로 변환합니다.
Swift가 Builder 구문을 Builder 타입의 메서드 호출로 변환하는 방법의 전체 목록은 resultBuilder를 참조하세요.
Result Builder를 활용하면 우리는 중첩된 데이터를 선언적이고 읽기 쉬운 방식으로 생성할 수 있어요. 우리는 조건부 로직과 반복을 포함하여 자연스러운 Swift 구문을 사용할 수 있죠. Result Builder는 도메인 특화 언어(DSL)를 만드는 강력한 도구이며, 코드의 의도를 명확하게 표현할 수 있게 해줍니다.