🔥 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()
}
}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()
}
}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!**" 출력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!**" 출력- 이 코드는 동작하지만 약간 어색해 보여요.
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
}
}@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
}
}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!**" 출력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!**" 출력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
}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는
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)
}
}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)
}
}- 위의 코드에서
for루프는 그림의 배열을 생성하고,buildArray(_:)메서드는 해당 배열을Line으로 변환합니다.
Swift가 Builder 구문을 Builder 타입의 메서드 호출로 변환하는 방법의 전체 목록은 resultBuilder를 참조하세요.
Result Builder를 활용하면 우리는 중첩된 데이터를 선언적이고 읽기 쉬운 방식으로 생성할 수 있어요. 우리는 조건부 로직과 반복을 포함하여 자연스러운 Swift 구문을 사용할 수 있죠. Result Builder는 도메인 특화 언어(DSL)를 만드는 강력한 도구이며, 코드의 의도를 명확하게 표현할 수 있게 해줍니다.











