# PropertyVisitor를 사용하여 프로퍼티 방문자 생성

> Follow this example to create a property visitor with the PropertyVisitor base class.

이 예시는 [`PropertyVisitor`](/engine/6000.0/script-reference/unity-properties/property-visitor.md) 기본 클래스를 사용하여 프로퍼티 방문자를 생성하는 방법을 보여 줍니다. [`IPropertyBagVisitor`](/engine/6000.0/script-reference/unity-properties/i-property-bag-visitor.md) 및 [`IPropertyVisitor`](/engine/6000.0/script-reference/unity-properties/i-property-visitor.md) 인터페이스를 사용하는 동일한 예시는 [로우레벨 API를 사용하여 프로퍼티 방문자 생성](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-visitors-low-level-api.md)을 참고하십시오.

## 개요 예시

이 예시에는 오브젝트의 현재 상태를 콘솔에 출력하는 프로퍼티 방문자를 만드는 단계별 명령이 포함되어 있습니다.

다음 유형이 있다고 가정해 봅시다.

```lang-csharp
public class Data
{
    public string Name = "Henry";
    public Vector2 Vec2 = Vector2.one;
    public List<Color> Colors = new List<Color> { Color.green, Color.red };
    public Dictionary<int, string> Dict = new Dictionary<int, string> {{5, "zero"}};
}
```

다음과 같이 유틸리티 메서드 `DebugUtilities`를 생성합니다.

```lang-csharp
public static class DebugUtilities
{
    public static void PrintObjectDump<T>(T value)
    {
        // Magic goes here.
    }
}
```

다음과 같이 `Data` 오브젝트로 `PrintObjectDump` 메서드를 호출합니다.

```lang-csharp
DebugUtilities.PrintObjectDump(new Data());
```

그러면 콘솔에 다음이 출력됩니다.

```lang-text
- Name {string} = Henry
- Vec2 {Vector2} = (1.00, 1.00)
- Colors {List<Color>}
  - [0] = {Color} RGBA(0.000, 1.000, 0.000, 1.000)
  - [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000)
- Dict {Dictionary<int, string>}
  - [5] {KeyValuePair<int, string>}
    - Key {int} = 5
    - Value {string} = five
```

## 방문자 생성

먼저 `DumpObjectVisitor` 클래스를 생성합니다. 클래스 내에서 [StringBuilder](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder?view=net-7.0)를 사용하여 오브젝트의 현재 상태를 나타내는 문자열을 만듭니다.

1. [`PropertyVisitor`](/engine/6000.0/script-reference/unity-properties/property-visitor.md)에서 상속하는 `DumpObjectVisitor` 클래스를 생성합니다.

2. 클래스에 `StringBuilder` 필드를 추가합니다.

3. `StringBuilder`를 지우고 들여쓰기 레벨을 재설정하는 `Reset` 메서드를 추가합니다.

4. 오브젝트의 현재 상태를 나타내는 문자열을 반환하는 `GetDump` 메서드를 추가합니다.

   완료된 클래스는 다음과 같습니다.

   ```lang-csharp
   // `PropertyVisitor` is an abstract class that you must subclass from it. 
   public class DumpObjectVisitor: PropertyVisitor
   {
       private const int k_InitialIndent = 0;
       private readonly StringBuilder m_Builder = new StringBuilder();
       
       private int m_IndentLevel = k_InitialIndent;
       
       private string Indent => new (' ', m_IndentLevel * 2);
       
       public void Reset()
       {
           m_Builder.Clear();
           m_IndentLevel = k_InitialIndent;
       }

       public string GetDump()
       {
           return m_Builder.ToString();
       }
   }
   ```

## 최상위 프로퍼티 가져오기

`DumpObjectVisitor` 클래스 내에서 [`VisitProperty`](/engine/6000.0/script-reference/unity-properties/property-visitor/visit-property.md) 메서드를 오버라이드하여 오브젝트의 각 프로퍼티를 방문하고 프로퍼티 이름을 기록합니다. [`PropertyVisitor`](/engine/6000.0/script-reference/unity-properties/property-visitor.md)에는 구현할 구성원이 필요하지 않으며 기본적으로 모든 프로퍼티를 방문하기만 하고 아무런 작업도 수행하지 않습니다.

1. `DumpObjectVisitor` 클래스 내에 다음 오버라이드 `VisitProperty` 메서드를 추가합니다.

   ```lang-csharp
   protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
   {
       m_Builder.AppendLine($"- {property.Name}");
   }
   ```

2. 이제 방문자가 거의 없으므로 유틸리티 메서드를 구현할 수 있습니다. `DebugUtilities` 클래스의 `PrintObjectDump` 메서드를 업데이트하여 새 `DumpObjectVisitor` 인스턴스를 생성하고 이를 사용하여 특정 오브젝트의 프로퍼티를 방문합니다.

   ```lang-csharp
   public static class DebugUtilities
   {
       private static readonly DumpObjectVisitor s_Visitor = new ();
       
       public static void PrintObjectDump<T>(T value)
       {
           s_Visitor.Reset();
           
           // This is the main entry point to run a visitor.
           PropertyContainer.Accept(s_Visitor, ref value);
           Debug.Log(s_Visitor.GetDump());
       }
   }
   ```

   그러면 다음과 같은 결과가 출력됩니다.

   ```lang-text
   - Name
   - Vec2
   - Colors
   - Dict
   ```

## 하위 프로퍼티 가져오기

이전 섹션의 출력은 [`VisitProperty`](/engine/6000.0/script-reference/unity-properties/property-visitor/visit-property.md) 메서드를 오버라이드할 때 오브젝트의 하위 프로퍼티를 자동으로 방문하지 않음을 나타냅니다. 하위 프로퍼티를 가져오려면 [`PropertyContainer.Accept`](/engine/6000.0/script-reference/unity-properties/property-container/accept.md) 메서드를 사용하여 각 값에 방문자를 재귀적으로 적용합니다.

* `DebugUtilities` 클래스 내에서 `VisitProperty` 메서드를 업데이트하여 중첩할 값에 방문자가 재귀적으로 적용합니다.

  ```lang-csharp
  protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
  {
      m_Builder.AppendLine($"{Indent}- {property.Name}");
      
      ++m_IdentLevel;
      // Apply this visitor recursively on the value to nest in.
      if (null != value)
          PropertyContainer.Accept(this, ref value);
      --m_IdentLevel;
  }
  ```

  그러면 다음과 같은 결과가 출력됩니다.

  ```lang-text
  - Name
  - Vec2
  - x
  - y
  - Colors
  - 0
      - r
      - g
      - b
      - a
  - 1
      - r
      - g
      - b
      - a 
  - Dict
  - 5
      - Key
      - Value
  ```

## 각 프로퍼티에 대한 세부 정보 표시

다음으로 컬렉션 항목의 프로퍼티 이름과 각 프로퍼티의 유형 및 값을 가져오겠습니다.

특정 프로퍼티는 특별한 이름을 가지며 특히 컬렉션 항목을 처리할 때 그렇습니다. 다음은 프로퍼티 이름에 대한 규칙입니다.

* 리스트 항목의 경우 이름은 인덱스에 해당합니다.
* 딕셔너리의 경우 이름은 키 값의 문자열 버전에서 파생됩니다.
* 세트의 경우 이름은 값의 문자열 버전에 기반합니다.

이를 더 명시적으로 구별하려면 프로퍼티 이름을 대괄호로 묶으십시오.

1. `DumpObjectVisitor` 클래스 내에 다음 메서드를 추가합니다.

   ```lang-csharp
   private static string GetPropertyName(IProperty property)
   {
       return property switch
       {
           // You can also treat `IListElementProperty`, `IDictionaryElementProperty`, and `ISetElementProperty` separately.
           ICollectionElementProperty => $"[{property.Name}]",
           _ => property.Name
       };
   }
   ```

2. [`TypeUtility.GetTypeDisplayName`](/engine/6000.0/script-reference/unity-properties/type-utility/get-type-display-name.md)을 사용하여 특정 유형의 표시 이름을 가져오도록 `VisitProperty` 메서드를 업데이트합니다.

   ```lang-csharp
   protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
   {
       var propertyName = GetPropertyName(property);
       
       // Get the concrete type of the property or its declared type if value is null.
       var typeName = TypeUtility.GetTypeDisplayName(value?.GetType() ?? property.DeclaredValueType());
       
       m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
       
       ++m_IndentLevel;
       if (null != value)
           PropertyContainer.Accept(this, ref value);
       --m_IndentLevel;
   }
   ```

   그러면 다음과 같은 결과가 출력됩니다.

   ```lang-text
   - Name = {string} Henry
   - Vec2 = {Vector2} (1.00, 1.00)
   - x = {float} 1
   - y = {float} 1
   - Colors = {List<Color>} System.Collections.Generic.List`1[UnityEngine.Color]
   - [1] = {Color} RGBA(0.000, 1.000, 0.000, 1.000)
       - r = {float} 0
       - g = {float} 1
       - b = {float} 0
       - a = {float} 1
   - [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000)
       - r = {float} 1
       - g = {float} 0
       - b = {float} 0
       - a = {float} 1
   - Dict = {Dictionary<int, string>} System.Collections.Generic.Dictionary`2[System.Int32,System.String]
   - [5] = {KeyValuePair<int, string>} [5, five]
       - Key = {int} 5
       - Value = {string} five
   ```

## 컬렉션 유형에 표시되는 정보의 양 줄이기

`List<T>`가 `ToString()` 메서드를 오버라이드하지 않으므로 리스트 값이 `System.Collections.Generic.List1[UnityEngine.Color]`로 표시됩니다. 표시되는 정보의 양을 줄이려면 [`TypeTraits.IsContainer`](/engine/6000.0/script-reference/unity-properties/type-traits/is-container.md) 유틸리티 메서드를 사용하여 프리미티브, 열거형, 문자열과 같이 하위 프로퍼티가 포함되지 않는 유형에 대한 값만 표시하도록 `VisitProperty`를 업데이트하십시오.

`DumpObjectVisitor` 클래스 내에서 [`TypeTraits.IsContainer`](/engine/6000.0/script-reference/unity-properties/type-traits/is-container.md)를 사용하여 값이 컨테이너 유형인지 확인하도록 `VisitProperty` 메서드를 업데이트합니다. 컨테이너 유형인 경우 값 없이 유형 이름을 표시합니다. 그렇지 않은 경우 유형 이름과 값을 표시합니다.

```lang-csharp
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
    var propertyName = GetPropertyName(property);

    var type = value?.GetType() ?? property.DeclaredValueType();
    var typeName = TypeUtility.GetTypeDisplayName(type);
    
    // Only display the values for primitives, enums and strings.
    if (TypeTraits.IsContainer(type))
        m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}");
    else
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
    
    ++m_IndentLevel;
    if (null != value)
        PropertyContainer.Accept(this, ref value);
    --m_IndentLevel;
}
```

그러면 다음과 같은 결과가 출력됩니다.

```lang-text
- Name = {string} Henry
- Vec2 {Vector2}
- x = {float} 1
- y = {float} 1
- Colors {List<Color>}
- [0] {Color}
    - r = {float} 0
    - g = {float} 1
    - b = {float} 0
    - a = {float} 1
- [1] {Color}
    - r = {float} 1
    - g = {float} 0
    - b = {float} 0
    - a = {float} 1
- Dict {Dictionary<int, string>}
- [5] {KeyValuePair<int, string>}
    - Key = {int} 5
    - Value = {string} five
```

**팁**:

표시되는 정보의 양을 줄이기 위해 다음 메서드를 사용하여 컬렉션 유형의 `Visit` 전문화를 오버라이드할 수도 있습니다.

```lang-csharp
protected override void VisitCollection<TContainer, TCollection, TElement>(Property<TContainer, TCollection> property, ref TContainer container, ref TCollection value) {}
protected override void VisitList<TContainer, TList, TElement>(Property<TContainer, TList> property, ref TContainer container, ref TList value) {}
protected override void VisitDictionary<TContainer, TDictionary, TKey, TValue>(Property<TContainer, TDictionary> property, ref TContainer container, ref TDictionary value) {}
protected override void VisitSet<TContainer, TSet, TValue>(Property<TContainer, TSet> property, ref TContainer container, ref TSet value) {}
```

이들은 [`VisitProperty`](/engine/6000.0/script-reference/unity-properties/property-visitor/visit-property.md) 메서드와 유사하지만 각 컬렉션 유형의 일반 파라미터를 노출합니다.

## 유형별 오버라이드 추가

마지막으로 유형별 오버라이드를 추가하여 더 컴팩트한 방식으로 [`Vector2`](/engine/6000.0/script-reference/unity-engine/vector-2.md) 및 [`Color`](/engine/6000.0/script-reference/unity-engine/color.md) 유형을 표시합니다.

[`IVisitPropertyAdapter`](/engine/6000.0/script-reference/unity-properties/i-visit-property-adapter.md)와 함께 [`PropertyVisitor`](/engine/6000.0/script-reference/unity-properties/property-visitor.md)를 사용하십시오. 특정 유형에 어댑터가 등록될 때마다 방문 중에 타겟 유형이 발견되면 [`VisitProperty`](/engine/6000.0/script-reference/unity-properties/property-visitor/visit-property.md) 메서드 대신 어댑터가 호출됩니다.

`DumpObjectVisitor` 클래스 내에 `Vector2` 및 `Color`의 `IVisitPropertyAdapter`를 추가합니다.

```lang-csharp
public class DumpObjectVisitor
    : PropertyVisitor
    , IVisitPropertyAdapter<Vector2>
    , IVisitPropertyAdapter<Color>
{
    public DumpObjectVisitor()
    {
        AddAdapter(this);
    }
    
    void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}");
    }

    void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}");
    }
}
```

완료된 `DumpObjectVisitor` 클래스는 다음과 같습니다.

```lang-csharp
public class DumpObjectVisitor
: PropertyVisitor
, IVisitPropertyAdapter<Vector2>
, IVisitPropertyAdapter<Color> 
{
    private const int k_InitialIndent = 0;
    
    // StringBuilder to store the dumped object's properties and values.
    private readonly StringBuilder m_Builder = new StringBuilder();
    private int m_IndentLevel = k_InitialIndent;
    
    // Helper property to get the current indentation.
    private string Indent => new (' ', m_IndentLevel * 2);

    public DumpObjectVisitor()
    {
        // Constructor, it initializes the DumpObjectVisitor and adds itself as an adapter
        // to handle properties of type Vector2 and Color.
        AddAdapter(this);
    }
    
    // Reset the visitor, clearing the StringBuilder and setting indentation to initial level.
    public void Reset()
    {
        m_Builder.Clear();
        m_IndentLevel = k_InitialIndent;
    }

    // Get the string representation of the dumped object.
    public string GetDump()
    {
        return m_Builder.ToString();
    }

    // Helper method to get the property name, handling collections and other property types.
    private static string GetPropertyName(IProperty property)
    {
        return property switch
        {
            // If it's a collection element property, display it with brackets
            ICollectionElementProperty => $"[{property.Name}]",
            // For other property types, display the name as it is
            _ => property.Name
        };
    }

    // This method is called when visiting each property of an object.
    // It determines the type of the value and formats it accordingly for display.
    protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
    {
        var propertyName = GetPropertyName(property);

        // Get the type of the value or property.
        var type = value?.GetType() ?? property.DeclaredValueType();
        var typeName = TypeUtility.GetTypeDisplayName(type);
        
        // Only display the values for primitives, enums, and strings, and treat other types as containers.
        if (TypeTraits.IsContainer(type))
            m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}");
        else
            m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
        
        // Increase indentation level before visiting child properties (if any).
        ++m_IndentLevel;
        if (null != value)
            PropertyContainer.Accept(this, ref value);
        // Decrease indentation level after visiting child properties.
        --m_IndentLevel;
    }

    // This method is a specialized override for Vector2 properties.
    // It displays the property name and its value as a Vector2.
    void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}");
    }

    // This method is a specialized override for Color properties.
    // It displays the property name and its value as a Color.
    void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}");
    }
}
```

## 하위 프로퍼티의 현재 상태 인쇄

기본적으로 데이터에서 방문자를 실행하면 해당 오브젝트에 대한 방문이 직접 시작됩니다. 모든 프로퍼티 방문자에 대해 오브젝트의 하위 등록 정보에 대한 방문을 시작하려면 [`PropertyContainer.Accept`](/engine/6000.0/script-reference/unity-properties/property-container/accept.md.md) 메서드에 [`PropertyPath`](/engine/6000.0/script-reference/unity-properties/property-path.md.md)를 전달합니다.

1. `DebugUtilities` 메서드를 업데이트하여 선택적 `PropertyPath`를 사용합니다.

   ```lang-csharp
   public static class DebugUtilities
   {
       private static readonly DumpObjectVisitor s_Visitor = new();

       public static void PrintObjectDump<T>(T value, PropertyPath path = default)
       {
           s_Visitor.Reset();
           if (path.IsEmpty)
               PropertyContainer.Accept(s_Visitor, ref value);
           else
               PropertyContainer.Accept(s_Visitor, ref value, path);
           Debug.Log(s_Visitor.GetDump());
       }
   }
   ```

2. `Data` 오브젝트로 `PrintObjectDump` 메서드를 호출합니다. 이를 통해 [원하는 출력](#desired-output)을 얻습니다.

## 추가 리소스

* [프로퍼티 방문자](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-visitors.md)
* [프로퍼티 백](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-bags.md)
* [프로퍼티 경로](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-paths.md)
* [로우레벨 API를 사용하여 프로퍼티 방문자 생성](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-visitors-low-level-api.md)
