Unity WebGL 디버깅 및 예외 처리 가이드
Unity WebGL에서의 디버깅과 예외 처리는 앱인토스 플랫폼에서 안정적인 게임 서비스를 위해 필수예요.
이 가이드는 효과적인 디버깅 방법과 예외 처리 전략을 제공해요.
디버깅 시스템
1. 통합 로깅 시스템
c#
using UnityEngine;
using System.Collections.Generic;
using System.Text;
using System;
public enum LogLevel
{
Debug,
Info,
Warning,
Error,
Critical
}
public class DebugLogger : MonoBehaviour
{
[Header("로깅 설정")]
public bool enableConsoleOutput = true;
public bool enableFileOutput = false;
public bool enableWebOutput = true;
public LogLevel minLogLevel = LogLevel.Debug;
[Header("웹 출력 설정")]
public int maxWebLogs = 100;
public bool sendToAppsInToss = true;
private static DebugLogger instance;
private Queue<LogEntry> logBuffer = new Queue<LogEntry>();
private StringBuilder logStringBuilder = new StringBuilder();
[System.Serializable]
public class LogEntry
{
public LogLevel level;
public string message;
public string stackTrace;
public DateTime timestamp;
public string tag;
public LogEntry(LogLevel level, string message, string stackTrace = "", string tag = "")
{
this.level = level;
this.message = message;
this.stackTrace = stackTrace;
this.timestamp = DateTime.Now;
this.tag = tag;
}
public string ToJson()
{
return JsonUtility.ToJson(this);
}
}
public static DebugLogger Instance
{
get
{
if (instance == null)
{
GameObject loggerGO = new GameObject("DebugLogger");
instance = loggerGO.AddComponent<DebugLogger>();
DontDestroyOnLoad(loggerGO);
}
return instance;
}
}
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
InitializeLogger();
}
else if (instance != this)
{
Destroy(gameObject);
}
}
private void InitializeLogger()
{
// Unity의 기본 로그 핸들러 오버라이드
Application.logMessageReceived += HandleUnityLog;
LogInfo("DebugLogger 초기화 완료", "System");
}
private void HandleUnityLog(string logString, string stackTrace, LogType type)
{
LogLevel level = ConvertLogType(type);
if (level >= minLogLevel)
{
LogEntry entry = new LogEntry(level, logString, stackTrace, "Unity");
AddLogEntry(entry);
}
}
private LogLevel ConvertLogType(LogType unityLogType)
{
switch (unityLogType)
{
case LogType.Log: return LogLevel.Info;
case LogType.Warning: return LogLevel.Warning;
case LogType.Error: return LogLevel.Error;
case LogType.Exception: return LogLevel.Critical;
case LogType.Assert: return LogLevel.Error;
default: return LogLevel.Info;
}
}
private void AddLogEntry(LogEntry entry)
{
logBuffer.Enqueue(entry);
// 버퍼 크기 제한
while (logBuffer.Count > maxWebLogs)
{
logBuffer.Dequeue();
}
// 출력 처리
if (enableConsoleOutput)
{
Debug.Log($"[{entry.level}][{entry.tag}] {entry.message}");
}
if (enableWebOutput)
{
SendLogToWeb(entry);
}
if (sendToAppsInToss)
{
SendLogToAppsInToss(entry);
}
}
private void SendLogToWeb(LogEntry entry)
{
string logData = entry.ToJson();
Application.ExternalCall("ReceiveUnityLog", logData);
}
private void SendLogToAppsInToss(LogEntry entry)
{
if (entry.level >= LogLevel.Error)
{
Application.ExternalCall("SendErrorToAppsInToss", entry.ToJson());
}
}
// 공개 로깅 메서드들
public static void LogDebug(string message, string tag = "")
{
Instance.AddLogEntry(new LogEntry(LogLevel.Debug, message, "", tag));
}
public static void LogInfo(string message, string tag = "")
{
Instance.AddLogEntry(new LogEntry(LogLevel.Info, message, "", tag));
}
public static void LogWarning(string message, string tag = "")
{
Instance.AddLogEntry(new LogEntry(LogLevel.Warning, message, "", tag));
}
public static void LogError(string message, string tag = "", Exception exception = null)
{
string stackTrace = exception?.StackTrace ?? Environment.StackTrace;
Instance.AddLogEntry(new LogEntry(LogLevel.Error, message, stackTrace, tag));
}
public static void LogCritical(string message, string tag = "", Exception exception = null)
{
string stackTrace = exception?.StackTrace ?? Environment.StackTrace;
Instance.AddLogEntry(new LogEntry(LogLevel.Critical, message, stackTrace, tag));
}
// 로그 내보내기
public string ExportLogs()
{
logStringBuilder.Clear();
foreach (LogEntry entry in logBuffer)
{
logStringBuilder.AppendLine($"[{entry.timestamp:yyyy-MM-dd HH:mm:ss}] [{entry.level}] [{entry.tag}] {entry.message}");
if (!string.IsNullOrEmpty(entry.stackTrace))
{
logStringBuilder.AppendLine($"Stack Trace: {entry.stackTrace}");
}
logStringBuilder.AppendLine();
}
return logStringBuilder.ToString();
}
}2. 시각적 디버그 도구
c#
using UnityEngine;
using System.Collections.Generic;
public class VisualDebugger : MonoBehaviour
{
[Header("디스플레이 설정")]
public bool showDebugGUI = false;
public KeyCode toggleKey = KeyCode.F12;
public Color debugColor = Color.green;
[Header("성능 모니터링")]
public bool showFPS = true;
public bool showMemory = true;
public bool showDrawCalls = true;
private static Dictionary<string, object> debugVariables = new Dictionary<string, object>();
private static List<DebugCommand> debugCommands = new List<DebugCommand>();
private Vector2 scrollPosition;
private string commandInput = "";
public class DebugCommand
{
public string name;
public string description;
public System.Action<string[]> action;
public DebugCommand(string name, string description, System.Action<string[]> action)
{
this.name = name;
this.description = description;
this.action = action;
}
}
private void Start()
{
RegisterDefaultCommands();
}
private void Update()
{
if (Input.GetKeyDown(toggleKey))
{
showDebugGUI = !showDebugGUI;
}
}
private void RegisterDefaultCommands()
{
// 기본 디버그 명령어들
RegisterCommand("fps", "FPS 표시 토글", (args) => {
showFPS = !showFPS;
DebugLogger.LogInfo($"FPS 표시: {showFPS}", "Debug");
});
RegisterCommand("memory", "메모리 정보 표시", (args) => {
long memory = System.GC.GetTotalMemory(false);
DebugLogger.LogInfo($"현재 메모리 사용량: {memory / 1024 / 1024} MB", "Debug");
});
RegisterCommand("quality", "품질 설정 변경", (args) => {
if (args.Length > 0 && int.TryParse(args[0], out int level))
{
QualitySettings.SetQualityLevel(level);
DebugLogger.LogInfo($"품질 레벨을 {level}로 변경", "Debug");
}
});
RegisterCommand("timescale", "타임 스케일 변경", (args) => {
if (args.Length > 0 && float.TryParse(args[0], out float scale))
{
Time.timeScale = scale;
DebugLogger.LogInfo($"타임 스케일을 {scale}로 변경", "Debug");
}
});
}
private void OnGUI()
{
if (!showDebugGUI) return;
GUILayout.BeginArea(new Rect(10, 10, Screen.width - 20, Screen.height - 20));
GUILayout.BeginVertical("box");
GUILayout.Label("Unity WebGL 디버그 콘솔", GUI.skin.box);
// 성능 정보
if (showFPS || showMemory || showDrawCalls)
{
GUILayout.BeginHorizontal();
if (showFPS)
{
float fps = 1.0f / Time.smoothDeltaTime;
GUILayout.Label($"FPS: {fps:F1}");
}
if (showMemory)
{
long memory = System.GC.GetTotalMemory(false);
GUILayout.Label($"Memory: {memory / 1024 / 1024} MB");
}
if (showDrawCalls)
{
GUILayout.Label($"Quality: {QualitySettings.GetQualityLevel()}");
}
GUILayout.EndHorizontal();
}
GUILayout.Space(10);
// 디버그 변수들
if (debugVariables.Count > 0)
{
GUILayout.Label("디버그 변수:", GUI.skin.box);
scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(150));
foreach (var kvp in debugVariables)
{
GUILayout.Label($"{kvp.Key}: {kvp.Value}");
}
GUILayout.EndScrollView();
}
// 명령어 입력
GUILayout.Label("명령어 입력:");
GUILayout.BeginHorizontal();
commandInput = GUILayout.TextField(commandInput);
if (GUILayout.Button("실행") || (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return))
{
ExecuteCommand(commandInput);
commandInput = "";
}
GUILayout.EndHorizontal();
// 사용 가능한 명령어 목록
GUILayout.Label("사용 가능한 명령어:");
foreach (var command in debugCommands)
{
GUILayout.Label($"• {command.name}: {command.description}");
}
GUILayout.EndVertical();
GUILayout.EndArea();
}
// 디버그 변수 등록/업데이트
public static void SetDebugVariable(string name, object value)
{
debugVariables[name] = value;
}
public static void RemoveDebugVariable(string name)
{
debugVariables.Remove(name);
}
// 디버그 명령어 등록
public static void RegisterCommand(string name, string description, System.Action<string[]> action)
{
debugCommands.Add(new DebugCommand(name, description, action));
}
private void ExecuteCommand(string input)
{
if (string.IsNullOrEmpty(input)) return;
string[] parts = input.Split(' ');
string commandName = parts[0].ToLower();
string[] args = parts.Length > 1 ?
new string[parts.Length - 1] : new string[0];
if (args.Length > 0)
{
System.Array.Copy(parts, 1, args, 0, args.Length);
}
foreach (var command in debugCommands)
{
if (command.name.Equals(commandName, System.StringComparison.OrdinalIgnoreCase))
{
command.action?.Invoke(args);
return;
}
}
DebugLogger.LogWarning($"알 수 없는 명령어: {commandName}", "Debug");
}
}예외 처리 시스템
1. 중앙 집중식 예외 처리
c#
using UnityEngine;
using System;
using System.Collections.Generic;
public class ExceptionHandler : MonoBehaviour
{
[Header("예외 처리 설정")]
public bool enableGlobalHandling = true;
public bool enableRecoveryAttempts = true;
public int maxRecoveryAttempts = 3;
private static ExceptionHandler instance;
private Dictionary<Type, Func<Exception, bool>> recoveryStrategies = new Dictionary<Type, Func<Exception, bool>>();
private Dictionary<Type, int> exceptionCounts = new Dictionary<Type, int>();
public static ExceptionHandler Instance
{
get
{
if (instance == null)
{
GameObject handlerGO = new GameObject("ExceptionHandler");
instance = handlerGO.AddComponent<ExceptionHandler>();
DontDestroyOnLoad(handlerGO);
}
return instance;
}
}
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
SetupExceptionHandling();
}
else if (instance != this)
{
Destroy(gameObject);
}
}
private void SetupExceptionHandling()
{
if (enableGlobalHandling)
{
Application.logMessageReceived += HandleLogMessage;
}
RegisterRecoveryStrategies();
}
private void RegisterRecoveryStrategies()
{
// OutOfMemoryException 복구 전략
recoveryStrategies[typeof(OutOfMemoryException)] = RecoverFromOutOfMemory;
// NullReferenceException 복구 전략
recoveryStrategies[typeof(NullReferenceException)] = RecoverFromNullReference;
// ArgumentException 복구 전략
recoveryStrategies[typeof(ArgumentException)] = RecoverFromInvalidArgument;
}
private void HandleLogMessage(string logString, string stackTrace, LogType type)
{
if (type == LogType.Exception)
{
HandleException(logString, stackTrace);
}
}
public bool HandleException(Exception exception)
{
return HandleException(exception.Message, exception.StackTrace, exception.GetType());
}
public bool HandleException(string message, string stackTrace, Type exceptionType = null)
{
// 예외 카운트 증가
if (exceptionType != null)
{
exceptionCounts[exceptionType] = exceptionCounts.GetValueOrDefault(exceptionType, 0) + 1;
}
// 로깅
DebugLogger.LogError($"예외 발생: {message}", "Exception");
DebugLogger.LogError($"스택 트레이스: {stackTrace}", "Exception");
// 복구 시도
bool recovered = false;
if (enableRecoveryAttempts && exceptionType != null)
{
recovered = AttemptRecovery(exceptionType);
}
// AppsInToss에 리포트
ReportExceptionToAppsInToss(message, stackTrace, exceptionType, recovered);
return recovered;
}
private bool AttemptRecovery(Type exceptionType)
{
if (recoveryStrategies.TryGetValue(exceptionType, out Func<Exception, bool> strategy))
{
try
{
return strategy(null); // 간소화된 호출
}
catch (Exception recoveryException)
{
DebugLogger.LogError($"복구 시도 중 예외 발생: {recoveryException.Message}", "Exception");
return false;
}
}
return false;
}
private bool RecoverFromOutOfMemory(Exception exception)
{
DebugLogger.LogInfo("메모리 부족 예외 복구 시도", "Recovery");
// 리소스 해제
Resources.UnloadUnusedAssets();
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
System.GC.Collect();
// 품질 설정 낮추기
QualitySettings.SetQualityLevel(0);
QualitySettings.masterTextureLimit = 2;
DebugLogger.LogInfo("메모리 부족 예외 복구 완료", "Recovery");
return true;
}
private bool RecoverFromNullReference(Exception exception)
{
DebugLogger.LogInfo("NullReference 예외 복구 시도", "Recovery");
// 필수 컴포넌트들 재초기화 시도
try
{
// 게임 매니저 재초기화
var gameManager = FindObjectOfType<GameManager>();
if (gameManager != null)
{
gameManager.SendMessage("Reinitialize", SendMessageOptions.DontRequireReceiver);
}
return true;
}
catch
{
return false;
}
}
private bool RecoverFromInvalidArgument(Exception exception)
{
DebugLogger.LogInfo("잘못된 인수 예외 복구 시도", "Recovery");
// 기본값으로 재설정
// 구체적인 복구 로직은 게임에 따라 다름
return true;
}
private void ReportExceptionToAppsInToss(string message, string stackTrace, Type exceptionType, bool recovered)
{
string exceptionData = JsonUtility.ToJson(new {
message = message,
stackTrace = stackTrace,
exceptionType = exceptionType?.Name ?? "Unknown",
recovered = recovered,
timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
exceptionCount = exceptionType != null ? exceptionCounts.GetValueOrDefault(exceptionType, 0) : 0
});
Application.ExternalCall("SendExceptionToAppsInToss", exceptionData);
}
// 예외 통계
public void GetExceptionStatistics()
{
DebugLogger.LogInfo("=== 예외 통계 ===", "Statistics");
foreach (var kvp in exceptionCounts)
{
DebugLogger.LogInfo($"{kvp.Key.Name}: {kvp.Value}회", "Statistics");
}
}
}2. 안전한 코루틴 래퍼
c#
using UnityEngine;
using System.Collections;
using System;
public static class SafeCoroutines
{
public static Coroutine StartSafe(MonoBehaviour behaviour, IEnumerator routine, string routineName = "")
{
return behaviour.StartCoroutine(SafeWrapper(routine, routineName));
}
private static IEnumerator SafeWrapper(IEnumerator routine, string routineName)
{
bool hasError = false;
while (true)
{
object current = null;
bool moveNext = false;
try
{
moveNext = routine.MoveNext();
current = routine.Current;
}
catch (Exception e)
{
hasError = true;
string name = string.IsNullOrEmpty(routineName) ? "Unknown" : routineName;
DebugLogger.LogError($"코루틴 '{name}'에서 예외 발생: {e.Message}", "Coroutine", e);
// 예외 처리
ExceptionHandler.Instance.HandleException(e);
// 코루틴 종료
yield break;
}
if (!moveNext)
break;
yield return current;
}
if (!hasError)
{
string name = string.IsNullOrEmpty(routineName) ? "Unknown" : routineName;
DebugLogger.LogDebug($"코루틴 '{name}' 정상 완료", "Coroutine");
}
}
}
// 사용 예제
public class SafeCoroutineExample : MonoBehaviour
{
private void Start()
{
SafeCoroutines.StartSafe(this, RiskyOperation(), "RiskyOperation");
}
private IEnumerator RiskyOperation()
{
for (int i = 0; i < 10; i++)
{
// 위험할 수 있는 작업
yield return new WaitForSeconds(1f);
if (i == 5)
{
// 의도적으로 예외 발생 시켜보기
throw new InvalidOperationException("테스트 예외");
}
}
}
}성능 이슈 진단
1. 자동 성능 진단 도구
c#
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class PerformanceDiagnostics : MonoBehaviour
{
[Header("진단 설정")]
public bool enableAutoDiagnosis = true;
public float diagnosisInterval = 5f;
public float fpsThreshold = 30f;
public float memoryThreshold = 100f; // MB
private List<DiagnosticResult> diagnosticHistory = new List<DiagnosticResult>();
[System.Serializable]
public class DiagnosticResult
{
public float timestamp;
public float fps;
public float memoryUsage;
public int activeObjects;
public int activeRenderers;
public string[] issues;
public string[] recommendations;
}
private void Start()
{
if (enableAutoDiagnosis)
{
InvokeRepeating(nameof(RunDiagnosis), diagnosisInterval, diagnosisInterval);
}
}
private void RunDiagnosis()
{
DiagnosticResult result = new DiagnosticResult
{
timestamp = Time.time,
fps = 1.0f / Time.smoothDeltaTime,
memoryUsage = System.GC.GetTotalMemory(false) / 1024f / 1024f,
activeObjects = FindObjectsOfType<GameObject>().Length,
activeRenderers = FindObjectsOfType<Renderer>().Where(r => r.enabled).Count()
};
List<string> issues = new List<string>();
List<string> recommendations = new List<string>();
// FPS 문제 진단
if (result.fps < fpsThreshold)
{
issues.Add($"낮은 FPS: {result.fps:F1}");
// 원인 분석
if (result.activeRenderers > 100)
{
recommendations.Add("렌더러 수 감소 (현재: " + result.activeRenderers + ")");
}
if (QualitySettings.GetQualityLevel() > 2)
{
recommendations.Add("품질 설정 낮추기");
}
}
// 메모리 문제 진단
if (result.memoryUsage > memoryThreshold)
{
issues.Add($"높은 메모리 사용량: {result.memoryUsage:F1}MB");
recommendations.Add("Resources.UnloadUnusedAssets() 호출");
recommendations.Add("GC.Collect() 실행");
}
// 오브젝트 수 진단
if (result.activeObjects > 1000)
{
issues.Add($"과도한 활성 오브젝트: {result.activeObjects}");
recommendations.Add("오브젝트 풀링 사용");
recommendations.Add("비활성화 가능한 오브젝트 정리");
}
result.issues = issues.ToArray();
result.recommendations = recommendations.ToArray();
diagnosticHistory.Add(result);
// 히스토리 크기 제한
if (diagnosticHistory.Count > 20)
{
diagnosticHistory.RemoveAt(0);
}
// 문제가 있는 경우 로그
if (issues.Count > 0)
{
DebugLogger.LogWarning($"성능 문제 감지: {string.Join(", ", issues)}", "Performance");
DebugLogger.LogInfo($"권장사항: {string.Join(", ", recommendations)}", "Performance");
}
// AppsInToss에 진단 결과 전송
SendDiagnosticResultToAppsInToss(result);
}
private void SendDiagnosticResultToAppsInToss(DiagnosticResult result)
{
string diagnosticData = JsonUtility.ToJson(result);
Application.ExternalCall("SendDiagnosticDataToAppsInToss", diagnosticData);
}
public DiagnosticResult GetLatestDiagnostic()
{
return diagnosticHistory.LastOrDefault();
}
public DiagnosticResult[] GetDiagnosticHistory()
{
return diagnosticHistory.ToArray();
}
}문제 해결 가이드
일반적인 WebGL 문제들
메모리 관련 문제
- Out of Memory 에러
- 가비지 컬렉션으로 인한 끊김
- 메모리 누수
성능 문제
- 낮은 프레임률
- 긴 로딩 시간
- 버벅거림
호환성 문제
- 브라우저별 차이
- 모바일 기기 대응
- 오래된 디바이스 지원
해결 전략
c#
public class TroubleshootingHelper : MonoBehaviour
{
public static void DiagnoseCommonIssues()
{
DebugLogger.LogInfo("=== 일반적인 문제 진단 ===", "Troubleshooting");
// 1. 메모리 체크
long memory = System.GC.GetTotalMemory(false);
if (memory > 100 * 1024 * 1024) // 100MB
{
DebugLogger.LogWarning("높은 메모리 사용량 감지", "Troubleshooting");
DebugLogger.LogInfo("권장사항: Resources.UnloadUnusedAssets() 호출", "Troubleshooting");
}
// 2. 품질 설정 체크
int qualityLevel = QualitySettings.GetQualityLevel();
if (qualityLevel > 2)
{
DebugLogger.LogInfo($"현재 품질 레벨: {qualityLevel} (높음)", "Troubleshooting");
DebugLogger.LogInfo("권장사항: 모바일 환경에서는 품질 낮추기", "Troubleshooting");
}
// 3. 렌더러 수 체크
int rendererCount = FindObjectsOfType<Renderer>().Length;
if (rendererCount > 100)
{
DebugLogger.LogWarning($"많은 렌더러 수: {rendererCount}", "Troubleshooting");
DebugLogger.LogInfo("권장사항: 배칭 최적화 또는 LOD 사용", "Troubleshooting");
}
// 4. 텍스처 설정 체크
Texture2D[] textures = Resources.FindObjectsOfTypeAll<Texture2D>();
int uncompressedTextures = textures.Count(t => t.format == TextureFormat.RGBA32);
if (uncompressedTextures > 10)
{
DebugLogger.LogWarning($"압축되지 않은 텍스처: {uncompressedTextures}개", "Troubleshooting");
DebugLogger.LogInfo("권장사항: 텍스처 압축 적용", "Troubleshooting");
}
}
}베스트 프랙티스
- 프로액티브 로깅 - 문제가 발생하기 전에 충분한 로그 수집
- 예외 복구 전략 - 예외 발생 시 게임이 계속 실행될 수 있도록 복구 로직 구현
- 성능 모니터링 - 지속적인 성능 모니터링으로 문제 조기 발견
- AppsInToss 연동 - 플랫폼 기능을 활용한 효과적인 디버깅
- 사용자 피드백 - 사용자가 겪는 문제를 쉽게 리포트할 수 있는 시스템 구축
이 가이드를 통해 Unity WebGL 게임의 안정성과 디버깅 효율성을 크게 향상시킬 수 있어요.
