글 작성자: Doublsb

저번 달에 유나이트 2026에 다녀왔다. 세션은 대부분 재밌었는데, 가장 좋았던 건 빌드 파이프라인이 개선된다는 거였다. 이미 세션 영상이 유튜브에 올라오긴 했지만, 개인적으로 한 번 정리하는 게 낫다고 생각해서 글을 쓴다.

 

개요

Unity 6.6 버전부터 기존의 AssetBundle을 대체하는 Content Directories를 지원한다.

강한 참조로 엮여 있었던 에셋번들은 프로젝트 크기가 클수록, 그리고 라이브 서비스가 길어질수록 관리가 어려웠다.

하지만 약한 참조를 도입한 콘텐츠 디렉토리를 사용한다면 이제 번들 관리는 신경쓰지 않아도 될 정도로 좋아졌다.

아직 Remote는 지원을 안 하는게 문제이긴 하지만. 뭐... 다행히 나는 이제 모바일이 아닌 스팀 기반 개발자가 되었으니까 하 하 하

 

 

왜 에셋번들이 바뀌어야 하는가

이건 시나리오로 나눠서 설명해보도록 하겠다.

 

개발자 A씨는 5년 정도 라이브중인 모바일 게임의 클라이언트 개발자다. 콘텐츠를 추가할 때마다 에셋번들을 빌드해서 CDN에 올려놓곤 했다. 그런데 기획팀에서 작은 수정이 있어서 번들을 다시 올려달라는 요청을 받았다. 고작 1kb의 파일 수정. 그러나 에셋번들을 빌드해서 올린 뒤, 모바일 기기에서 다운받아 테스트하는 개발자 A씨는 회의감이 든다. 1kb를 바꿨을 뿐인데 50MB인 에셋번들을 매번 다운받는 게 유저 입장에서 짜증나지 않을까 하는 생각이었다.

 

개발자 B씨는 최적화 빌런이다. 그는 번들을 작게 많이 유지하는 게 정석이라고 생각한다. 왜냐면 런타임에 번들을 상주시킬 때의 메모리 최적화, 그리고 번들 비동기 로딩 시간의 효율을 위함이다. 문제는 그 번들 구조가 곧 폴더 구조라는 것이다. 디자이너 C씨가 UI 텍스처 폴더를 정리하려고 했더니, B씨는 함부로 바꾸지 말라는 요구를 한다. 옆 팀은 그냥 고민 없이 리소스를 넣으면 된다는데... 디자이너 C씨는 리소스 하나 옮기는 데 허락을 받는 게 좀 버겁다.

 

뭐 대충 실무에서 있을 법한 시나리오를 쓰긴 했는데, 이걸 개발적으로 정리하면 다음과 같다.

 

1. 번들 단위 무효화

에셋 하나만 변경되어도 해당 번들 전체를 재빌드/다운받아야 하는 문제

 

2. 모놀리식 결합

에셋 단위가 아니라 번들 단위로 의존성을 기록하기 때문에, 번들끼리의 참조가 있었을 경우 나머지 에셋까지 로드해야 하는 문제

 

3. 레이아웃 최적화 사이의 트레이드 오프

완벽한 레이아웃은 존재하지 않는 문제. 한 쪽을 선택하면 다른 한 쪽은 버릴 수밖에 없는 에셋번들 고유의 문제.

(1) 에셋 중복 줄이기 <-> 적게 상주하는 번들

(2) 패치 사이즈 줄이기 <-> 런타임 퍼포먼스

(3) 관리 쉬운 레이아웃 <-> 최적화된 레이아웃

 

 

콘텐츠 디렉토리는 이것을 어떻게 해결했는가

이름을 기반으로 얘기하자면, OS의 디렉토리처럼 약한 참조의 형태로 해결했다고 보면 된다.

 

머티리얼 A가 있었다고 가정했을 때, 기존에는 A가 어느 번들에 속하는지가 의존성의 단위였기 때문에, A만 고쳐도 A를 참조하는 모든 번들이 연쇄적으로 영향을 받았다.

하지만 이제는 참조가 Loadable<T> 내의 LoadableObjectId를 통하게 되어, 의존성이 번들이 아니라 에셋 단위로 기록된다. 무엇이 바뀌었는지를 에셋 단위로 알 수 있게 되었다.

 

그리고 이 변경만으로 얻는 것이 많아지게 된다.

 

콘텐츠 디렉토리를 구운다면, 기존에 구운 결과물에서 '정말로 달라진' 에셋들만 재빌드할 수 있는 것이다.

캐릭터 프리팹이 모델, 머티리얼을 의존성으로 갖고 있었다면 이전에는 전부 빌드했어야 하는 부분들이, 변경을 추적할 수 있게 되어 실제로 변경된 머티리얼만 재빌드하면 되도록 바뀌었다.

 

구운 콘텐츠를 다운받는 유저들도 정말로 변경된 에셋들만 다운로드할 수 있게 되어 꽤나 행복해질 것이다. 개발팀의 서버비도 그만큼 줄어들 것이고. 번들 레이아웃을 어떻게 구성할 것인지 깊게 고민할 필요도 없어지게 된다.

 

 

콘텐츠 디렉토리는 어떤 방식으로 빌드하는가

루트 에셋을 설정하면, 해당 에셋으로부터 의존성을 검사하며 가지로 뻗어나오면서 빌드하는 방식이다. 이 때 빌드 레이아웃은 자동으로 구성되어 내놓는다.

 

그럼 루트 에셋은 정말 아무 에셋이나 골라서 루트 에셋으로 정하는 건가? 아니다. 스크립터블 오브젝트로 만들어야 한다고 한다.

 

그리고 해당 스크립터블 오브젝트가 public이나 serializedField 등으로 거대한 프리팹을 들고 있는 건 피하는 게 좋다. 콘텐츠 디렉토리가 등록될 때 그 에셋들도 자동으로 메모리에 상주하기 때문이다. 만약 들고 있어야 한다면 어드레서블처럼 Loadable로 선언하면 된다.

 

[CreateAssetMenu(fileName = "SeasonPassRoot", menuName = "Content/SeasonPass Root")]
public class SeasonPassRoot : ScriptableObject
{

	//Direct reference - loaded with the root
    public GameObject directPrefab;
    
    //Loadable reference - loaded on demand
    public Loadable<GameObject> loadablePrefab;
    
}

//출처 Unity Korea의 [Unite Seoul 2026] Unity의 차세대 콘텐츠 빌드 & 로드 시스템

 

콘텐츠 디렉토리가 어떤 스텝으로 빌드되는지는 분석 툴을 제공한다고 하니, 에러 확인도 쉬워질 것이다. 이 데이터는 JSON이라고 하니 커스텀 도구를 만들거나 AI에게 질의하기도 쉽다고 한다.

 

 

콘텐츠 디렉토리는 런타임에 어떻게 제어하면 되는가

아래의 단계로 나뉜다. 어드레서블처럼 작동한다고 봐도 문제가 없을 것 같다.

 

등록

출처: 유니티 코리아 유튜브

핸들을 선언하고, 어떤 콘텐츠 디렉토리를 로드할 것인지 경로를 등록한다.

루트 에셋 및 직접 참조된 에셋들만 로드되는 것이지 LoadAble 에셋들을 로드하는 것은 아직 아니므로 참고.

 

발견 & 로드

출처: 유니티 코리아 유튜브

루트 에셋의 타입을 명시해서 객체로 가져온 뒤에, 여러 에셋들을 로드하면 된다.

이 때, 아까 설명했지만 직접 참조된 에셋은 루트 에셋을 로드할 때 전부 로드된다. Loadable 에셋들은 어드레서블 로드하듯이 따로 로드해야 한다.

 

해제

출처: 유니티 코리아 유튜브

더 이상 필요 없는 온디맨드 콘텐츠들을 해제하려면 Loadable들을 명시적으로 Release하면 되고, 콘텐츠 디렉토리 자체를 내리려면 등록을 해제하면 된다.

 

 

요약

아직 Remote가 안 된다는 게 자꾸 사용하길 미루는 요인이 되긴 할 것 같다. 그래도 Unity 7 버전에서는 Remote를 지원할 것이라고 하니 미리미리 익혀 둬야겠다.

 

별개로 Retro님 세션은 언제나 명쾌해서 절로 갓트로를 외치게 된다. 짱짱맨.

반응형