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

코드 분할 도구 사용 가이드

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();
        }
    }
}
#endif

WASM 코드 분할을 통해 초기 로딩 시간을 단축하고 필요한 기능만 점진적으로 로딩하여 메모리 효율성을 높이세요.
의존성 관계를 명확히 하고 우선순위에 따른 로딩 전략을 수립하는 것이 중요해요.