앱인토스 개발자센터 로고
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;
    }
}

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