로딩 최적화
앱인토스 Unity 게임에서 효율적인 로딩 시스템을 구현하여 사용자 경험을 향상시키고 메모리 사용량을 최적화하는 방법을 제공해요.
1. 로딩 시스템 개요
앱인토스 로딩 전략
🔄 앱인토스 로딩 시스템 구조
├── 초기 로딩 (Critical Loading)
│ ├── 게임 엔진 초기화
│ ├── 앱인토스 SDK 초기화
│ ├── 토스 인증 시스템
│ └── 첫 씬 필수 에셋
├── 백그라운드 로딩 (Background Loading)
│ ├── 다음 레벨 에셋
│ ├── 사운드 및 음악
│ └── 선택적 기능 에셋
└── 온디맨드 로딩 (On-Demand Loading)
├── 사용자 요청 시 로딩
└── IAP 관련 에셋로딩 성능 목표
- 초기 로딩: 5초 이내
- 씬 전환: 2초 이내
- 백그라운드 로딩: 게임플레이에 영향 없음
- 메모리 효율성: 언로드 에셋 관리
2. 통합 로딩 매니저
핵심 로딩 시스템
c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AppsInTossLoadingManager : MonoBehaviour
{
public static AppsInTossLoadingManager Instance { get; private set; }
[System.Serializable]
public class LoadingGroup
{
public string groupName;
public LoadingPriority priority;
public List<string> addressableKeys = new List<string>();
public List<UnityEngine.Object> resourceAssets = new List<UnityEngine.Object>();
public bool unloadOnSceneChange = true;
public float maxLoadTime = 10f;
}
public enum LoadingPriority
{
Critical, // 즉시 로딩 (게임 시작 전 완료 필요)
High, // 우선 로딩 (첫 씬에서 필요)
Medium, // 백그라운드 로딩 (게임 중 로딩)
Low // 지연 로딩 (필요시 로딩)
}
[Header("로딩 그룹 설정")]
public LoadingGroup[] loadingGroups;
[Header("앱인토스 연동")]
public bool enableTossIntegration = true;
public bool showLoadingProgress = true;
public GameObject loadingScreenPrefab;
// 로딩 상태 추적
private Dictionary<string, bool> groupLoadingStatus = new Dictionary<string, bool>();
private Dictionary<string, List<object>> loadedAssets = new Dictionary<string, List<object>>();
private AppsInTossLoadingScreen currentLoadingScreen;
// 이벤트
public System.Action<string, float> OnGroupProgressUpdated;
public System.Action<string> OnGroupLoadingComplete;
public System.Action OnAllCriticalLoadingComplete;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeLoadingSystem();
}
else
{
Destroy(gameObject);
}
}
void InitializeLoadingSystem()
{
Debug.Log("앱인토스 로딩 시스템 초기화");
// 로딩 상태 초기화
foreach (var group in loadingGroups)
{
groupLoadingStatus[group.groupName] = false;
loadedAssets[group.groupName] = new List<object>();
}
// 로딩 화면 생성
if (showLoadingProgress && loadingScreenPrefab != null)
{
var screenGO = Instantiate(loadingScreenPrefab);
currentLoadingScreen = screenGO.GetComponent<AppsInTossLoadingScreen>();
}
// 자동 로딩 시작
StartCoroutine(AutoLoadingSequence());
}
IEnumerator AutoLoadingSequence()
{
// 1. Critical 우선순위 그룹 로딩
yield return StartCoroutine(LoadGroupsByPriority(LoadingPriority.Critical));
OnAllCriticalLoadingComplete?.Invoke();
// 2. High 우선순위 그룹 로딩
yield return StartCoroutine(LoadGroupsByPriority(LoadingPriority.High));
// 3. Medium, Low 우선순위는 백그라운드에서 로딩
StartCoroutine(LoadGroupsByPriority(LoadingPriority.Medium));
StartCoroutine(LoadGroupsByPriority(LoadingPriority.Low));
// 로딩 화면 숨기기
if (currentLoadingScreen != null)
{
currentLoadingScreen.HideLoadingScreen();
}
}
IEnumerator LoadGroupsByPriority(LoadingPriority priority)
{
var targetGroups = System.Array.FindAll(loadingGroups, g => g.priority == priority);
foreach (var group in targetGroups)
{
yield return StartCoroutine(LoadGroup(group));
}
}
public IEnumerator LoadGroup(string groupName)
{
var group = System.Array.Find(loadingGroups, g => g.groupName == groupName);
if (group != null)
{
yield return StartCoroutine(LoadGroup(group));
}
}
IEnumerator LoadGroup(LoadingGroup group)
{
if (groupLoadingStatus.ContainsKey(group.groupName) && groupLoadingStatus[group.groupName])
{
yield break; // 이미 로딩된 그룹
}
Debug.Log($"로딩 그룹 시작: {group.groupName} (우선순위: {group.priority})");
float startTime = Time.realtimeSinceStartup;
int totalAssets = group.addressableKeys.Count + group.resourceAssets.Count;
int loadedCount = 0;
// Addressable 에셋 로딩
foreach (var key in group.addressableKeys)
{
yield return StartCoroutine(LoadAddressableAsset(key, group.groupName));
loadedCount++;
// 진행률 업데이트
float progress = (float)loadedCount / totalAssets;
OnGroupProgressUpdated?.Invoke(group.groupName, progress);
UpdateLoadingUI(group.groupName, progress);
// 타임아웃 체크
if (Time.realtimeSinceStartup - startTime > group.maxLoadTime)
{
Debug.LogWarning($"로딩 그룹 타임아웃: {group.groupName}");
break;
}
}
// Resources 에셋 로딩
foreach (var asset in group.resourceAssets)
{
if (asset != null)
{
loadedAssets[group.groupName].Add(asset);
loadedCount++;
float progress = (float)loadedCount / totalAssets;
OnGroupProgressUpdated?.Invoke(group.groupName, progress);
UpdateLoadingUI(group.groupName, progress);
}
yield return null; // 프레임 분산
}
// 그룹 로딩 완료 처리
groupLoadingStatus[group.groupName] = true;
float loadTime = Time.realtimeSinceStartup - startTime;
Debug.Log($"로딩 그룹 완료: {group.groupName} ({loadTime:F2}초, {loadedCount}/{totalAssets} 에셋)");
OnGroupLoadingComplete?.Invoke(group.groupName);
// 앱인토스 분석에 로딩 데이터 전송
ReportLoadingMetrics(group.groupName, loadTime, loadedCount, totalAssets);
}
IEnumerator LoadAddressableAsset(string key, string groupName)
{
var handle = Addressables.LoadAssetAsync<UnityEngine.Object>(key);
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
loadedAssets[groupName].Add(handle.Result);
Debug.Log($"Addressable 에셋 로딩 성공: {key}");
}
else
{
Debug.LogError($"Addressable 에셋 로딩 실패: {key} - {handle.OperationException}");
}
}
void UpdateLoadingUI(string groupName, float progress)
{
if (currentLoadingScreen != null)
{
currentLoadingScreen.UpdateGroupProgress(groupName, progress);
}
// 앱인토스 네이티브 로딩 UI 업데이트
if (enableTossIntegration)
{
AppsInToss.UpdateLoadingProgress(progress, $"{groupName} 로딩 중...");
}
}
void ReportLoadingMetrics(string groupName, float loadTime, int loadedCount, int totalAssets)
{
var metrics = new Dictionary<string, object>
{
{"group_name", groupName},
{"load_time", loadTime},
{"loaded_assets", loadedCount},
{"total_assets", totalAssets},
{"success_rate", (float)loadedCount / totalAssets},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
AppsInToss.SendAnalytics("loading_performance", metrics);
}
// 공개 API
public bool IsGroupLoaded(string groupName)
{
return groupLoadingStatus.ContainsKey(groupName) && groupLoadingStatus[groupName];
}
public void UnloadGroup(string groupName)
{
if (!loadedAssets.ContainsKey(groupName)) return;
Debug.Log($"로딩 그룹 언로드: {groupName}");
foreach (var asset in loadedAssets[groupName])
{
if (asset is AsyncOperationHandle)
{
Addressables.Release((AsyncOperationHandle)asset);
}
}
loadedAssets[groupName].Clear();
groupLoadingStatus[groupName] = false;
}
public void ForceLoadGroup(string groupName, System.Action<bool> onComplete = null)
{
StartCoroutine(ForceLoadGroupCoroutine(groupName, onComplete));
}
IEnumerator ForceLoadGroupCoroutine(string groupName, System.Action<bool> onComplete)
{
yield return StartCoroutine(LoadGroup(groupName));
onComplete?.Invoke(IsGroupLoaded(groupName));
}
}3. 스마트 에셋 로더
동적 에셋 관리
c#
public class SmartAssetLoader : MonoBehaviour
{
[System.Serializable]
public class AssetCategory
{
public string categoryName;
public AssetType assetType;
public List<string> assetAddresses;
public int maxCacheSize = 10;
public float cacheTimeout = 300f; // 5분
}
public enum AssetType
{
Texture,
AudioClip,
GameObject,
Material,
Animation,
Font
}
[Header("에셋 카테고리")]
public AssetCategory[] assetCategories;
[Header("앱인토스 최적화 설정")]
public bool enableMemoryOptimization = true;
public long maxMemoryUsageMB = 150; // 앱인토스 권장 제한
// 캐시 관리
private Dictionary<string, CachedAsset> assetCache = new Dictionary<string, CachedAsset>();
private Queue<string> recentlyUsedAssets = new Queue<string>();
[System.Serializable]
private class CachedAsset
{
public object asset;
public float lastAccessTime;
public int accessCount;
public long memorySize;
public AsyncOperationHandle handle;
}
void Start()
{
// 메모리 모니터링 시작
if (enableMemoryOptimization)
{
InvokeRepeating(nameof(OptimizeMemoryUsage), 30f, 30f);
}
}
// 에셋 로딩 API
public void LoadAssetAsync<T>(string address, System.Action<T> onComplete, System.Action<string> onError = null) where T : UnityEngine.Object
{
StartCoroutine(LoadAssetCoroutine<T>(address, onComplete, onError));
}
IEnumerator LoadAssetCoroutine<T>(string address, System.Action<T> onComplete, System.Action<string> onError) where T : UnityEngine.Object
{
// 캐시 확인
if (assetCache.ContainsKey(address))
{
var cached = assetCache[address];
cached.lastAccessTime = Time.realtimeSinceStartup;
cached.accessCount++;
onComplete?.Invoke(cached.asset as T);
yield break;
}
// 새로운 에셋 로딩
var handle = Addressables.LoadAssetAsync<T>(address);
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
// 캐시에 추가
var cachedAsset = new CachedAsset
{
asset = handle.Result,
lastAccessTime = Time.realtimeSinceStartup,
accessCount = 1,
memorySize = EstimateAssetMemorySize(handle.Result),
handle = handle
};
assetCache[address] = cachedAsset;
recentlyUsedAssets.Enqueue(address);
// 메모리 체크
CheckMemoryUsage();
onComplete?.Invoke(handle.Result);
Debug.Log($"에셋 로딩 완료: {address}");
}
else
{
string error = $"에셋 로딩 실패: {address} - {handle.OperationException}";
Debug.LogError(error);
onError?.Invoke(error);
}
}
// 배치 로딩
public void LoadAssetsAsync<T>(List<string> addresses, System.Action<List<T>> onComplete, System.Action<float> onProgress = null) where T : UnityEngine.Object
{
StartCoroutine(LoadAssetsBatchCoroutine<T>(addresses, onComplete, onProgress));
}
IEnumerator LoadAssetsBatchCoroutine<T>(List<string> addresses, System.Action<List<T>> onComplete, System.Action<float> onProgress) where T : UnityEngine.Object
{
var results = new List<T>();
int loadedCount = 0;
foreach (var address in addresses)
{
bool loadCompleted = false;
T loadedAsset = null;
LoadAssetAsync<T>(address,
(asset) => {
loadedAsset = asset;
loadCompleted = true;
},
(error) => {
loadCompleted = true;
}
);
// 로딩 완료 대기
yield return new WaitUntil(() => loadCompleted);
if (loadedAsset != null)
{
results.Add(loadedAsset);
}
loadedCount++;
float progress = (float)loadedCount / addresses.Count;
onProgress?.Invoke(progress);
}
onComplete?.Invoke(results);
}
long EstimateAssetMemorySize(UnityEngine.Object asset)
{
// 에셋 타입별 메모리 크기 추정
if (asset is Texture2D texture)
{
return texture.width * texture.height * 4; // RGBA32 기준
}
else if (asset is AudioClip audio)
{
return audio.samples * audio.channels * 2; // 16-bit PCM 기준
}
else if (asset is Mesh mesh)
{
return mesh.vertexCount * 32; // 추정값
}
else
{
return 1024 * 1024; // 1MB 기본값
}
}
void CheckMemoryUsage()
{
if (!enableMemoryOptimization) return;
long totalMemory = 0;
foreach (var cached in assetCache.Values)
{
totalMemory += cached.memorySize;
}
long currentMemoryMB = totalMemory / (1024 * 1024);
if (currentMemoryMB > maxMemoryUsageMB)
{
Debug.LogWarning($"에셋 캐시 메모리 사용량 초과: {currentMemoryMB}MB > {maxMemoryUsageMB}MB");
OptimizeMemoryUsage();
}
}
void OptimizeMemoryUsage()
{
var sortedAssets = new List<KeyValuePair<string, CachedAsset>>();
foreach (var kvp in assetCache)
{
sortedAssets.Add(kvp);
}
// 접근 빈도와 최근 사용 시간을 고려하여 정렬
sortedAssets.Sort((a, b) => {
float scoreA = a.Value.accessCount / (Time.realtimeSinceStartup - a.Value.lastAccessTime + 1);
float scoreB = b.Value.accessCount / (Time.realtimeSinceStartup - b.Value.lastAccessTime + 1);
return scoreA.CompareTo(scoreB);
});
// 하위 30% 에셋 언로드
int assetsToUnload = Mathf.CeilToInt(sortedAssets.Count * 0.3f);
for (int i = 0; i < assetsToUnload && i < sortedAssets.Count; i++)
{
string address = sortedAssets[i].Key;
UnloadAsset(address);
}
Debug.Log($"메모리 최적화 완료: {assetsToUnload}개 에셋 언로드");
}
public void UnloadAsset(string address)
{
if (assetCache.ContainsKey(address))
{
var cached = assetCache[address];
if (cached.handle.IsValid())
{
Addressables.Release(cached.handle);
}
assetCache.Remove(address);
Debug.Log($"에셋 언로드: {address}");
}
}
public void PreloadCategory(string categoryName, System.Action<bool> onComplete = null)
{
var category = System.Array.Find(assetCategories, c => c.categoryName == categoryName);
if (category != null)
{
StartCoroutine(PreloadCategoryCoroutine(category, onComplete));
}
}
IEnumerator PreloadCategoryCoroutine(AssetCategory category, System.Action<bool> onComplete)
{
Debug.Log($"카테고리 사전 로딩 시작: {category.categoryName}");
int loadedCount = 0;
int totalAssets = category.assetAddresses.Count;
foreach (var address in category.assetAddresses)
{
bool loadCompleted = false;
LoadAssetAsync<UnityEngine.Object>(address,
(asset) => {
loadedCount++;
loadCompleted = true;
},
(error) => {
loadCompleted = true;
}
);
yield return new WaitUntil(() => loadCompleted);
}
bool success = loadedCount == totalAssets;
Debug.Log($"카테고리 사전 로딩 완료: {category.categoryName} ({loadedCount}/{totalAssets})");
onComplete?.Invoke(success);
}
// 통계 및 디버깅
public void LogCacheStatistics()
{
Debug.Log($"=== 에셋 캐시 통계 ===");
Debug.Log($"캐시된 에셋 수: {assetCache.Count}");
long totalMemory = 0;
foreach (var cached in assetCache.Values)
{
totalMemory += cached.memorySize;
}
Debug.Log($"총 캐시 메모리: {totalMemory / (1024 * 1024)}MB");
Debug.Log($"메모리 제한: {maxMemoryUsageMB}MB");
}
void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
// 앱이 백그라운드로 갈 때 메모리 최적화
OptimizeMemoryUsage();
}
}
}4. 씬별 로딩 관리
씬 전환 로딩 시스템
c#
public class SceneLoadingManager : MonoBehaviour
{
[System.Serializable]
public class SceneLoadingConfig
{
public string sceneName;
public List<string> requiredAssetGroups = new List<string>();
public List<string> preloadAssetGroups = new List<string>();
public bool showLoadingScreen = true;
public float minLoadingTime = 1f; // 최소 로딩 시간 (UX)
}
[Header("씬별 로딩 설정")]
public SceneLoadingConfig[] sceneConfigs;
[Header("앱인토스 연동")]
public GameObject sceneTransitionPrefab;
public bool enableTossSceneAnalytics = true;
private string currentScene;
private float sceneLoadStartTime;
void Start()
{
currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
}
public void LoadSceneAsync(string sceneName, System.Action onComplete = null)
{
StartCoroutine(LoadSceneCoroutine(sceneName, onComplete));
}
IEnumerator LoadSceneCoroutine(string sceneName, System.Action onComplete)
{
sceneLoadStartTime = Time.realtimeSinceStartup;
var config = System.Array.Find(sceneConfigs, c => c.sceneName == sceneName);
if (config == null)
{
Debug.LogError($"씬 로딩 설정을 찾을 수 없습니다: {sceneName}");
yield break;
}
// 로딩 화면 표시
AppsInTossLoadingScreen loadingScreen = null;
if (config.showLoadingScreen && sceneTransitionPrefab != null)
{
var screenGO = Instantiate(sceneTransitionPrefab);
loadingScreen = screenGO.GetComponent<AppsInTossLoadingScreen>();
loadingScreen.ShowSceneTransition(sceneName);
}
// 1. 필수 에셋 그룹 로딩
foreach (var groupName in config.requiredAssetGroups)
{
if (!AppsInTossLoadingManager.Instance.IsGroupLoaded(groupName))
{
yield return AppsInTossLoadingManager.Instance.LoadGroup(groupName);
}
}
// 2. 이전 씬 정리
yield return StartCoroutine(CleanupCurrentScene());
// 3. 새로운 씬 로딩
var sceneLoadOp = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName);
sceneLoadOp.allowSceneActivation = false;
// 씬 로딩 진행률 모니터링
while (sceneLoadOp.progress < 0.9f)
{
if (loadingScreen != null)
{
loadingScreen.UpdateProgress(sceneLoadOp.progress);
}
yield return null;
}
// 4. 사전 로딩 그룹 (백그라운드)
foreach (var groupName in config.preloadAssetGroups)
{
AppsInTossLoadingManager.Instance.ForceLoadGroup(groupName);
}
// 최소 로딩 시간 대기 (UX 개선)
float currentLoadTime = Time.realtimeSinceStartup - sceneLoadStartTime;
if (currentLoadTime < config.minLoadingTime)
{
yield return new WaitForSeconds(config.minLoadingTime - currentLoadTime);
}
// 씬 활성화
sceneLoadOp.allowSceneActivation = true;
yield return sceneLoadOp;
// 로딩 화면 숨기기
if (loadingScreen != null)
{
loadingScreen.HideLoadingScreen();
}
// 씬 전환 완료 처리
OnSceneLoadComplete(sceneName);
onComplete?.Invoke();
}
IEnumerator CleanupCurrentScene()
{
Debug.Log($"이전 씬 정리 시작: {currentScene}");
// 현재 씬의 언로드 대상 에셋 그룹 찾기
var currentConfig = System.Array.Find(sceneConfigs, c => c.sceneName == currentScene);
if (currentConfig != null)
{
// 씬별 전용 에셋 언로드
foreach (var groupName in currentConfig.requiredAssetGroups)
{
var group = System.Array.Find(AppsInTossLoadingManager.Instance.loadingGroups,
g => g.groupName == groupName);
if (group != null && group.unloadOnSceneChange)
{
AppsInTossLoadingManager.Instance.UnloadGroup(groupName);
}
}
}
// 메모리 정리
System.GC.Collect();
yield return Resources.UnloadUnusedAssets();
Debug.Log("이전 씬 정리 완료");
}
void OnSceneLoadComplete(string sceneName)
{
float totalLoadTime = Time.realtimeSinceStartup - sceneLoadStartTime;
Debug.Log($"씬 로딩 완료: {currentScene} → {sceneName} ({totalLoadTime:F2}초)");
currentScene = sceneName;
// 앱인토스 분석에 씬 전환 데이터 전송
if (enableTossSceneAnalytics)
{
var sceneMetrics = new Dictionary<string, object>
{
{"from_scene", currentScene},
{"to_scene", sceneName},
{"load_time", totalLoadTime},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
AppsInToss.SendAnalytics("scene_transition", sceneMetrics);
}
// 성능 목표 체크 (2초 이내)
if (totalLoadTime > 2f)
{
Debug.LogWarning($"씬 전환 시간이 목표를 초과: {totalLoadTime:F2}s > 2s");
AppsInToss.ReportPerformanceIssue("scene_load_slow", totalLoadTime);
}
}
// 빠른 씬 전환 (미리 로딩된 씬)
public void QuickLoadScene(string sceneName)
{
var config = System.Array.Find(sceneConfigs, c => c.sceneName == sceneName);
if (config == null) return;
// 모든 필수 에셋이 로딩되었는지 확인
bool allAssetsReady = true;
foreach (var groupName in config.requiredAssetGroups)
{
if (!AppsInTossLoadingManager.Instance.IsGroupLoaded(groupName))
{
allAssetsReady = false;
break;
}
}
if (allAssetsReady)
{
// 즉시 씬 전환
UnityEngine.SceneManagement.SceneManager.LoadScene(sceneName);
OnSceneLoadComplete(sceneName);
}
else
{
// 일반 로딩 방식으로 폴백
LoadSceneAsync(sceneName);
}
}
}5. 프리로딩 시스템
인텔리전트 프리로딩
c#
public class IntelligentPreloader : MonoBehaviour
{
[System.Serializable]
public class PreloadingRule
{
public string ruleName;
public PreloadTrigger trigger;
public List<string> targetAssetGroups;
public float probability = 1.0f; // 프리로딩 확률 (0-1)
public int maxConcurrentLoads = 2;
}
public enum PreloadTrigger
{
OnSceneStart,
OnUserIdle,
OnMenuOpen,
OnGamePause,
OnLowActivity,
OnTossPayReady // 앱인토스 특화
}
[Header("프리로딩 규칙")]
public PreloadingRule[] preloadingRules;
[Header("사용자 행동 분석")]
public bool enableUserBehaviorAnalysis = true;
public float idleThreshold = 5f; // 유휴 상태 판정 시간
private float lastUserActivity;
private Dictionary<string, float> assetUsageFrequency = new Dictionary<string, float>();
private Queue<string> recentScenes = new Queue<string>();
void Start()
{
lastUserActivity = Time.realtimeSinceStartup;
if (enableUserBehaviorAnalysis)
{
StartCoroutine(AnalyzeUserBehavior());
}
// 씬 시작 시 프리로딩 규칙 적용
ExecutePreloadingRules(PreloadTrigger.OnSceneStart);
}
void Update()
{
// 사용자 활동 감지
if (Input.anyKeyDown || Input.touchCount > 0)
{
lastUserActivity = Time.realtimeSinceStartup;
}
}
IEnumerator AnalyzeUserBehavior()
{
while (true)
{
yield return new WaitForSeconds(1f);
// 유휴 상태 감지
float idleTime = Time.realtimeSinceStartup - lastUserActivity;
if (idleTime > idleThreshold)
{
ExecutePreloadingRules(PreloadTrigger.OnUserIdle);
yield return new WaitForSeconds(10f); // 중복 실행 방지
}
}
}
public void ExecutePreloadingRules(PreloadTrigger trigger)
{
var applicableRules = System.Array.FindAll(preloadingRules, r => r.trigger == trigger);
foreach (var rule in applicableRules)
{
// 확률 기반 실행
if (UnityEngine.Random.Range(0f, 1f) <= rule.probability)
{
StartCoroutine(ExecutePreloadingRule(rule));
}
}
}
IEnumerator ExecutePreloadingRule(PreloadingRule rule)
{
Debug.Log($"프리로딩 규칙 실행: {rule.ruleName}");
var loadingTasks = new List<Coroutine>();
foreach (var groupName in rule.targetAssetGroups)
{
// 동시 로딩 수 제한
while (loadingTasks.Count >= rule.maxConcurrentLoads)
{
yield return null;
// 완료된 작업 제거
loadingTasks.RemoveAll(task => task == null);
}
// 이미 로딩된 그룹은 스킵
if (AppsInTossLoadingManager.Instance.IsGroupLoaded(groupName))
{
continue;
}
// 사용 빈도 기반 우선순위 적용
float frequency = assetUsageFrequency.ContainsKey(groupName) ?
assetUsageFrequency[groupName] : 0f;
if (frequency > 0.3f || rule.trigger == PreloadTrigger.OnSceneStart) // 30% 이상 사용률 or 씬 시작
{
var loadTask = StartCoroutine(PreloadAssetGroup(groupName));
loadingTasks.Add(loadTask);
}
}
// 모든 프리로딩 작업 완료 대기
foreach (var task in loadingTasks)
{
if (task != null)
{
yield return task;
}
}
Debug.Log($"프리로딩 규칙 완료: {rule.ruleName}");
}
IEnumerator PreloadAssetGroup(string groupName)
{
yield return AppsInTossLoadingManager.Instance.LoadGroup(groupName);
// 사용 빈도 업데이트
if (assetUsageFrequency.ContainsKey(groupName))
{
assetUsageFrequency[groupName] += 0.1f;
}
else
{
assetUsageFrequency[groupName] = 0.1f;
}
}
// 앱인토스 특화 프리로딩
public void PreloadTossPayAssets()
{
ExecutePreloadingRules(PreloadTrigger.OnTossPayReady);
// 토스페이 관련 에셋 사전 로딩
var tossPayAssets = new List<string> { "TossPayUI", "PaymentIcons", "ReceiptTemplates" };
foreach (var assetGroup in tossPayAssets)
{
if (AppsInTossLoadingManager.Instance.IsGroupLoaded(assetGroup))
{
continue;
}
AppsInTossLoadingManager.Instance.ForceLoadGroup(assetGroup, (success) => {
if (success)
{
Debug.Log($"토스페이 에셋 프리로딩 완료: {assetGroup}");
}
});
}
}
// 씬 전환 패턴 학습
public void OnSceneChanged(string newScene)
{
recentScenes.Enqueue(newScene);
if (recentScenes.Count > 5)
{
recentScenes.Dequeue();
}
// 패턴 기반 예측 프리로딩
PredictivePreload(newScene);
}
void PredictivePreload(string currentScene)
{
// 씬 전환 패턴 분석 후 다음 씬 예측 프리로딩
var sceneHistory = recentScenes.ToArray();
// 간단한 패턴 매칭 (실제로는 더 복잡한 ML 알고리즘 적용 가능)
if (sceneHistory.Length >= 3)
{
string predictedNext = PredictNextScene(sceneHistory);
if (!string.IsNullOrEmpty(predictedNext))
{
var sceneConfig = FindObjectOfType<SceneLoadingManager>()?.sceneConfigs
?.FirstOrDefault(c => c.sceneName == predictedNext);
if (sceneConfig != null)
{
// 예측된 씬의 에셋 사전 로딩
foreach (var groupName in sceneConfig.requiredAssetGroups)
{
if (!AppsInTossLoadingManager.Instance.IsGroupLoaded(groupName))
{
StartCoroutine(PreloadAssetGroup(groupName));
}
}
}
}
}
}
string PredictNextScene(string[] sceneHistory)
{
// 간단한 패턴 매칭 로직
// 실제 구현에서는 머신러닝 또는 더 정교한 예측 알고리즘 사용
return null; // 구현 필요
}
}6. 성능 모니터링 및 최적화
로딩 성능 분석 도구
c#
public class LoadingPerformanceAnalyzer : MonoBehaviour
{
[System.Serializable]
public class LoadingMetrics
{
public string assetGroup;
public float loadTime;
public int assetCount;
public long memoryUsage;
public bool success;
public string errorMessage;
public System.DateTime timestamp;
}
private List<LoadingMetrics> performanceHistory = new List<LoadingMetrics>();
private const int maxHistorySize = 100;
void Start()
{
// 로딩 이벤트 구독
if (AppsInTossLoadingManager.Instance != null)
{
AppsInTossLoadingManager.Instance.OnGroupLoadingComplete += OnGroupLoadingComplete;
}
}
void OnGroupLoadingComplete(string groupName)
{
AnalyzeLoadingPerformance(groupName);
}
void AnalyzeLoadingPerformance(string groupName)
{
var metrics = new LoadingMetrics
{
assetGroup = groupName,
loadTime = Time.realtimeSinceStartup,
memoryUsage = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(false),
success = AppsInTossLoadingManager.Instance.IsGroupLoaded(groupName),
timestamp = System.DateTime.UtcNow
};
performanceHistory.Add(metrics);
// 히스토리 크기 제한
if (performanceHistory.Count > maxHistorySize)
{
performanceHistory.RemoveAt(0);
}
// 성능 이슈 감지
DetectPerformanceIssues(metrics);
// 앱인토스 분석 시스템에 전송
SendLoadingAnalytics(metrics);
}
void DetectPerformanceIssues(LoadingMetrics metrics)
{
var issues = new List<string>();
// 로딩 시간 체크
if (metrics.loadTime > 10f)
{
issues.Add($"긴 로딩 시간: {metrics.loadTime:F2}초");
}
// 메모리 사용량 체크
long memoryMB = metrics.memoryUsage / (1024 * 1024);
if (memoryMB > 200)
{
issues.Add($"높은 메모리 사용량: {memoryMB}MB");
}
// 로딩 실패 체크
if (!metrics.success)
{
issues.Add("로딩 실패");
}
if (issues.Count > 0)
{
Debug.LogWarning($"로딩 성능 이슈 감지 [{metrics.assetGroup}]: {string.Join(", ", issues)}");
foreach (var issue in issues)
{
AppsInToss.ReportPerformanceIssue($"loading_{metrics.assetGroup}", issue);
}
}
}
void SendLoadingAnalytics(LoadingMetrics metrics)
{
var analyticsData = new Dictionary<string, object>
{
{"asset_group", metrics.assetGroup},
{"load_time", metrics.loadTime},
{"memory_mb", metrics.memoryUsage / (1024f * 1024f)},
{"success", metrics.success},
{"device_model", SystemInfo.deviceModel},
{"timestamp", metrics.timestamp.ToString("o")}
};
AppsInToss.SendAnalytics("loading_performance", analyticsData);
}
// 성능 리포트 생성
public string GeneratePerformanceReport()
{
if (performanceHistory.Count == 0)
{
return "성능 데이터가 없습니다.";
}
var report = new System.Text.StringBuilder();
report.AppendLine("=== 로딩 성능 리포트 ===");
report.AppendLine($"분석 기간: {performanceHistory.First().timestamp:yyyy-MM-dd HH:mm:ss} ~ {performanceHistory.Last().timestamp:yyyy-MM-dd HH:mm:ss}");
report.AppendLine();
// 전체 통계
float avgLoadTime = performanceHistory.Average(m => m.loadTime);
float maxLoadTime = performanceHistory.Max(m => m.loadTime);
float avgMemoryMB = performanceHistory.Average(m => m.memoryUsage) / (1024f * 1024f);
float successRate = performanceHistory.Count(m => m.success) / (float)performanceHistory.Count * 100f;
report.AppendLine($"평균 로딩 시간: {avgLoadTime:F2}초");
report.AppendLine($"최대 로딩 시간: {maxLoadTime:F2}초");
report.AppendLine($"평균 메모리 사용량: {avgMemoryMB:F1}MB");
report.AppendLine($"성공률: {successRate:F1}%");
report.AppendLine();
// 그룹별 통계
var groupStats = performanceHistory
.GroupBy(m => m.assetGroup)
.Select(g => new {
Group = g.Key,
AvgTime = g.Average(m => m.loadTime),
MaxTime = g.Max(m => m.loadTime),
Count = g.Count(),
SuccessRate = g.Count(m => m.success) / (float)g.Count() * 100f
});
report.AppendLine("=== 에셋 그룹별 성능 ===");
foreach (var stat in groupStats.OrderByDescending(s => s.AvgTime))
{
report.AppendLine($"[{stat.Group}] 평균: {stat.AvgTime:F2}초, 최대: {stat.MaxTime:F2}초, 성공률: {stat.SuccessRate:F1}% ({stat.Count}회)");
}
return report.ToString();
}
// 최적화 제안 생성
public List<string> GenerateOptimizationSuggestions()
{
var suggestions = new List<string>();
if (performanceHistory.Count == 0) return suggestions;
// 느린 로딩 그룹 식별
var slowGroups = performanceHistory
.GroupBy(m => m.assetGroup)
.Where(g => g.Average(m => m.loadTime) > 5f)
.Select(g => g.Key);
foreach (var group in slowGroups)
{
suggestions.Add($"{group} 그룹의 로딩 시간 최적화 필요 (5초 초과)");
}
// 높은 메모리 사용량 그룹
var memoryIntensiveGroups = performanceHistory
.GroupBy(m => m.assetGroup)
.Where(g => g.Average(m => m.memoryUsage) > 100 * 1024 * 1024) // 100MB
.Select(g => g.Key);
foreach (var group in memoryIntensiveGroups)
{
suggestions.Add($"{group} 그룹의 메모리 사용량 최적화 필요 (100MB 초과)");
}
// 실패율이 높은 그룹
var unreliableGroups = performanceHistory
.GroupBy(m => m.assetGroup)
.Where(g => g.Count(m => !m.success) / (float)g.Count() > 0.1f) // 10% 이상 실패
.Select(g => g.Key);
foreach (var group in unreliableGroups)
{
suggestions.Add($"{group} 그룹의 로딩 안정성 개선 필요 (실패율 10% 이상)");
}
return suggestions;
}
}7. 체크리스트 및 권장사항
로딩 시스템 체크리스트
- 통합 로딩 매니저 구현
- 우선순위 기반 로딩 시스템 적용
- 스마트 에셋 캐싱 시스템 구현
- 씬별 로딩 관리 시스템 설정
- 인텔리전트 프리로딩 시스템 적용
- 메모리 사용량 모니터링 시스템
- 로딩 성능 분석 도구 설치
- 앱인토스 네이티브 연동 확인
- 다양한 기기에서 성능 테스트
- 사용자 행동 패턴 기반 최적화
앱인토스 특화 권장사항
- 토스 브랜딩: 로딩 화면에 토스 디자인 시스템 적용
- 메모리 제한: 150MB 이내 캐시 메모리 사용량 유지
- 토스페이 연동: 결제 관련 에셋 사전 로딩
- 사용자 분석: 앱인토스 분석 시스템과 연동
- 성능 모니터링: 실시간 성능 데이터 수집 및 분석
효율적인 로딩 시스템은 사용자 경험의 핵심이에요. 우선순위 기반 로딩과 지능적인 캐싱으로 최적의 성능을 달성하세요.
