FBXLoader 메모리 누수
한 줄 요약
고객사 시연 중 3D 모델 페이지에서 반복적으로 흰 화면 크래시가 발생했고, Chrome DevTools Memory 프로파일링으로 Three.js FBXLoader의 메모리 누수를 원인으로 규명했으나, dispose()만으로는 구조적 한계를 극복할 수 없었다.
문제 상황
드론 기반 3D 점검 SaaS의 3D 모델 페이지에서 모델을 렌더링할 때, 메모리 사용량이 시간이 지남에 따라 지속적으로 증가했다. 페이지를 벗어나도 동일한 메모리가 해제되지 않고 유지되면서, 서비스 전체의 속도가 점진적으로 저하됐다.
이 문제는 고객사 앞에서의 서비스 시연 중 크래시로 드러났다. 브라우저 탭이 흰 화면과 함께 자동 리로드되는 현상이 반복적으로 발생하면서 시연이 중단됐다. 이는 Chrome의 OOM(Out of Memory) 보호 메커니즘이 작동한 것으로, 탭의 메모리 사용량이 브라우저가 허용하는 한계를 초과했다는 뜻이었다.
원인 추적 과정
Chrome DevTools Memory 프로파일링
DevTools의 Memory 탭 자체는 이전에도 알고 있던 도구였지만, 이 문제를 계기로 처음으로 체계적으로 사용했다. "These 5 Bad JavaScript Practices Will Lead to Memory Leaks" 아티클(javascript.plainenglish.io)을 참고하여 Allocation instrumentation on timeline 기능의 사용법을 익혔다.
핵심은 녹화 방식의 설계에 있었다. 단순히 한 번 페이지를 열어보는 것이 아니라, 실제 사용자의 반복적 행동 패턴을 재현했다:
프로젝트 페이지 → 3D 모델 페이지 → 프로젝트 페이지 → 3D 모델 페이지 (반복)
이 패턴으로 녹화하면 "페이지를 벗어났을 때 메모리가 제대로 해제되는지"를 직접 관찰할 수 있다. 정상적인 경우라면 페이지를 떠날 때 메모리가 감소해야 하지만, 녹화 결과에서는 메모리가 계속 누적되기만 하는 패턴이 확인됐다.
Shallow Size vs Retained Size로 범인 특정
녹화된 Snapshot 표에서 각 Constructor를 열어보며 분석했을 때, Three.js의 FBXLoader가 원인으로 특정됐다.
핵심 단서는 두 가지 수치의 극단적 불균형이었다:
- Shallow Size (객체 자신의 크기): 작음
- Retained Size (이 객체가 참조하는 전체 객체 트리의 크기): 비정상적으로 큼
이 불균형이 의미하는 것은, FBXLoader 객체 자체는 작지만, 이 객체가 참조 체인을 통해 거대한 메모리 트리를 붙잡고 있어서 가비지 컬렉터가 해당 메모리를 회수하지 못한다는 것이었다.
코드 레벨에서의 확인
3D 뷰어 컴포넌트의 useEffect 내에서 new FBXLoader()로 FBX 데이터를 로드하여 뷰어에 추가하는 로직이 있었다. 해당 useEffect를 주석 처리하고 다시 Snapshot을 녹화했을 때, 급격한 메모리 사용 막대가 나타나지 않았다. 이 과정에서 material, scene에 추가된 모델들이 대량의 메모리를 할당받고 있었다.
특히 FBX 모델 로드 실패 시 리로드하는 과정이 치명적이었다. 로드에 실패한 메모리는 GC에 의해 정리되지 않은 채 유지되면서, 새로운 로드 시도가 추가 메모리를 누적시켰다.
해결 시도와 그 한계
dispose() 적용
컴포넌트가 unmount된 이후에 모든 텍스처와 모델에 대해 .dispose()를 호출하는 코드를 추가했다:
- Function Retained Size: 2,280,144 → 771,263 (3배 감소)
- Worker Retained Size: 1,827,488 → 324,659 (5배 감소)
Retained Size가 줄었다는 것은 해당 Constructor를 참조하는 오브젝트의 양이 줄었다는 의미다. 수치상으로는 의미 있는 변화였다.
왜 이것만으로는 부족했는가
dispose()로 참조 체인은 일부 끊었지만, 메모리 변동폭 자체가 개선되지 않았다. 3D 모델 페이지를 반복 방문하면 여전히 메모리가 누적되었고, 충분히 반복하면 결국 크래시로 이어졌다. Three.js의 FBXLoader 내부에서 발생하는 근본적인 메모리 관리 문제를 외부에서 완전히 통제하는 것은 불가능했다.
단순한 기술 문제가 아니었다
FBXLoader 메모리 누수는 결국 기존 뷰어에서 Cesium.js로의 전면 전환이라는 결정으로 이어졌다. 하지만 이 결정은 메모리 문제 하나만으로 이루어진 것이 아니었다. 여러 요인이 복합적으로 작용했다:
- 메모리 문제의 구조적 한계: dispose()로도 메모리 변동폭이 개선되지 않음
- 비즈니스 요구사항: 고객사가 Point Cloud뿐 아니라 Mesh 시각화를 필수로 요구. 모델을 GLB로 통으로 뽑아서 올릴 수는 있지만, 메모리 최적화가 불가능하여 드론 이미지 기반 대용량 Mesh 모델에는 한계
- 개발 자유도: 기존 뷰어는 커스텀 기능 개발과 커스텀 디자인 적용이 어려움
- 포맷 지원 범위: 건설 업계에서 사용하는 다양한 파일 포맷 지원이 Cesium.js에 비해 부족
- 대안 검토: Three.js도 고려했으나, 당시 기준으로 대용량 모델 렌더링에 부적합하다고 판단 (최근에는 개선되었을 수 있음)
이 다섯 가지가 함께 작용하여, 단순한 라이브러리 교체가 아닌 3D 뷰어 시스템의 전면 교체라는 결정이 내려졌다.
이 경험에서 추출한 원칙
-
"현상"이 아니라 "재현 시나리오"를 먼저 설계하라. 단순히 "느리다"가 아니라, "반복적 페이지 전환" 같은 구체적 시나리오가 있어야 문제를 측정할 수 있다.
-
Shallow Size와 Retained Size의 불균형은 참조 체인 누수의 강력한 신호다. 객체 자체는 작은데 Retained가 비정상적으로 크면, 그 객체가 GC를 방해하는 참조 사슬의 시작점일 가능성이 높다.
-
기술적 해결이 구조적 한계에 부딪힐 때, 도구 교체를 두려워하지 마라. 다만 교체 결정은 기술적 이유 하나가 아니라, 비즈니스 요구사항·개발 자유도·생태계 지원 범위를 함께 고려한 복합적 판단이어야 한다.
FBXLoader 메모리 누수
문제 해결