앱인토스 개발자센터 로고
Skip to content

커스텀 로딩 화면

앱인토스 Unity 게임에서 브랜드에 맞는 커스텀 로딩 화면을 구현하여 사용자 경험을 향상시키는 방법을 다뤄요.

1. 커스텀 로딩 시스템 개요

로딩 화면 구성 요소

🎨 앱인토스 로딩 화면 구조
├── 브랜드 영역
│   ├── 게임 로고
│   └── 브랜딩 배경
├── 진행률 표시
│   ├── 프로그레스 바
│   ├── 퍼센트 텍스트
│   └── 로딩 메시지
├── 인터랙션 요소
│   ├── 애니메이션 효과
│   ├── 터치 힌트
│   └── 스킵 버튼 (선택적)
└── 시스템 정보
    ├── 네트워크 상태
    ├── 에러 메시지
    └── 재시도 버튼

커스텀 로딩 매니저

c#
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using TMPro;

public class AppsInTossLoadingManager : MonoBehaviour
{
    public static AppsInTossLoadingManager Instance { get; private set; }
    
    [System.Serializable]
    public class LoadingConfig
    {
        [Header("브랜딩 설정")]
        public Sprite tossLogo;
        public Sprite gameLogo;
        public Color primaryColor = Color.blue;
        public Color secondaryColor = Color.white;
        public Gradient backgroundGradient;
        
        [Header("애니메이션 설정")]
        public float logoAnimationDuration = 2f;
        public AnimationCurve logoScaleCurve = AnimationCurve.EaseInOut(0, 0.8f, 1, 1f);
        public bool enableParticleEffect = true;
        public ParticleSystem backgroundParticles;
        
        [Header("진행률 설정")]
        public bool showProgressBar = true;
        public bool showPercentage = true;
        public bool showLoadingTips = true;
        public string[] loadingTips;
        
        [Header("터치 인터랙션")]
        public bool enableTouchToSkip = false;
        public float skipAfterSeconds = 3f;
        public string skipHintText = "화면을 탭하여 건너뛰기";
    }
    
    [Header("로딩 설정")]
    public LoadingConfig config;
    
    [Header("UI 컴포넌트")]
    public Canvas loadingCanvas;
    public Image backgroundImage;
    public Image tossLogoImage;
    public Image gameLogoImage;
    public Slider progressSlider;
    public TextMeshProUGUI progressText;
    public TextMeshProUGUI statusText;
    public TextMeshProUGUI tipText;
    public Button skipButton;
    public GameObject errorPanel;
    public TextMeshProUGUI errorText;
    public Button retryButton;
    
    [Header("애니메이션 컴포넌트")]
    public Animator loadingAnimator;
    public ParticleSystem[] particleSystems;
    
    // 내부 상태
    private float currentProgress = 0f;
    private string currentStatus = "";
    private bool isLoadingComplete = false;
    private bool canSkip = false;
    private int currentTipIndex = 0;
    private Coroutine tipRotationCoroutine;
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializeLoadingScreen();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    void InitializeLoadingScreen()
    {
        // 브랜딩 적용
        ApplyBrandingSettings();
        
        // 초기 UI 설정
        SetupInitialUI();
        
        // 애니메이션 시작
        StartLoadingAnimations();
        
        // 이벤트 리스너 등록
        RegisterEventListeners();
        
        Debug.Log("AppsInToss 커스텀 로딩 화면 초기화 완료");
    }
    
    void ApplyBrandingSettings()
    {
        // 로고 설정
        if (config.tossLogo != null)
            tossLogoImage.sprite = config.tossLogo;
        if (config.gameLogo != null)
            gameLogoImage.sprite = config.gameLogo;
        
        // 색상 테마 적용
        progressSlider.fillRect.GetComponent<Image>().color = config.primaryColor;
        
        // 배경 그라디언트 적용
        if (config.backgroundGradient != null)
        {
            ApplyGradientBackground();
        }
        
        // 파티클 효과 설정
        if (config.enableParticleEffect && particleSystems.Length > 0)
        {
            SetupParticleEffects();
        }
    }
    
    void ApplyGradientBackground()
    {
        // 그라디언트 머티리얼 생성 및 적용
        var gradientMaterial = new Material(Shader.Find("UI/Gradient"));
        gradientMaterial.SetColor("_TopColor", config.backgroundGradient.Evaluate(0f));
        gradientMaterial.SetColor("_BottomColor", config.backgroundGradient.Evaluate(1f));
        backgroundImage.material = gradientMaterial;
    }
    
    void SetupParticleEffects()
    {
        foreach (var particles in particleSystems)
        {
            if (particles != null)
            {
                var main = particles.main;
                main.startColor = config.primaryColor;
                particles.Play();
            }
        }
    }
    
    void SetupInitialUI()
    {
        // 진행률 초기화
        progressSlider.value = 0f;
        progressSlider.gameObject.SetActive(config.showProgressBar);
        
        // 텍스트 초기화
        progressText.gameObject.SetActive(config.showPercentage);
        tipText.gameObject.SetActive(config.showLoadingTips);
        
        // 스킵 버튼 설정
        skipButton.gameObject.SetActive(false);
        skipButton.onClick.AddListener(OnSkipButtonClicked);
        
        // 재시도 버튼 설정
        retryButton.onClick.AddListener(OnRetryButtonClicked);
        errorPanel.SetActive(false);
        
        // 초기 상태 설정
        UpdateStatus("게임을 준비하고 있습니다...");
    }
    
    void StartLoadingAnimations()
    {
        // 로고 등장 애니메이션
        StartCoroutine(AnimateLogoEntrance());
        
        // 로딩 팁 순환 시작
        if (config.showLoadingTips && config.loadingTips.Length > 0)
        {
            tipRotationCoroutine = StartCoroutine(RotateLoadingTips());
        }
        
        // 스킵 가능 시점 설정
        if (config.enableTouchToSkip)
        {
            StartCoroutine(EnableSkipAfterDelay());
        }
    }
    
    IEnumerator AnimateLogoEntrance()
    {
        // 토스 로고 애니메이션
        tossLogoImage.transform.localScale = Vector3.one * 0.8f;
        gameLogoImage.transform.localScale = Vector3.one * 0.8f;
        
        float elapsedTime = 0f;
        while (elapsedTime < config.logoAnimationDuration)
        {
            elapsedTime += Time.deltaTime;
            float progress = elapsedTime / config.logoAnimationDuration;
            float scaleValue = config.logoScaleCurve.Evaluate(progress);
            
            tossLogoImage.transform.localScale = Vector3.one * scaleValue;
            gameLogoImage.transform.localScale = Vector3.one * scaleValue;
            
            yield return null;
        }
        
        // 최종 스케일 설정
        tossLogoImage.transform.localScale = Vector3.one;
        gameLogoImage.transform.localScale = Vector3.one;
        
        // 로고 펄스 애니메이션 시작
        if (loadingAnimator != null)
        {
            loadingAnimator.SetTrigger("StartPulse");
        }
    }
    
    IEnumerator RotateLoadingTips()
    {
        while (!isLoadingComplete)
        {
            if (config.loadingTips.Length > 0)
            {
                tipText.text = config.loadingTips[currentTipIndex];
                currentTipIndex = (currentTipIndex + 1) % config.loadingTips.Length;
            }
            
            yield return new WaitForSeconds(3f);
        }
    }
    
    IEnumerator EnableSkipAfterDelay()
    {
        yield return new WaitForSeconds(config.skipAfterSeconds);
        
        if (!isLoadingComplete)
        {
            canSkip = true;
            skipButton.gameObject.SetActive(true);
            
            // 스킵 힌트 표시
            StartCoroutine(ShowSkipHint());
        }
    }
    
    IEnumerator ShowSkipHint()
    {
        var hintText = skipButton.GetComponentInChildren<TextMeshProUGUI>();
        hintText.text = config.skipHintText;
        
        // 깜박임 효과
        while (canSkip && !isLoadingComplete)
        {
            hintText.color = new Color(hintText.color.r, hintText.color.g, hintText.color.b, 0.5f);
            yield return new WaitForSeconds(0.5f);
            hintText.color = new Color(hintText.color.r, hintText.color.g, hintText.color.b, 1f);
            yield return new WaitForSeconds(0.5f);
        }
    }
    
    void RegisterEventListeners()
    {
        AppsInToss.OnEvent += HandleAppsInTossEvent;
        AppsInToss.OnError += HandleAppsInTossError;
    }
    
    void HandleAppsInTossEvent(string eventName, Dictionary<string, object> data)
    {
        switch (eventName)
        {
            case "loading_progress":
                float progress = (float)data["progress"];
                string status = data["status"] as string;
                UpdateProgress(progress, status);
                break;
                
            case "loading_complete":
                CompleteLoading();
                break;
                
            case "asset_loaded":
                string assetName = data["asset_name"] as string;
                UpdateStatus($"{assetName} 로딩 완료");
                break;
        }
    }
    
    void HandleAppsInTossError(string errorType, string errorMessage)
    {
        ShowError(errorMessage);
    }
    
    public void UpdateProgress(float progress, string status = null)
    {
        currentProgress = Mathf.Clamp01(progress);
        
        // UI 업데이트
        if (config.showProgressBar)
        {
            progressSlider.value = currentProgress;
        }
        
        if (config.showPercentage)
        {
            progressText.text = $"{currentProgress * 100f:F0}%";
        }
        
        if (!string.IsNullOrEmpty(status))
        {
            UpdateStatus(status);
        }
        
        // 완료 체크
        if (currentProgress >= 1f)
        {
            CompleteLoading();
        }
    }
    
    public void UpdateStatus(string status)
    {
        currentStatus = status;
        statusText.text = status;
        
        Debug.Log($"로딩 상태 업데이트: {status}");
    }
    
    public void ShowError(string errorMessage)
    {
        errorText.text = errorMessage;
        errorPanel.SetActive(true);
        
        // 로딩 애니메이션 일시 정지
        if (loadingAnimator != null)
        {
            loadingAnimator.speed = 0f;
        }
        
        Debug.LogError($"로딩 에러: {errorMessage}");
    }
    
    public void CompleteLoading()
    {
        if (isLoadingComplete) return;
        
        isLoadingComplete = true;
        canSkip = false;
        
        // 팁 순환 중지
        if (tipRotationCoroutine != null)
        {
            StopCoroutine(tipRotationCoroutine);
        }
        
        // 완료 애니메이션 시작
        StartCoroutine(PlayCompletionAnimation());
    }
    
    IEnumerator PlayCompletionAnimation()
    {
        UpdateStatus("게임 시작 준비 완료!");
        
        // 진행률 100% 표시
        if (config.showProgressBar)
        {
            progressSlider.value = 1f;
        }
        if (config.showPercentage)
        {
            progressText.text = "100%";
        }
        
        // 완료 애니메이션
        if (loadingAnimator != null)
        {
            loadingAnimator.SetTrigger("Complete");
        }
        
        // 완료 효과
        PlayCompletionEffects();
        
        yield return new WaitForSeconds(1f);
        
        // 로딩 화면 페이드 아웃
        yield return StartCoroutine(FadeOutLoadingScreen());
        
        // 게임 씬 로드
        LoadGameScene();
    }
    
    void PlayCompletionEffects()
    {
        // 파티클 효과 강화
        foreach (var particles in particleSystems)
        {
            if (particles != null)
            {
                var emission = particles.emission;
                emission.rateOverTime = emission.rateOverTime.constant * 3f;
            }
        }
        
        // 로고 펄스 효과
        StartCoroutine(PulseLogo(tossLogoImage.transform));
        StartCoroutine(PulseLogo(gameLogoImage.transform));
    }
    
    IEnumerator PulseLogo(Transform logoTransform)
    {
        Vector3 originalScale = logoTransform.localScale;
        Vector3 targetScale = originalScale * 1.1f;
        
        // 확대
        float duration = 0.3f;
        float elapsedTime = 0f;
        
        while (elapsedTime < duration)
        {
            elapsedTime += Time.deltaTime;
            float progress = elapsedTime / duration;
            logoTransform.localScale = Vector3.Lerp(originalScale, targetScale, progress);
            yield return null;
        }
        
        // 축소
        elapsedTime = 0f;
        while (elapsedTime < duration)
        {
            elapsedTime += Time.deltaTime;
            float progress = elapsedTime / duration;
            logoTransform.localScale = Vector3.Lerp(targetScale, originalScale, progress);
            yield return null;
        }
        
        logoTransform.localScale = originalScale;
    }
    
    IEnumerator FadeOutLoadingScreen()
    {
        CanvasGroup canvasGroup = loadingCanvas.GetComponent<CanvasGroup>();
        if (canvasGroup == null)
        {
            canvasGroup = loadingCanvas.gameObject.AddComponent<CanvasGroup>();
        }
        
        float fadeTime = 1f;
        float elapsedTime = 0f;
        
        while (elapsedTime < fadeTime)
        {
            elapsedTime += Time.deltaTime;
            canvasGroup.alpha = 1f - (elapsedTime / fadeTime);
            yield return null;
        }
        
        canvasGroup.alpha = 0f;
        loadingCanvas.gameObject.SetActive(false);
    }
    
    void LoadGameScene()
    {
        // 게임 씬 로드
        UnityEngine.SceneManagement.SceneManager.LoadScene("GameScene");
        
        // 분석 데이터 전송
        SendLoadingAnalytics();
    }
    
    void SendLoadingAnalytics()
    {
        var analyticsData = new Dictionary<string, object>
        {
            {"loading_completed", true},
            {"final_progress", currentProgress},
            {"last_status", currentStatus},
            {"skip_used", false},
            {"completion_time", Time.time},
            {"device_model", SystemInfo.deviceModel},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("custom_loading_complete", analyticsData);
    }
    
    void OnSkipButtonClicked()
    {
        if (canSkip && !isLoadingComplete)
        {
            Debug.Log("사용자가 로딩을 건너뛰었습니다");
            
            // 스킵 분석 데이터
            var skipData = new Dictionary<string, object>
            {
                {"skip_time", Time.time},
                {"progress_at_skip", currentProgress},
                {"status_at_skip", currentStatus}
            };
            
            AppsInToss.SendAnalytics("loading_skipped", skipData);
            
            // 강제 완료
            CompleteLoading();
        }
    }
    
    void OnRetryButtonClicked()
    {
        // 에러 패널 숨기기
        errorPanel.SetActive(false);
        
        // 로딩 재시작
        currentProgress = 0f;
        isLoadingComplete = false;
        
        if (loadingAnimator != null)
        {
            loadingAnimator.speed = 1f;
        }
        
        UpdateStatus("다시 시도하는 중...");
        
        // 재시도 이벤트 발생
        AppsInToss.SendEvent("loading_retry", new Dictionary<string, object>());
    }
    
    // 터치 입력 처리
    void Update()
    {
        if (config.enableTouchToSkip && canSkip && !isLoadingComplete)
        {
            if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
            {
                OnSkipButtonClicked();
            }
            
            // 마우스 클릭도 지원 (에디터 테스트용)
            if (Input.GetMouseButtonDown(0))
            {
                OnSkipButtonClicked();
            }
        }
    }
    
    // 공개 API
    public bool IsLoadingComplete()
    {
        return isLoadingComplete;
    }
    
    public float GetCurrentProgress()
    {
        return currentProgress;
    }
    
    public string GetCurrentStatus()
    {
        return currentStatus;
    }
    
    public void SetCustomTip(string tip)
    {
        if (tipText != null)
        {
            tipText.text = tip;
        }
    }
    
    void OnDestroy()
    {
        AppsInToss.OnEvent -= HandleAppsInTossEvent;
        AppsInToss.OnError -= HandleAppsInTossError;
    }
}

2. 로딩 화면 템플릿

기본 템플릿

c#
public class BasicLoadingTemplate : MonoBehaviour
{
    [Header("기본 설정")]
    public Image backgroundImage;
    public Image logoImage;
    public Slider progressBar;
    public Text statusText;
    
    public void SetupBasicTemplate(Color brandColor, Sprite logo)
    {
        // 브랜드 색상 적용
        progressBar.fillRect.GetComponent<Image>().color = brandColor;
        
        // 로고 설정
        logoImage.sprite = logo;
        
        // 단순한 배경색
        backgroundImage.color = new Color(brandColor.r, brandColor.g, brandColor.b, 0.1f);
    }
}

고급 템플릿

c#
public class AdvancedLoadingTemplate : MonoBehaviour
{
    [Header("고급 설정")]
    public ParticleSystem backgroundParticles;
    public Animator logoAnimator;
    public Image[] decorativeElements;
    public TextMeshProUGUI[] animatedTexts;
    
    [Header("애니메이션 설정")]
    public AnimationCurve fadeInCurve;
    public AnimationCurve scaleCurve;
    public float animationDuration = 2f;
    
    public void SetupAdvancedTemplate(AppsInTossLoadingManager.LoadingConfig config)
    {
        // 파티클 시스템 설정
        SetupParticles(config);
        
        // 로고 애니메이션 설정
        SetupLogoAnimation(config);
        
        // 장식 요소 애니메이션
        SetupDecorativeAnimations(config);
        
        // 텍스트 애니메이션
        SetupTextAnimations(config);
    }
    
    void SetupParticles(AppsInTossLoadingManager.LoadingConfig config)
    {
        if (backgroundParticles != null)
        {
            var main = backgroundParticles.main;
            main.startColor = config.primaryColor;
            main.startSize = 0.1f;
            
            var emission = backgroundParticles.emission;
            emission.rateOverTime = 50;
            
            var shape = backgroundParticles.shape;
            shape.shapeType = ParticleSystemShapeType.Rectangle;
            
            backgroundParticles.Play();
        }
    }
    
    void SetupLogoAnimation(AppsInTossLoadingManager.LoadingConfig config)
    {
        if (logoAnimator != null)
        {
            logoAnimator.SetFloat("Duration", config.logoAnimationDuration);
            logoAnimator.SetTrigger("StartAnimation");
        }
    }
    
    void SetupDecorativeAnimations(AppsInTossLoadingManager.LoadingConfig config)
    {
        for (int i = 0; i < decorativeElements.Length; i++)
        {
            StartCoroutine(AnimateDecorativeElement(decorativeElements[i], i * 0.2f));
        }
    }
    
    IEnumerator AnimateDecorativeElement(Image element, float delay)
    {
        yield return new WaitForSeconds(delay);
        
        element.color = new Color(element.color.r, element.color.g, element.color.b, 0f);
        
        float elapsedTime = 0f;
        while (elapsedTime < animationDuration)
        {
            elapsedTime += Time.deltaTime;
            float progress = fadeInCurve.Evaluate(elapsedTime / animationDuration);
            
            element.color = new Color(element.color.r, element.color.g, element.color.b, progress);
            element.transform.localScale = Vector3.one * scaleCurve.Evaluate(progress);
            
            yield return null;
        }
    }
    
    void SetupTextAnimations(AppsInTossLoadingManager.LoadingConfig config)
    {
        foreach (var text in animatedTexts)
        {
            StartCoroutine(AnimateText(text));
        }
    }
    
    IEnumerator AnimateText(TextMeshProUGUI text)
    {
        string originalText = text.text;
        text.text = "";
        
        for (int i = 0; i <= originalText.Length; i++)
        {
            text.text = originalText.Substring(0, i);
            yield return new WaitForSeconds(0.05f);
        }
    }
}

3. 에디터 도구

로딩 화면 프리뷰어

c#
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(AppsInTossLoadingManager))]
public class LoadingManagerEditor : Editor
{
    private float previewProgress = 0f;
    private bool isPreviewMode = false;
    
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();
        
        EditorGUILayout.Space();
        EditorGUILayout.LabelField("프리뷰 도구", EditorStyles.boldLabel);
        
        AppsInTossLoadingManager manager = target as AppsInTossLoadingManager;
        
        // 프리뷰 모드 토글
        isPreviewMode = EditorGUILayout.Toggle("프리뷰 모드", isPreviewMode);
        
        if (isPreviewMode)
        {
            // 진행률 슬라이더
            previewProgress = EditorGUILayout.Slider("진행률", previewProgress, 0f, 1f);
            
            if (Application.isPlaying && manager != null)
            {
                manager.UpdateProgress(previewProgress, $"테스트 진행률: {previewProgress * 100f:F0}%");
            }
            
            EditorGUILayout.Space();
            
            // 테스트 버튼들
            EditorGUILayout.BeginHorizontal();
            
            if (GUILayout.Button("에러 테스트"))
            {
                if (Application.isPlaying && manager != null)
                {
                    manager.ShowError("테스트 에러 메시지입니다.");
                }
            }
            
            if (GUILayout.Button("완료 테스트"))
            {
                if (Application.isPlaying && manager != null)
                {
                    manager.CompleteLoading();
                }
            }
            
            EditorGUILayout.EndHorizontal();
        }
        
        EditorGUILayout.Space();
        
        // 설정 검증 버튼
        if (GUILayout.Button("설정 검증"))
        {
            ValidateLoadingConfig(manager);
        }
        
        // 브랜딩 가이드라인 버튼
        if (GUILayout.Button("브랜딩 가이드라인 확인"))
        {
            ShowBrandingGuidelines();
        }
    }
    
    void ValidateLoadingConfig(AppsInTossLoadingManager manager)
    {
        var issues = new List<string>();
        
        // 필수 컴포넌트 체크
        if (manager.tossLogoImage == null)
            issues.Add("토스 로고 이미지가 설정되지 않았습니다.");
        
        if (manager.progressSlider == null && manager.config.showProgressBar)
            issues.Add("진행률 바가 활성화되었지만 Slider 컴포넌트가 없습니다.");
        
        // 브랜딩 체크
        if (manager.config.tossLogo == null)
            issues.Add("토스 로고 스프라이트가 설정되지 않았습니다.");
        
        // 결과 표시
        if (issues.Count == 0)
        {
            EditorUtility.DisplayDialog("검증 완료", "로딩 설정이 올바릅니다.", "확인");
        }
        else
        {
            string message = "다음 문제들을 해결해주세요:\n\n" + string.Join("\n", issues);
            EditorUtility.DisplayDialog("검증 실패", message, "확인");
        }
    }
    
    void ShowBrandingGuidelines()
    {
        string guidelines = @"AppsInToss 브랜딩 가이드라인:

1. 로고 사용
   - 토스 로고는 항상 게임 로고와 함께 표시
   - 최소 크기: 64x64 픽셀
   - 명확한 여백 확보

2. 색상 사용
   - 주 색상: 토스 블루 (#3182F7)
   - 보조 색상: 화이트 (#FFFFFF)
   - 배경: 그라디언트 권장

3. 애니메이션
   - 부드러운 전환 효과 사용
   - 과도한 움직임 피하기
   - 2초 이내 완료 권장

4. 텍스트
   - 한글: 본고딕 또는 시스템 기본
   - 영문: Roboto 또는 시스템 기본
   - 가독성 우선";
        
        EditorUtility.DisplayDialog("브랜딩 가이드라인", guidelines, "확인");
    }
}
#endif

4. 성능 최적화

로딩 성능 모니터

c#
public class LoadingPerformanceMonitor : MonoBehaviour
{
    private float loadingStartTime;
    private Dictionary<string, float> phaseTimings = new Dictionary<string, float>();
    
    void Start()
    {
        loadingStartTime = Time.realtimeSinceStartup;
        AppsInToss.OnEvent += TrackLoadingPhases;
    }
    
    void TrackLoadingPhases(string eventName, Dictionary<string, object> data)
    {
        if (eventName.StartsWith("loading_"))
        {
            float currentTime = Time.realtimeSinceStartup - loadingStartTime;
            phaseTimings[eventName] = currentTime;
            
            Debug.Log($"로딩 단계: {eventName} - {currentTime:F2}초");
        }
        
        if (eventName == "loading_complete")
        {
            GeneratePerformanceReport();
        }
    }
    
    void GeneratePerformanceReport()
    {
        float totalTime = Time.realtimeSinceStartup - loadingStartTime;
        
        var report = new Dictionary<string, object>
        {
            {"total_loading_time", totalTime},
            {"phase_timings", phaseTimings},
            {"device_model", SystemInfo.deviceModel},
            {"device_memory", SystemInfo.systemMemorySize},
            {"graphics_device", SystemInfo.graphicsDeviceName}
        };
        
        AppsInToss.SendAnalytics("loading_performance", report);
        
        Debug.Log($"로딩 성능 보고서: 총 {totalTime:F2}초 소요");
    }
    
    void OnDestroy()
    {
        AppsInToss.OnEvent -= TrackLoadingPhases;
    }
}

브랜드 일관성을 유지하면서도 사용자가 기다리는 시간을 즐겁게 만드는 로딩 화면을 설계하세요.
진행률과 상태를 명확히 표시하여 사용자 불안감을 해소하는 것이 중요해요.