Conditional로 릴리스 빌드에서 로그 호출을 없애는 Debug 래퍼
릴리스 빌드에는 콘솔 로그가 필요 없다. UnityEngine.Debug를 감싼 정적 클래스 Debug를 전역 네임스페이스에 두고 각 메서드에 [Conditional]을 붙여, 에디터와 개발 빌드에서만 로그 호출이 남게 한다. 클래스 이름이 같아서 기존 코드의 Debug.Log(...) 호출을 그대로 둘 수 있다.
Conditional의 동작
섹션 제목: “Conditional의 동작”[Conditional("SYMBOL")]을 붙인 메서드는 호출하는 쪽에서 그 심벌이 정의되어 있을 때만 호출이 컴파일된다. 정의되어 있지 않으면 호출이 IL에 들어가지 않는다. 메서드 정의는 그대로 컴파일된다.
- 반환형이
void여야 한다. [Conditional]을 여러 개 붙이면 그중 하나라도 정의되어 있을 때 호출이 포함된다(OR).- 포함 여부는 호출하는 위치에서 정의된 심벌로 정한다.
- 호출이 빠질 때는 인수 계산도 함께 빠진다.
Debug.Log($"state: {BuildDump()}")라면 릴리스 빌드에서BuildDump()도 실행되지 않고 문자열도 만들어지지 않는다. 인수를 먼저 지역 변수에 담으면 그 대입문은 남는다. - 인수의 타입 검사는 호출이 빠져도 한다.
출처: C# 참조: Conditional 특성, ConditionalAttribute API, C# 6 초안 명세: Conditional methods(“the call (including evaluation of the receiver and parameters of the call) is omitted”)
Unity가 정의하는 심벌 중 여기서 쓰는 것은 다음과 같다(조건부 컴파일).
| 심벌 | 정의되는 경우 |
|---|---|
UNITY_EDITOR |
에디터에서 컴파일할 때 |
DEVELOPMENT_BUILD |
Development Build 옵션을 켜고 만든 플레이어 |
UNITY_ANDROID, UNITY_IOS, UNITY_STANDALONE_WIN |
해당 플랫폼용으로 컴파일할 때 |
래퍼 코드
섹션 제목: “래퍼 코드”원래 코드는 #if DEVELOPMENT_BUILD 안에서 UNITY_ANDROID·UNITY_IOS·UNITY_STANDALONE_WIN용 [Conditional]을 추가로 붙여 개발 빌드의 대상 플랫폼을 고른다. 아래는 플랫폼 제한 없이 UNITY_EDITOR와 DEVELOPMENT_BUILD 두 심벌로 같은 조건을 만든 형태다. 메서드 이름은 일반적인 이름으로 바꿨다.
using System;using System.Diagnostics;using Object = UnityEngine.Object;
public static partial class Debug{ public enum LogColor { Green, Red, Blue, Yellow, Grey, White, Cyan, Brown }
public static bool isDebugBuild => UnityEngine.Debug.isDebugBuild;
private static string Stamp(object message) => "[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + message + Environment.NewLine;
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void Log(object message) => UnityEngine.Debug.Log(Stamp(message));
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void Log(object message, Object context) => UnityEngine.Debug.Log(Stamp(message), context);
// 색 이름은 Unity 리치 텍스트가 받는 이름과 같게 둔다. [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void Log(object message, LogColor color) => UnityEngine.Debug.Log(Stamp($"<color={color.ToString().ToLowerInvariant()}>{message}</color>"));
// 통신·UI처럼 분류별로 묶어 보고 싶은 로그 [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void LogCategory(string category, object message) => UnityEngine.Debug.Log(Stamp($"<color=purple>[{category}] {message}</color>"));
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void LogFormat(string format, params object[] args) => UnityEngine.Debug.LogFormat(format, args);
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void LogWarning(object message) => UnityEngine.Debug.LogWarning(Stamp(message));
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void LogError(object message) => UnityEngine.Debug.LogError(Stamp(message));
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void LogException(Exception e) => UnityEngine.Debug.LogException(e);
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] public static void Assert(bool condition, object message) { if (condition) return;
LogError(message); throw new Exception(message?.ToString()); }
// DrawLine, DrawRay, Break 등도 같은 방식으로 감싼다.}원래 코드에서 고친 점은 다음과 같다.
- 색 지정
Log(object, DebugColor)에[Conditional]이 빠져 있어 릴리스 빌드에도 호출이 남았다. 다른 메서드와 같은 특성을 붙인다. Assert(bool, object)가 조건과 상관없이LogError를 먼저 호출했다. 조건이 거짓일 때만 기록한다.- 시각 포맷 일부가
"HH: mm:ss.fff"로 공백이 들어가 있었다.
콘솔 더블클릭으로 호출한 코드 열기
섹션 제목: “콘솔 더블클릭으로 호출한 코드 열기”래퍼를 거치면 콘솔 로그의 콜스택 맨 위가 래퍼 메서드가 된다. 로그를 더블클릭하면 호출한 코드 대신 래퍼 파일이 열린다. [OnOpenAsset] 콜백으로 이 동작을 가로채, 콘솔 창에서 선택된 로그 텍스트를 리플렉션으로 읽고 콜스택의 두 번째 (at 경로:줄)을 연다.
래퍼 파일은 런타임 코드에서 호출하므로 Editor 폴더에 둘 수 없다. 에디터 API를 쓰는 부분은 #if UNITY_EDITOR로 감싸야 플레이어 빌드가 깨지지 않는다.
#if UNITY_EDITORusing System.Reflection;using System.Text.RegularExpressions;using UnityEditor;using UnityEditor.Callbacks;using UnityEditorInternal;using UnityEngine;
public static partial class Debug{ [OnOpenAsset] private static bool OnOpenDebugLog(int instanceID, int line) { Object asset = EditorUtility.InstanceIDToObject(instanceID); if (asset == null || asset.name != nameof(Debug)) return false;
// UnityEditor.ConsoleWindow는 공개 API가 아니어서 리플렉션으로 접근한다. System.Type consoleWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ConsoleWindow"); FieldInfo windowField = consoleWindowType?.GetField("ms_ConsoleWindow", BindingFlags.Static | BindingFlags.NonPublic); object consoleWindow = windowField?.GetValue(null); if (consoleWindow == null || consoleWindow != (object)EditorWindow.focusedWindow) return false;
FieldInfo activeTextField = consoleWindowType.GetField("m_ActiveText", BindingFlags.Instance | BindingFlags.NonPublic); string activeText = activeTextField?.GetValue(consoleWindow)?.ToString(); if (string.IsNullOrEmpty(activeText)) return false;
// 첫 번째 (at ...)는 래퍼 자신이므로 건너뛴다. Match match = Regex.Match(activeText, @"\(at (.+):(\d+)\)"); if (!match.Success) return false; match = match.NextMatch(); if (!match.Success) return false;
string projectRoot = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Assets", System.StringComparison.Ordinal)); InternalEditorUtility.OpenFileAtLineExternal(projectRoot + match.Groups[1].Value, int.Parse(match.Groups[2].Value)); return true; // 직접 열었으므로 기본 동작을 막는다. }}#endif앞의 래퍼와 같은 partial 클래스에 둔다. true를 돌려주면 Unity가 에셋을 따로 열지 않고, false면 다음 콜백이나 기본 동작으로 넘어간다.
OnOpenAsset 시그니처와 Unity 버전
섹션 제목: “OnOpenAsset 시그니처와 Unity 버전”- Unity 6.3 이전에는
static bool OnOpenAsset(int instanceID, int line)형태를 쓴다. - Unity 6.3부터
int인스턴스 ID를 받는OnOpenAsset은 obsolete가 되었고EntityId를 받는 시그니처를 쓴다.EditorUtility.InstanceIDToObject도EditorUtility.EntityIdToObject로 바뀐다. 특성이 두 시그니처를 모두 받아서int버전은 경고 없이 컴파일되므로 직접 찾아 바꿔야 한다(InstanceID에서 EntityId로 이전, 6000.3.0a2 릴리스 노트).
// Unity 6.3 이상[OnOpenAsset]private static bool OnOpenDebugLog(EntityId entityId, int line){ Object asset = EditorUtility.EntityIdToObject(entityId); // 이후 처리는 같다. return false;}최신 문서의 시그니처 목록: OnOpenAssetAttribute
HideInCallstack
섹션 제목: “HideInCallstack”[HideInCallstack](UnityEngine.HideInCallstackAttribute)을 메서드에 붙이고 콘솔 메뉴에서 Strip logging callstack을 켜면, 그 메서드가 콘솔 상세 영역의 콜스택에서 빠진다. 2021.3·2022.3 Scripting API 문서에 있다(2022.3 문서).
래퍼 메서드에 붙이면 콜스택 표시에서 래퍼를 지울 수 있다. 다만 Unity 문서는 이 특성이 사소한 헬퍼 메서드를 숨기려는 용도이고, 실제로 로그를 쓰는 메서드에 붙이면 이름은 숨겨져도 콘솔이 그 메서드로 가는 클릭 링크를 계속 제공한다고 적는다(최신 문서). 이 래퍼처럼 UnityEngine.Debug.Log를 직접 호출하는 구조에서는 [HideInCallstack]만으로 더블클릭 위치가 호출한 코드로 바뀐다고 볼 수 없어서, 위의 OnOpenAsset 처리는 계속 필요하다.