React Three Fiber 도입 후기 - 선언적 3D의 장단점
회사에서 3D 대시보드 프로젝트를 맡았습니다. 기존에는 바닐라 Three.js를 썼는데, 팀원 대부분이 React에 익숙해서 React Three Fiber(R3F) 도입을 검토했습니다. 2주간의 마이그레이션 후기입니다.
기존 코드의 문제 #
바닐라 Three.js로 작성된 코드는 이랬습니다.
// scene.js
let scene, camera, renderer, cube;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 조명
const light = new THREE.DirectionalLight(0xffffff, 1);
scene.add(light);
// 이벤트
window.addEventListener('resize', onResize);
document.addEventListener('click', onClick);
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onClick(e) {
// 레이캐스팅...
}
function dispose() {
window.removeEventListener('resize', onResize);
document.removeEventListener('click', onClick);
geometry.dispose();
material.dispose();
renderer.dispose();
}
문제점:
- 전역 변수: scene, camera 등이 전역에 노출
- 수동 정리: dispose를 빠뜨리면 메모리 누수
- 상태 동기화: React 상태와 3D 씬 동기화가 복잡
- 재사용 어려움: 컴포넌트화가 안 됨
R3F로 전환 #
같은 코드를 R3F로 작성하면 이렇습니다.
import { Canvas } from '@react-three/fiber';
function Scene() {
return (
<Canvas>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 10]} />
<Box position={[0, 0, 0]} />
</Canvas>
);
}
function Box({ position }) {
const [hovered, setHovered] = useState(false);
return (
<mesh
position={position}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
<boxGeometry />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
);
}
차이점:
- 선언적: JSX로 씬 구조가 명확함
- 자동 정리: 언마운트 시 자동 dispose
- React 통합: useState, useEffect 그대로 사용
- 컴포넌트화: 재사용 가능한 3D 컴포넌트
좋았던 점 #
1. 생산성 향상 #
새 기능 추가 시간이 절반으로 줄었습니다.
// 기존: 별도 함수로 관리
function addDataPoint(x, y, z, value) {
const geometry = new THREE.SphereGeometry(0.1);
const material = new THREE.MeshStandardMaterial({
color: valueToColor(value)
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
scene.add(mesh);
dataPoints.push(mesh);
}
// R3F: 그냥 map
{data.map((point, i) => (
<mesh key={i} position={[point.x, point.y, point.z]}>
<sphereGeometry args={[0.1]} />
<meshStandardMaterial color={valueToColor(point.value)} />
</mesh>
))}
2. 상태 관리 통합 #
Redux, Zustand 등과 자연스럽게 연동됩니다.
import { useStore } from './store';
function DataVisualization() {
const data = useStore(state => state.data);
const selectedId = useStore(state => state.selectedId);
return (
<>
{data.map(item => (
<DataPoint
key={item.id}
data={item}
isSelected={item.id === selectedId}
/>
))}
</>
);
}
3. 코드 리뷰가 쉬워짐 #
팀원들이 3D 코드를 이해하기 쉬워졌습니다. "이건 빨간 박스를 렌더링하는 거구나"가 코드에서 바로 보입니다.
4. drei 라이브러리 #
자주 쓰는 기능이 이미 구현되어 있습니다.
import { OrbitControls, Environment, Html, useGLTF } from '@react-three/drei';
function Scene() {
const { scene } = useGLTF('/model.glb');
return (
<>
<OrbitControls />
<Environment preset="city" />
<primitive object={scene} />
<Html position={[0, 2, 0]}>
<div className="tooltip">클릭하세요</div>
</Html>
</>
);
}
어려웠던 점 #
1. 성능 함정 #
처음에 성능 문제가 생겼습니다.
// 나쁜 예: 매 렌더마다 새 geometry 생성
function Box() {
return (
<mesh>
<boxGeometry args={[1, 1, 1]} /> {/* 매번 새로 생성! */}
<meshStandardMaterial color="red" />
</mesh>
);
}
해결: useMemo 또는 geometry를 외부에서 정의
const geometry = new THREE.BoxGeometry(1, 1, 1);
function Box() {
return (
<mesh geometry={geometry}>
<meshStandardMaterial color="red" />
</mesh>
);
}
2. useFrame 남용 #
useFrame은 매 프레임 실행되므로 주의가 필요합니다.
// 나쁜 예: 불필요한 계산
function AnimatedBox() {
useFrame(() => {
// 무거운 계산이 매 프레임 실행됨
const result = heavyCalculation();
});
}
// 좋은 예: 필요한 것만
function AnimatedBox() {
const meshRef = useRef();
useFrame((state, delta) => {
meshRef.current.rotation.y += delta;
});
return <mesh ref={meshRef}>...</mesh>;
}
3. 기존 Three.js 코드 통합 #
레거시 Three.js 코드를 R3F에 통합하는 게 까다로웠습니다.
// 기존 Three.js 클래스를 R3F에서 사용
function LegacyEffect() {
const { gl, scene, camera } = useThree();
useEffect(() => {
const effect = new LegacyPostProcessEffect(gl, scene, camera);
return () => effect.dispose();
}, [gl, scene, camera]);
return null;
}
4. 디버깅 #
React DevTools에서 3D 객체 상태를 보기 어렵습니다. drei의 Perf 컴포넌트가 도움됩니다.
import { Perf } from 'r3f-perf';
<Canvas>
<Perf position="top-left" />
{/* ... */}
</Canvas>
최종 아키텍처 #
마이그레이션 후 구조입니다.
src/
├── components/
│ ├── 3d/ # R3F 컴포넌트
│ │ ├── Scene.jsx
│ │ ├── DataPoint.jsx
│ │ ├── Camera.jsx
│ │ └── Effects.jsx
│ └── ui/ # 일반 React 컴포넌트
│ ├── Dashboard.jsx
│ └── Controls.jsx
├── hooks/
│ ├── useDataAnimation.js # 3D 애니메이션 훅
│ └── useInteraction.js
├── store/
│ └── index.js # Zustand 스토어
└── utils/
└── three-helpers.js # Three.js 유틸리티
결론 #
R3F 도입을 추천하는 경우:
- React 기반 프로젝트
- 팀원들이 React에 익숙함
- UI와 3D의 상태 동기화가 많음
- 재사용 가능한 3D 컴포넌트가 필요함
바닐라 Three.js가 나은 경우:
- 성능이 극도로 중요함
- React 없는 프로젝트
- 복잡한 커스텀 렌더링 파이프라인
- Three.js 깊은 제어가 필요함
우리 팀에서는 R3F 도입 후 3D 기능 개발 속도가 확실히 빨라졌습니다. 특히 신규 팀원 온보딩이 수월해졌어요. "React 할 줄 알면 R3F도 금방 배운다"는 말이 실제로 맞았습니다.
- Previous: 인터랙티브 3D 명함 만들기 - 사이드 프로젝트 기록
- Next: 셰이더 입문자의 좌절과 극복 - GLSL 첫 한 달