Cheondi
개발 · 웹

WasmGC와 Unity WebGL의 메모리

WasmGC가 브라우저에 들어온 뒤 Unity WebGL의 선형 메모리와 무엇이 다른지, 메모리 문제를 어느 층에서 봐야 하는지 정리했습니다.

  • #wasm
  • #wasmgc
  • #unity
  • #webgl
  • #memory

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 객체 수가 돌아오는지를 따로 보면 “브라우저 메모리가 높다”는 막연한 증상을 재현 가능한 문제로 바꿀 수 있다.

참고: V8의 WasmGC 언어 포팅 설명