Unity WebGL 빌드에서 가장 낯설었던 부분 중 하나는 메모리였다. 데스크톱에서는 잘 열리던 장면이 브라우저에서 부담스러워지고, 초기 메모리 설정이나 에셋 크기를 따로 봐야 했다. C#으로 작성했는데 실행 결과는 WebAssembly 선형 메모리 위에 올라간다는 사실도 처음에는 잘 와닿지 않았다.
그러다 Chrome과 Firefox가 WasmGC를 지원하기 시작했다는 글을 봤다. 이름만 보면 WebAssembly 메모리를 브라우저가 알아서 정리해 주는 기능처럼 들린다. 그러면 Unity WebGL의 메모리 문제도 사라지는 걸까? 이 역시 같은 이름의 ‘GC’를 서로 다른 층에서 본 오해였다.
기존 WebAssembly의 선형 메모리
전통적인 WebAssembly 모듈은 큰 연속 바이트 배열 같은 WebAssembly.Memory를 사용한다. C, C++, IL2CPP 같은 도구 체인은 이 공간 안에 객체를 배치하고 자체 런타임 규칙으로 관리한다.
const memory = new WebAssembly.Memory({
initial: 256,
maximum: 1024
});
console.log(memory.buffer.byteLength / 1024 / 1024, 'MiB');
위 예에서 페이지 하나는 64KiB다. initial: 256이면 시작 시점의 논리적 크기는 16MiB다. 메모리를 늘릴 수는 있지만 일반적인 선형 메모리는 임의로 줄이는 방식이 아니다. 브라우저가 JavaScript 객체를 수거한다고 해서 이 안의 C# 객체 배치를 이해하는 것도 아니다.
Unity WebGL에서는 대략 다음 흐름을 거친다.
C# 코드와 관리 객체
↓ IL2CPP 변환
C++ 코드와 Unity 런타임
↓ Emscripten / WebAssembly
선형 메모리 안의 데이터
Unity 런타임은 C# 객체 수명을 관리하고, WebAssembly는 그 실행에 필요한 선형 주소 공간을 제공한다. 브라우저 JavaScript GC와 Unity 관리 힙을 같은 것으로 보면 메모리 스냅샷을 해석하기 어렵다.
WasmGC가 추가한 것
WasmGC는 언어 런타임이 구조체와 배열 같은 관리 객체를 WebAssembly의 GC 타입으로 표현하도록 기능을 확장했다. 호스트 GC가 참조를 추적할 수 있어, Kotlin이나 Dart처럼 GC 언어를 웹에 포팅할 때 언어별 수집기를 통째로 선형 메모리에 넣어야 했던 부담을 줄일 수 있다.
개념을 아주 단순하게 적으면 다음과 같다.
(type $point (struct
(field $x (mut f64))
(field $y (mut f64))))
(func (export "newPoint") (result (ref $point))
(struct.new $point (f64.const 10) (f64.const 20)))
이것은 일반 JavaScript에서 바로 작성할 실사용 코드라기보다, Wasm 모듈 내부에서 관리 참조 타입을 표현한다는 개념 예제다. 브라우저와 언어 컴파일러 양쪽이 이 모델을 지원해야 이점을 얻는다.
| 구분 | 선형 메모리 기반 런타임 | WasmGC 타입 |
|---|---|---|
| 객체 배치 | 바이트 주소를 런타임이 관리 | 구조체·배열 참조를 VM이 이해 |
| 수집기 | 언어 런타임이 포함할 수 있음 | 호스트 GC 활용 가능 |
| 상호 운용 | 경계 변환 필요 | 관리 참조 타입 활용 가능 |
| 기존 모듈 변화 | 현재 도구 체인 유지 | 컴파일러·런타임 지원 필요 |
Unity 빌드와 바로 연결되지 않는 이유
WasmGC가 브라우저에 들어왔다고 기존 IL2CPP 출력이 자동으로 WasmGC 객체 모델로 바뀌지는 않는다. Unity의 컴파일러, 런타임, 직렬화, 네이티브 플러그인 경계가 그 방식을 선택하고 지원해야 한다. 브라우저 기능 지원과 엔진 빌드 파이프라인 지원은 따로 확인해야 한다.
따라서 현재 Unity WebGL 메모리를 줄일 때는 여전히 실제 산출물을 봐야 한다.
- 초기 다운로드와 압축 해제 뒤 에셋 크기
- 텍스처·오디오·번들 로딩 시점
- 선형 메모리 증가와 브라우저 탭 전체 메모리
- Unity 객체 해제와
Resources.UnloadUnusedAssets시점 - JavaScript 플러그인이 잡고 있는 DOM·ArrayBuffer 참조
간단한 측정 로그도 층을 나누면 해석하기 편하다.
function reportMemory(label, wasmMemory) {
const wasmMiB = wasmMemory.buffer.byteLength / 1024 / 1024;
const jsMiB = performance.memory
? performance.memory.usedJSHeapSize / 1024 / 1024
: null;
console.table({ label, wasmMiB, jsMiB });
}
이 값은 모든 브라우저에서 제공되는 완전한 프로파일러가 아니다. 그래도 JS 힙과 Wasm 선형 메모리를 하나의 숫자로 부르지 않는 습관을 만드는 데는 도움이 된다.
WasmGC는 분명 중요한 변화다. 다만 내가 당장 얻은 교훈은 새 기능에 대한 기대보다 메모리의 소유자를 먼저 구분하는 것이었다. Unity 객체, Wasm 선형 메모리, JavaScript 객체가 어디에서 생기고 누가 해제하는지부터 확인해야 문제를 제대로 좁힐 수 있다.
측정할 때도 한 번의 숫자보다 장면 전환 전후의 변화를 보는 편이 낫다. 첫 로드, 큰 에셋 로드, 화면 종료, 같은 화면 재진입을 같은 순서로 반복하고 각 층의 메모리를 기록한다. 재진입할 때마다 선형 메모리가 커지는지, JS 참조가 남는지, Unity 객체 수가 돌아오는지를 따로 보면 “브라우저 메모리가 높다”는 막연한 증상을 재현 가능한 문제로 바꿀 수 있다.