[Unity] CustomEditor/CustomPropertyDrawer - 인스펙터를 개조해보자
서론
유니티 인스펙터는 잘 만들었지만, 사용하다보면 불편한 점이 꽤 많다.
예를 들면, Dictionary는 에디터에 나오지 않는다던가.
조금 더 나아가면, 본인의 게임에 맞게 개조할 일도 생긴다. GUI로 스킬을 조립한다던지, 카드를 만든다던지 등등.
오늘은 그런 경우에 도움이 될, CustomEditor와 CustomPropertyDrawer에 대해 간단히 알아보자.
CustomEditor. 왜 써야 할까?
앞서 언급했듯, 유니티의 인스펙터는 가끔 불편한 점이 있다. 그중 대표적인 게 바로 Dictionary가 나오지 않는다는 것.
예를 들어, 아래 같은 상황이다.


이런 식으로 변수를 선언한 뒤 인스펙터로 가보면, 나머진 모두 잘 나오지만 Dictionary만큼은 나오지 않는다.
이제, CustomEditor를 써보며 어떻게 개조하는지 살펴보자.
CustomEditor란?
CustomEditor는, 유니티에서 제공하는 특정 스크립트의 Inspector를 개조하는 방법이다. 만드는 방법은 마치 안드로이드 프로그래밍과 유사한데, 간단한 예제들을 보며 맛만 보자.
CustomEditor 생성 및 코딩 방법
CustomEditor는 단순히, C# Script 생성을 통해 만들 수 있다.
이때 주의할 점은, CustomEditor가 정상적으로 작동하려면 Editor 폴더 내에 만들어야 한다는 것이다.
Assets 폴더 바로 밑에 Editor 폴더가 있을 필욘 없고, 스크립트가 든 폴더 이름이 Editor이기만 하면 된다.

간단히 아래처럼 코딩을 해보았다.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(CustomEditorExample))]
public class CustomEditorTest : Editor
{
public override void OnInspectorGUI()
{
EditorGUI.LabelField(GUILayoutUtility.GetRect(0, 20), "레이블 만들기");
Debug.Log("커스텀 에디터 생성 완료");
}
}
Custom Editor를 만들려면, Editor 클래스를 상속받아야 한다. 이 안에 많은 함수들이 이미 작성되어 있으며, 우린 그걸 override해서 쓰면 된다.
가장 먼저, CustomEditor Attribute를 추가해야 한다. 이 뒤엔 커스텀 에디터를 적용할 클래스의 타입을 적어준다.
이제 가장 기본이 되는 OnInspectorGUI() 함수를 override 해준다. 이 함수는 Inspector를 주목 중일 때 계속 실행되는, 커스텀 에디터 계의 Update 문이라 보면 된다.
기본 작동 원리를 알기 위해 LabelField를 하나 추가했다. 위치는 Layout의 현재 위치에서 높이 20짜리 사각형, 내용은 "레이블 만들기"라고 적었다. 밑엔 Debug도 하나 추가해줬다.


그럼 이렇게, 내 변수들이 모두 날아간 것을 알 수 있다!
오른쪽 사진에서 알 수 있듯, 내가 해당 클래스의 Inspector를 보고 있으면 디버그가 계속 출력된다.
그런데, 우리가 항상 처음부터 만들고 싶진 않을 것이다. 기존 변수들은 원래대로 띄워주고, 거기에 추가 작업을 하고 싶을 땐 어떻게 해야 할까?
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(CustomEditorExample))]
public class CustomEditorTest : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUI.LabelField(GUILayoutUtility.GetRect(0, 20), "레이블 만들기");
}
}

그럴 땐 base의 OnInspectorGUI를 먼저 그리면 된다. 보다시피 기존 변수들이 나타나고, 그 밑에 내가 만든 레이블이 나타난다.
조금 더 나아가면 base.OnInspectorGUI()의 호출 위치를 조정해 위/아래에 원하는 UI를 추가할 수도 있다.

여기서 유추할 수 있겠지만, Editor.OnInspectorGUI는 virtual 함수다. 내용은 단순.
당연히 base.OnInspectorGUI() 대신 DrawDefaultInspector()를 호출해도 된다.
몇 번 더 타고 들어가면 유니티가 기본 인스펙터를 어떻게 만들었는지도 알 수 있으니, 관심이 있다면 한번 찾아보자.
변수를 가져오는 방법
우린 인스펙터를 어떤 목적으로 쓰는가? 당연히, 변수에 값을 설정하기 위해 쓰고 있다. 우린 인스펙터로 함수는 건드리지 않는다.
그렇다면, 당연히 기본적으론 나타나지 않는 변수를 인스펙터에 띄워내는 게, 우리의 목표가 되지 않겠는가?
먼저, 간단히 값을 가져오는 예제를 살펴보자.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(CustomEditorExample))]
public class CustomEditorTest : Editor
{
private SerializedProperty stringExample;
string myString = "텍스트 입력";
private void OnEnable()
{
stringExample = serializedObject.FindProperty("stringExample");
}
public override void OnInspectorGUI()
{
EditorGUILayout.PropertyField(stringExample);
myString = EditorGUILayout.TextField("문자열", myString);
}
}
SerializedProperty로 변수를 선언하고, serializedObject.FindProperty로 변수(필드, 프로퍼티)를 가져온다. 여기서 serializedObject는 gameObject처럼 클래스의 이미 할당된 변수로, 현재 내가 주목 중인(inspected) 컴포넌트를 뜻한다.
OnInspectorGUI에선 2가지 방법으로 텍스트 필드를 만들었다.
PropertyField는 해당 프로퍼티의 기본값으로 띄워주는 함수고,
TextField는 그저 텍스트 입력 필드를 생성해주는 함수다.

그 결과, 윗줄엔 변수의 이름과 입력 필드가 나타났고, 아랫줄엔 내가 작성한 이름과 입력 필드가 나타났다.
전자의 경우는, 기본값이 없는 프로퍼티는 나타나지 않는 문제가 있다. 그리고 후자는, 사실 변수를 띄우는 용도라기보단 인스펙터에 입력 필드를 띄우는 것으로, 변수와는 무관하다.
그렇다면, 우리가 개조하다보면 입력 필드는 필드대로 만들고, 그 값을 클래스의 변수에 저장하고 싶어지지 않곘는가? 그럴 땐 어떻게 해야 할까?
public override void OnInspectorGUI()
{
EditorGUILayout.PropertyField(stringExample);
myString = EditorGUILayout.TextField("문자열", myString);
stringExample.stringValue = myString;
serializedObject.ApplyModifiedProperties();
}
그럴 땐 이렇게, 프로퍼티의 Value에 값을 집어넣은 뒤, ApplyModifiedProperties()로 저장하면 된다.

눈치챘겠지만, SerializedProperty는 프로퍼티기에, 다양한 버전의 Value 값을 지원한다. 꼭 알맞은 타입에 값을 넣어 안전하고 행복한 코딩을 하자. 다른 밸류값을 넣으면 어떻게 될까?
하고 싶은 건 많지만
이외에도 버튼 만들고 내용 코딩하기, 특정 클래스의 하위 클래스를 버튼으로 선택하고, 선택된 내용에 따라 다른 변수들 띄우기 등 설명하고 싶은 건 많지만, 이번 글에선 간단히 맛만 볼 것이니 다른 블로그들을 참고하자. 좋은 블로그들이 많다.


이번 시간엔 간단하면서도 모두의 염원인, Dictionary 띄우기로 마무리 지어보려 한다.
코드는 없지만 결과는 보여준다.

CustomEditor를 이용하면, 위처럼 Dictionary를 에디터에 띄울 수도 있다! 더이상 Dictionary가 직렬화되지 않는다며 List를 따로 만들고, 매칭하고, 에디터에서 불편하게 값을 넣을 필요가 없는 것이다.
이렇게 CustomEditor를 잘 이용하면, 우리의 개발 속도를 몇 배나 향상시킬 수 있다.
이번엔 코드가 왜 없냐면, 블로그용 예제를 짜려고 GPT를 돌려 코드를 짜서 그렇다. 내가 짠 코드가 아니다보니 막상 짜고 보니 블로그에 배포하기엔 미묘했다.
만약 코드가 필요하다면 GPT에게 의뢰하고, 대신 여기선 CustomEditor의 한계를 살펴보겠다.
1. 예기치 못한 에디터 에러가 발생할 수 있다.
만약 당신이 직렬화 이해도가 낮고, Custom Editor를 깊게 공부해보지 않았다면 콘솔 창이 온갖 에러와 워닝으로 가득찰 것이다. Editor 폴더 내 스크립트들은 Editor가 실행되는 중 동작하는데, 그러다보니 게임 모드를 켜지 않아도 상시로 코드가 돌아간다.
만약, 코드를 잘못 짜서 Stack Overflow가 발생한다거나, 무한 루프에 빠진다면... 뒷일은 상상에 맡기겠다.
물론, 요즘 GPT는 그런 에러까지 처리해달라면 처리해주지만, 그래도 사용자가 공부해서 원리를 이해하는 게 가장 중요하다. 각 함수들이 어떤 역할을 하고, 어떤 원리로 동작하는지 이해해서, 우리의 생산성만 향상시키자.
2. Generic을 지원하지 않는다.
커스텀 에디터는 클래스를 대상으로 하는데, 안타깝게도 이때 제네릭 클래스는 대상으로 지정할 수 없다.
그래서 사실, 위 예제도 String - Int Dictionary만 콕 집어서 구현한 거고, 다른 타입의 Dictionary는 구현하지 못한다.
3. 클래스(컴포넌트) 단위로만 구성할 수 있다.
이건 커스텀 에디터의 약점 중 하나로, 클래스 단위로 적용된다는 점이다.
만약 당신이, 다른 건 다 놔두고 Dictionary만 에디터에 띄우고 싶다면 어떨까? 시뮬레이션을 돌려보면 알겠지만, Dictionary가 선언된 모든 클래스에 커스텀 에디터를 제작해야 한다. 이는 매우 번거로운 작업이고, 그 과정에서 실수하면 바로 에디터 폭파다.
다행히, 유니티는 이런 단점을 해결하기 위해 CustomPropertyDrawer를 제공한다. 이는 클래스가 인스펙터에 나타날 때를 디자인하는 게 아닌, 특정 프로퍼티가 인스펙터에 나타날 때를 디자인하게 해준다.
즉, 위 예제에서 커스텀 에디터는 CustomEditorExample 스크립트 전체에 대해 작동하지만, 커스텀 프로퍼티 드로어는 CustomEditorExample 내부 Dictionary 클래스에만 한정하여 띄울 수 있는 것이다.
CustomPropertyDrawer를 사용해보자!
이번에도 간단한 예제와 함께 CustomPropertyDrawer를 맛보고, Dictionary 예제를 보여주며 마무리 짓겠다. 기존 예제와 별개로 코드도 안 까는 Dictionary 예제를 보여주는 건, 내가 이걸 몰랐을 때 가장 크게 고통받았던 부분이다보니 이런 것도 된다!를 보여주고 싶어서다.
우리만의 클래스를 만들어보자.
게임 개발 중엔 우리에게 필요한 클래스를 만들고, 또 그걸 변수로 삼는 일이 정말 빈번하다. 그러나, Serializable을 붙인다한들 유니티에서 기본 제공해주는 타입만 작성이 가능하고, 위처럼 제공해주지 않는 변수들은 어쩔 도리가 없다.
using System.Collections.Generic;
[System.Serializable]
public class CustomProperty
{
public string myName;
public Dictionary<string, string> myDictionary;
}
간단히 클래스를 만들어봤다. [System.Serializable] 어트리뷰트가 뭔지 모른다면, 아래 글이 도움이 될 것이다.
[Unity] 직렬화(Serialization)를 이해해보자
서론유니티로 개발하다보면, 알기 싫어도 알게 되고, 또 모르면 가끔 크게 데이는 것들이 있다. 그중 하나가 바로 직렬화다. 이번 시간엔 앞서 예고했듯, 직렬화란 무엇인지 차근차근 이해해보
autumncat.tistory.com
이제, 기존 CustomEditorExample을 고쳐보자.
using System.Collections.Generic;
using UnityEngine;
public class CustomEditorExample : MonoBehaviour
{
public int intExample = 0;
public string stringExample = "Test";
public CustomProperty customProperty;
public Dictionary<string, int> dictionaryExample = new Dictionary<string, int> { { "Test", 1 } };
}
Dictionary는 물론, 방금 만든 따끈따끈한 클래스도 변수로 받았다!
이 상황에서, 커스텀 에디터도, 커스텀 프로퍼티 드로어도 없으면 어떻게 될까?

정답은 위와 같이, Dictionary만 도려낸 듯 나타나지 않는다. 우리가 만든 클래스는 접혀서 나타나고, 그 중 직렬화 가능한 기본형은 바로 표시된다.
여기에, CustomPropertyDrawer를 살짝 얹어보자. 먼저, CustomProperty 클래스에 대해 살짝 건드려보겠다.
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(CustomProperty))]
public class CustomPropertyDrawerTest : PropertyDrawer
{
string myString;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
base.OnGUI(position, property, label);
myString = EditorGUILayout.TextField(myString);
}
}
CustomPropertyDrawer는 CustomEditor와 유사하다. 아니, 유사하다 못해 사실상 거의 똑같다. EditorGUI나 EditorGUILayout 클래스는 공통으로 사용되는데, 즉 우린 내부를 꾸밀 땐 동일한 방법으로 꾸민다는 거다.
차이점이라곤 Editor 대신 PropertyDrawer를 상속받는 것, Attribute 이름이 다른 것, 함수 이름이 다른 것 정도다.
다만, OnInspectorGUI와 달리 OnGUI엔 매개변수가 있는데, 이는 커스텀 에디터는 인스펙터 전체가 기준이었지만, 커스텀 프로퍼티 드로어는 인스펙터 중 변수를 띄울 공간이 한정되어 있기 때문이다. property, label 역시 비슷한 맥락으로, 자기 자신을 할당하고, 자신의 이름을 할당한 것이다.
여튼, 위처럼 base 함수를 유지하고 TextField를 두면 어떻게 될까?

위에서 똑같다고 한 게 무색하게, 에러가 뜬다.
CustomPropertyDrawer는 CustomEditor와 다르게, 적용하면 이 프로퍼티에 관한 건 내가 모두 책임지고 그린다는 의미가 된다. 그래서 base를 호출하면 위와 같은 에러가 나타나고, 내가 작성한 텍스트 필드는 잘 작동한다.

좋다. 그럼 우리가 전부 책임을 지겠다. 하지만 기본형은 기본형대로 띄우고 싶다. 그럼 어떻게 해야 할까?
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(CustomProperty))]
public class CustomPropertyDrawerTest : PropertyDrawer
{
string myString;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.PropertyField(position, property, label, true);
myString = EditorGUILayout.TextField("텍스트 입력", myString);
}
}
바로 이렇게, base.OnGUI를 쓰는 대신 EditorGUI.PropertyField로 자기 자신을 띄워줘야 한다. 그러면 우리가 지정한 클래스에서, 기본형의 프로퍼티가 잘 나타난다.

그렇다. 잘 나타난다. 값 자체는 말이다.
우리가 이 악마의 스크립트를 쓰는 순간, 유니티는 아무 일도 하지 않는다. 그것이 설령, 프로퍼티 출력 후 다음칸까지 띄워주는 일일 지라도...
CustomEditor 땐 이정도야 지원해줬으니 몰랐겠지만, 우린 변수를 인스펙터에 표시할 때 어떤 위치에, 어떤 크기로, 얼마만큼의 공간을 차지해서 띄울지 일일이 적어줘야만 한다. 그게 우리가 무심코 지나쳤던 Rect의 존재 의의다.
그럼 이제 어떡하지? 우린 우리의 꿈을 포기해야 하는 걸까?
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(CustomProperty))]
public class CustomPropertyDrawerTest : PropertyDrawer
{
string myString;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.PropertyField(position, property, label, true);
myString = EditorGUILayout.TextField("텍스트 입력", myString);
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, true);
}
}
아니다. 아직 포기하긴 이르다. 위치를 우리가 적어줘야 한다면, 적어주면 될 일이다.
이 작업을 하려면 GetPropertyHeight 함수를 오버라이딩 해주기만 하면 된다. 쓸데없이 이상한데서 친절하다. 다 해주던가.
위처럼, EditorGUI.GetPropertyHeight 함수로 PropertyDrawer.GetPropertyHeight를 대신해주면...!

드디어, 우리가 원하는 대로 기본형은 기본형대로, 추가형은 추가형대로 인스펙터에 띄울 수 있게 되었다.
물론 텍스트 필드는 CustomProperty에 속하지 않아 Indent를 걸어줘야 하는 함정이 있지만, 자세한 건 따로 공부해보자.
이런 식으로, 인스펙터 중에서도 특정 프로퍼티에만 내 마음대로 띄우는 것도 할 수 있다.

그럼 대체 이걸 왜 쓰는 건데?
이렇게 모든 걸 다 설정해줘야하는, 공부의 끝이 가늠도 안 되는 이런 스크립트를, 그럼 왜 써야 할까?
CustomEditor의 단점과도, 앞서 말한 CustomPropertyDrawer의 존재 의의와도 상통하지만, 특정 프로퍼티들을 전부 띄울 수 있기 때문이다.
하나를 작성하는 게 좀 까다롭긴 하지만, Dictionary를 추가할 때마다 새로 작성하고, 없애면 지워야하는 CustomEditor와, Dictionary 타입에 지정해두면 어떤 스크립트에서 사용하든 자동으로 인스펙터에 띄워주는 CustomPropertyDrawer. 당연히 후자가 낫지 않겠는가. 게다가, CustomPropertyDrawer는 제네릭 타입도 지원해준다!

GPT를 잘 괴롭히고, 그 코드들을 이해해 이상한 점을 찾을 수 있다면, 이런 식으로 천추의 한을 풀 수도 있다.
그러니 어려워도... 배워야겠지?
마무리
오늘은 CustomEditor와 CustomPropertyDrawer에 대해 맛만 봤다. 이외에도 Lighting 탭 같은 Window 추가하기, 다양한 어트리뷰트 적용하기, 에디터를 변수 출력이 아닌 게임급으로 만들어서 에디터로 코딩하기 등 발전 가능성은 무궁무진하다. 만약 당신이 에셋스토어를 구경하다 Inspector에서 신기하게 사용하는 것들을 보았다면, 아마 Custom Editor를 사용한 게 아닐까?
물론 이걸 다 만들고 있는 게 어려울 수도 있다. 그런 사람들을 위해 Odin Inspector가 진짜 사용하기 편하게 나왔다는데, 나는 유니티를 좀 더 파헤치기 위해라고 쓰고 돈이 없어서라고 읽는다 직접 에디터를 개조하는 방향으로 찾아보았다.
실제로 현재 개발중인 게임에도 적용 중인데, 아직 더 공부할 것도 있고 갈 길이 멀지만 확실히 생산성은 증가했다고 본다.
여러분도 유니티가 제공해주는대로만 코딩하는 대신, 직접 뜯어가며 불편한 건 없애버리고 즐거운 코딩 라이프를 즐기자!
참고 자료
- Special Thanks to Chat-GPT
- Rito15, "유니티 - Custom Editor (커스텀 에디터)", GitHub io, 2021. 3. 13., https://rito15.github.io/posts/unity-editor-custom-editor/
- AlgorFati, "[Unity] 커스텀 에디터 제작 방법 (Editor Customize)", 티스토리, 2020. 6. 30., https://algorfati.tistory.com/23
- 원소랑, "[Unity] 커스텀 에디터 속성", 네이버 블로그, 2022. 11. 7., https://m.blog.naver.com/sorang226/222922764888
- surplusus, "Unity - CustomPropertyDrawer 사용하기", 2020. 2. 8., https://surplusus.tistory.com/125
- Vader87, "Custom Inspector GUI - PropertyDrawer", 티스토리, 2015. 11. 20., https://ukprog.tistory.com/1