좀 더 인터랙티브한 웹 페이지를 만들어보고 싶어서 Three.js를 공부하게 되었다. 처음엔 그냥 3D 라이브러리 정도로 생각했는데, 막상 써보니까 렌더링 구조 자체를 이해해야 하는 도구라서 생각보다 개념 잡는 데 시간이 좀 걸렸다. 그래서 헷갈렸던 부분 없이 한 번에 다시 볼 수 있게 핵심 개념 위주로 정리해보려고 한다.
핵심 개념 5가지
Three.js는 결국 이 5개로 시작한다고 보면 된다.
- Scene
- Camera
- Renderer
- Mesh
- Light
Scene (무대)
Scene은 말 그대로 3D 공간 전체다. 모든 오브젝트는 Scene 위에 올라간다. 그냥 모든 게 존재하는 공간이라고 생각하면 된다.
scene.add(mesh);
scene.remove(mesh);
Camera (시점)
Camera는 이 Scene을 바라보는 눈이다. 보통 PerspectiveCamera를 사용한다. 사람 눈처럼 보이는 카메라이다.
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
- 75 → 시야각(FOV)
- aspect → 화면 비율
- near / far → 렌더링 범위
Renderer (그려주는 역할)
Scene + Camera를 합쳐서 실제 화면에 그려주는 역할이다.
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
그리고 핵심은 아래의 코드가 있어야 화면이 그려진다.
renderer.render(scene, camera);
Mesh (실제 3D 오브젝트)
Mesh는 Geometry + Material 조합이다. 보이는 실제 물체이다.
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
- Geometry → 형태 (큐브, 구 등)
- Material → 색, 질감
Light (조명)
Light는 생각보다 중요하다. 특히 MeshStandardMaterial은 Light가 없으면 그냥 안 보인다.
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);
Material 종류
Three.js에서 헷갈리는 부분 중 하나가 Material이다.
MeshBasicMaterial
new THREE.MeshBasicMaterial({ color: "red" });
- 빛 영향 없음
- 항상 같은 색
MeshStandardMaterial
new THREE.MeshStandardMaterial({
color: "white",
metalness: 0.5,
roughness: 0.5,
});
- 빛 영향 받음
- 현실적인 재질
MeshNormalMaterial
new THREE.MeshNormalMaterial();
- 방향을 색으로 표현
- 디버깅용
애니메이션 루프
Three.js에서 애니메이션은 requestAnimationFrame 으로 매 프레임 함수를 반복 호출해서 만든다.
function animate() {
requestAnimationFrame(animate);
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
GLTFLoader로 모델 불러오기
3D 모델은 직접 만들 필요 없이 Kenney.nl, Sketchfab 같은 사이트에서 무료로 받을 수 있다. Three.js에서는 glTF/glb 포맷을 쓰면 된다. 모델 불러오기는 시간이 걸리는 작업이라 콜백 함수 방식으로 "다 불러왔으면 그때 이걸 해줘"라고 약속해두는 구조다.
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
loader.load("/model.glb", (gltf) => {
scene.add(gltf.scene);
});
텍스처 입히기
glb 파일 안에 텍스처가 포함되지 않은 경우 외부 이미지를 직접 입혀야 한다.
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/colormap.png')
texture.flipY = false // glb 모델은 텍스처 방향이 반대라 꺼줘야 함
texture.colorSpace = THREE.SRGBColorSpace // 색상이 어둡게 보이는 문제 해결
model.traverse((child) => {
if (child.isMesh) {
child.material.map = texture
child.material.needsUpdate = true
}
})
flipY = false 는 텍스처 방향 문제, colorSpace = THREE.SRGBColorSpace 는 색상 밝기 문제를 해결한다. traverse 는 모델의 모든 부품을 순회하면서 isMesh 인 것만 골라 텍스처를 입힌다. 3D 모델은 여러 부품으로 나뉘어져 있어서 이렇게 하나씩 처리해야 한다.
마우스 인터랙션
마우스 위치를 -1 ~ 1로 변환해서 사용한다.
const mouse = { x: 0, y: 0 };
window.addEventListener("mousemove", (e) => {
mouse.x = (e.clientX / window.innerWidth - 0.5) * 2;
mouse.y = -(e.clientY / window.innerHeight - 0.5) * 2;
});
animate 함수 안에서 lerp(선형 보간)으로 부드럽게 따라오게 만든다.
model.rotation.y += (mouse.x * 0.3 - model.rotation.y) * 0.05;
목표값과 현재값의 차이에 작은 수를 곱해서 더하는 방식이다. 목표랑 멀면 빠르게, 가까워질수록 느리게 따라와서 부드러운 느낌이 난다. 이걸 lerp라고 한다.
Raycaster로 클릭 감지
3D 공간에서 마우스가 어떤 물체를 클릭했는지 감지할 때 Raycaster를 쓴다. 마우스 위치에서 화면 안쪽으로 광선을 쏴서 어떤 물체에 맞는지 확인하는 방식이다.
const raycaster = new THREE.Raycaster()
const pointer = new THREE.Vector2()
window.addEventListener('click', (e) => {
pointer.x = (e.clientX / window.innerWidth) * 2 - 1
pointer.y = -(e.clientY / window.innerHeight) * 2 + 1
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObject(model, true)
if (intersects.length > 0) {
// 클릭한 물체에 반응
}
})
GSAP 연동
Three.js만으로는 "몇 초 동안 부드럽게 이동해줘" 같은 애니메이션을 직접 계산해야 한다. GSAP을 쓰면 한 줄로 해결된다.
import gsap from 'gsap'
gsap.to(model.position, {
y: 1,
duration: 2,
ease: 'power2.out'
})
- ease 종류에 따라 움직임 느낌이 달라진다. power2.in 은 처음엔 느리다가 끝에 빠르게, power2.out 은 처음엔 빠르다가 끝에 느리게, sine.inOut 은 사인 파형으로 자연스럽게 움직인다.
- yoyo: true 와 repeat: -1 을 조합하면 무한 반복 왕복 애니메이션을 만들 수 있다.
- onComplete 은 애니메이션이 끝났을 때 실행할 함수를 지정한다.
버텍스 애니메이션
Geometry의 꼭짓점을 매 프레임 직접 조작해서 물결 효과를 만들 수 있다.
const time = Date.now() * 0.001
const position = seaGeometry.attributes.position
for (let i = 0; i < position.count; i++) {
const x = position.getX(i)
const y = position.getY(i)
const z = Math.sin(x * 0.5 + time) * 0.2 + Math.sin(y * 0.5 + time * 0.8) * 0.2
position.setZ(i, z)
}
position.needsUpdate = true // 꼭짓점 바뀌었다고 Three.js에 알리기
Math.sin 으로 사인파를 만들어 각 꼭짓점 높이를 계산한다. Geometry를 세분화할수록(분할 수를 높일수록) 물결이 자연스러워진다.
파티클
수많은 점을 한번에 렌더링할 때 Points 를 쓴다. Mesh보다 훨씬 가볍게 많은 양을 처리할 수 있다. Float32Array 는 GPU에 데이터를 넘길 때 쓰는 효율적인 배열 형식이다.
const geometry = new THREE.BufferGeometry()
const positions = new Float32Array(count * 3) // x, y, z 순서
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
const material = new THREE.PointsMaterial({
size: 0.1,
sizeAttenuation: true // 멀수록 작아지는 원근감
})
const points = new THREE.Points(geometry, material)
scene.add(points)
내가 만든 인터랙션 구조
이 프로젝트에서는 단순히 3D 모델을 띄우는 게 아니라 움직이는 웹 페이지 느낌을 만드는 데 집중했다.
마우스 기반 배 방향 제어
해적선 모델은 마우스 움직임에 따라 자연스럽게 방향이 바뀌도록 만들었다.
model.rotation.y += (mouse.x * 0.3 - model.rotation.y) * 0.05;
스크롤 인터랙션 (삭제됨)
스크롤에 따라 배가 확대/축소되게 만들었었는데 다른 애니메이션이랑 섞이면서 꼬여서 현재는 제거했다.
배경 구성 (바다 + 하늘 + 섬)
- 바다 → vertex animation으로 파도 구현
- 하늘 → 고정 배경
- 섬 → 무인도처럼 배치
해적선 이동 인터랙션
배를 클릭하면 파도 흐름 따라 이동하면서 오른쪽으로 항해 → 다시 왼쪽에서 등장하는 구조로 만들었다. 계속 움직이는 느낌을 주고 싶었다.
한계점
전체적으로 원하는 인터랙티브 구조는 만들었지만 Three.js 이해도가 아직 완벽하지 않아서 이런 부분은 부족했다...
- 물리 기반 자연스러운 움직임
- 카메라 연출
- 최적화
그래서 이후에는 더 깊게 공부하면서 개선할 예정이다.
'💻 STUDY > Frontend' 카테고리의 다른 글
| Pagination과 Infinite Scroll, React에서는 어떻게 구현할까? (0) | 2026.05.10 |
|---|---|
| Tailwind CSS가 가벼운 이유 (0) | 2026.05.05 |
| React 렌더링 최적화에 대해서 (0) | 2026.04.15 |
| React 렌더링 원리와 Virtual DOM (0) | 2026.03.13 |
| useReducer와 성능 최적화 (0) | 2026.03.13 |