Three.js 렌더링 파이프라인: Scene → Camera → Renderer 한 번에 이해하기

Three.js가 장면을 화면에 그리는 최소 구성(Scene/Camera/Renderer)과 렌더 루프(requestAnimationFrame)의 의미를 "왜 이렇게 설계됐는지" 관점에서 정리한다.

Seobway · · 12분

이 글의 목표

Three.js를 처음 접하면 코드가 대개 이렇게 시작한다.

이 글은 "어떻게 쓰는지"보다 왜 이런 모양인지(내부 동작/역할 분리)를 설명한다. 이후 최적화 글에서 무엇을 멈추고( pause ), 무엇을 줄이고(DPR), 무엇을 스로틀(rAF) 해야 하는지 판단할 수 있게 만드는 게 목적이다.


전체 흐름 한눈에

%% desc: Scene → Camera → Renderer → 화면 렌더링 파이프라인
flowchart LR
  RAF["requestAnimationFrame\n(브라우저 repaint 직전 호출)"]
  Update["씬 업데이트\nmesh.rotation, position 등"]
  Render["renderer.render(scene, camera)\n실제 GPU 드로우콜"]
  Canvas["Canvas\n(화면 픽셀)"]

  RAF --> Update --> Render --> Canvas
  Canvas -.->|"다음 프레임 예약"| RAF

  style RAF    fill:#1e3a5f,color:#7dd3fc,stroke:#334155
  style Update fill:#1c3a2b,color:#6ee7b7,stroke:#334155
  style Render fill:#3b1f5e,color:#c4b5fd,stroke:#334155
  style Canvas fill:#4a1f1f,color:#fca5a5,stroke:#334155

1) Scene: "렌더링 대상"을 담는 그래프(컨테이너)

Three.js의 Scene은 실제로 "화면"이 아니라 렌더링할 오브젝트들을 담는 컨테이너다.
Mesh/Light/Group 같은 것들이 트리 형태로 붙고, 그 결과가 렌더러에 전달된다.

핵심 포인트:


2) Camera: "어떤 시점으로 볼지" + "어디까지 보이는지(절두체)"

카메라는 2가지를 동시에 결정한다.

실전에서 가장 많이 쓰는 건 PerspectiveCamera다. (원근감이 생김)

// fov: 수직 시야각(도), aspect: 가로/세로 비율, near/far: 렌더 범위
const camera = new THREE.PerspectiveCamera(
  75, // fov
  canvas.clientWidth / canvas.clientHeight, // aspect
  0.1, // near
  100, // far
);
camera.position.set(0, 2, 5);

이 지점이 최적화와 연결된다:


3) WebGLRenderer: "Scene+Camera를 캔버스 픽셀로 바꾸는 엔진"

Three.js에서 실제로 GPU(WebGL) 호출을 담당하는 쪽이 WebGLRenderer다.

가장 중요한 1줄은 이거다.

renderer.render(scene, camera);

render()"지금 이 순간의 Scene을, 지금 이 Camera로 봤을 때의 결과를" 캔버스에 그린다.

즉, 렌더링 비용은 대체로 다음으로 구성된다.


4) requestAnimationFrame(rAF): "프레임 타이밍에 맞추는 루프"

브라우저는 화면을 계속 repaint 한다. requestAnimationFrame()은 "다음 repaint 직전에 콜백을 호출해 달라"는 API다.[1]

가장 단순한 형태는 이렇게 생겼다.

function animate() {
  // 씬 업데이트
  mesh.rotation.y += 0.01;

  // 실제 렌더
  renderer.render(scene, camera);

  // 다음 프레임 예약 (one-shot이므로 반드시 재호출)
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

중요한 디테일:

루프를 멈추고 싶을 때는 cancelAnimationFrame(rafId)를 쓴다.

let rafId = null;

function startLoop() {
  function tick() {
    renderer.render(scene, camera);
    rafId = requestAnimationFrame(tick);
  }
  rafId = requestAnimationFrame(tick);
}

function stopLoop() {
  if (rafId !== null) {
    cancelAnimationFrame(rafId);
    rafId = null;
  }
}

5) "항상 루프"가 답은 아니다: Rendering on Demand

Three.js 공식 매뉴얼은 "움직이지 않는 화면을 굳이 매 프레임 렌더링하는 건 낭비"라고 말한다.[2]

핵심은 이거다.

이 관점은 "UI가 뜨는 순간" 최적화와 직결된다.

그래서 최적화 글에서는 "오버레이 열릴 때 3D pause" 같은 기술이 자연스럽게 등장한다.


6) 인터랙티브 데모

아래 데모에서 파이프라인을 직접 체험할 수 있다.


참고

[1] Window: requestAnimationFrame() — MDN Web Docs

[2] Rendering on Demand — Three.js Manual

[3] Responsive Design — Three.js Manual


관련 글