React Three Fiber: React 개발자를 위한 선언적 3D
명령형 Three.js에서 선언형 R3F로 — Canvas, useFrame, drei까지 React 컴포넌트로 3D 씬 구성하기
읽는 데 50분
- #react-three-fiber
- #r3f
- #three.js
- #3D
- #react
- #declarative
- #drei
이 문서의 목차
적용 환경: React 19+, Next.js 16+, three.js r182, @react-three/fiber 9+, @react-three/drei 10+
Three.js로 3D 씬을 만드는 과정은 본질적으로 명령형(imperative)입니다. Scene을 생성하고, Camera를 만들어 위치를 지정하고, Renderer를 초기화해서 DOM에 붙이고, requestAnimationFrame으로 루프를 돌리고, 컴포넌트가 언마운트될 때 일일이 dispose()를 호출해야 합니다. React의 선언적 패러다임과는 근본적으로 맞지 않는 방식이며, useEffect 안에서 이 모든 것을 수동으로 관리하다 보면 코드가 급격히 복잡해집니다.
React Three Fiber(R3F)는 이 문제를 해결하기 위해 만들어진 Three.js용 React 렌더러입니다. react-dom이 React 엘리먼트를 DOM 노드로 변환하듯, R3F는 React 엘리먼트를 Three.js 오브젝트로 변환합니다. Scene, Camera, Renderer 설정은 <Canvas> 컴포넌트가 자동으로 처리하고, 3D 오브젝트는 JSX로 선언하며, 애니메이션은 useFrame 훅으로, 이벤트는 onClick/onPointerOver props로 처리합니다. React 개발자가 이미 알고 있는 패턴 — 컴포넌트 분리, 상태 관리, 조건부 렌더링 — 이 그대로 3D 세계에 적용되는 것입니다.
R3F를 사용하려면 두 가지 패키지가 필요합니다. @react-three/fiber는 핵심 렌더러이고, @react-three/drei는 자주 사용하는 헬퍼 컴포넌트 모음입니다. Three.js 자체가 peer dependency이므로, 프로젝트에 이미 three가 설치되어 있어야 합니다.
npm install @react-three/fiber @react-three/drei설치 후 가장 먼저 해야 할 것은 <Canvas> 컴포넌트를 렌더링하는 것입니다. 이 하나의 컴포넌트가 vanilla Three.js에서 수동으로 설정해야 했던 Scene, PerspectiveCamera, WebGLRenderer를 모두 자동으로 생성하고 관리합니다.
import { Canvas } from '@react-three/fiber'
export default function App() {
return (
<div style={{ width: '100%', height: '100vh' }}>
<Canvas>
{/* Three.js 오브젝트를 JSX로 선언 */}
</Canvas>
</div>
)
}Canvas가 자동으로 해주는 것들
<Canvas>는 내부적으로 THREE.Scene, THREE.PerspectiveCamera(기본 위치 [0, 0, 5], FOV 75), THREE.WebGLRenderer(antialias, 적절한 pixelRatio 포함)를 생성합니다. 또한 requestAnimationFrame 기반의 렌더 루프를 자동으로 실행하고, 부모 컨테이너의 크기가 변하면 카메라 종횡비와 렌더러 크기를 자동으로 업데이트합니다. 컴포넌트가 언마운트되면 모든 리소스가 자동으로 정리됩니다.
<Canvas>는 부모 요소의 크기를 그대로 채우므로, 부모에 명시적인 크기가 설정되어 있어야 합니다. width: 100%와 height: 100vh 조합이 가장 일반적이지만, 문서 안에 임베딩할 때는 aspect-ratio: 16/9 같은 고정 비율을 사용하는 것이 더 적합합니다.
R3F의 핵심 아이디어는 Three.js 클래스가 곧 JSX 태그라는 것입니다. new THREE.Mesh()는 <mesh>로, new THREE.BoxGeometry(2, 2, 2)는 <boxGeometry args={[2, 2, 2]} />로, new THREE.MeshBasicMaterial({ color: 'indigo' })는 <meshBasicMaterial color="indigo" />로 대응됩니다. Three.js 클래스의 이름을 camelCase로 변환한 것이 곧 JSX 태그 이름입니다.
생성자에 전달하던 인자는 args prop을 통해 배열로 넘기고, position, rotation, scale 같은 속성은 그대로 props로 전달합니다.
function Box() {
return (
<mesh rotation={[0.5, 0.5, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshBasicMaterial color="#6366f1" />
</mesh>
)
}const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshBasicMaterial({ color: 0x6366f1 });
const cube = new THREE.Mesh(geometry, material);
cube.rotation.x = 0.5;
cube.rotation.y = 0.5;
scene.add(cube);같은 큐브를 만드는 코드인데, R3F 버전에서는 scene.add()도 없고, Geometry와 Material을 변수에 담을 필요도 없습니다. JSX의 부모-자식 관계가 Three.js의 오브젝트 트리를 자동으로 구성합니다. <mesh> 안에 선언된 <boxGeometry>와 <meshBasicMaterial>은 자동으로 해당 Mesh의 geometry와 material로 설정됩니다.
소문자 규칙과 args 패턴
R3F에서 모든 Three.js 클래스는 소문자 camelCase로 사용합니다. THREE.PointLight는 <pointLight>, THREE.IcosahedronGeometry는 <icosahedronGeometry>입니다. 생성자 인자는 항상 args 배열로 전달하며, 배열의 순서는 Three.js 생성자의 매개변수 순서와 동일합니다.
조명도 동일한 패턴을 따릅니다. new THREE.AmbientLight(0xffffff, 0.5)는 <ambientLight intensity={0.5} />가 되고, new THREE.DirectionalLight()는 <directionalLight>가 됩니다. Material을 meshStandardMaterial로 바꾸면 PBR 렌더링이 적용되어 조명에 반응하는 입체적인 표면이 만들어집니다.
function LitBox() {
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} />
<mesh rotation={[0.5, 0.5, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#6366f1" metalness={0.3} roughness={0.4} />
</mesh>
</>
)
}vanilla Three.js에서는 조명을 scene.add(light)로 추가했지만, R3F에서는 JSX 트리에 선언하기만 하면 됩니다. <>...</> Fragment 안에 조명과 메시를 함께 놓으면, R3F가 이들을 모두 Scene에 추가합니다.
vanilla Three.js에서 애니메이션을 구현하려면 requestAnimationFrame으로 루프를 구성하고, 매 프레임 오브젝트의 상태를 변경한 뒤 renderer.render()를 호출해야 했습니다. R3F에서는 useFrame 훅이 이 모든 과정을 대체합니다. <Canvas> 내부에서 useFrame을 호출하면 매 렌더 프레임마다 콜백이 실행되며, 렌더링은 R3F가 자동으로 처리합니다.
오브젝트를 직접 조작하려면 React의 useRef로 Three.js 인스턴스에 접근합니다.
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import type { Mesh } from 'three'
function SpinningBox() {
const meshRef = useRef<Mesh>(null)
useFrame((state, delta) => {
if (!meshRef.current) return
meshRef.current.rotation.x += delta
meshRef.current.rotation.y += delta * 0.5
})
return (
<mesh ref={meshRef}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#6366f1" metalness={0.3} roughness={0.4} />
</mesh>
)
}function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();useFrame 콜백의 delta 파라미터는 이전 프레임으로부터 경과한 시간(초 단위)입니다. 고정값 0.01 대신 delta를 곱하면, 모니터 주사율에 관계없이 동일한 속도로 애니메이션이 재생됩니다. 60fps에서는 delta ≈ 0.016, 144fps에서는 delta ≈ 0.007이므로, 결과적으로 단위 시간당 회전량이 일정하게 유지됩니다.
useFrame의 state 객체
useFrame의 첫 번째 인자 state에는 clock(경과 시간), camera, scene, gl(렌더러), mouse(정규화된 마우스 좌표), viewport(뷰포트 크기) 등 현재 렌더 상태에 대한 정보가 담겨 있습니다. 시간 기반 애니메이션에는 state.clock.elapsedTime을, 마우스 기반 효과에는 state.mouse를 활용할 수 있습니다.
vanilla Three.js에서 3D 오브젝트의 클릭이나 호버를 감지하려면, 마우스 좌표를 정규화하고 Raycaster를 설정해서 교차 검사를 수행하는 과정이 필요합니다. R3F에서는 이 모든 것이 React의 이벤트 시스템으로 추상화되어 있습니다. DOM 엘리먼트에 onClick을 붙이듯, 3D 메시에도 동일한 방식으로 이벤트 핸들러를 부착할 수 있습니다.
import { useState } from 'react'
function InteractiveBox() {
const [hovered, setHovered] = useState(false)
const [clicked, setClicked] = useState(false)
return (
<mesh
scale={clicked ? 1.3 : 1}
onClick={() => setClicked(!clicked)}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color={hovered ? '#f59e0b' : '#6366f1'} />
</mesh>
)
}useState로 호버와 클릭 상태를 관리하고, 그 상태에 따라 color와 scale을 조건부로 설정합니다. R3F가 내부적으로 Raycaster를 운용하므로, 개발자는 "어떤 오브젝트가 클릭되면 어떤 상태를 바꿀 것인가"라는 비즈니스 로직에만 집중하면 됩니다. 아래 데모에는 시각적 효과를 위해 useFrame 기반의 미세한 회전이 추가되어 있습니다.
지원되는 이벤트
R3F는 onClick, onDoubleClick, onPointerOver, onPointerOut, onPointerMove, onPointerDown, onPointerUp, onWheel 등의 이벤트를 지원합니다. 이벤트 객체에는 point(교차점 3D 좌표), distance(카메라로부터의 거리), face(교차한 면), normal(법선 벡터) 같은 3D 공간 정보가 포함되어 있어, 클릭 위치에 따라 효과를 다르게 적용하는 것도 가능합니다.
@react-three/drei는 R3F 생태계에서 가장 많이 사용되는 헬퍼 라이브러리로, 100개 이상의 사전 구축된 컴포넌트와 훅을 제공합니다. 카메라 컨트롤, 환경맵 로딩, 3D 텍스트 렌더링, HTML 임베딩 등 Three.js에서 자주 필요하지만 직접 구현하면 번거로운 기능들이 한 줄의 import로 해결됩니다.
OrbitControls — 카메라 컨트롤
vanilla Three.js에서 OrbitControls를 설정하려면 addons에서 import하고, Camera와 DOM element를 전달하고, 애니메이션 루프에서 update()를 호출해야 했습니다. drei에서는 <OrbitControls />를 선언하는 것만으로 동일한 기능이 활성화됩니다.
import { OrbitControls } from '@react-three/drei'
// Canvas 내부에서:
<OrbitControls enableDamping dampingFactor={0.05} />Environment — 환경 조명
현실적인 라이팅을 만들려면 보통 여러 개의 조명을 배치하고 강도를 조정해야 합니다. <Environment>는 HDRI 환경맵을 한 줄로 적용하여, 프로덕션급 조명을 즉시 제공합니다.
import { Environment } from '@react-three/drei'
// preset: "sunset", "dawn", "night", "warehouse", "forest", "apartment", "studio", "city", "park", "lobby"
<Environment preset="sunset" />preset을 바꾸는 것만으로 씬의 분위기가 완전히 달라집니다. "sunset"은 따뜻한 황금빛을, "city"는 도시의 차가운 반사를, "studio"는 중립적인 스튜디오 조명을 제공합니다.
Text — 3D 텍스트 렌더링
Three.js에서 텍스트를 3D 공간에 렌더링하는 것은 TextGeometry + 폰트 로딩이라는 상당히 복잡한 과정을 필요로 합니다. drei의 <Text>는 SDF(Signed Distance Field) 기반으로 고품질 텍스트를 간단하게 렌더링합니다.
import { Text } from '@react-three/drei'
<Text
fontSize={1}
color="#6366f1"
anchorX="center"
anchorY="middle"
>
Hello R3F
</Text>Html — 3D 공간 속 React 컴포넌트
<Html>은 Three.js 씬의 3D 좌표에 일반 HTML/React 컴포넌트를 배치합니다. 3D 오브젝트 위에 라벨을 띄우거나, 인터랙티브한 UI 패널을 3D 공간에 배치하는 데 유용합니다.
import { Html } from '@react-three/drei'
<Html position={[0, 2, 0]} center>
<div className="bg-white p-2 rounded shadow">
<p>3D 공간 속의 HTML</p>
</div>
</Html>center prop은 HTML 요소를 3D 좌표 중심에 정렬하고, distanceFactor로 카메라 거리에 따른 크기 변화를 제어할 수 있습니다.
아래 데모에서 OrbitControls, Environment, Text를 결합한 씬을 확인할 수 있습니다. 마우스 드래그로 시점을 자유롭게 바꿔보면, Environment가 적용한 환경 반사가 오브젝트 표면에서 실시간으로 변화하는 것을 볼 수 있습니다.
drei에는 이 외에도 수십 가지 헬퍼가 있습니다
useGLTF(3D 모델 로딩), Float(부유 애니메이션), MeshDistortMaterial(왜곡 재질), Instances(인스턴싱), Stars(별 배경), ContactShadows(접촉 그림자), Bounds(자동 카메라 프레이밍) 등 프로덕션에서 바로 사용할 수 있는 도구들이 계속 추가되고 있습니다. 전체 목록은 drei 공식 저장소 (새 창)에서 확인할 수 있습니다.
지금까지 배운 개념들 — JSX로 3D 오브젝트 선언, useFrame으로 애니메이션, 이벤트 시스템으로 인터랙션, drei 헬퍼로 환경 구성 — 을 하나의 씬에 결합하면 다음과 같은 결과를 얻을 수 있습니다.
import { useState, useRef } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { OrbitControls, Environment, Text } from '@react-three/drei'
import * as THREE from 'three'
function InteractiveSphere({ position, color, label }) {
const meshRef = useRef<THREE.Mesh>(null)
const [hovered, setHovered] = useState(false)
const [clicked, setClicked] = useState(false)
useFrame((state, delta) => {
if (!meshRef.current) return
meshRef.current.rotation.y += delta * 0.5
const targetScale = clicked ? 1.3 : hovered ? 1.1 : 1
meshRef.current.scale.lerp(
new THREE.Vector3(targetScale, targetScale, targetScale),
0.1
)
})
return (
<group position={position}>
<mesh
ref={meshRef}
onClick={() => setClicked(!clicked)}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
<icosahedronGeometry args={[0.8, 2]} />
<meshStandardMaterial
color={hovered ? '#f59e0b' : color}
metalness={0.5}
roughness={0.3}
/>
</mesh>
<Text position={[0, -1.3, 0]} fontSize={0.25} color="#94a3b8">
{label}
</Text>
</group>
)
}
export default function App() {
return (
<Canvas camera={{ position: [0, 1, 6], fov: 50 }}>
<OrbitControls enableDamping />
<Environment preset="city" />
<InteractiveSphere position={[-2.5, 0, 0]} color="#6366f1" label="useFrame" />
<InteractiveSphere position={[0, 0, 0]} color="#22d3ee" label="Events" />
<InteractiveSphere position={[2.5, 0, 0]} color="#a78bfa" label="drei" />
</Canvas>
)
}InteractiveSphere는 일반적인 React 컴포넌트와 동일한 구조를 가지면서, 내부에 3D 오브젝트, 애니메이션, 이벤트 처리, 텍스트 라벨을 모두 포함하고 있습니다. 이 컴포넌트를 props만 바꿔가며 여러 번 재사용할 수 있다는 것이 R3F의 핵심적인 이점입니다. React의 컴포넌트 모델이 3D 씬 구성에 그대로 적용되기 때문에, 복잡한 씬도 작은 단위로 분해하여 관리할 수 있습니다.
vanilla Three.js의 명령형 패턴에서 R3F의 선언형 패턴으로의 전환을 한 단계씩 따라가 보았습니다.
이 문서에서 다룬 핵심 개념
Canvas — Scene, Camera, Renderer, 렌더 루프, 리사이즈 처리를 자동으로 관리하는 R3F의 진입점입니다.
JSX 3D — Three.js 클래스가 소문자 camelCase JSX 태그로 대응되며, 생성자 인자는 args prop으로, 속성은 일반 props로 전달합니다. 부모-자식 관계가 Three.js의 오브젝트 트리를 자동으로 구성합니다.
useFrame — requestAnimationFrame을 대체하는 훅으로, 매 프레임 실행되는 콜백을 등록합니다. delta 파라미터로 프레임 독립적 애니메이션을 구현하고, useRef로 Three.js 인스턴스에 접근합니다.
이벤트 시스템 — Raycaster 설정 없이 onClick, onPointerOver 등의 React 이벤트 패턴으로 3D 오브젝트와의 인터랙션을 구현합니다.
drei — OrbitControls, Environment, Text, Html 등 100개 이상의 사전 구축된 헬퍼를 제공하는 생태계 라이브러리입니다.
여기서 더 나아가려면 useGLTF로 외부 3D 모델(.glb)을 로딩하는 것이 자연스러운 다음 단계입니다. @react-three/rapier로 물리 엔진을 연동하거나, @react-three/postprocessing으로 Bloom이나 Depth of Field 같은 후처리 효과를 추가하는 것도 가능합니다. R3F의 선언적 패턴 위에서 이 모든 것이 React 컴포넌트로 결합됩니다.
- React Three Fiber 공식 문서 (새 창)
- drei 공식 문서 (새 창)
- R3F Examples (새 창)
- Three.js 공식 문서 (새 창)
- pmndrs 생태계 (새 창) — R3F, drei, zustand, rapier 등을 관리하는 Poimandres 커뮤니티
관련 문서
글쓴이 mirunamu00



