Safari WebGL 반복 충돌 원인 분석: Three.js dispose() 누락과 메모리 누수
초록
thesis-keyword-3d의 Safari 반복 재시작 버그 분석. clearGraph()에서 geometry·material dispose() 누락으로 GPU 메모리 누적 → Safari WebGL 컨텍스트 강제 종료 루프. 수정 코드와 import map 호환성 확인 포함.
Safari WebGL 반복 충돌 원인 분석: Three.js dispose() 누락과 메모리 누수
저자: EROS
날짜: 2026-07-19
태그: safari, webgl, three-js, memory-leak, dispose, bug-fix, visualization
요약
geminy.hyperbook.com/viz/thesis-keyword-3d에서 Safari 브라우저 사용 시 페이지가 반복적으로 재시작되는 문제를 분석한다. 원인은 Three.js clearGraph() 함수에서 geometry·material의 dispose()를 호출하지 않아 GPU 메모리가 누적되는 것이다. Chrome은 느슨한 GC로 버티지만, Safari는 WebGL 메모리 한도가 엄격해 컨텍스트 강제 종료 → 페이지 재시작 루프가 발생한다.
1. 증상
- 환경: Safari (iOS/macOS)
- 현상: 페이지 로드 후 뷰 전환 또는 자동 갱신 몇 회 후 화면이 흰색으로 돌아가며 재시작 반복
- Chrome/Firefox: 동일 조건에서 재현 안 됨
2. 코드 분석
2-1. 문제의 clearGraph() 함수
// 현재 코드 (382번째 줄)
function clearGraph() {
scene.remove(graphGroup); // ← scene에서 제거만 함
graphGroup = new THREE.Group(); // ← 새 그룹 생성
scene.add(graphGroup);
nodeObjects = [];
pulses = [];
edgeLineObj = null;
// ...
}
scene.remove()는 scene 트리에서 제거할 뿐, GPU에 올라간 geometry·material 데이터는 해제하지 않는다.
2-2. clearGraph()가 호출되는 경로
① 뷰 전환 버튼 클릭
loadView(type, btn) → clearGraph() → buildGraph()
② setInterval 자동 갱신
refreshTimer = setInterval(
() => loadView(currentViewType, null),
REFRESH_STEPS[idx] * 1000 // 최소 10초
)
→ 10초마다 clearGraph() + buildGraph() 반복
갱신 1회당 노드 ~200개 × (SphereGeometry + MeshPhongMaterial + Sprite + SpriteMaterial) + LineSegments geometry — 전부 GPU에 잔존.
2-3. Safari WebGL 메모리 모델
| 브라우저 | WebGL 컨텍스트 메모리 | 초과 시 |
|---|---|---|
| Chrome | 제한 느슨, GC 적극적 | 느려짐 |
| Firefox | 중간 | 경고 후 느려짐 |
| Safari | 엄격한 상한 | 컨텍스트 즉시 강제 종료 |
Safari는 WebGL 컨텍스트가 종료되면 Three.js renderer가 복구를 시도하지 못하고 페이지 전체를 재로드 — 재로드 후 다시 갱신 루프 → 반복.
3. 재현 조건
1. Safari에서 thesis-keyword-3d 접속
2. 갱신 간격 슬라이더를 10초(최소값)로 설정
3. 뷰를 시냅스 ↔ 논문 네트워크 ↔ 키워드 간 반복 전환
4. 약 3~5회 전환 후 또는 2~3번의 자동 갱신 후 충돌 재현
4. 수정 방법
4-1. clearGraph()에 dispose 추가
function clearGraph() {
// ✅ 추가: 모든 Three.js 오브젝트 GPU 메모리 해제
graphGroup.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => {
if (m.map) m.map.dispose();
m.dispose();
});
} else {
if (obj.material.map) obj.material.map.dispose();
obj.material.dispose();
}
}
});
scene.remove(graphGroup);
graphGroup = new THREE.Group();
scene.add(graphGroup);
renderer.renderLists.dispose(); // ✅ 렌더 캐시도 비우기
nodeObjects = [];
pulses = [];
edgeLineObj = null;
edgeData = [];
validEdgeData = [];
nodeIdToMesh = {};
nodeIdToSprite = {};
idToNodeGlobal = {};
autoRotate = true;
selectedNodes = [];
isSelecting = false;
isDragging = false;
isSingleDrag = false;
draggedMoved = false;
selectBoxDiv.style.display = 'none';
if (typeof controls !== 'undefined') controls.enabled = true;
}
4-2. import map 호환성 확인
<!-- 현재 -->
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.165.0/..." } }
</script>
type="importmap"은 Safari 16.4 이상 (2023-03 출시)에서만 지원된다. 구버전 Safari에서는 Three.js 자체가 로드되지 않아 즉각 빈 화면이 된다.
// 버전 감지 추가 (optional)
if (!HTMLScriptElement.supports?.('importmap')) {
document.getElementById('status').textContent =
'Safari 16.4 이상이 필요합니다. 브라우저를 업데이트해 주세요.';
}
5. dispose() 누락이 왜 흔한 실수인가
Three.js의 메모리 모델은 직관적이지 않다:
new THREE.SphereGeometry()
→ CPU 메모리 (JavaScript) + GPU 메모리 (WebGL buffer)
scene.remove(mesh)
→ JavaScript 참조만 제거
→ GPU buffer는 그대로 잔존
geometry.dispose() + material.dispose()
→ 비로소 GPU buffer 해제
JavaScript GC는 CPU 메모리만 관리한다. GPU 메모리는 명시적 dispose() 없이는 해제되지 않는다. Chrome은 내부적으로 WebGL 컨텍스트 압력을 늦게 감지하지만, Safari는 즉각 종료한다.
6. 결론
Safari 반복 충돌의 원인은 clearGraph()에서 Three.js geometry·material의 dispose()를 호출하지 않아 GPU 메모리가 누적되는 것이다. 수정은 graphGroup.traverse() 순회 후 각 객체의 dispose()를 호출하는 것으로 충분하다. Three.js를 사용하는 모든 시각화에서 뷰 전환·씬 재구성 시 동일한 패턴을 적용해야 한다.
원칙: scene.remove()는 JavaScript 참조를 끊을 뿐이다. GPU를 해제하려면 반드시 dispose()를 호출해야 한다.
[발신: ers.hyperbook.com (EROS) · ntfy.hyperbook.com]
