코드 분할 도구 사용 가이드
AppsInToss Unity 게임에서 WebAssembly 코드 분할을 통해 초기 로딩 속도를 개선하고 메모리 사용량을 최적화하는 방법을 다뤄요.
1. WASM 코드 분할 개념
코드 분할 전략
📦 WASM 코드 분할 구조
├── Core Module (핵심 모듈)
│ ├── 게임 엔진 핵심
│ ├── 렌더링 시스템
│ ├── 입력 관리
│ └── 기본 물리 연산
├── Feature Modules (기능별 모듈)
│ ├── AI 시스템
│ ├── 네트워킹
│ ├── 오디오 처리
│ └── 고급 물리 연산
├── Content Modules (콘텐츠 모듈)
│ ├── 레벨별 로직
│ ├── 캐릭터 시스템
│ ├── 스킬 시스템
│ └── UI 시스템
└── Platform Modules (플랫폼별)
├── AppsInToss 통합
├── 소셜 기능
├── 결제 시스템
└── 광고 시스템WASM 분할 매니저
c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class WasmSplitManager : MonoBehaviour
{
public static WasmSplitManager Instance { get; private set; }
[System.Serializable]
public class WasmModule
{
public string moduleName;
public string moduleUrl;
public ModulePriority priority;
public string[] dependencies;
public bool preload = false;
public float estimatedSizeMB;
public ModuleState state = ModuleState.NotLoaded;
}
public enum ModulePriority
{
Critical = 0, // 게임 시작 전 필수
High = 1, // 초기 로딩 시 필요
Medium = 2, // 기능 사용 시 로딩
Low = 3, // 선택적 로딩
OnDemand = 4 // 요청 시에만 로딩
}
public enum ModuleState
{
NotLoaded,
Loading,
Loaded,
Failed
}
[Header("WASM 모듈 설정")]
public WasmModule[] wasmModules;
[Header("로딩 설정")]
public bool enableAsyncLoading = true;
public int maxConcurrentLoads = 2;
public float loadTimeoutSeconds = 30f;
// 내부 상태
private Dictionary<string, WasmModule> moduleMap = new Dictionary<string, WasmModule>();
private HashSet<string> loadingModules = new HashSet<string>();
private Queue<WasmModule> loadQueue = new Queue<WasmModule>();
private int activeLoads = 0;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeWasmSplit();
}
else
{
Destroy(gameObject);
}
}
void InitializeWasmSplit()
{
// 모듈 맵 생성
foreach (var module in wasmModules)
{
moduleMap[module.moduleName] = module;
}
// Critical 모듈들 즉시 로딩 시작
LoadCriticalModules();
// 백그라운드 로딩 시작
StartCoroutine(ProcessLoadQueue());
Debug.Log("WASM 코드 분할 시스템 초기화 완료");
}
void LoadCriticalModules()
{
var criticalModules = System.Array.FindAll(wasmModules,
m => m.priority == ModulePriority.Critical);
foreach (var module in criticalModules)
{
QueueModuleLoad(module.moduleName);
}
Debug.Log($"Critical 모듈 로딩 시작: {criticalModules.Length}개");
}
public void QueueModuleLoad(string moduleName)
{
if (moduleMap.ContainsKey(moduleName) &&
moduleMap[moduleName].state == ModuleState.NotLoaded &&
!loadingModules.Contains(moduleName))
{
loadQueue.Enqueue(moduleMap[moduleName]);
}
}
IEnumerator ProcessLoadQueue()
{
while (true)
{
while (loadQueue.Count > 0 && activeLoads < maxConcurrentLoads)
{
var module = loadQueue.Dequeue();
StartCoroutine(LoadWasmModule(module));
}
yield return new WaitForSeconds(0.1f);
}
}
IEnumerator LoadWasmModule(WasmModule module)
{
loadingModules.Add(module.moduleName);
module.state = ModuleState.Loading;
activeLoads++;
Debug.Log($"WASM 모듈 로딩 시작: {module.moduleName}");
float startTime = Time.realtimeSinceStartup;
// 의존성 체크
yield return StartCoroutine(EnsureDependencies(module));
// 실제 모듈 로딩
bool loadSuccess = false;
#if UNITY_WEBGL && !UNITY_EDITOR
// WebGL에서 실제 WASM 모듈 로딩
loadSuccess = yield return StartCoroutine(LoadWasmModuleWebGL(module));
#else
// 에디터/다른 플랫폼에서는 시뮬레이션
yield return new WaitForSeconds(0.5f); // 로딩 시뮬레이션
loadSuccess = true;
#endif
// 로딩 결과 처리
float loadTime = Time.realtimeSinceStartup - startTime;
if (loadSuccess)
{
module.state = ModuleState.Loaded;
OnModuleLoadSuccess(module, loadTime);
}
else
{
module.state = ModuleState.Failed;
OnModuleLoadFailed(module, loadTime);
}
loadingModules.Remove(module.moduleName);
activeLoads--;
}
IEnumerator EnsureDependencies(WasmModule module)
{
foreach (var dependency in module.dependencies)
{
if (moduleMap.ContainsKey(dependency))
{
var depModule = moduleMap[dependency];
if (depModule.state == ModuleState.NotLoaded)
{
// 의존성 모듈을 먼저 로딩
yield return StartCoroutine(LoadWasmModule(depModule));
}
else if (depModule.state == ModuleState.Loading)
{
// 의존성 모듈 로딩 완료 대기
while (depModule.state == ModuleState.Loading)
{
yield return new WaitForSeconds(0.1f);
}
}
if (depModule.state == ModuleState.Failed)
{
Debug.LogError($"의존성 모듈 로딩 실패: {dependency}");
yield break;
}
}
}
}
#if UNITY_WEBGL && !UNITY_EDITOR
IEnumerator LoadWasmModuleWebGL(WasmModule module)
{
// JavaScript와 상호작용하여 WASM 모듈 로딩
string loadCommand = $"loadWasmModule('{module.moduleName}', '{module.moduleUrl}')";
// JS 함수 호출
Application.ExternalCall("eval", loadCommand);
// 로딩 완료 대기
float timeout = Time.realtimeSinceStartup + loadTimeoutSeconds;
while (Time.realtimeSinceStartup < timeout)
{
// JS에서 로딩 상태 확인
string status = GetWasmModuleStatus(module.moduleName);
if (status == "loaded")
{
yield return true;
}
else if (status == "failed")
{
yield return false;
}
yield return new WaitForSeconds(0.1f);
}
// 타임아웃
Debug.LogError($"WASM 모듈 로딩 타임아웃: {module.moduleName}");
yield return false;
}
[DllImport("__Internal")]
private static extern string GetWasmModuleStatus(string moduleName);
#endif
void OnModuleLoadSuccess(WasmModule module, float loadTime)
{
Debug.Log($"WASM 모듈 로딩 성공: {module.moduleName} ({loadTime:F2}초)");
// 성공 이벤트 발송
AppsInToss.SendEvent("wasm_module_loaded", new Dictionary<string, object>
{
{"module_name", module.moduleName},
{"load_time", loadTime},
{"estimated_size_mb", module.estimatedSizeMB}
});
// 분석 데이터 전송
SendModuleAnalytics(module, true, loadTime);
// High 우선순위 모듈들 자동 로딩
TriggerHighPriorityLoading();
}
void OnModuleLoadFailed(WasmModule module, float loadTime)
{
Debug.LogError($"WASM 모듈 로딩 실패: {module.moduleName}");
// 실패 이벤트 발송
AppsInToss.SendEvent("wasm_module_failed", new Dictionary<string, object>
{
{"module_name", module.moduleName},
{"load_time", loadTime}
});
// 분석 데이터 전송
SendModuleAnalytics(module, false, loadTime);
// 재시도 로직 (중요한 모듈만)
if (module.priority <= ModulePriority.High)
{
StartCoroutine(RetryModuleLoad(module, 5f));
}
}
void TriggerHighPriorityLoading()
{
var highPriorityModules = System.Array.FindAll(wasmModules,
m => m.priority == ModulePriority.High &&
m.state == ModuleState.NotLoaded);
foreach (var module in highPriorityModules)
{
QueueModuleLoad(module.moduleName);
}
}
IEnumerator RetryModuleLoad(WasmModule module, float delay)
{
yield return new WaitForSeconds(delay);
module.state = ModuleState.NotLoaded;
QueueModuleLoad(module.moduleName);
Debug.Log($"WASM 모듈 재시도: {module.moduleName}");
}
void SendModuleAnalytics(WasmModule module, bool success, float loadTime)
{
var analyticsData = new Dictionary<string, object>
{
{"module_name", module.moduleName},
{"priority", module.priority.ToString()},
{"estimated_size_mb", module.estimatedSizeMB},
{"success", success},
{"load_time", loadTime},
{"device_model", SystemInfo.deviceModel},
{"browser_info", GetBrowserInfo()},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
AppsInToss.SendAnalytics("wasm_module_load", analyticsData);
}
string GetBrowserInfo()
{
#if UNITY_WEBGL && !UNITY_EDITOR
return Application.ExternalEval("navigator.userAgent");
#else
return "Editor";
#endif
}
// 공개 API
public bool IsModuleLoaded(string moduleName)
{
return moduleMap.ContainsKey(moduleName) &&
moduleMap[moduleName].state == ModuleState.Loaded;
}
public bool IsModuleLoading(string moduleName)
{
return loadingModules.Contains(moduleName);
}
public ModuleState GetModuleState(string moduleName)
{
return moduleMap.ContainsKey(moduleName) ?
moduleMap[moduleName].state :
ModuleState.NotLoaded;
}
public void RequestModuleLoad(string moduleName)
{
if (moduleMap.ContainsKey(moduleName))
{
var module = moduleMap[moduleName];
if (module.priority >= ModulePriority.Medium)
{
QueueModuleLoad(moduleName);
}
}
}
public float GetTotalLoadProgress()
{
int totalModules = wasmModules.Length;
int loadedModules = 0;
foreach (var module in wasmModules)
{
if (module.state == ModuleState.Loaded)
{
loadedModules++;
}
}
return totalModules > 0 ? (float)loadedModules / totalModules : 1f;
}
public string[] GetLoadedModules()
{
var loadedModules = new List<string>();
foreach (var kvp in moduleMap)
{
if (kvp.Value.state == ModuleState.Loaded)
{
loadedModules.Add(kvp.Key);
}
}
return loadedModules.ToArray();
}
}2. JavaScript 통합
WASM 로더 JavaScript
javascript
// wasm-loader.js
class WasmModuleLoader {
constructor() {
this.loadedModules = new Map();
this.loadingModules = new Map();
}
async loadWasmModule(moduleName, moduleUrl) {
if (this.loadedModules.has(moduleName)) {
return true;
}
if (this.loadingModules.has(moduleName)) {
return await this.loadingModules.get(moduleName);
}
console.log(`Loading WASM module: ${moduleName}`);
const loadPromise = this.doLoadModule(moduleName, moduleUrl);
this.loadingModules.set(moduleName, loadPromise);
try {
const result = await loadPromise;
this.loadedModules.set(moduleName, result);
this.loadingModules.delete(moduleName);
console.log(`WASM module loaded successfully: ${moduleName}`);
return true;
} catch (error) {
console.error(`Failed to load WASM module: ${moduleName}`, error);
this.loadingModules.delete(moduleName);
return false;
}
}
async doLoadModule(moduleName, moduleUrl) {
const response = await fetch(moduleUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const wasmModule = await WebAssembly.compile(arrayBuffer);
const wasmInstance = await WebAssembly.instantiate(wasmModule);
// Unity WebGL 런타임에 모듈 등록
if (window.unityInstance && window.unityInstance.Module) {
window.unityInstance.Module.wasmModules = window.unityInstance.Module.wasmModules || {};
window.unityInstance.Module.wasmModules[moduleName] = wasmInstance;
}
return wasmInstance;
}
getModuleStatus(moduleName) {
if (this.loadedModules.has(moduleName)) {
return 'loaded';
} else if (this.loadingModules.has(moduleName)) {
return 'loading';
} else {
return 'not_loaded';
}
}
isModuleLoaded(moduleName) {
return this.loadedModules.has(moduleName);
}
getModule(moduleName) {
return this.loadedModules.get(moduleName);
}
}
// 전역 인스턴스
const wasmLoader = new WasmModuleLoader();
// Unity에서 호출할 함수들
window.loadWasmModule = (moduleName, moduleUrl) => {
wasmLoader.loadWasmModule(moduleName, moduleUrl);
};
window.getWasmModuleStatus = (moduleName) => {
return wasmLoader.getModuleStatus(moduleName);
};
window.isWasmModuleLoaded = (moduleName) => {
return wasmLoader.isModuleLoaded(moduleName);
};3. 에디터 도구
WASM 분할 분석기
c#
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
public class WasmSplitAnalyzer : EditorWindow
{
private WasmSplitManager splitManager;
private Vector2 scrollPosition;
private bool showAnalysis = true;
[MenuItem("AppsInToss/WASM 분할 분석기")]
public static void ShowWindow()
{
GetWindow<WasmSplitAnalyzer>("WASM 분할 분석기");
}
void OnGUI()
{
GUILayout.Label("WASM 코드 분할 분석", EditorStyles.boldLabel);
splitManager = EditorGUILayout.ObjectField(
"WASM Split Manager",
splitManager,
typeof(WasmSplitManager),
true
) as WasmSplitManager;
if (splitManager == null)
{
EditorGUILayout.HelpBox("WasmSplitManager를 선택해주세요.", MessageType.Warning);
return;
}
EditorGUILayout.Space();
if (GUILayout.Button("모듈 분석 실행"))
{
AnalyzeModules();
}
if (GUILayout.Button("최적화 제안 생성"))
{
GenerateOptimizationSuggestions();
}
EditorGUILayout.Space();
showAnalysis = EditorGUILayout.Foldout(showAnalysis, "분석 결과");
if (showAnalysis)
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
DrawModuleAnalysis();
EditorGUILayout.EndScrollView();
}
}
void AnalyzeModules()
{
Debug.Log("=== WASM 모듈 분석 시작 ===");
float totalSizeMB = 0f;
var priorityGroups = new Dictionary<WasmSplitManager.ModulePriority, List<WasmSplitManager.WasmModule>>();
foreach (var module in splitManager.wasmModules)
{
totalSizeMB += module.estimatedSizeMB;
if (!priorityGroups.ContainsKey(module.priority))
{
priorityGroups[module.priority] = new List<WasmSplitManager.WasmModule>();
}
priorityGroups[module.priority].Add(module);
}
Debug.Log($"총 모듈 수: {splitManager.wasmModules.Length}");
Debug.Log($"총 예상 크기: {totalSizeMB:F2}MB");
foreach (var group in priorityGroups)
{
float groupSize = 0f;
foreach (var module in group.Value)
{
groupSize += module.estimatedSizeMB;
}
Debug.Log($"{group.Key} 우선순위: {group.Value.Count}개 모듈, {groupSize:F2}MB");
}
AnalyzeDependencies();
}
void AnalyzeDependencies()
{
Debug.Log("\n=== 의존성 분석 ===");
var dependencyCount = new Dictionary<string, int>();
foreach (var module in splitManager.wasmModules)
{
foreach (var dependency in module.dependencies)
{
dependencyCount[dependency] = dependencyCount.ContainsKey(dependency) ?
dependencyCount[dependency] + 1 : 1;
}
}
foreach (var kvp in dependencyCount)
{
if (kvp.Value > 1)
{
Debug.Log($"공통 의존성: {kvp.Key} ({kvp.Value}개 모듈에서 참조)");
}
}
}
void GenerateOptimizationSuggestions()
{
var suggestions = new List<string>();
// 크기 기반 제안
foreach (var module in splitManager.wasmModules)
{
if (module.estimatedSizeMB > 5f && module.priority == WasmSplitManager.ModulePriority.Critical)
{
suggestions.Add($"{module.moduleName}: 크기가 큰 Critical 모듈입니다. 우선순위를 낮추거나 분할을 고려하세요.");
}
if (module.dependencies.Length > 3)
{
suggestions.Add($"{module.moduleName}: 의존성이 많습니다. 구조 개선을 고려하세요.");
}
}
// 결과 표시
if (suggestions.Count > 0)
{
string message = "최적화 제안:\n\n" + string.Join("\n\n", suggestions);
EditorUtility.DisplayDialog("최적화 제안", message, "확인");
}
else
{
EditorUtility.DisplayDialog("최적화 분석", "현재 구조가 잘 최적화되어 있습니다.", "확인");
}
}
void DrawModuleAnalysis()
{
if (splitManager.wasmModules == null) return;
foreach (var module in splitManager.wasmModules)
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField(module.moduleName, EditorStyles.boldLabel);
EditorGUILayout.LabelField($"우선순위: {module.priority}");
EditorGUILayout.LabelField($"예상 크기: {module.estimatedSizeMB:F2}MB");
EditorGUILayout.LabelField($"의존성: {module.dependencies.Length}개");
if (Application.isPlaying && WasmSplitManager.Instance != null)
{
var state = WasmSplitManager.Instance.GetModuleState(module.moduleName);
EditorGUILayout.LabelField($"상태: {state}");
}
EditorGUILayout.EndVertical();
}
}
}
#endifWASM 코드 분할을 통해 초기 로딩 시간을 단축하고 필요한 기능만 점진적으로 로딩하여 메모리 효율성을 높이세요.
의존성 관계를 명확히 하고 우선순위에 따른 로딩 전략을 수립하는 것이 중요해요.
