-
[프론트엔드] Three.js 렌더링 구조와 최적화Frontend 2026. 5. 15. 17:05반응형

Three.js 렌더링 구조와 최적화 GPU 비용의 실체
Three.js를 처음 사용할 때는 대부분 이런 흐름으로 시작한다.
const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(); const renderer = new THREE.WebGLRenderer(); renderer.render(scene, camera);처음에는 잘 동작한다.
하지만 프로젝트 규모가 커지면 갑자기 문제가 발생한다.
- FPS 하락
- GPU 사용량 증가
- 모바일 발열
- 메모리 누수
- 브라우저 탭 크래시
- 특정 기기에서만 느려지는 현상
문제는 단순하다.
많은 개발자들이 Three.js API는 배우지만,
렌더링 구조 자체는 이해하지 못한 상태로 개발하기 때문이다.Three.js 최적화는 단순 팁 모음이 아니다.
- GPU가 무엇을 비용으로 느끼는지
- 브라우저 렌더링이 어떻게 동작하는지
- Draw Call이 왜 위험한지
- Texture가 왜 메모리를 폭발시키는지
- React re-render가 왜 치명적인지
여러가지 것들을 구조적으로 이해해야 한다.
1. Three.js는 결국 WebGL Wrapper다
Three.js는 GPU를 직접 렌더링하는 엔진이 아니다.
실제로는:
Three.js ↓ WebGL API ↓ OpenGL ES ↓ GPU Driver ↓ GPU이러한 구조를 가지고 있다.
즉 Three.js는:
- Scene Graph 관리
- Matrix 계산
- Material 관리
- Draw Call batching 일부 처리
- Shader 관리
- State cache
같은 작업을 대신해주는 추상화 레이어다.
진짜 비용은 결국 GPU에서 발생한다.
그래서 Three.js 최적화는:
"JavaScript 최적화"
가 아니라,
"GPU 작업량 최적화"
에 가깝다.
2. 렌더링 파이프라인의 이해
Three.js 렌더링은 내부적으로 꽤 많은 과정을 거친다.
대략적으로는:
Scene Update ↓ Matrix Update ↓ Frustum Culling ↓ Material Program Binding ↓ Geometry Binding ↓ Texture Binding ↓ Draw Call ↓ Fragment Shader ↓ Framebuffer Output이런 순서로 진행된다.
이 흐름을 이해하면:
- 무엇이 CPU 비용인지
- 무엇이 GPU 비용인지
- 무엇이 병목인지
구분할 수 있게 된다.
3. CPU 병목 vs GPU 병목
Three.js 성능 문제는 크게 두 종류다.
CPU 병목
주로:
- JS 계산
- Physics 계산
- Matrix 계산
- Scene traversal
- React re-render
- Animation update
이런 문제로 발생한다.
그에 따라 "메인 스레드 점유율 증가", "JS profiling에서 긴 task 발생", "일정하지 않은 FPS" 같은 증상들이 나타날 수 있다.
GPU 병목
주로:
- Draw Call 증가
- Overdraw
- 고해상도 Texture
- 복잡한 Fragment Shader
- Post Processing
에서 발생한다.
그에 따라 아래와 같은 증상들이 나타날 수 있다.
- GPU usage 증가
- 발열
- 모바일 성능 저하
- 특정 GPU에서만 느림
실무에서는 GPU 병목이 훨씬 많다.
4. Draw Call이 왜 위험한가?
Three.js 최적화에서 가장 중요한 개념 중 하나다.
Draw Call은:
CPU가 GPU에게 "이 객체를 렌더링해"라고 요청하는 작업이다.
예를 들어 mesh 하나를 렌더링하면:
renderer.render(scene, camera);내부적으로:
bind shader bind geometry bind texture upload uniforms draw triangles같은 GPU 상태 변경이 발생한다.
문제는 GPU 연산보다 상태 변경 비용이 더 비싼 경우가 많다는 점이다.
즉:
10000 triangles 1개보다
100 triangles 100개가 더 느릴 수 있다.
왜냐하면 Draw Call이 100번 발생하기 때문이다.
5. Instancing이 중요한 이유
예를 들어 나무 5000개를 렌더링한다고 해보자.
잘못된 방식:
for (let i = 0; i < 5000; i++) { scene.add(new THREE.Mesh(geometry, material)); }이 방식은 Draw Call이 매우 많이 발생한다.
대신 InstancedMesh를 사용한다.
const mesh = new THREE.InstancedMesh( geometry, material, 5000 );이 방식은:
5000개의 mesh → 1개의 Draw Call로 줄일 수 있다.
이 차이는 엄청나다.
실제로 Three.js 프로젝트 성능 최적화의 상당수는 "Draw Call 줄이기" 라고 봐도 될 정도이다.
6. Geometry가 많은 것도 위험하다
Geometry는 vertex 데이터다.
예:
position normal uv tangent color skin weight등이 GPU Buffer에 올라간다.
문제는 vertex 수가 많아질수록:
- Vertex Shader 실행 증가
- Memory bandwidth 증가
- Upload 비용 증가
가 발생한다.
특히 GLTF 모델을 그대로 가져오면:
- vertex 수 과다
- hidden mesh 존재
- 불필요한 subdivision
가 굉장히 많다.
그래서 실무에서는 Blender에서:
- Decimate
- Mesh Merge
- Hidden geometry 제거
- LOD 생성
을 먼저 진행하는 경우가 많다.
7. Texture가 가장 위험한 경우도 많다
많은 개발자들이 polygon 수만 신경쓴다.
하지만 실제로는 Texture가 GPU 메모리를 가장 많이 먹는 경우가 많다.
예를 들어:
4096 x 4096 RGBA Texture는:
4096 × 4096 × 4 bytes ≈ 64MB를 사용한다.
여기에:
- mipmap
- normal map
- roughness map
- AO map
까지 추가되면 GPU 메모리가 급격히 증가한다.
모바일에서는 특히 치명적이다.
8. Texture 최적화 전략
KTX2 Compression
현재 가장 중요한 texture 최적화 중 하나.
Basis Universal 기반 compressed texture다.
장점:
- GPU compressed format 사용
- 메모리 감소
- bandwidth 감소
- loading 감소
실무에서는 거의 필수에 가깝다.
Texture Atlas
여러 texture를 하나로 합치는 방식.
장점:
- texture binding 감소
- draw call 감소
특히 sprite 기반 렌더링에서 중요하다.
해상도 줄이기
대부분 texture는 생각보다 높은 해상도가 필요 없다.
예:
4K → 1K만 줄여도 체감 품질 차이가 거의 없는 경우가 많다.
9. Fragment Shader 비용이 진짜 비싸다
GPU에서 가장 비싼 영역 중 하나다.
왜냐하면 "화면의 픽셀마다 실행"되기 때문이다.
예를 들어:
1920 x 1080 ≈ 2 million pixels즉 fragment shader는 프레임마다 수백만 번 실행된다.
그래서:
- 복잡한 lighting
- noise
- raymarching
- blur
- refraction
같은 shader는 비용이 매우 높다.
특히 모바일 GPU는 fragment 성능이 약하다.
10. Overdraw 문제
Overdraw는 "이미 그린 픽셀 위에 또 그리는 현상" 을 말한다.
예:
- transparent object
- particle
- glass
- alpha blending
이 많을 때 발생한다.
GPU는 같은 픽셀을 여러 번 계산해야 한다.
특히 particle system이 많은 경우 FPS가 급격히 떨어진다.
11. Transparency가 어려운 이유
transparent material은 depth sorting 문제가 있다.
Three.js는 transparent object를 뒤에서 앞으로 정렬한다.
하지만:
- intersecting mesh
- particle
- glass
에서는 sorting artifact가 자주 발생한다.
그리고 transparency는:
- early-z optimization 불가능
- overdraw 증가
- blending 비용 증가
문제도 발생한다.
그래서 실무에서는 "가능한 transparent 사용을 줄인다."
12. Frustum Culling 이해하기
카메라 밖의 객체는 렌더링할 필요가 없다.
Three.js는 기본적으로 Frustum Culling을 수행한다.
즉:
Camera View 밖 → Draw Call 생략한다.
하지만:
- skinned mesh
- particle
- custom shader
에서는 bounding 계산이 부정확할 수 있다.
이 경우:
mesh.frustumCulled = false;를 사용하는데,
남용하면 성능이 급격히 떨어진다.
13. Shadow는 생각보다 엄청 비싸다
많은 사람들이 shadow를 켜고 시작한다.
renderer.shadowMap.enabled = true;하지만 shadow는:
추가 렌더링 패스
다.
즉 그림자를 위해 scene을 다시 렌더링한다.
광원이 여러 개면 비용은 더 증가한다.
특히:
- PointLight shadow
- large shadow map
- soft shadow
는 비용이 크다.
실무에서는:
- baked shadow
- fake shadow
- contact shadow
를 많이 사용한다.
14. Post Processing의 진짜 비용
Bloom 같은 효과는 보기엔 단순해 보인다.
하지만 내부적으로는:
Scene Render → Extract Bright Area → Blur Pass → Blur Pass → Composite같은 multi-pass rendering이 발생한다.
즉 화면 전체를 여러 번 다시 렌더링한다.
그래서 후처리는 GPU 비용이 굉장히 높다.
특히
- SSAO
- DOF
- Motion Blur
는 모바일에서 위험하다.
15. React Three Fiber에서 자주 발생하는 성능 문제
R3F는 편하지만 React 특유의 문제가 있다.
매 frame state 변경
이건 매우 위험하다.
useFrame(() => { setCount(v => v + 1); });이 코드는:
60fps → 초당 60번 React render를 유발할 수 있다.
대부분의 animation state는:
ref.current.position.x += delta;처럼 imperative하게 처리해야 한다.
불필요한 component re-render
<MyMesh position={position} />에서 position 객체가 계속 새로 생성되면 rerender가 발생한다.
그래서:
- useMemo
- stable reference
- object pooling
이 중요하다.
16. GPU 메모리 누수는 실무에서 진짜 많이 발생한다
Three.js는 GPU 리소스를 자동 정리하지 않는다.
예:
geometry.dispose(); material.dispose(); texture.dispose();를 하지 않으면:
- GPU memory leak
- context loss
- browser crash
가 발생할 수 있다.
특히 위험한 건:
- RenderTarget
- EffectComposer
- Dynamic Texture
- Video Texture
- GLTF 교체
다.
SPA 환경에서는 페이지 이동마다 누수가 쌓인다.
17. WebGL Context Loss 이해하기
브라우저는 GPU 메모리가 부족하면:
WebGL Context Lost를 발생시킨다.
이 순간:
- texture
- buffer
- shader
가 모두 사라질 수 있다.
모바일에서 특히 자주 발생한다.
그래서:
- dispose 철저히 수행
- texture 최적화
- render target 최소화
가 중요하다.
18. 실무에서 가장 효과 큰 최적화 순서
많은 사람들이 shader micro optimization부터 한다.
하지만 실제로 효과가 큰 순서는 다르다.
1순위
Draw Call 줄이기.
2순위
Texture 메모리 줄이기.
3순위
Post Processing 줄이기.
4순위
Transparency 줄이기.
5순위
Shadow 최적화.
마지막
Shader micro optimization.
실제로 대부분 병목은 shader 코드 몇 줄이 아니라:
- draw call
- texture
- overdraw
- shadow
에서 발생한다.
19. 디버깅 도구는 반드시 사용해야 한다
실무에서는 감으로 최적화하면 안 된다.
꼭 사용하는 도구:
Spector.js
WebGL frame 분석 도구.
확인 가능:
- draw call
- shader
- texture binding
- framebuffer
Chrome Performance
CPU 병목 분석.
Chrome GPU Tab
GPU 상태 확인.
stats.js
FPS 및 frame time 확인.
Drei PerformanceMonitor
adaptive quality 구현 가능.
20. 결국 Three.js 최적화의 핵심은 이것이다
Three.js 최적화는:
"JavaScript 코드 최적화"
보다,
"GPU가 해야 하는 일을 줄이는 것"
에 훨씬 가깝다.
그리고 좋은 Three.js 개발자는:
- draw call을 줄이고
- texture를 압축하고
- overdraw를 줄이고
- shadow를 최소화하고
- GPU 메모리를 관리하고
- React re-render를 통제한다.
즉:
렌더링 구조 자체를 이해한다.
이게 단순히 예쁜 3D를 만드는 사람과,
실제 서비스에서 안정적으로 동작하는 3D 시스템을 만드는 사람의 가장 큰 차이다.반응형'Frontend' 카테고리의 다른 글
"Jog가 느린 이유".... 원인은 네 군데! (0) 2026.06.29 프론트엔드 폴더 스트럭처 구성 종류 (폴더링 패턴) (0) 2025.09.11 모바일에서 웹 캐러셀 중첩 스크롤 처리 (safari) (3) 2025.07.29 프론트엔드 고화질 이미지 다루는 방법 (1) 2025.06.24 강력새로고침 하면 불러오는데 일반새로고침은 불러오지 못한다? _ service worker (0) 2025.06.04