Frontend

[프론트엔드] Three.js 렌더링 구조와 최적화

crayonzgrim 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 시스템을 만드는 사람의 가장 큰 차이다.

반응형