---
title: "AssetBundle 은 에셋을 Player 밖 파일로 꺼낸다"
description: "유니티는 Scene List 에서 참조를 따라 에셋을 Player 에 싣고, Resources 폴더는 참조가 없어도 통째로 싣습니다. AssetBundle 은 에셋 일부를 Player 밖 아카이브 파일로 빼서 따로 받고, 따로 열고, 따로 내리게 합니다."
author: "유니얼"
pubDate: 2026-10-19
category: "game-dev/unity/runtime"
tags: ["Unity", "메모리"]
canonical: https://blog.unialgames.com/posts/unity-assetbundle-why/
---
<x-repo>
<script type="application/json">
{
  "repo": "SeoBYP/AssetBundleStudy",
  "text": "AssetBundle 과 Addressables 의 문서 내용을 Windows Player 에서 직접 재서 확인한 기록입니다.",
  "chips": ["Unity 6000.6.3f1", "URP 17.6.0", "Windows Player"],
  "links": [
    { "text": "README (측정 결과)", "path": "README.md" },
    { "text": "번들 빌드 코드", "path": "Assets/Measure/Editor/MeasureBuild.cs" }
  ]
}
</script>
</x-repo>

게임이 커지면 모든 에셋을 설치 파일 하나에 넣을 수 없게 됩니다. 스토어 용량 제한에 걸리고, 이벤트 맵 하나를 고치려고 앱 전체를 다시 심사받아야 하고, 쓰지도 않는 에셋이 처음부터 메모리를 차지합니다.

**AssetBundle** 은 **에셋 일부를 Player 밖의 아카이브 파일로 빼내서, 따로 받고 따로 열고 따로 내리게 하는 장치**입니다. 그 전에 유니티가 에셋을 Player 에 어떻게 싣는지부터 보면 왜 필요한지가 보입니다.

## Player 빌드는 참조를 따라 에셋을 싣는다

Player 를 빌드하면 유니티는 Scene List 에 넣은 씬에서 출발합니다. 씬이 가리키는 프리팹, 프리팹이 가리키는 머티리얼, 머티리얼이 가리키는 셰이더와 텍스처를 차례로 따라가며 실을 것을 모읍니다. 따라가는 길은 에셋 파일에 저장된 GUID 와 fileID 입니다. GUID 는 `.meta` 에 적힌 "어느 파일인지", fileID 는 "그 파일 안의 어느 오브젝트인지" 입니다.

그래서 어느 씬에서도 참조하지 않는 에셋은 프로젝트에 있어도 빌드에 안 들어갑니다. 예외가 하나 있습니다. **Resources 폴더**입니다.

<x-wiring>
<script type="application/json">
{
  "label": "Player 빌드가 에셋을 고르는 길",
  "width": 600,
  "height": 470,
  "nodes": [
    { "id": "list", "title": "Scene List", "sub": "빌드에 넣은 씬", "x": 170, "y": 10, "w": 220 },
    { "id": "ref", "title": "참조된 에셋", "sub": "GUID 를 따라 닿은 것", "x": 170, "y": 125, "w": 220 },
    { "id": "res", "title": "Resources 폴더", "sub": "참조 여부와 상관없이 전부", "x": 170, "y": 240, "w": 220 },
    { "id": "player", "title": "Player 데이터", "sub": "설치 파일 안", "x": 170, "y": 360, "w": 220 },
    { "id": "bundle", "title": "AssetBundle", "sub": "Player 밖 파일", "x": 430, "y": 360, "w": 160, "aside": true }
  ],
  "edges": [
    { "from": "list", "to": "ref", "label": "참조를 따라간다" },
    { "from": "ref", "to": "res", "label": "더한다" },
    { "from": "res", "to": "player", "label": "모두 싣는다" }
  ],
  "steps": [
    { "node": "list", "why": "빌드는 Scene List 의 씬에서 출발합니다." },
    { "edge": 0, "why": "씬에서 참조로 닿은 에셋만 고릅니다. 아무도 안 가리키는 에셋은 빠집니다." },
    { "edge": 1, "why": "Resources 폴더는 다릅니다. 참조가 없어도 폴더 안의 것을 전부 더합니다." },
    { "edge": 2, "why": "고른 것을 전부 설치 파일 안에 싣습니다." },
    { "node": "bundle", "why": "AssetBundle 로 뺀 에셋은 이 길에 없습니다. Player 밖 파일이 되어 따로 배포합니다." }
  ],
  "note": "Resources 는 문자열로 찾기 때문에 빌드가 누가 쓰는지 알 수 없습니다. 그래서 전부 싣습니다."
}
</script>
</x-wiring>

Resources 폴더에 아무도 참조하지 않는 텍스트 파일 하나, Resources 밖에 똑같이 참조 없는 파일 하나를 두고 빌드해 빌드 리포트를 봤습니다. Resources 안의 것은 `resources.assets` 에 들어가 Player 에서 `Resources.Load` 로 읽혔고, 밖의 것은 빌드에 없었습니다.

`Resources.Load("bg")` 처럼 **문자열로 찾기 때문**입니다. 빌드는 어느 코드가 어떤 이름을 부를지 모르니 전부 실을 수밖에 없습니다. Unity 의 가이드는 Resources 에 대해 "쓰지 마라(Don't use it)" 고까지 적습니다. 메모리를 세밀하게 관리하기 어렵고, 에셋이 많으면 시작할 때 인덱스를 만드느라 시간이 들고, 플랫폼별 콘텐츠나 부분 업데이트를 할 수 없다는 이유입니다.

## AssetBundle 은 그 일부를 Player 밖으로 뺀다

매뉴얼은 AssetBundle 을 **에셋을 묶어 두는 아카이브 파일**이라고 정의하고, 쓰는 이유를 셋으로 듭니다.

- **필요할 때 받아 오기.** DLC, 에피소드 업데이트, 라이브 서비스처럼 앱을 다시 내지 않고 콘텐츠를 더합니다
- **설치 크기 줄이기.** 처음 설치 파일에는 꼭 필요한 것만 넣고 나머지는 나중에 받습니다
- **플랫폼에 맞추기.** 플랫폼마다 번들을 따로 만들어 다른 플랫폼용 에셋을 싣지 않습니다

빌드는 "어느 에셋을 어느 번들에 넣을지" 를 적어 넘기면 됩니다.

```csharp
var builds = new[]
{
    new AssetBundleBuild { assetBundleName = "cube", assetNames = new[] { "Assets/Prefab/Cube.prefab" } },
    new AssetBundleBuild { assetBundleName = "scene", assetNames = new[] { "Assets/Scenes/InDoor.unity" } },
};
BuildPipeline.BuildAssetBundles("Builds/Bundles", builds,
    BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows64);
```

결과로 `cube`, `scene` 같은 번들 파일과 번들끼리의 의존 관계를 담은 매니페스트 번들이 나옵니다. 번들 안을 열어 보면 `CAB-<해시>` 라는 직렬화 파일과, 텍스처나 메시처럼 큰 데이터를 담은 `CAB-<해시>.resS` 가 들어 있습니다. Player 의 데이터 파일과 같은 형식을 아카이브 하나에 담은 것입니다.

런타임에는 파일을 열고, 안에서 에셋을 이름으로 꺼냅니다.

```csharp
AssetBundle bundle = AssetBundle.LoadFromFile(path);
GameObject prefab = bundle.LoadAsset<GameObject>("Cube");
Instantiate(prefab);
// ...
bundle.Unload(true);   // 번들과, 번들에서 꺼낸 에셋을 내린다
```

## 번들 하나가 받기, 열기, 내리기의 단위가 된다

빼낸 대가로 새 단위가 생겼습니다. **받는 것도, 여는 것도, 내리는 것도 번들 단위**입니다. 에셋 하나를 쓰려 해도 그 에셋이 든 번들을 열어야 하고, 그 에셋의 머티리얼이 다른 번들에 있으면 그 번들도 열어야 합니다. 어떤 에셋을 어느 번들에 넣지 않으면, 그 에셋을 쓰는 번들마다 몰래 복사됩니다. 내릴 때도 번들을 통째로 내리거나 남겨 둬야 합니다.

그래서 AssetBundle 을 쓰는 일은 대부분 **번들을 어떻게 나누고, 어떤 순서로 열고, 언제 내릴지** 를 정하는 일이 됩니다. 매뉴얼이 저수준 API 대신 Addressables 를 권하는 이유도 이 관리 부담 때문입니다.

번들에 넣을 수 없는 것도 있습니다. 매뉴얼은 "AssetBundle 은 어셈블리를 담을 수 없다" 고 적습니다. 새 C# 클래스나 바뀐 코드는 번들로 배포할 수 없고, `ScriptableObject` 같은 직렬화된 데이터만 담깁니다.

## 직접 확인하는 테스트 코드

Resources 의 동작은 빌드 리포트로 확인합니다. 아래 에디터 스크립트는 Player 를 빌드하고 리포트에서 두 파일이 어디에 들어갔는지 찍습니다. `Assets/Resources/Unreferenced.txt` 와 `Assets/Unused/NotInResources.txt` 를 미리 만들어 두고, 둘 다 어디서도 참조하지 않습니다.

```csharp
[MenuItem("Tools/Check Resources Packing")]
static void CheckResourcesPacking()
{
    BuildReport report = BuildPipeline.BuildPlayer(new BuildPlayerOptions
    {
        scenes = new[] { "Assets/Scenes/SampleScene.unity" },
        locationPathName = "Builds/Check/Game.exe",
        target = BuildTarget.StandaloneWindows64,
    });

    foreach (string asset in new[] { "Assets/Resources/Unreferenced.txt", "Assets/Unused/NotInResources.txt" })
    {
        string where = "빌드에 없음";
        foreach (PackedAssets packed in report.packedAssets)
            foreach (PackedAssetInfo info in packed.contents)
                if (info.sourceAssetPath == asset)
                    where = $"{Path.GetFileName(packed.shortPath)} ({info.packedSize} B)";
        Debug.Log($"{asset}: {where}");
    }
}
```

Resources 안의 파일은 `resources.assets` 로, 밖의 파일은 "빌드에 없음" 으로 나오면 같은 결과입니다.

## 주의할 점

### 1. Resources 에는 시작할 때 꼭 필요한 최소한만 둔다

**안 지키면 생기는 문제.** Resources 에 넣은 것은 아무도 안 써도 전부 설치 파일에 실리고, 문자열로 찾으니 빌드가 정리해 주지 못합니다. 나중에 번들로 옮기려 해도 어디서 그 이름을 부르는지 코드 전체를 뒤져야 합니다.

**이렇게 쓴다.** Resources 는 부트스트랩용 설정처럼 처음부터 끝까지 필요한 작은 것만 둡니다. 나머지는 씬 참조나 번들로 싣습니다.

### 2. 코드 변경은 번들로 내보낼 수 없다

**안 지키면 생기는 문제.** 번들에 새 컴포넌트를 붙인 프리팹을 넣어 배포하면, 설치된 Player 에 그 클래스가 없어서 컴포넌트가 빠진 채로 올라옵니다.

**이렇게 쓴다.** 번들로 내보낼 변경은 이미 Player 에 있는 클래스의 데이터만 바꿉니다. 새 스크립트가 필요한 변경은 앱 업데이트로 냅니다.

## 정리

AssetBundle 은 **에셋 일부를 Player 밖 아카이브 파일로 빼서 따로 받고 따로 열고 따로 내리게 합니다.**

- Player 빌드는 Scene List 에서 참조를 따라 싣고, Resources 폴더는 참조가 없어도 전부 싣습니다.
- 번들에는 데이터만 들어가고 코드는 못 들어갑니다. 안은 Player 데이터와 같은 직렬화 파일과 `.resS` 입니다.
- 대신 받기, 열기, 내리기가 모두 번들 단위가 되어, 번들을 나누고 여는 순서를 정하는 일이 생깁니다.