Skip to main content
민트라

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();
}

문제점:

  1. 전역 변수: scene, camera 등이 전역에 노출
  2. 수동 정리: dispose를 빠뜨리면 메모리 누수
  3. 상태 동기화: React 상태와 3D 씬 동기화가 복잡
  4. 재사용 어려움: 컴포넌트화가 안 됨

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>
  );
}

차이점:

  1. 선언적: JSX로 씬 구조가 명확함
  2. 자동 정리: 언마운트 시 자동 dispose
  3. React 통합: useState, useEffect 그대로 사용
  4. 컴포넌트화: 재사용 가능한 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 도입을 추천하는 경우:

바닐라 Three.js가 나은 경우:

우리 팀에서는 R3F 도입 후 3D 기능 개발 속도가 확실히 빨라졌습니다. 특히 신규 팀원 온보딩이 수월해졌어요. "React 할 줄 알면 R3F도 금방 배운다"는 말이 실제로 맞았습니다.