Game/Unity

종속성 분석 툴 제작 - 1

hvv_an 2025. 3. 24. 00:46

 

 

종속성 분석 툴 제작

설계나 리팩토링 과정에서 AI에게 코드를 분석해 달라고 요청했더니 클래스를 도식화하여 종속성을 나타내 보여주었는데 이러한 툴이 있다면 참 편할 것 같다고 생각하여 툴을 제작하려 한다.

언리얼 엔진의 경우에는 참조 그래프를 그려주는 내장 툴이 있는데 유니티는 그래프는 아니고 다음과 같이만 표시를 해줬다.

이걸로 어느 정도 알 수 있지만 클래스 간의 참조 관계를 알 수 없다.

어디에 사용되는지 정도만 표시되는 것 같다.

그래서 간단하게 스크립트를 지정하면 종속성을 그래프로 표현해 주는 툴을 제작하려 한다.


 

 

 

 

 

필요 기능 정리

해당 툴에 필요한 기능을 간단하게 정리해보자.

  • 에디터 윈도우
  • 스크립트 선택
  • 종속성 검사
  • 그래프 그리기

 

 

 

 

 

에디터 윈도우

해당 툴은 에디터에서 구조를 편하게 확인하려는 목적이기 때문에 EditorWindow를 구현하면 좋을 것 같다.

우선, EditorWindow를 상속받으면 유니티 에디터 상에서 사용할 수 있는 창을 만들 수 있다.

EditorWindow에는 OnGUI라는 함수가 있고 이를 오버라이딩하여 원하는 방식으로 창을 구성할 수 있다.

public class DependencyGraphTool : EditorWindow 
{
    [MenuItem("Tools/Dependency Graph")]
    public static void ShowWindow()
    {
        GetWindow<DependencyGraphTool>("Dependency Graph");
    }

    private void OnGUI()
    {
    }
}

또한, MenuItem이라는 속성으로 에디터 툴바에 메뉴 아이템을 추가할 수 있다.

그러면 위와 같이 설정한 경로에 맞게 메뉴 아이템이 추가된다.


 

 

 

 

 

에디터 GUI 구성

이제 에디터 창의 layout을 구성해 보자.

설계한 툴의 모습은 위와 같다.

왼쪽 부분에는 에셋을 선택할 수 있는 부분과 버튼들을 배치하였다.

오른쪽 부분에는 실제로 그래프가 그려지는 패널이다.

이를 구성하기 위해서는 OnGUI함수에서 레이아웃을 설정하고 내용과 기능을 채워 넣으면 된다.

우선, 패널부터 그려보자.

private void OnGUI()
{
    mainRect = new Rect(panelPadding, panelPadding, position.width - panelPadding * 2, position.height - panelPadding * 2);

    GUILayout.BeginHorizontal();
    
    // 왼쪽 패널
    GUILayout.BeginVertical(GUILayout.Width(300));
    DrawControlPanel();
    GUILayout.EndVertical();
    
    // 오른쪽 패널
    GUILayout.BeginVertical();
    DrawGraphView();
    GUILayout.EndVertical();
    
    GUILayout.EndHorizontal();
}

 GUILayout을 통해 레이아웃을 설정할 수 있다.

가로 뷰를 시작하고 왼쪽 패널과 오른쪽 패널을 그리게 하여 가로축으로 두 패널이 그려지도록 만들었다.

 

그럼 왼쪽 패널부터 그려보자.

우선 박스를 그려서 두 패널이 구분되면 좋겠어서 박스를 추가하였다.

private void DrawGraphView()
{
    rightPanelRect = new Rect( leftPanelRect.x + leftPanelWidth + panelSpacing, mainRect.y, mainRect.width - leftPanelWidth - panelSpacing, mainRect.height);
    GUI.Box(rightPanelRect, "");
    
    GUILayout.BeginArea(rightPanelRect);

    GUILayout.Label("오른쪽 패널", EditorStyles.boldLabel);

    GUILayout.EndArea();
    
}

private void DrawControlPanel()
{
    leftPanelRect = new Rect(mainRect.x, mainRect.y, leftPanelWidth, mainRect.height);
    GUI.Box(leftPanelRect, "");

    GUILayout.BeginArea(leftPanelRect);

    GUILayout.Label("왼쪽 패널", EditorStyles.boldLabel);

    GUILayout.EndArea();
}

Rectr를 이용하여 어디부터 어느 정도 크기를 그릴지 나타낼 수 있다.

그리고 패널 제목이 너무 패널과 붙어 있는 것 같으니 Content Rect를 추가하여 padding을 넣어보자. 

 private void DrawControlPanel()
{
    leftPanelRect = new Rect(mainRect.x, mainRect.y, leftPanelWidth, mainRect.height);
    leftContentRect = new Rect(leftPanelRect.x + contentPadding, leftPanelRect.y + contentPadding, leftPanelWidth - contentPadding, mainRect.height - contentPadding);
    GUI.Box(leftPanelRect, "");

    GUILayout.BeginArea(leftContentRect);

    GUILayout.Label("왼쪽 패널", EditorStyles.boldLabel);

    GUILayout.EndArea();
}

private void DrawGraphView()
{
    rightPanelRect = new Rect( leftPanelRect.x + leftPanelWidth + panelSpacing, mainRect.y, mainRect.width - leftPanelWidth - panelSpacing, mainRect.height);
    rightContentRect = new Rect(rightPanelRect.x + contentPadding, rightPanelRect.y + contentPadding, rightPanelRect.width - contentPadding, rightPanelRect.height - contentPadding);

    GUI.Box(rightPanelRect, "");
    
    GUILayout.BeginArea(rightContentRect);

    GUILayout.Label("오른쪽 패널", EditorStyles.boldLabel);

    GUILayout.EndArea();
    
}

이제 레이아웃을 얼추 해결했으니 내용을 채워보자.

기능은 제외하고 GUI 컴포넌트만 추가해 보자.

 

우선, 에셋을 선택할 수 있는 부분부터 만들어 보자.

에디터에서 List를 관리하는 부분의 UI가 훌륭하다고 생각하여 이를 응용하려 한다.

에디터에서 List를 나타내는 UI의 이름은 ReorderableList이다.

각 액션에 대해 리스너를 달아주고 이를 표시하면 된다.

private void InitializeAssetList()
{
    assetList = new ReorderableList(selectedAssets, typeof(string), true, true, true, true);
    
    // 헤더 설정
    assetList.drawHeaderCallback = (Rect rect) => {
        EditorGUI.LabelField(rect, "선택된 에셋");
    };
    
    // 요소 그리기 
    assetList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {

    };
    
    // 추가 버튼 이벤트
    assetList.onAddCallback = (ReorderableList list) => {

    };
    
    // 항목 제거 버튼 이벤트
    assetList.onRemoveCallback = (ReorderableList list) => {

    };
    
    // 선택 이벤트
    assetList.onSelectCallback = (ReorderableList list) => {

    };
}
private void DrawControlPanel()
{
    ...
    EditorGUILayout.LabelField("에셋 목록", EditorStyles.boldLabel);
    assetListScrollPosition = EditorGUILayout.BeginScrollView(assetListScrollPosition, GUILayout.Height(150));
    
    if (assetList != null)
    {
        assetList.DoLayoutList();
    }
    
    EditorGUILayout.EndScrollView();
    ...
}

ReorderableList.DoLayoutList()를 통해 창에 나타낼 수 있다.

에셋 목록이 잘 보인다.

편의를 위해 현재 선택된 에셋을 바로 넣을 수 있도록 버튼을 추가해 보자.

if (GUILayout.Button("현재 선택된 에셋 추가"))
{
}

 

이제 에셋 목록 아래에 그래프를 그릴 수 있게 하는 버튼을 추가해 보자.

해당 버튼은 선택된 에셋이 없다면 활성화되지 않아야 한다.

따라서, DisableGroup을 활용하여 제어하면 좋을 것 같다.

EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("분석 도구", EditorStyles.boldLabel);

EditorGUI.BeginDisabledGroup(selectedAssets.Count == 0);
if (GUILayout.Button("선택한 에셋 분석", GUILayout.Height(30)))
{
    // 여기에 분석 로직이 들어갑니다 (아직 구현하지 않음)
    Debug.Log("에셋 분석 시작 - 총 " + selectedAssets.Count + "개 에셋");
}
EditorGUI.EndDisabledGroup();

EditorGUILayout.EndVertical();

이 정도면 우선 컨트롤 패널의 레이아웃은 어느 정도 만들어진 것 같다.

 

이제 그래프 뷰를 그려보자.

그래프 뷰에는 상단에 뷰를 제어하는 버튼과 확대/축소 등 toolbar를 만들고 하단에는 그래프에 대한 정보를 표시하면 좋을 것 같다.

우선, 툴바는 다음과 같이 만들 수 있다.

EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
    
GUILayout.Button(EditorGUIUtility.IconContent("d_ToolHandleLocal"), EditorStyles.toolbarButton, GUILayout.Width(30));
GUILayout.Button(EditorGUIUtility.IconContent("d_GridAndSnap"), EditorStyles.toolbarButton, GUILayout.Width(30));
GUILayout.Button(EditorGUIUtility.IconContent("d_ToolHandlePivot"), EditorStyles.toolbarButton, GUILayout.Width(30));
    
GUILayout.FlexibleSpace();
    
EditorGUILayout.LabelField("확대/축소:", GUILayout.Width(60));
float zoom = GUILayout.HorizontalSlider(1.0f, 0.1f, 2.0f, GUILayout.Width(100));
    
GUILayout.Button("100%", EditorStyles.toolbarButton, GUILayout.Width(50));
    
EditorGUILayout.EndHorizontal();

툴바스타일의 Horizontal을 만들고 툴바 버튼을 추가한다.

이후, 오른쪽 끝으로 공간을 이동한 뒤 요소들을 추가한다.

 

그리고 실제로 그래프가 그려지는 부분은 다음과 같이 만들 수 있다.

Rect graphScrollArea = EditorGUILayout.GetControlRect(false, rightContentRect.height - 60);
GUI.Box(graphScrollArea, "", EditorStyles.helpBox);

그리고 여기에 scroll을 추가해 보자.

scrollPosition = GUI.BeginScrollView(graphScrollArea, scrollPosition, new Rect(0, 0, 2000, 2000));
GUI.EndScrollView();

마지막으로 하단에 그래프에 대한 정보를 표시해 보자.

// 하단 상태 표시줄
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);

GUILayout.Label("노드: 6개", EditorStyles.miniLabel);
GUILayout.Label("|", EditorStyles.miniLabel);
GUILayout.Label("연결: 5개", EditorStyles.miniLabel);

GUILayout.FlexibleSpace();

EditorGUILayout.EndHorizontal();


 

 

 

 

 

전체 코드

using System;
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using UnityEditorInternal;
using Object = UnityEngine.Object;

public class DependencyGraphTool : EditorWindow 
{
    private Vector2 scrollPosition;
    private Vector2 assetListScrollPosition;
    private List<string> selectedAssets = new List<string>();
    
    private Rect mainRect;
    private Rect leftPanelRect;
    private Rect rightPanelRect;
    private Rect leftContentRect;
    private Rect rightContentRect;
    
    private float leftPanelWidth = 240f;
    private float panelSpacing = 10f;
    private float panelPadding = 10f;
    private float contentPadding = 10f;
    
    private ReorderableList assetList;
    
    [MenuItem("Tools/Dependency Graph")]
    public static void ShowWindow()
    {
        GetWindow<DependencyGraphTool>("Dependency Graph");
    }

    private void OnEnable()
    {
        InitializeAssetList();
    }

    private void InitializeAssetList()
    {
        assetList = new ReorderableList(selectedAssets, typeof(string), true, true, true, true);
        
        // 헤더 설정
        assetList.drawHeaderCallback = (Rect rect) => {
            EditorGUI.LabelField(rect, "선택된 에셋");
        };
        
        // 요소 그리기 
        assetList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
        };
        
        // 추가 버튼 이벤트
        assetList.onAddCallback = (ReorderableList list) => {
        };
        
        // 항목 제거 버튼 이벤트
        assetList.onRemoveCallback = (ReorderableList list) => {
        };
        
        // 선택 이벤트
        assetList.onSelectCallback = (ReorderableList list) => {
        };
    }

    private void OnGUI()
    {
        mainRect = new Rect(panelPadding, panelPadding, position.width - panelPadding * 2, position.height - panelPadding * 2);
        
        GUILayout.BeginHorizontal();
        
        // 왼쪽 패널
        GUILayout.BeginVertical(GUILayout.Width(300));
        DrawControlPanel();
        GUILayout.EndVertical();
        
        // 오른쪽 패널
        GUILayout.BeginVertical();
        DrawGraphView();
        GUILayout.EndVertical();
        
        GUILayout.EndHorizontal();
    }
    
    private void DrawControlPanel()
    {
        leftPanelRect = new Rect(mainRect.x, mainRect.y, leftPanelWidth, mainRect.height);
        leftContentRect = new Rect(leftPanelRect.x + contentPadding, leftPanelRect.y + contentPadding, leftPanelWidth - contentPadding, mainRect.height - contentPadding);
        GUI.Box(leftPanelRect, "");
    
        GUILayout.BeginArea(leftContentRect);
        EditorGUILayout.BeginVertical(EditorStyles.helpBox);
        
        GUILayout.Label("에셋 종속성 분석", EditorStyles.boldLabel);
        
        GUILayout.Space(10);
        
        // 현재 선택된 에셋 추가 버튼
        if (GUILayout.Button("현재 선택된 에셋 추가"))
        {
            
        }
        
        GUILayout.Space(10);
        
        // 에셋 목록
        EditorGUILayout.LabelField("에셋 목록", EditorStyles.boldLabel);
        assetListScrollPosition = EditorGUILayout.BeginScrollView(assetListScrollPosition, GUILayout.Height(150));
        
        if (assetList != null) assetList.DoLayoutList();
        
        EditorGUILayout.EndScrollView();
        EditorGUILayout.EndVertical();
        
        GUILayout.Space(10);
        
        GUILayout.BeginVertical(EditorStyles.helpBox);

        GUILayout.Label("분석 도구", EditorStyles.boldLabel);
        
        EditorGUI.BeginDisabledGroup(selectedAssets.Count == 0);
        if (GUILayout.Button("선택한 에셋 분석", GUILayout.Height(30)))
        {
            Debug.Log("에셋 분석 시작 - 총 " + selectedAssets.Count + "개 에셋");
        }
        EditorGUI.EndDisabledGroup();
        
        GUILayout.EndVertical();
        
        GUILayout.EndArea();
    }

    private void DrawGraphView()
    {
        rightPanelRect = new Rect( leftPanelRect.x + leftPanelWidth + panelSpacing, mainRect.y, mainRect.width - leftPanelWidth - panelSpacing, mainRect.height);
        rightContentRect = new Rect(rightPanelRect.x + contentPadding, rightPanelRect.y + contentPadding, rightPanelRect.width - contentPadding, rightPanelRect.height - contentPadding);

        GUI.Box(rightPanelRect, "");
        
        GUILayout.BeginArea(rightContentRect);

        GUILayout.Label("종속성 그래프", EditorStyles.boldLabel);
    
        // 그래프 제어 도구 모음 (상단)
        EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
    
        GUILayout.Button(EditorGUIUtility.IconContent("d_ToolHandleLocal"), EditorStyles.toolbarButton, GUILayout.Width(30));
        GUILayout.Button(EditorGUIUtility.IconContent("d_GridAndSnap"), EditorStyles.toolbarButton, GUILayout.Width(30));
        GUILayout.Button(EditorGUIUtility.IconContent("d_ToolHandlePivot"), EditorStyles.toolbarButton, GUILayout.Width(30));
    
        GUILayout.FlexibleSpace();
    
        EditorGUILayout.LabelField("확대/축소:", GUILayout.Width(60));
        float zoom = GUILayout.HorizontalSlider(1.0f, 0.1f, 2.0f, GUILayout.Width(100));
    
        GUILayout.Button("100%", EditorStyles.toolbarButton, GUILayout.Width(50));
    
        EditorGUILayout.EndHorizontal();
    
        // 그래프 영역
        Rect graphScrollArea = EditorGUILayout.GetControlRect(false, rightContentRect.height - 60);
        GUI.Box(graphScrollArea, "", EditorStyles.helpBox);
    
        // 스크롤 뷰 (가상 캔버스 영역)
        scrollPosition = GUI.BeginScrollView(graphScrollArea, scrollPosition, new Rect(0, 0, 2000, 2000));
        GUI.EndScrollView();
    
        // 하단 상태 표시줄
        EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
    
        GUILayout.Label("노드: 6개", EditorStyles.miniLabel);
        GUILayout.Label("|", EditorStyles.miniLabel);
        GUILayout.Label("연결: 5개", EditorStyles.miniLabel);
    
        GUILayout.FlexibleSpace();
        
        EditorGUILayout.EndHorizontal();

        GUILayout.EndArea();
    }
}