https://developer.apple.com/videos/play/wwdc2023/10164/?time=574
Swift 5.9에서 새롭게 나온 개념
assert(max(a,b) == c)
는 조건이 거짓이면 프로그램을 중지시키지만, 정확히 무엇이 잘못되었는지에 대한 정보를 거의 얻지 못한다.
디버거에 로깅 추가하거나, 프로그램을 trap 해야 한다.
이걸 개선하면?
XCAssertEqual(max(a, b), c)
적어도 두 값이 같지 않는다는건 알 수 있다.
하지만 둘 중 어떤값이 문제인지는 알 수 없다.
max(a, b)인지 c인지
뭐가 잘못된것인지?
다시 assert로 돌아가서...
우린 assert가 실패했을 때 알고싶은 정보가 너무 많다.
a,b,c의 값은 무엇이고 Max는 어떤걸 리턴했는지?
이런 정보를 얻기위해 사용자 정의 기능없이는 개선을 못했는데,
매크로를 사용하면 가능해진다.
wwdc예시에서
#assert 구문은 assert라는 매크로를 확장한다.
이 매크로는 assert가 fail이 될 때 더 많은 경험을 제공할 수 있다.
이렇게.
매크로는 API이므로 매크로를 정의하는 모듈을 가져와 액세스할 수 있다.
패키지 형태로 배포된다.
assert 매크로의 경우 PowerAssert라는 패키지를 까보면 내부 코드를 볼 수 있는데,
이런식으로 되어있다.
Public macro assert(_ condition: Bool)
import PowerAssert
#assert(max(a, b) == c)
macro 키워드와 함께 써있지만, 그외에는 함수처럼 보인다.
매크로가 값을 생성하면, Result Type은 일반적인 화살표 구문으로 작성된다.
대부분의 매크로는 문자열을 통해 매크로 구현을 위한 모듈 및 type을 지정하는 "external macro"(외부 매크로)로 정의된다.
public macro assert(_ condition: Bool) = #externalMacro(
module: "PowerAssertPlugin",
type: "PowerAssertMacro"
)
external macro의 타입은 Compiler 플러그인 역할을 하는 "별도의 프로그램"에서 정의된다.
위 사진에서 매크로는 Assertion을 코드에 확장시켜 개별 값을 캡쳐하고, 소스코드에서 보여야할 곳에 위치시킨다.
매크로가 대신 보일러플레이트 코드를 작성해준다.
매크로 선언에는 하나의 추가정보인 "Macro Role"이 있다.
@freestanding(expression)
public macro assert(_ condition: Bool) = #externalMacro(
module: "PowerAssertPlugin",
type: "PowerAssertMacro"
)
여기서 assert 매크로는 freestanding(독립된) expression(식) 매크로 이다.
Foundation Predicate API는 expression 매크로의 훌륭한 예시 이다.
// Predicate expression macro
let pred = #Predicate<Person> {
$0.favoriteColor == .blue
}
let blueLovers = people.filter(pred)
predicate 매크로를 사용하면 클로저를 사용하여 type-safe한 방식으로 조건자(Predicate)를 작성할 수 있다.
predicate의 결과 값은 아주 많은 API와 함께 사용될 수 있다.
예를들어, Swift Collection 작업인 SwiftUI와 SwiftData를 포함해서
// Predicate expression macro
@freestanding(expression)
public macro Predicate<each Input>(
_ body: (repeat each Input) -> Bool
) -> Predicate<repeat each Input>
let pred = #Predicate<Person> {
$0.favoriteColor == .blue
}
let blueLovers = people.filter(pred)
macro 자체는 input type set(입력 유형 집합)에 대해 generic 하다.
(repeat each Input) -> Bool
는 밑에 예시의
{
$0.favoriteColor == .blue
}
해당 입력 유형의 값에 대해 작동하는 함수인 클로저 인수를 통해 Boolean 값을 반환한다.
input의 집합이 일치한가 아닌가에 대한 Boolean 값. (예시에서는 favoriteColor가 blu인지에 대한..)
-> Predicate<repeat each Input>
let pred = #Predicate<Person> { $0.favoriteColor == .blue }
그리고 App의 다른곳에서 사용할 수 있는 Predicate형 인스턴스를 반환한다.
그럼 이 인스턴스를 받아서..
let blueColorPred = #Predicate<Person> { $0.favoriteColor == .blue }
let bluePerson = personList.filter(blueColorPred)
요런식으로 filter Operation과 사용할 수 있음이다.
오호라!
애플이 매크로를 내놓은 이유는 보일러플레이트 코드를 매크로를 대체해서 코드를 줄이기 위함
// Testing for a specific enum case
enum Path {
case relative(String)
case absolute(String)
}
애플은 코드 내에서 열거형을 많이 사용한다는 것을 알게 되었다!
이런 Path enum이 있다고 할 때,
collection에서 absolute path만 필터링 해야하는 등의 특정 사례를 확인해야 하는 경우가 종종 있다.
어떻게 해야할까?
Path의 extension으로 특정 Case일 때만 filter를 걸 수 있게 Boolean 변수를 추가할 수 있다.
// Testing for a specific enum case
enum Path {
case relative(String)
case absolute(String)
}
let absPaths = paths.filter { $0.isAbsolute }
extension Path {
var isAbsolute: Bool {
if case .absoulte = self { true }
else { false }
}
}
extension Path {
var isAbsolute: Bool {
if case .absoulte = self { true }
else { false }
}
}
오우..하지만 case가 추가될 때 마다 보일러플레이트 코드가 늘어나게 된다.
extension Path {
var isAbsolute: Bool {
if case .absoulte = self { true }
else { false }
}
}
extension Path {
var isRelative: Bool {
if case .relative = self { true }
else { false }
}
}
이런식으로.
여기서 매크로를 사용해 보일러플레이트를 없앨 수 있다.
@CaseDetection
enum Path {
case relative(String)
case absolute(String)
}
let absPaths = paths.filter { $0.isAbsolute }
@CaseDetection은 프로퍼티 래퍼처럼 custom-attribute 구문을 사용하여 작성된. attached 매크로이다.
Attached Macro란 적용된 syntax 그 자체(여기서는 enum 자체)를 Input으로 사용하여 새 코드를 작성한다.
이 Macro-expanded 코드는 normal한 스위프트 코드이다.
컴파일러가 프로그램에 통합하는.
요 코드들을 마음대로 쓸 수 있다.
Attached Macro는 첨부된 declaratin(선언)을 확장하는 방법에 따라 5가지 역할로 분류할 수 있다.
final class Person: ObservableObject {
@Published var name: String
@Published var age: Int
@Published var isFavorite: Bool
}
struct ContentView: View {
@ObservedObject var person: Person
var body: some View {
Text("Hello, \(person.name)")
}
}
Observation은 SwiftUI 중 하나 이다.
클래스의 속성에 대한 변경 사항을 관찰하려면
class가 ObservableObject를 준수하도록 만들고 모든 Property를 Published로 마크하고, 뷰에서 ObservedObject wrappr을 사용하기만 하면 된다.
길다 길어!!
여기서 매크로를 사용한다면?
@Observable final class Person {
var name: String
var age: Int
var isFavorite: Bool
}
struct ContentView: View {
var persoon: Person
var body: some View {
Text("Hello, \(person.name)")
}
}
@Observable 매크로를 클래스에 연결하면 모든 StoredProperty를 관찰할 수 있습니다.
Observable 매크로는 3가지 macro roles로 작동한다.
@attached(member, names: ...)
@attached(memberAttribute)
@attached(conformance)
public macro Observable() = #externalMacro(...)
@Observable final class Person {
var name: String
var age: Int
var isFavorite: Bool
}
각 Macro Role은 Person 클래스가 Observable 매크로에 의해 보강되는 특정 방식에 해당합니다.
@Observable 매크로의 코드는 이런식으로 되어있고,
사실은
@Observable final class Person: Observable {
@ObservationTracked var name: String { get {...} set {...} }
@ObservationTracked var age: Int { get {...} set {...} }
@ObservationTracked var isFavorite: Bool { get {...} set {...} }
}
이런식으로 되어있는데, 이게 Observable 매크로 안에 다 접혀있는것이다..!
프로그램에 미치는 영향을 더 잘 이해하기 위해 매크로가 어떻게 확장되는지 확인해야 될 때 마다 XCode에서 바로 확인 가능