Version: 2023.2
언어: 한국어
C# 스크립트에서 런타임 바인딩 생성
바인딩 모드 및 업데이트 트리거 정의

런타임 바인딩을 위한 데이터 소스 정의

바인딩 오브젝트를 생성할 때는 데이터 소스를 정의해야 합니다. 데이터 소스는 바인딩하려는 프로퍼티가 포함된 오브젝트입니다. 모든 C# 오브젝트를 런타임 바인딩 데이터 소스로 사용할 수 있습니다.

바인딩 시스템이 데이터 소스에 액세스할 수 있도록 하려면 데이터 소스 오브젝트에 바인딩 오브젝트의 dataSource 프로퍼티를 정의해야 합니다. 예를 들어 다음과 같은 데이터 소스 오브젝트와 UI 요소가 있다고 가정해 보겠습니다.

using UnityEngine;
using UnityEngine.UIElements;
using Unity.Properties;

public class DataSource
{
    public Vector3 vector3 { get; set; } 
}

var element = new VisualElement();

그런 다음 아래와 같이 element.dataSource 프로퍼티를 데이터 소스 오브젝트에 정의할 수 있습니다.

element.dataSource = new DataSource();

이렇게 하면 요소에 적용된 바인딩이 DataSource 오브젝트에 액세스할 수 있습니다.

요소에 적용된 바인딩이 DataSource 오브젝트의 vector3 필드에 액세스할 수 있게 하려면 다음을 추가합니다.

element.dataSourcePath = PropertyPath.FromName(nameof(DataSource.vector3));

자식 요소에 적용된 바인딩이 DataSource 오브젝트의 vector3 필드에 액세스할 수 있게 하려면 다음을 추가합니다.

var child = new VisualElement();
child.dataSourcePath = PropertyPath.FromName(nameof(DataSource.vector3));
element.Add(child)

프로퍼티 백

UI 툴킷은 Unity.Properties 모듈을 사용하여 두 오브젝트 간의 데이터 바인딩을 위한 프로퍼티 백을 생성합니다. 이는 사용 가능한 C# 타입 정보를 기반으로 프로퍼티 백을 생성합니다. 그러나 특정 빌트인 Unity 타입의 경우 예상하는 프로퍼티가 생성된 프로퍼티 백에 포함되지 않을 수 있습니다. 이는 이러한 타입에 필요한 속성이 부족한 경우에 발생할 수 있습니다. 예를 들어 Rect 타입에 [SerializeField]로 어트리뷰트되지 않은 공용 프로퍼티와 프라이빗 필드가 있거나, 네이티브 측에서 필드를 정의한 경우(런타임 시 확인할 수 없음)입니다.

참고: 값 타입을 데이터 소스로 사용하는 경우, VisualElement.dataSource가 오브젝트 프로퍼티로 정의되기 때문에 박싱 비용이 발생합니다. 따라서 dataSource 프로퍼티에 할당하기 전에 값 타입을 박싱해야 합니다. 박싱 작업에는 메모리 할당 및 복사의 오버헤드가 발생하므로 성능 비용이 듭니다. 소규모 데이터 세트나 가끔 사용하는 경우에는 이러한 데이터 영향이 크지 않을 수 있습니다. 그러나 성능이 중요한 시나리오나 대량의 데이터를 처리하는 경우에는 박싱 비용이 문제가 될 수 있습니다.

런타임 바인딩 및 저작이나 직렬화 목적으로 데이터 소스를 정의하려면 아래와 같은 일반적인 패턴을 사용합니다.

[!code-cs[(Modules/UIElements/Tests/UIElementsExamples/Assets/ui-toolkit-manual-code-examples/runtime-data-binding/DataSource-common.cs)]

참고: 바인딩할 수 있는 이러한 프로퍼티는 본질적으로 다형성 특성을 갖습니다.

버전 관리 및 변경 트래킹 통합

성능 향상을 위해 바인딩 데이터 소스에 버전 관리 및 변경 트래킹을 통합할 수 있습니다. 기본적으로 바인딩 시스템은 데이터 소스를 지속적으로 폴링하고 모든 수정 사항에 대해 UI를 업데이트합니다. 이때 마지막 업데이트 이후 실제로 변경된 사항이 있는지는 파악하지 않습니다. 이 접근 방식은 간단한 프로젝트에는 편리하지만, 수많은 바인딩을 처리하는 경우에는 효율적으로 확장되지 않습니다.

소스에 대한 버전 관리 및 변경 트래킹은 신중하게 활성화해야 하는 선택적 기능입니다. 기본적으로 활성 바인딩 오브젝트는 프레임마다 업데이트되므로 리소스가 많이 드는 프로세스가 될 수 있습니다. 처리 오버헤드를 최소화하려면 두 가지 인터페이스를 구현하여 소스와 연결된 바인딩을 업데이트할 시점을 바인딩 시스템에 알릴 수 있습니다.

  • IDataSourceViewHashProvider 인터페이스는 소스에 연결된 모든 바인딩을 업데이트할 시점을 나타내는 뷰 해시 코드를 제공합니다.
  • INotifyBindablePropertyChanged 인터페이스를 사용하면 프로퍼티별 변경 알림을 통해 수정된 프로퍼티에 관한 개별 바인딩에 대해서만 업데이트를 트리거할 수 있습니다.

이러한 인터페이스를 개별적으로 또는 함께 구현하여 더 효과적으로 제어할 수 있습니다.

참고: 현재 두 인터페이스 중 하나를 구현하는 타입은 어셈블리가 [assembly: Unity.Properties.GeneratePropertyBagsForAssembly]로 태그될 경우 코드 생성을 자동으로 옵트인합니다. 하지만 이 동작은 변경될 수 있습니다.

IDataSourceViewHashProvider 구현

특정 소스에 대한 뷰 해시 코드를 제공하려면 IDataSourceViewHashProvider 인터페이스를 구현합니다. 이 인터페이스를 사용하면 소스가 마지막 업데이트 이후 변경되지 않은 경우 바인딩 시스템이 특정 바인딩 오브젝트의 업데이트를 건너뛸 수 있습니다.

다음 예시에서는 변경 사항을 즉시 보고하는 데이터 소스를 생성합니다.

using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider
{
    public int intValue;
    public float floatValue;

    // Determines if the data source has changed. If the hash code is different, then the data source
    // has changed and the bindings are updated.
    public long  GetViewHashCode()
    {
        return HashCode.Combine(intValue, floatValue);
    }
}

The IDataSourceViewHashProvider interface also buffers changes. 이 버퍼링 기능은 데이터가 자주 변경되지만 UI가 모든 변경 사항을 즉시 반영할 필요가 없는 경우에 특히 유용합니다.

변경 사항을 버퍼링하려면 IDataSourceViewHashProvider 인터페이스를 구현하고 데이터 소스가 변경된 것을 바인딩 시스템에 알리고자 할 때 CommitChanges 메서드를 호출합니다.

기본적으로 바인딩 시스템은 데이터 소스의 버전이 변경되지 않은 경우 바인딩 오브젝트를 업데이트하지 않습니다. 그러나 바인딩 오브젝트의 MarkDirty 메서드를 호출하거나 updateTriggerBindingUpdateTrigger.EveryFrame으로 설정하면 버전이 변경되지 않은 경우에도 바인딩 오브젝트가 계속 업데이트될 수 있습니다. IDataSourceViewHashProvider를 사용하여 변경 사항을 버퍼링하는 경우, 목록에서 항목을 추가 또는 제거하거나 하위 필드 또는 하위 프로퍼티의 타입을 변경하는 등의 소스 내 구조적 변경을 피해야 합니다.

다음 예시에서는 변경 사항을 버퍼링하는 데이터 소스를 생성합니다.

using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider
{
    private long m_Version;

    public int intValue;
    
    public void CommitChanges()
    {
        ++m_Version;
    }
    
    // Required by IDataSourceViewHashProvider
    public long  GetViewHashCode()
    {
        return m_Version;
    }
}

INotifyBindablePropertyChanged 구현

특정 프로퍼티 변경에 대해 바인딩 시스템에 알리려면 INotifyBindablePropertyChanged 인터페이스를 구현합니다. 이 인터페이스를 구현하면 바인딩 시스템은 프로퍼티 경로에서 변경이 감지될 때 관련 바인딩만 업데이트합니다. 예를 들어, MyAwesomeObject 프로퍼티에 대한 변경이 감지되면 바인딩 시스템은 MyAwesomeObject 접두사가 있는 데이터 소스 경로와 연결된 모든 바인딩을 업데이트합니다. 소스에 연결된 다른 바인딩 오브젝트는 영향을 받지 않습니다.

이 접근 방식은 바인딩 시스템이 최소한의 작업만 수행하므로 UI를 매우 효율적으로 업데이트할 수 있습니다.

다음 예시에서는 프로퍼티별로 변경 사항을 알리는 데이터 소스를 생성합니다.

using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine.UIElements;

public class DataSource : INotifyBindablePropertyChanged
{
    private int m_Value;
    
    // Required by INotifyBindablePropertyChanged
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;

    [CreateProperty]
    public int value
    {
        get => m_Value;
        set
        {
            if (m_Value == value)
                return;

            m_Value = value;
            Notify();
        }
    }

    void Notify([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
}

참고: INotifyBindablePropertyChanged 인터페이스를 구현하면 바인딩 시스템이 변경 알림을 받을 때 검사를 수행하지 않습니다. 변경을 보고하지 않으면 바인딩 시스템이 해당 프로퍼티와 관련된 바인딩을 업데이트하지 않는 것입니다. 따라서 필요한 경우에만 변경 사항을 보고해야 합니다.

IDataSourceViewHashProviderINotifyBindablePropertyChanged 구현

최적의 바인딩 성능을 얻으려면 IDataSourceViewHashProviderINotifyBindablePropertyChanged 인터페이스를 모두 구현합니다. 바인딩 시스템은 뷰의 해시 코드가 변경될 때까지 변경된 프로퍼티를 추적합니다. 이 시점에서는 변경된 프로퍼티에 연결되어 있으며 영향을 받는 바인딩만 효율적으로 업데이트합니다.

이렇게 하면 보일러플레이트 코드가 추가로 필요하지만 유연성 및 성능 이점을 극대화할 수 있습니다.

다음 예시에서는 두 인터페이스를 모두 구현하는 데이터 소스를 생성합니다. 데이터 소스는 변경이 발생하면 바인딩 시스템에 알립니다. 그러나 바인딩을 즉시 업데이트하는 대신 Publish() 메서드가 호출될 때까지 업데이트가 보류됩니다. 이 접근 방식은 매 프레임마다 UI를 업데이트하면 성능 비용이 발생하는 변동성이 큰 데이터를 처리할 때 특히 유용합니다.

using System;
using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider, INotifyBindablePropertyChanged
{
    private long m_ViewVersion;
    private int m_Value;
    private int m_OtherValue;
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
    [CreateProperty]
    public int value
    {
        get => m_Value;
        set
        {
            if (m_Value == value)
                return;
            m_Value = value;
            Notify();
        }
    }
    [CreateProperty]
    public int otherValue
    {
        get => m_OtherValue;
        set
        {
            if (m_OtherValue == value)
                return;
            m_OtherValue = value;
            Notify();
        }
    }
    public void Publish()
    {
        ++m_ViewVersion;
    }
    public long GetViewHashCode()
    {
        return m_ViewVersion;
    }
    void Notify([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
}

베스트 프랙티스

다음 팁과 베스트 프랙티스를 준수하여 성능을 최적화합니다.

  • 바인딩 가능한 프로퍼티에 C# 프로퍼티 사용: 바인딩 가능한 프로퍼티를 정의할 때는 필드 대신 C# 프로퍼티를 사용합니다. 이렇게 하면 확인, 알림 또는 커스텀 동작을 유연하게 통합할 수 있어 더욱 강력하고 유지 관리하기 쉬운 코드를 만들 수 있습니다.

  • C# 프로퍼티에서 대규모 계산 피하기: 프로퍼티에서 상당한 양의 처리를 필요로 하는 경우, 필요할 때만 계산을 수행하고 후속 바인딩에는 캐시된 값을 사용합니다.

  • 불필요한 알림 피하기: 값에 실질적인 변경이 없는 경우 변경 사항을 알리는 것에 주의하십시오. 값이 동일하게 유지되는 경우 알림을 보낼 필요가 없습니다.

  • 버전 관리 및 변경 트래킹 구현: 데이터 소스에서 버전 관리를 사용합니다. 최적의 성능을 위해 버전 관리와 변경 트래킹을 모두 사용합니다.

  • 데이터 소스를 데이터와 UI 사이의 버퍼로 사용: 가능하면 데이터를 직접 사용하는 대신 데이터 소스를 데이터와 UI 사이의 매개체로 구현합니다. 이 접근 방식은 몇 가지 이점을 제공합니다.

  • 데이터 플로를 더 효과적으로 제어하고 UI에서 발생하는 변경 사항을 쉽게 트래킹할 수 있습니다. 데이터가 업데이트되는 시기와 방법을 관리할 수 있습니다.

  • 모든 UI 데이터를 한곳에 중앙집중화하여 데이터 액세스를 간소화하고 애플리케이션 전반의 복잡성을 줄입니다.

  • 원본 데이터의 깔끔함과 효율성을 유지하여 타입에 대한 추가 계측의 필요성을 없애고 데이터 무결성을 보장합니다.

제한 사항 이해

다음 섹션에서는 런타임 바인딩 데이터 소스의 알려진 제한 사항을 간략히 소개합니다.

정적 타입

정적 타입은 데이터 소스로 사용할 수 없습니다. 시스템이 작동하려면 해당 타입의 인스턴스를 생성해야 합니다.

메서드

타입에 대해 생성된 프로퍼티 백은 필드와 프로퍼티만 고려합니다. 따라서 메서드나 빌트인 이벤트에 바인딩할 수 없습니다.

그러나 Action 또는 Func 델리게이트 타입과 같은 델리게이트에는 바인딩할 수 있습니다. 델리게이트 필드 또는 프로퍼티에 바인딩하려면 += 또는 -= 대신 = 연산자를 사용합니다. 델리게이트를 할당하는 대신 추가하거나 제거해야 하는 경우 커스텀 바인딩 타입을 구현해야 할 수 있습니다.

인터페이스

정적 타입 섹션에서 언급한 것처럼 데이터 소스에 대한 오브젝트 인스턴스를 생성해야 합니다. 바인딩 시스템은 인터페이스와 함께 작동하지만, [CreateProperty]로 태그된 프로퍼티가 있는 인터페이스를 구현하는 타입에 대해서는 바인딩 가능한 프로퍼티가 자동으로 생성되지 않습니다. 각 타입에 대해 필드와 프로퍼티에 각각 태그를 지정하여 바인딩 가능하도록 해야 합니다. 이 제한 사항은 향후 릴리스에서 해결될 예정입니다.

빌트인 컴포넌트 및 오브젝트

C#의 프로퍼티 백 생성 프로세스는 주로 사용자 정의 타입과 함께 작동하도록 설계되었습니다. 따라서 현재 Unity의 빌트인 컴포넌트 및 오브젝트에 대한 지원은 제한적입니다. 여기에는 빌트인 타입의 필드가 네이티브 코드에 정의되어 있거나, 엔진에서 직렬화를 명시적으로 처리하거나, [SerializeField] 속성이 없는 등 다양한 요인이 있습니다. 그러나 사용자 정의 컴포넌트 및 스크립트 가능한 오브젝트의 필드와 프로퍼티는 예상대로 작동합니다.

이 제한 사항은 향후 릴리스에서 해결될 예정입니다. 그 동안에는 두 가지 해결 방법을 사용할 수 있습니다.

  • 빌트인 기본 클래스에서 필드 또는 프로퍼티를 노출하려면 자체 클래스에 private 프로퍼티를 추가하여 바인딩 시스템에 노출합니다.
  • Transform과 같은 빌트인 타입에서 필드 또는 프로퍼티를 사용하려면 필요한 프로퍼티를 노출하는 래퍼 타입을 생성합니다.

추가 리소스

C# 스크립트에서 런타임 바인딩 생성
바인딩 모드 및 업데이트 트리거 정의