Swift

Mirror

GREEN.1229 2023. 5. 16. 02:54

안녕하세요. 그린입니다🍏

이번 포스팅에서는 Swift의 Mirror라는 타입에 대해 알아보겠습니다🙌

 

이전 debugPrint와 print를 포스팅하다 dump에 대해서 알아보게 되었는데요.

여기서 dump는 reflecting으로 오늘 알아볼 Mirror라는 개념을 사용해 해당 정보를 가공해서 보여준다는걸 알아봤습니다.

그래서 여기서 Mirror라는것이 Swift에서는 뭔지 알아보겠습니다🕺🏻

(혹시 이전 포스팅을 못보셨어도 무방하지만 dump가 무엇인지 궁금하시면 사전에 보셔도 좋습니다!)

https://green1229.tistory.com/355

 

debugPrint와 print 알고쓰기 (feat. dump)

안녕하세요. 그린입니다🍏 이번 포스팅에서는 debugPrint와 print에 대해 알아보려 합니다 (조금 더 나아가서 간단히 dump까지!)🙋🏻 사실 많은 iOS 개발자라면 이미 익숙하고 차이도 잘 아실텐데 한

green1229.tistory.com

 

자 그럼 Mirror 뿌셔볼까요!?

 

Mirror?

Mirror는 모든 타입의 인스턴스에 대한 하위 구조, 구성이나 화면 표시 스타일의 표현입니다.

iOS 8.0 이상부터 사용할 수 있으며 기본적으로 Swift 언어에서 제공하는 스탠다드 라이브러리에 속한 기능입니다.

 

선언은 아래와 같아요.

struct Mirror

구조체 타입으로 단순합니다.

 

Mirror는 인스턴스의 저장 프로퍼티, 컬렉션, 튜플 요소, 활성화된 열거 케이스와 같은 특정 인스턴스를 구성하는 부분들을 설명하고 나타내줍니다.

 

또 위에서 Mirror는 모든 타입의 인스턴스에 대해 화면 표시 스타일의 표현이라고 했는데요.

실제로 Mirror는 렌더링되는 방식을 제안하는 "디스플레이 스타일" 속성을 제공해줍니다.

 

플레이 그라운드 및 디버거에서 Mirror 타입을 사용해 모든 타입의 값 표현을 표시할 수 있습니다.

예시로 dump 메서드를 볼 수 있는데요.

여기서 dump 메서드를 사용하면 실제로 해당 인스턴스의 런타임 콘텐츠를 렌더링하는데 Mirror가 사용된다고 합니다👍

그렇기에 dump는 Mirror를 사용한다고 볼 수 있습니다.

 

공식문서의 예제 코드를 한번 간단히 보시죠!

struct Point {
  let x: Int, y: Int
}

let p = Point(x: 21, y: 30)
print(String(reflecting: p))
// Prints "▿ Point
//           - x: 21
//           - y: 30"

커스텀한 Mirror 표현을 정의하려면 CustomReflectable 프로토콜을 채택하면 됩니다.

그럼 위와 같이 reflecting하여 인스턴스를 가공하여 표현할 수 있습니다.

 

또한, Mirror가 가지는 이니셜라이저는 아래와 같이 Subject, children, displayStyle, reflecting 등 다양한 인자를 통해 구성할 수 있습니다.

여기서 주로 사용되는 파라미터 요소들을 살펴볼까요?

- children: 반사된 Subject의 구조를 나타내는 하위 요소의 컬렉션입니다.

- displayStyle: 반사된 Subject에 대해 화면 표시 스타일을 제안하는 프로퍼티입니다.

- subjectType: 반사되는 Subject의 정적 타입입니다.

- superclassMirror: 존재하는 경우 Subject의 상위 클래스 Mirror입니다.

 

또한 enum 타입으로 아래와 같이 두가지를 가집니다.

- AncestorRepresentation: 상위 클래스에서 사용할 표현

- DisplayStyle: Mirror의 Subject가 어떻게 해석되어 보여져야 하는지에 대한 화면 표시 스타일의 제안입니다.

아래는 DisplayStyle의 케이스들로 다양하게 표현할 수 있습니다.

 

마지막으로 두가지의 타입 별칭이 사용되요🙋🏻

- Child: 반사된 인스턴스 구조의 요소입니다.

- Children: 하위 구조를 나타내는데 사용되는 타입입니다.

 

자 그럼 여기서 자꾸 반사된다는 개념이 나와요.

해석을 해서 그렇지 공식 문서에서는 reflecting으로 계속 표현하고 있습니다.

 

요게 핵심인것 같은데 바로 이어서 Mirror의 작동 방식을 보면서 알아보시죠😃

 

Mirror의 작동 방식

Swift에서 Reflecting을 사용하면 Mirror API를 사용하게 되고 런타임에 임의의 값을 검사하고 조작할 수 있습니다.

즉 Swift의 Reflection은 값에 대한 메타데이터 정보를 보여줍니다.

 

예를들어 아래와 같은 코드가 있다고 가정해볼까요?

// Person 타입
struct Person {
  let name: String
  let age: Int
}

let green = Person(name: "Green", age: 10)
let mirror = Mirror(reflecting: green)

mirror.children.forEach { child in
  print("\(child.label ?? "")is \(child.value)")
}

// Prints:
// name is Green
// age is 10

요렇게 Mirror를 사용하여 메타데이터에 대한 정보를 뽑을 수 있습니다.

근데 여기서 name, age는 다 저장 타입이죠?

중요한 사실은 reflection은 해당 타입에 존재하는 저장 프로퍼티 즉 stored property의 값만 읽을 수 있습니다.

 

그럼 한번 다른 값들을 넣고 실제 그러한지 봐볼까요?

 

// Person 타입
struct Person {
  let name: String
  let age: Int
  
  // computed property
  var gender: String { 
    return name == "Green" ? "male" : "female"
  }
  
  // type property
  static let color: String = "Green"
  
  // method
  func test() {
    print("test ok")
  }
}

let green = Person(name: "Green", age: 10)
let mirror = Mirror(reflecting: green)

mirror.children.forEach { child in
  print("\(child.label ?? "")is \(child.value)")
}

// Prints:
// name is Green
// age is 10

실제로 연산 프로퍼티, 타입 프로퍼티, 메서드들을 구성해서 추가해줬는데 프린트되어 보여지는건 저장 프로퍼티만인걸 볼 수 있죠😲

즉, 클래스든 구조체든 Mirror API를 사용하면 모든 저장된 프로퍼티가 반사됩니다.

 

튜플의 경우에도 마찬가지로 아래와 같이 반사됩니다.

let tuple = ("Green", 10)
Mirror(reflecting: tuple).children.forEach { child in
  print("\(child.label ?? "")is \(child.value)")
}
// Prints:
// .0 is Green
// .1 is 10

이 외 열거형의 표현도 마찬가지이며 열거형을 reflecting할때 중요한건 해당 케이스에 어떤 인자가 포함되면 값을 출력하지만 없다면 출력하지 않습니다.

만약 어떤 케이스인지 인자 값이 없더라도 알고 싶다면 이에 맞는 프로토콜을 정의하거나 별도 설정을 통해 구성해줄 수 있습니다!

 

자 요렇게 Mirror와 Reflection에 대해 알아봤어요.
마지막으로는 CustomReflectable 프로토콜에 대해 알아보고 사용해보시죠!

 

CustomReflectable?

앞서 간단히 설명한걸 돌이켜보면 Mirror를 명시적으로 커스텀할 수 있도록 도와주는 프로토콜입니다.

선언은 아래와 같아요.

protocol CustomReflectable

실제 이니셜라이저를 통해 모든 타입에 대해 Mirror를 생성할 수 있지만 기본 제공되는 형식이 원하지 않는 형식이고 조금 더 가공하거나 커스텀하게 표현하고 싶다면 해당 프로토콜을 준수하고 구현해낼 수 있습니다.

 

해당 프로토콜에서 사용할 수 있는 속성으로는 customMirror가 있습니다.

즉 요걸 이용해야 하는것이죠!

 

그럼 한번 이용해볼까요?

 

예시로 아래와 같이 배열을 가진 인스턴스가 있고 이를 Mirror 시키면 label은 존재하지 않으니 비게 나옵니다.

let people = ["Green", "Red", "Blue"]
Mirror(reflecting: people).children.forEach { child in
  print("\(child.label ?? "No Label")is \(child.value)")
}
// Prints:
// No Label is Green
// No Label is Red
// No Label is Blue

요렇게 말이죠.

 

근데 만약 레이블을 붙여주려면 우리는 우선 이에 해당하는 레이블을 붙인 구조체등 타입을 만들고 이를 Mirror 시켜줄 수 있었어요.

여기서 또 특정한 레이블만의 정보만 출력되는걸 원하면 아래와 같이 CustomReflectable 프로토콜을 채택해 사용할 수 있습니다.

extension Person: CustomReflectable {
  public var customMirror: Mirror {
    return Mirror(self, children: ["name": name])
  }
}

let green = Person(name: "Green", age: 10)
Mirror(reflecting: green).children.forEach { child in
  print("\(child.label ?? "") is \(child.value)")
}

// Before custom reflection:
// name is Green
// age is 10
//
// After custom reflection:
// name is Green

요렇게 선택적으로 필요한 레이블만을 처리할 수도 있습니다.

 

간단하쥬~?

 

마무리

Mirror API는 dump 메서드뿐만 아니라 Datadog 및 기타 에러 수집과 관련된 툴에 보고할때도 많이 사용되는것 같아요!

조금 개념이 생소해서 낯설게 느껴졌는데 막상 해보니 할만하네요😃

 

[참고 자료]

https://developer.apple.com/documentation/swift/mirror

 

Mirror | Apple Developer Documentation

A representation of the substructure and display style of an instance of any type.

developer.apple.com

https://www.avanderlee.com/swift/reflection-how-mirror-works/

 

Reflection in Swift: How Mirror works

Reflection in Swift using the Mirror API allows you to extract metadata for any value during runtime allowing for advanced solutions.

www.avanderlee.com