Three.js 체험기: 브라우저에서 3D 세계 만들기
Scene, Camera, Renderer부터 Lighting, Animation, OrbitControls까지 — 프론트엔드 개발자의 Three.js 첫걸음
읽는 데 27분
- #three.js
- #3D
- #WebGL
- #interactive
- #animation
- #canvas
이 문서의 목차
적용 환경: 모던 브라우저 (WebGL 2.0 지원), three.js r182, TypeScript 5+
프론트엔드 개발자라면 DOM과 CSS를 다루는 데는 익숙하지만, 브라우저 안에서 3D 오브젝트를 렌더링한다는 것은 전혀 다른 영역처럼 느껴집니다. div를 배치하고 flex로 정렬하는 감각과, 3차원 공간에 물체를 놓고 카메라로 바라보는 감각은 근본적으로 다르기 때문입니다.
Three.js는 이 간극을 메워주는 라이브러리입니다. WebGL이라는 저수준 API를 직접 다루지 않고도, JavaScript 객체를 조합하는 방식으로 3D 씬을 구성할 수 있습니다. React에서 컴포넌트를 조합하듯, Three.js에서는 Scene, Camera, Mesh, Light를 조합하여 하나의 3D 세계를 만들어냅니다.
이 문서는 Three.js를 처음 접하는 프론트엔드 개발자의 관점에서, 빈 캔버스에서 시작해 인터랙티브한 3D 씬까지 한 단계씩 만들어가는 과정을 기록한 것입니다. 각 단계마다 실제로 동작하는 데모가 포함되어 있으므로, 코드를 따라 치면서 결과를 확인할 수 있습니다.
Three.js로 무언가를 화면에 그리려면, 반드시 세 가지 요소가 필요합니다. 영화 촬영에 비유하면 이해가 빠른데, Scene은 배우와 소품이 놓일 촬영장이고, Camera는 그 장면을 바라보는 카메라이며, Renderer는 카메라가 찍은 영상을 스크린에 출력하는 프로젝터에 해당합니다.
import * as THREE from "three";
// 1. 촬영장 만들기
const scene = new THREE.Scene();
// 2. 카메라 배치하기
const camera = new THREE.PerspectiveCamera(
75, // 시야각(FOV)
window.innerWidth / window.innerHeight, // 종횡비
0.1, // 가까운 클리핑 면
1000 // 먼 클리핑 면
);
camera.position.z = 5; // 카메라를 뒤로 물러나게 함
// 3. 프로젝터 설정하기
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 렌더링 실행
renderer.render(scene, camera);PerspectiveCamera의 첫 번째 인자인 FOV(Field of View)는 카메라가 바라보는 시야의 넓이를 도(degree) 단위로 지정합니다. 75도 정도면 인간의 시야와 비슷한 자연스러운 원근감을 얻을 수 있으며, 값을 높이면 광각 렌즈처럼, 낮추면 망원 렌즈처럼 보이게 됩니다.
아래 데모에서 빈 Scene이 렌더링된 결과를 확인할 수 있습니다. 아직 아무것도 추가하지 않았기 때문에 배경색만 보입니다.
촬영장은 준비되었지만 텅 비어 있습니다. 이제 이 무대 위에 첫 번째 오브젝트를 올려보겠습니다.
Three.js에서 눈에 보이는 3D 오브젝트를 만들려면 두 가지 재료가 필요합니다. Geometry는 오브젝트의 형태(꼭짓점과 면의 배치)를 정의하고, Material은 그 표면이 어떻게 보이는지(색상, 질감, 반사)를 결정합니다. 이 둘을 결합한 것이 바로 Mesh입니다.
// 형태: 가로 2, 세로 2, 깊이 2인 정육면체
const geometry = new THREE.BoxGeometry(2, 2, 2);
// 표면: 단순한 단색 재질 (조명 영향을 받지 않음)
const material = new THREE.MeshBasicMaterial({ color: 0x6366f1 });
// Mesh = Geometry + Material
const cube = new THREE.Mesh(geometry, material);
// 여러 면이 보이도록 비스듬히 회전
cube.rotation.x = 0.5;
cube.rotation.y = 0.5;
// Scene에 추가
scene.add(cube);여기서 사용한 MeshBasicMaterial은 조명의 영향을 전혀 받지 않는 가장 단순한 재질입니다. 어떤 빛을 비추든, 지정한 색상 그대로 렌더링됩니다. 디버깅이나 프로토타이핑 단계에서는 유용하지만, 현실적인 외관을 원한다면 다른 Material이 필요합니다.
왜 MeshBasicMaterial부터 시작하는가
MeshBasicMaterial은 조명과 무관하게 동작하므로, Scene/Camera/Renderer가 올바르게 설정되었는지 검증하기에 적합합니다. 만약 이 단계에서 아무것도 보이지 않는다면, 조명 문제가 아니라 카메라 위치나 Renderer 설정에 문제가 있다는 것을 바로 알 수 있습니다.
큐브가 화면에 나타났고, 비스듬히 놓여 있어 세 면이 동시에 보입니다. 그런데 면마다 색상 차이가 전혀 없이 모든 면이 동일한 단색으로 렌더링되고 있습니다. MeshBasicMaterial은 조명을 무시하기 때문에, 빛이 어느 방향에서 오든 표면의 밝기가 변하지 않습니다. 다음 단계에서 조명과 함께 Material을 교체하면, 같은 큐브가 얼마나 다르게 보이는지 확인할 수 있습니다.
현실 세계에서 물체의 형태를 인식할 수 있는 것은 빛 덕분입니다. 빛이 표면에 닿는 각도에 따라 밝은 면과 어두운 면이 생기고, 이 명암의 차이가 깊이감을 만들어냅니다. Three.js에서도 마찬가지로, 조명을 추가해야 비로소 3D 오브젝트가 입체적으로 보이기 시작합니다.
단, 조명의 효과를 받으려면 Material도 변경해야 합니다. MeshBasicMaterial은 조명을 무시하므로, 빛에 반응하는 MeshStandardMaterial로 교체합니다. 이 Material은 물리 기반 렌더링(PBR)을 사용하여, metalness(금속성)와 roughness(거칠기) 두 가지 파라미터로 재질의 특성을 표현합니다.
// MeshBasicMaterial → MeshStandardMaterial로 교체
const material = new THREE.MeshStandardMaterial({
color: 0x6366f1,
metalness: 0.3, // 0(비금속) ~ 1(완전 금속)
roughness: 0.4, // 0(거울같이 매끈) ~ 1(완전 무광)
});
// 환경광: 모든 방향에서 균일하게 비추는 빛
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 방향광: 특정 방향에서 오는 평행한 빛 (태양광과 유사)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);AmbientLight는 방향 없이 씬 전체를 균일하게 밝히는 빛입니다. 이것만으로는 명암이 생기지 않지만, 그림자가 완전히 검은색이 되는 것을 방지하는 역할을 합니다. DirectionalLight는 태양처럼 한 방향에서 평행하게 들어오는 빛으로, 이 빛이 표면에 닿는 각도에 따라 밝고 어두운 면이 구분됩니다.
앞선 데모와 비교하면 차이가 확연합니다. 같은 각도에서 바라본 같은 큐브인데도, 조명이 닿는 면은 밝고 반대편은 어둡게 렌더링되어 면마다 명확한 명암 차이가 생겼습니다. 이것이 3D 그래픽에서 조명이 핵심적인 요소인 이유입니다.
지금까지의 큐브는 정지 상태입니다. renderer.render(scene, camera)를 한 번만 호출했기 때문에, 화면에 찍힌 하나의 프레임만 보이는 것입니다. 오브젝트를 움직이게 하려면, 매 프레임마다 상태를 변경하고 다시 렌더링하는 애니메이션 루프가 필요합니다.
브라우저에서 이 루프를 구현하는 표준적인 방법은 requestAnimationFrame입니다. 이 API는 브라우저가 다음 화면을 그리기 직전에 콜백을 실행해주므로, 디스플레이의 주사율에 맞춘 부드러운 애니메이션이 가능합니다.
function animate() {
requestAnimationFrame(animate); // 다음 프레임 예약
// 매 프레임마다 큐브를 조금씩 회전
cube.rotation.x += 0.01; // 라디안 단위
cube.rotation.y += 0.01;
renderer.render(scene, camera); // 변경된 상태를 렌더링
}
animate(); // 루프 시작rotation 값은 라디안 단위이며, 매 프레임 0.01 라디안씩 증가시키면 약 60fps 기준으로 1초에 약 0.6 라디안(약 34도) 회전합니다. 이 값을 조절하면 회전 속도를 자유롭게 바꿀 수 있습니다.
requestAnimationFrame과 성능
requestAnimationFrame은 탭이 비활성화되면 자동으로 호출을 멈추기 때문에, setInterval과 달리 백그라운드에서 불필요한 연산을 하지 않습니다. 또한 브라우저의 렌더링 사이클과 동기화되어, 화면 찢김(tearing) 없는 부드러운 애니메이션을 보장합니다.
이제 큐브가 끊임없이 회전하고 있습니다. 하지만 카메라는 고정되어 있기 때문에, 큐브의 뒷면을 보려면 직접 회전시켜볼 수 있는 방법이 필요합니다.
Three.js에는 사용자 입력으로 카메라를 조작할 수 있는 다양한 컨트롤러가 내장되어 있습니다. 그중 가장 널리 쓰이는 것이 OrbitControls로, 마우스 드래그로 공전(orbit), 스크롤로 확대/축소, 우클릭으로 패닝(pan)을 할 수 있습니다.
OrbitControls는 Three.js의 핵심 라이브러리가 아닌 addons에 포함되어 있으므로, import 경로가 다르다는 점에 주의해야 합니다.
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// 카메라와 렌더러의 DOM 요소를 전달
const controls = new OrbitControls(camera, renderer.domElement);
// 관성 효과 활성화 - 드래그 후 서서히 멈춤
controls.enableDamping = true;
controls.dampingFactor = 0.05;
function animate() {
requestAnimationFrame(animate);
controls.update(); // damping 적용을 위해 매 프레임 호출 필수
renderer.render(scene, camera);
}enableDamping을 활성화하면 마우스를 놓은 후에도 관성이 적용되어, 카메라가 서서히 멈추는 자연스러운 느낌을 줍니다. 이 옵션을 사용할 때는 반드시 애니메이션 루프 안에서 controls.update()를 호출해야 합니다.
OrbitControls와 스크롤 충돌
OrbitControls는 마우스 휠 이벤트를 가로채서 줌으로 사용합니다. 문서 안에 임베딩된 캔버스에서는 이 동작이 페이지 스크롤과 충돌할 수 있으므로, 캔버스 위에서 스크롤할 때 줌이 동작할 수 있다는 점을 고려해야 합니다.
마우스로 자유롭게 시점을 바꿔보면, 하나의 정육면체도 보는 각도에 따라 전혀 다르게 느껴진다는 것을 알 수 있습니다. 카메라 조작이 가능해지면 3D 씬은 단순한 이미지가 아닌 탐험할 수 있는 공간이 됩니다.
빈 캔버스에서 시작해 사용자가 직접 조작할 수 있는 3D 씬까지, Three.js의 핵심 구조를 한 단계씩 구성해보았습니다.
이 문서에서 다룬 핵심 개념
Scene, Camera, Renderer — 모든 Three.js 앱의 기반이 되는 삼위일체. Scene은 오브젝트가 존재하는 공간이고, Camera는 그 공간을 바라보는 시점이며, Renderer는 이를 2D 화면에 출력합니다.
Mesh = Geometry + Material — 3D 오브젝트는 형태와 재질의 조합입니다. MeshBasicMaterial로 빠르게 검증하고, MeshStandardMaterial로 물리 기반 렌더링을 적용하는 것이 일반적인 워크플로우입니다.
Lighting — AmbientLight로 기본 밝기를 확보하고, DirectionalLight로 방향성 명암을 만드는 조합이 가장 기초적인 조명 설정입니다. PBR 재질은 조명 없이는 검게 렌더링되므로, Material과 Light는 항상 함께 고려해야 합니다.
Animation Loop — requestAnimationFrame 기반의 루프가 매 프레임 상태를 갱신하고 렌더링합니다. 탭 비활성화 시 자동으로 멈추는 것이 setInterval 대비 핵심적인 장점입니다.
OrbitControls — addons에 포함된 카메라 컨트롤러로, 마우스 드래그(공전), 스크롤(줌), 우클릭(패닝)을 지원합니다. enableDamping으로 관성 효과를 더하면 자연스러운 조작감을 얻을 수 있습니다.
여기서 더 나아가려면 Particle System(Points), 커스텀 GLSL 셰이더(ShaderMaterial), Post-Processing(EffectComposer, UnrealBloomPass), Raycaster 기반 인터랙션, 텍스처 매핑, glTF 모델 로딩 같은 주제가 기다리고 있습니다. React 환경에서는 React Three Fiber를 사용하면 선언적인 방식으로 Three.js 씬을 구성할 수 있어, 컴포넌트 기반 개발에 익숙한 프론트엔드 개발자에게 자연스러운 다음 단계가 됩니다.
- Three.js 공식 문서 (새 창)
- Three.js Fundamentals (새 창)
- Three.js Journey (새 창) — Bruno Simon의 인터랙티브 강의
- React Three Fiber (새 창) — React에서 Three.js를 선언적으로 사용
관련 문서
글쓴이 mirunamu00



