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

게임 시작 속도 향상

AppsInToss 미니앱에서 게임 시작 속도는 첫인상을 좌우하는 핵심 요소예요. 게임이 3초 안에 시작되지 않으면, 사용자가 쉽게 이탈할 수 있어요.

1. 시작 성능 목표

성능 벤치마크

🎯 목표 시작 시간
├── 우수: < 2초 (네트워크 양호)
├── 양호: < 3초 (일반적 환경)  
├── 최소: < 5초 (느린 네트워크)
└── 임계: > 5초 (개선 필수)

측정 기준점

  • 로딩 시작: 사용자가 게임 링크 클릭
  • 첫 프레임: 게임 UI가 화면에 표시
  • 상호작용 가능: 사용자 입력 받기 시작
  • 완전 로드: 모든 초기 리소스 로드 완료

Unity WebGL 시작 단계

📱 시작 과정 (총 소요시간: 목표 3초)
├── 1. HTML/JS 다운로드 (0.2초)
├── 2. WASM 다운로드 (0.8초)  
├── 3. WASM 컴파일 (0.5초)
├── 4. Unity 초기화 (0.3초)
├── 5. 첫 씬 로드 (0.7초)
├── 6. 스크립트 실행 (0.3초)
└── 7. 첫 프레임 렌더링 (0.2초)

병목 지점 식별

c#
// 성능 측정 도구
public class StartupProfiler : MonoBehaviour
{
    private static float startTime;
    
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void OnBeforeSceneLoad()
    {
        startTime = Time.realtimeSinceStartup;
        Debug.Log($"[Startup] Before Scene Load: {startTime}s");
    }
    
    void Awake()
    {
        float awakeTime = Time.realtimeSinceStartup - startTime;
        Debug.Log($"[Startup] Awake: {awakeTime}s");
    }
    
    void Start()
    {
        float startMethodTime = Time.realtimeSinceStartup - startTime;
        Debug.Log($"[Startup] Start: {startMethodTime}s");
        
        // AppsInToss 분석 시스템에 전송
        AppsInToss.Analytics.LogEvent("startup_timing", new Dictionary<string, object>
        {
            {"awake_time", awakeTime},
            {"start_time", startMethodTime}
        });
    }
}

3. 파일 크기 최적화

WASM 파일 최적화

c#
// Build Settings 최적화
public class BuildOptimizer
{
    [MenuItem("AppsInToss/Optimize Build Settings")]
    static void OptimizeBuildSettings()
    {
        // IL2CPP 설정 최적화
        PlayerSettings.SetScriptingBackend(BuildTargetGroup.WebGL, ScriptingImplementation.IL2CPP);
        
        // 코드 최적화 레벨
        PlayerSettings.SetIl2CppCompilerConfiguration(BuildTargetGroup.WebGL, Il2CppCompilerConfiguration.Release);
        
        // 불필요한 코드 제거
        PlayerSettings.stripEngineCode = true;
        PlayerSettings.SetManagedStrippingLevel(BuildTargetGroup.WebGL, ManagedStrippingLevel.High);
        
        // 압축 설정
        PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli;
    }
}

에셋 최적화

c#
// 텍스처 압축 자동화
public class TextureOptimizer : AssetPostprocessor
{
    void OnPreprocessTexture()
    {
        var importer = assetImporter as TextureImporter;
        
        // WebGL 플랫폼 설정
        var platformSettings = new TextureImporterPlatformSettings
        {
            name = "WebGL",
            overridden = true,
            maxTextureSize = 1024, // 모바일 고려
            format = TextureImporterFormat.DXT5,
            compressionQuality = 80, // 품질 vs 크기 균형
            allowsAlphaSplitting = true
        };
        
        importer.SetPlatformTextureSettings(platformSettings);
    }
}

4. 점진적 로딩 전략

필수 vs 선택적 리소스 분리

c#
public class ProgressiveLoader : MonoBehaviour
{
    [System.Serializable]
    public class LoadPhase
    {
        public string name;
        public Object[] assets;
        public bool isEssential;
        public float maxLoadTime;
    }
    
    public LoadPhase[] loadPhases;
    
    async void Start()
    {
        // Phase 1: 즉시 필요한 필수 리소스만
        await LoadPhase("Essential");
        ShowBasicUI(); // 빠르게 사용자에게 보여주기
        
        // Phase 2: 게임플레이 리소스 (백그라운드)
        _ = LoadPhase("Gameplay");
        
        // Phase 3: 부가 기능들 (지연 로딩)
        _ = LoadPhase("Optional");
    }
    
    async Task LoadPhase(string phaseName)
    {
        var phase = loadPhases.First(p => p.name == phaseName);
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        
        foreach (var asset in phase.assets)
        {
            await LoadAssetAsync(asset);
            
            // 시간 제한으로 반응성 유지
            if (phase.maxLoadTime > 0 && stopwatch.ElapsedMilliseconds > phase.maxLoadTime)
            {
                Debug.LogWarning($"Phase {phaseName} exceeded time limit");
                break;
            }
        }
    }
}

Addressable 활용

c#
public class AddressableStartup : MonoBehaviour
{
    [SerializeField] private AssetReference essentialUI;
    [SerializeField] private AssetReference gameplayAssets;
    
    async void Start()
    {
        // 1단계: 필수 UI 로드
        var uiHandle = essentialUI.InstantiateAsync();
        await uiHandle.Task;
        
        ShowLoadingScreen();
        
        // 2단계: 게임플레이 에셋 로드 (프로그레스 표시)
        var gameplayHandle = gameplayAssets.LoadAssetAsync<GameObject>();
        
        while (!gameplayHandle.IsDone)
        {
            UpdateLoadingProgress(gameplayHandle.PercentComplete);
            await Task.Yield();
        }
        
        StartGame();
    }
}

5. 첫 프레임 최적화

첫 프레임에서 피해야 할 작업들

c#
public class FirstFrameOptimizer : MonoBehaviour
{
    void Start()
    {
        // ❌ 피해야 할 작업들 (첫 프레임에서)
        
        // 1. 대량의 오브젝트 생성
        // for (int i = 0; i < 1000; i++) Instantiate(prefab);
        
        // 2. 복잡한 계산
        // CalculateComplexAlgorithm();
        
        // 3. 파일 I/O
        // File.ReadAllText("large_file.json");
        
        // 4. 네트워크 요청  
        // UnityWebRequest.Get(url).SendWebRequest();
        
        // ✅ 대신 코루틴이나 비동기로 처리
        StartCoroutine(InitializeGameSystems());
    }
    
    IEnumerator InitializeGameSystems()
    {
        // 프레임별로 작업 분산
        yield return StartCoroutine(LoadEssentialData());
        yield return StartCoroutine(InitializeUI());
        yield return StartCoroutine(SetupGameplay());
        
        OnInitializationComplete();
    }
    
    IEnumerator LoadEssentialData()
    {
        int itemsPerFrame = 10;
        
        for (int i = 0; i < dataItems.Count; i++)
        {
            ProcessDataItem(dataItems[i]);
            
            // 매 N개마다 프레임 양보
            if (i % itemsPerFrame == 0)
                yield return null;
        }
    }
}

오브젝트 풀링 사전 준비

c#
public class StartupObjectPool : MonoBehaviour
{
    [System.Serializable]
    public class PoolData
    {
        public GameObject prefab;
        public int preloadCount;
        public bool loadOnStart;
    }
    
    public PoolData[] pools;
    
    void Start()
    {
        // 필수 오브젝트만 즉시 로드
        foreach (var pool in pools.Where(p => p.loadOnStart))
        {
            StartCoroutine(PreloadPool(pool));
        }
        
        // 나머지는 지연 로드
        StartCoroutine(PreloadRemainingPools());
    }
    
    IEnumerator PreloadPool(PoolData pool)
    {
        var poolContainer = new GameObject($"Pool_{pool.prefab.name}");
        
        for (int i = 0; i < pool.preloadCount; i++)
        {
            var obj = Instantiate(pool.prefab, poolContainer.transform);
            obj.SetActive(false);
            
            // 2개마다 프레임 양보
            if (i % 2 == 0) yield return null;
        }
    }
}

6. 메모리 사전 최적화

시작 시 메모리 설정

c#
public class MemoryPreoptimizer : MonoBehaviour
{
    void Awake()
    {
        // 텍스처 스트리밍 설정
        QualitySettings.streamingMipmapsActive = true;
        QualitySettings.streamingMipmapsMemoryBudget = 128; // MB
        
        // 오디오 설정 최적화
        AudioSettings.GetConfiguration(out var config);
        config.sampleRate = 22050; // 모바일에서는 낮은 샘플레이트
        config.speakerMode = AudioSpeakerMode.Stereo;
        AudioSettings.Reset(config);
        
        // 가비지 컬렉션 최적화
        System.GC.Collect();
        System.GC.WaitForPendingFinalizers();
    }
}

7. 네트워크 최적화

CDN 및 캐싱 전략

c#
public class NetworkOptimizer : MonoBehaviour
{
    private static readonly Dictionary<string, byte[]> assetCache = new Dictionary<string, byte[]>();
    
    public async Task<T> LoadAssetFromCDN<T>(string url) where T : Object
    {
        // 로컬 캐시 확인
        if (assetCache.TryGetValue(url, out var cachedData))
        {
            return DeserializeAsset<T>(cachedData);
        }
        
        // CDN에서 로드
        using var request = UnityWebRequest.Get(url);
        
        // 압축 헤더 설정
        request.SetRequestHeader("Accept-Encoding", "gzip, deflate, br");
        
        await request.SendWebRequest();
        
        if (request.result == UnityWebRequest.Result.Success)
        {
            var data = request.downloadHandler.data;
            assetCache[url] = data; // 캐싱
            
            return DeserializeAsset<T>(data);
        }
        
        return null;
    }
}

8. 브라우저별 최적화

###브라우저 감지 및 최적화

c#
// JavaScript 최적화 (HTML Template에 추가)
class BrowserOptimizer {
    static detectBrowser() {
        const ua = navigator.userAgent;
        
        if (ua.includes('Chrome')) {
            return 'chrome';
        } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
            return 'safari';
        } else if (ua.includes('Firefox')) {
            return 'firefox';
        }
        
        return 'unknown';
    }
    
    static optimizeForBrowser() {
        const browser = this.detectBrowser();
        
        switch (browser) {
            case 'chrome':
                this.optimizeForChrome();
                break;
            case 'safari':
                this.optimizeForSafari();
                break;
            case 'firefox':
                this.optimizeForFirefox();
                break;
        }
    }
    
    static optimizeForSafari() {
        // Safari WebGL 컨텍스트 최적화
        Module.canvas.addEventListener('webglcontextlost', function(event) {
            event.preventDefault();
            console.log('WebGL context lost, attempting recovery...');
        });
        
        // Safari 메모리 최적화
        if (window.performance && window.performance.memory) {
            setInterval(() => {
                if (window.performance.memory.usedJSHeapSize > 100 * 1024 * 1024) {
                    console.log('High memory usage detected, triggering GC');
                    window.gc && window.gc();
                }
            }, 30000);
        }
    }
}

// Unity 로드 전 최적화 실행
BrowserOptimizer.optimizeForBrowser();

9. 개발자 도구 및 측정

시작 성능 측정 도구

c#
public class StartupBenchmark : MonoBehaviour
{
    private static readonly List<(string phase, float time)> benchmarks = new List<(string, float)>();
    
    public static void MarkPhase(string phaseName)
    {
        float time = Time.realtimeSinceStartup;
        benchmarks.Add((phaseName, time));
        
        Debug.Log($"[Benchmark] {phaseName}: {time:F2}s");
        
        // AppsInToss 분석에 전송
        AppsInToss.Analytics.LogEvent("startup_phase", new Dictionary<string, object>
        {
            {"phase", phaseName},
            {"time", time},
            {"device_model", SystemInfo.deviceModel},
            {"memory_size", SystemInfo.systemMemorySize}
        });
    }
    
    public static void GenerateReport()
    {
        var report = new StringBuilder();
        report.AppendLine("=== 시작 성능 리포트 ===");
        
        for (int i = 0; i < benchmarks.Count; i++)
        {
            var current = benchmarks[i];
            float deltaTime = i > 0 ? current.time - benchmarks[i-1].time : current.time;
            
            report.AppendLine($"{current.phase}: {current.time:F2}s (delta: {deltaTime:F2}s)");
        }
        
        Debug.Log(report.ToString());
    }
}

10. 체크리스트 및 모범 사례

시작 최적화 체크리스트

  • WASM 파일 크기 < 10MB
  • 첫 씬 에셋 크기 < 5MB
  • 첫 프레임에서 무거운 작업 제거
  • 점진적 로딩 구현
  • 브라우저별 최적화 적용
  • 성능 측정 및 분석 도구 적용
  • CDN 캐싱 전략 구현
  • 메모리 사전 최적화

일반적인 함정들

c#
// ❌ 피해야 할 패턴들

// 1. Start()에서 모든 것을 초기화
void Start() 
{
    LoadAllData(); // 블로킹
    InitializeAllSystems(); // 무거운 작업
    ConnectToServer(); // 네트워크 지연
}

// 2. Resources.Load 남용
void LoadAssets()
{
    // 동기적 로딩으로 프레임 드롭
    var texture = Resources.Load<Texture2D>("LargeTexture");
    var audio = Resources.Load<AudioClip>("LargeAudio");
}

// 3. 첫 프레임에서 복잡한 UI 생성
void Start()
{
    // 수백 개의 UI 요소를 한번에 생성
    for (int i = 0; i < 500; i++)
    {
        Instantiate(uiElementPrefab);
    }
}

올바른 패턴들

c#
// 분산된 초기화
async void Start()
{
    ShowSplashScreen();
    
    // 단계별 로딩
    await LoadEssentialData();
    ShowBasicUI();
    
    await LoadGameplayData();
    EnableGameplay();
    
    await LoadOptionalFeatures();
    OnFullyLoaded();
}

// 비동기 리소스 로딩
async Task LoadAssets()
{
    var textureTask = LoadAssetAsync<Texture2D>("LargeTexture");
    var audioTask = LoadAssetAsync<AudioClip>("LargeAudio");
    
    await Task.WhenAll(textureTask, audioTask);
}

// 점진적 UI 생성
IEnumerator CreateUI()
{
    int elementsPerFrame = 5;
    
    for (int i = 0; i < totalElements; i++)
    {
        CreateUIElement(i);
        
        if (i % elementsPerFrame == 0)
            yield return null; // 프레임 양보
    }
}

사용자는 완벽한 게임보다 빠르게 시작되는 게임을 선호해요.
필수 기능부터 빠르게 보여주고 나머지는 점진적으로 로드해요