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

AssetBundle 가이드

앱인토스 Unity 게임에서 AssetBundle을 효율적으로 사용하여 앱 크기를 최소화하고 동적 콘텐츠 로딩을 구현하는 방법을 제공해요.

1. 앱인토스 AssetBundle 전략

AssetBundle 구성 전략

📦 앱인토스 AssetBundle 구조
├── Core Bundle (필수 번들) - 10MB 이내
│   ├── 게임 엔진 핵심 에셋
│   ├── 앱인토스 SDK 리소스
│   ├── 기본 UI 시스템
│   └── 토스 브랜딩 에셋
├── Feature Bundles (기능별 번들) - 각 5MB 이내
│   ├── 게임플레이 번들
│   ├── 사운드 번들
│   ├── 이펙트 번들
│   └── 토스페이 연동 번들
├── Level Bundles (레벨별 번들) - 각 3MB 이내
│   ├── 스테이지 1-10
│   ├── 스테이지 11-20
│   └── 보스 레벨
└── Seasonal Bundles (시즌 컨텐츠) - 각 2MB 이내
    ├── 이벤트 에셋
    ├── 한정 스킨
    └── 특별 아이템

AssetBundle 관리자 구현

c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class AppsInTossAssetBundleManager : MonoBehaviour
{
    public static AppsInTossAssetBundleManager Instance { get; private set; }
    
    [System.Serializable]
    public class BundleConfig
    {
        public string bundleName;
        public BundlePriority priority;
        public string cdnUrl;
        public long bundleSize;
        public string version;
        public bool persistent = true;
        public List<string> dependencies = new List<string>();
    }
    
    public enum BundlePriority
    {
        Critical = 0,    // 앱 시작 전 필수 로딩
        High = 1,        // 게임 시작 시 로딩
        Medium = 2,      // 백그라운드 로딩
        Low = 3,         // 필요시 로딩
        Toss = 4         // 앱인토스 특화 번들
    }
    
    [Header("번들 설정")]
    public BundleConfig[] bundleConfigs;
    
    [Header("앱인토스 CDN 설정")]
    public string tossCdnBaseUrl = "https://cdn.appintoss.com/bundles/";
    public bool enableTossCaching = true;
    public long maxCacheSizeMB = 100;
    
    // 내부 관리
    private Dictionary<string, AssetBundle> loadedBundles = new Dictionary<string, AssetBundle>();
    private Dictionary<string, BundleConfig> bundleConfigMap = new Dictionary<string, BundleConfig>();
    private HashSet<string> downloadingBundles = new HashSet<string>();
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializeBundleManager();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    void InitializeBundleManager()
    {
        // 번들 설정 맵 생성
        foreach (var config in bundleConfigs)
        {
            bundleConfigMap[config.bundleName] = config;
        }
        
        // 로컬 캐시 정리
        StartCoroutine(CleanupOldBundles());
        
        // Critical 우선순위 번들 자동 로딩
        StartCoroutine(LoadCriticalBundles());
    }
    
    IEnumerator LoadCriticalBundles()
    {
        var criticalBundles = System.Array.FindAll(bundleConfigs, 
            b => b.priority == BundlePriority.Critical || b.priority == BundlePriority.Toss);
        
        foreach (var bundle in criticalBundles)
        {
            yield return StartCoroutine(LoadBundleCoroutine(bundle.bundleName));
        }
        
        Debug.Log("Critical AssetBundle 로딩 완료");
    }
    
    public void LoadBundleAsync(string bundleName, System.Action<bool> onComplete = null)
    {
        StartCoroutine(LoadBundleAsyncCoroutine(bundleName, onComplete));
    }
    
    IEnumerator LoadBundleAsyncCoroutine(string bundleName, System.Action<bool> onComplete)
    {
        yield return StartCoroutine(LoadBundleCoroutine(bundleName));
        onComplete?.Invoke(IsBundleLoaded(bundleName));
    }
    
    IEnumerator LoadBundleCoroutine(string bundleName)
    {
        if (IsBundleLoaded(bundleName))
        {
            yield break;
        }
        
        if (downloadingBundles.Contains(bundleName))
        {
            yield return new WaitUntil(() => !downloadingBundles.Contains(bundleName));
            yield break;
        }
        
        downloadingBundles.Add(bundleName);
        
        var config = bundleConfigMap[bundleName];
        
        // 의존성 번들 먼저 로딩
        foreach (var dependency in config.dependencies)
        {
            if (!IsBundleLoaded(dependency))
            {
                yield return StartCoroutine(LoadBundleCoroutine(dependency));
            }
        }
        
        // 번들 다운로드 또는 로컬 로딩
        string bundlePath = GetBundlePath(bundleName);
        AssetBundle bundle = null;
        
        if (File.Exists(bundlePath))
        {
            // 로컬에서 로딩
            var bundleLoadRequest = AssetBundle.LoadFromFileAsync(bundlePath);
            yield return bundleLoadRequest;
            bundle = bundleLoadRequest.assetBundle;
        }
        else
        {
            // CDN에서 다운로드
            yield return StartCoroutine(DownloadBundle(config));
            
            if (File.Exists(bundlePath))
            {
                var bundleLoadRequest = AssetBundle.LoadFromFileAsync(bundlePath);
                yield return bundleLoadRequest;
                bundle = bundleLoadRequest.assetBundle;
            }
        }
        
        if (bundle != null)
        {
            loadedBundles[bundleName] = bundle;
            Debug.Log($"AssetBundle 로딩 성공: {bundleName}");
            
            // 앱인토스 분석에 번들 로딩 성공 전송
            SendBundleAnalytics(bundleName, true, 0);
        }
        else
        {
            Debug.LogError($"AssetBundle 로딩 실패: {bundleName}");
            SendBundleAnalytics(bundleName, false, 0);
        }
        
        downloadingBundles.Remove(bundleName);
    }
    
    IEnumerator DownloadBundle(BundleConfig config)
    {
        string downloadUrl = tossCdnBaseUrl + config.bundleName;
        string savePath = GetBundlePath(config.bundleName);
        
        Debug.Log($"AssetBundle 다운로드 시작: {config.bundleName}");
        float startTime = Time.realtimeSinceStartup;
        
        using (var www = new WWW(downloadUrl))
        {
            yield return www;
            
            if (string.IsNullOrEmpty(www.error))
            {
                // 파일 저장
                Directory.CreateDirectory(Path.GetDirectoryName(savePath));
                File.WriteAllBytes(savePath, www.bytes);
                
                float downloadTime = Time.realtimeSinceStartup - startTime;
                Debug.Log($"AssetBundle 다운로드 완료: {config.bundleName} ({downloadTime:F2}초)");
                
                SendBundleAnalytics(config.bundleName, true, downloadTime);
            }
            else
            {
                Debug.LogError($"AssetBundle 다운로드 실패: {config.bundleName} - {www.error}");
                SendBundleAnalytics(config.bundleName, false, 0);
            }
        }
    }
    
    string GetBundlePath(string bundleName)
    {
        return Path.Combine(Application.persistentDataPath, "Bundles", bundleName);
    }
    
    // 에셋 로딩 API
    public void LoadAssetFromBundle<T>(string bundleName, string assetName, 
        System.Action<T> onComplete, System.Action<string> onError = null) where T : UnityEngine.Object
    {
        StartCoroutine(LoadAssetFromBundleCoroutine<T>(bundleName, assetName, onComplete, onError));
    }
    
    IEnumerator LoadAssetFromBundleCoroutine<T>(string bundleName, string assetName,
        System.Action<T> onComplete, System.Action<string> onError) where T : UnityEngine.Object
    {
        // 번들이 로딩되어 있지 않으면 먼저 로딩
        if (!IsBundleLoaded(bundleName))
        {
            yield return StartCoroutine(LoadBundleCoroutine(bundleName));
        }
        
        if (!IsBundleLoaded(bundleName))
        {
            onError?.Invoke($"번들 로딩 실패: {bundleName}");
            yield break;
        }
        
        var bundle = loadedBundles[bundleName];
        var assetRequest = bundle.LoadAssetAsync<T>(assetName);
        
        yield return assetRequest;
        
        if (assetRequest.asset != null)
        {
            onComplete?.Invoke(assetRequest.asset as T);
        }
        else
        {
            onError?.Invoke($"에셋을 찾을 수 없습니다: {assetName}");
        }
    }
    
    // 토스 특화 번들 로딩
    public void LoadTossBundles(System.Action onComplete = null)
    {
        StartCoroutine(LoadTossBundlesCoroutine(onComplete));
    }
    
    IEnumerator LoadTossBundlesCoroutine(System.Action onComplete)
    {
        var tossBundles = System.Array.FindAll(bundleConfigs, 
            b => b.priority == BundlePriority.Toss);
        
        foreach (var bundle in tossBundles)
        {
            yield return StartCoroutine(LoadBundleCoroutine(bundle.bundleName));
        }
        
        Debug.Log("토스 특화 번들 로딩 완료");
        onComplete?.Invoke();
    }
    
    // 번들 언로딩
    public void UnloadBundle(string bundleName, bool unloadAllLoadedObjects = false)
    {
        if (loadedBundles.ContainsKey(bundleName))
        {
            loadedBundles[bundleName].Unload(unloadAllLoadedObjects);
            loadedBundles.Remove(bundleName);
            
            Debug.Log($"AssetBundle 언로드: {bundleName}");
        }
    }
    
    // 캐시 관리
    IEnumerator CleanupOldBundles()
    {
        string bundleDir = Path.Combine(Application.persistentDataPath, "Bundles");
        
        if (!Directory.Exists(bundleDir))
        {
            yield break;
        }
        
        var files = Directory.GetFiles(bundleDir);
        long totalSize = 0;
        
        foreach (var file in files)
        {
            totalSize += new FileInfo(file).Length;
        }
        
        long maxCacheSize = maxCacheSizeMB * 1024 * 1024;
        
        if (totalSize > maxCacheSize)
        {
            Debug.Log($"번들 캐시 정리 시작: {totalSize / (1024 * 1024)}MB > {maxCacheSizeMB}MB");
            
            // 최근 접근 시간 기준으로 정렬
            var fileInfos = new List<FileInfo>();
            foreach (var file in files)
            {
                fileInfos.Add(new FileInfo(file));
            }
            
            fileInfos.Sort((a, b) => a.LastAccessTime.CompareTo(b.LastAccessTime));
            
            // 오래된 파일부터 삭제
            foreach (var fileInfo in fileInfos)
            {
                if (totalSize <= maxCacheSize) break;
                
                totalSize -= fileInfo.Length;
                File.Delete(fileInfo.FullName);
                
                Debug.Log($"오래된 번들 삭제: {fileInfo.Name}");
            }
        }
    }
    
    void SendBundleAnalytics(string bundleName, bool success, float downloadTime)
    {
        var analyticsData = new Dictionary<string, object>
        {
            {"bundle_name", bundleName},
            {"success", success},
            {"download_time", downloadTime},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("assetbundle_loading", analyticsData);
    }
    
    // 공개 API
    public bool IsBundleLoaded(string bundleName)
    {
        return loadedBundles.ContainsKey(bundleName);
    }
    
    public string[] GetLoadedBundleNames()
    {
        var names = new string[loadedBundles.Count];
        loadedBundles.Keys.CopyTo(names, 0);
        return names;
    }
    
    public long GetTotalCacheSize()
    {
        string bundleDir = Path.Combine(Application.persistentDataPath, "Bundles");
        
        if (!Directory.Exists(bundleDir))
        {
            return 0;
        }
        
        long totalSize = 0;
        var files = Directory.GetFiles(bundleDir);
        
        foreach (var file in files)
        {
            totalSize += new FileInfo(file).Length;
        }
        
        return totalSize;
    }
}

2. 번들 빌드 최적화

자동화된 번들 빌드 시스템

c#
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Collections.Generic;

public class AppsInTossBundleBuilder
{
    [MenuItem("AppsInToss/Build All Bundles")]
    public static void BuildAllBundles()
    {
        BuildBundlesForTarget(BuildTarget.Android);
        BuildBundlesForTarget(BuildTarget.iOS);
        
        Debug.Log("앱인토스 번들 빌드 완료");
    }
    
    static void BuildBundlesForTarget(BuildTarget target)
    {
        string outputPath = Path.Combine("AssetBundles", target.ToString());
        
        if (!Directory.Exists(outputPath))
        {
            Directory.CreateDirectory(outputPath);
        }
        
        // 앱인토스 최적화 설정
        var buildOptions = BuildAssetBundleOptions.ChunkBasedCompression | 
                          BuildAssetBundleOptions.StrictMode |
                          BuildAssetBundleOptions.DeterministicAssetBundle;
        
        // 번들 빌드 실행
        var manifest = BuildPipeline.BuildAssetBundles(
            outputPath, 
            buildOptions, 
            target
        );
        
        if (manifest != null)
        {
            OptimizeBundlesForAppsInToss(outputPath, manifest);
            GenerateBundleConfig(outputPath, manifest);
        }
    }
    
    static void OptimizeBundlesForAppsInToss(string outputPath, AssetBundleManifest manifest)
    {
        var allBundles = manifest.GetAllAssetBundles();
        
        foreach (var bundleName in allBundles)
        {
            string bundlePath = Path.Combine(outputPath, bundleName);
            var fileInfo = new FileInfo(bundlePath);
            
            // 번들 크기 체크 (앱인토스 권장 크기)
            long sizeMB = fileInfo.Length / (1024 * 1024);
            
            if (sizeMB > 10) // 10MB 초과
            {
                Debug.LogWarning($"번들 크기가 권장 크기를 초과합니다: {bundleName} ({sizeMB}MB)");
            }
            
            // 압축률 정보 출력
            Debug.Log($"번들 정보: {bundleName} - {sizeMB}MB");
        }
    }
    
    static void GenerateBundleConfig(string outputPath, AssetBundleManifest manifest)
    {
        var config = new AppsInTossBundleConfig();
        var allBundles = manifest.GetAllAssetBundles();
        
        foreach (var bundleName in allBundles)
        {
            string bundlePath = Path.Combine(outputPath, bundleName);
            var fileInfo = new FileInfo(bundlePath);
            
            var bundleInfo = new AppsInTossBundleConfig.BundleInfo
            {
                name = bundleName,
                size = fileInfo.Length,
                hash = manifest.GetAssetBundleHash(bundleName).ToString(),
                dependencies = manifest.GetAllDependencies(bundleName)
            };
            
            config.bundles.Add(bundleInfo);
        }
        
        // JSON으로 설정 파일 저장
        string configPath = Path.Combine(outputPath, "bundle_config.json");
        string json = JsonUtility.ToJson(config, true);
        File.WriteAllText(configPath, json);
        
        Debug.Log($"번들 설정 파일 생성: {configPath}");
    }
}

[System.Serializable]
public class AppsInTossBundleConfig
{
    [System.Serializable]
    public class BundleInfo
    {
        public string name;
        public long size;
        public string hash;
        public string[] dependencies;
    }
    
    public List<BundleInfo> bundles = new List<BundleInfo>();
    public string version = "1.0.0";
    public string buildTime;
    
    public AppsInTossBundleConfig()
    {
        buildTime = System.DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
    }
}
#endif

AssetBundle을 사용해 앱 크기를 줄이고, 필요한 콘텐츠만 동적으로 업데이트하세요. 앱인토스 환경에서는 번들 크기와 다운로드 속도에 특히 주의해야 해요.