ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [프론트엔드] 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 시스템을 만드는 사람의 가장 큰 차이다.

    반응형
Designed by Tistory.