게임 시작 속도 향상
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; // 프레임 양보
}
}사용자는 완벽한 게임보다 빠르게 시작되는 게임을 선호해요.
필수 기능부터 빠르게 보여주고 나머지는 점진적으로 로드해요
