본문 바로가기

React

React와 Three.js를 이용한 3D 객체 렌더링

React와 Three.js를 결합하면 웹에서 상호작용 가능한 3D 경험을 효율적으로 구현할 수 있습니다. 실무에서는 @react-three/fiber(이하 R3F)를 사용해 React 컴포넌트처럼 3D 씬을 선언적으로 구성하는 방식이 유지보수에 유리합니다. 아래는 빠르게 3D 박스와 GLTF 모델을 렌더링하고, 상호작용과 성능 최적화를 적용하는 실전 가이드입니다.

1. 개요

목표는 다음과 같습니다: Canvas로 씬 생성, 박스 메쉬 렌더링 및 회전 애니메이션, OrbitControls로 카메라 조작, GLTF 모델 로딩, 기본적인 이벤트 처리와 성능 최적화입니다.

2. 설치

R3F와 유틸을 설치합니다. three는 필수이며, drei는 컨트롤과 로더 등 편의 컴포넌트를 제공합니다.

// Vite 또는 CRA 프로젝트에서 설치합니다
npm i three @react-three/fiber @react-three/drei

3. 기본 씬 구성

Canvas 내부에 조명, 카메라, 메쉬를 배치합니다. useFrame으로 박스를 매 프레임 회전시킵니다.

import { Canvas, useFrame } from '@react-three/fiber'
import { useRef } from 'react'

function Box() {
  const meshRef = useRef()
  useFrame((state, delta) => {
    if (!meshRef.current) return
    meshRef.current.rotation.y += delta
    meshRef.current.rotation.x += delta * 0.5
  })
  return (
    <mesh ref={meshRef} castShadow>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="orange" />
    </mesh>
  )
}

export default function Scene() {
  return (
    <Canvas
      shadows
      dpr={[1, 2]} // 레티나 대응 및 성능 균형
      camera={{ position: [3, 3, 3], fov: 50 }}
      gl={{ antialias: true, powerPreference: 'high-performance' }}
    >
      <ambientLight intensity={0.4} />
      <directionalLight position={[5, 5, 5]} intensity={1} castShadow />
      <Box />
    </Canvas>
  )
}

4. 상호작용(OrbitControls)

카메라를 마우스로 회전/줌하려면 OrbitControls를 추가합니다. damping을 켜면 움직임이 부드러워집니다.

import { OrbitControls } from '@react-three/drei'

export default function Scene() {
  return (
    <Canvas camera={{ position: [3, 3, 3], fov: 50 }}>
      <ambientLight intensity={0.4} />
      <directionalLight position={[5, 5, 5]} intensity={1} />
      <Box />
      <OrbitControls enableDamping dampingFactor={0.1} />
    </Canvas>
  )
}

5. 모델 로딩(GLTF)

실무에서는 GLTF/GLB 모델을 많이 사용합니다. public 디렉터리에 모델을 두고 useGLTF로 로딩합니다. Suspense로 로딩 상태를 처리할 수 있습니다.

import { Suspense } from 'react'
import { useGLTF } from '@react-three/drei'

function Model(props) {
  const { scene } = useGLTF('/models/robot.glb') // public/models/robot.glb
  return <primitive object={scene} {...props} />
}

// 미리 로드(초기 화면 지연 감소)
useGLTF.preload('/models/robot.glb')

export default function Scene() {
  return (
    <Canvas camera={{ position: [2, 2, 4], fov: 45 }}>
      <ambientLight intensity={0.5} />
      <directionalLight position={[5, 5, 5]} intensity={1} />
      <Suspense fallback={null}>
        <Model position={[0, 0, 0]} />
      </Suspense>
      <OrbitControls />
    </Canvas>
  )
}

6. 성능 최적화 체크리스트

3D는 비용이 큽니다. 다음 옵션을 조합해 성능을 확보합니다.

- Canvas dpr을 배열로 설정해 고해상도에서 부하를 낮춥니다(dpr={[1,2]}).
- frameloop="demand"로 변경하면 상태 변화가 없을 때 렌더를 멈출 수 있습니다.
- 그림자(shadow) 사용 시 필요한 오브젝트에만 castShadow/receiveShadow를 설정합니다.
- 재사용 가능한 Geometry/Material은 useMemo로 캐싱하고, 컴포넌트 언마운트 시 dispose를 호출합니다.
- 무거운 모델은 텍스처 압축(KTX2)과 드라코(Draco) 압축을 적용합니다.

import * as THREE from 'three'
import { Canvas } from '@react-three/fiber'
import { useMemo, useEffect } from 'react'

function OptimizedBox() {
  const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), [])
  const material = useMemo(() => new THREE.MeshStandardMaterial({ color: '#4e9' }), [])
  useEffect(() => () => {
    geometry.dispose()
    material.dispose()
  }, [geometry, material])
  return <mesh geometry={geometry}></mesh>
}

function Scene() {
  return (
    <Canvas frameloop="demand" dpr={[1, 2]}>
      <ambientLight intensity={0.4} />
      <OptimizedBox />
    </Canvas>
  )
}

7. 이벤트 및 상태 연동

메쉬에 포인터 이벤트를 연결해 하이라이트나 선택을 구현합니다. React 상태로 색상이나 회전 속도를 제어합니다.

import { useState } from 'react'

function SelectableBox() {
  const [hovered, setHovered] = useState(false)
  const [active, setActive] = useState(false)
  return (
    <mesh
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
      onClick={() => setActive((p) => !p)}
      scale={active ? 1.2 : 1}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  )
}

8. 좌표계와 카메라 팁

Three.js는 오른손 좌표계를 사용합니다. 기본 단위는 임의의 단위(미터가 아님)이며, 씬에 맞는 스케일을 결정하는 것이 중요합니다. 카메라 near/far는 너무 작거나 크면 Z-fighting이나 깊이 정밀도 문제가 생깁니다. 예: camera={{ near: 0.1, far: 100 }}로 시작하고 상황에 맞게 조정합니다.

9. 흔한 문제 해결

- 모델 경로: Vite/CRA에서는 public 폴더에 GLB/텍스처를 넣고 "/models/xxx.glb"로 참조합니다.
- CORS: 외부 CDN에서 모델을 가져오면 CORS 헤더가 필요합니다. 가능하면 같은 도메인에 배치합니다.
- 성능 급락: 그림자 해상도를 너무 높게 설정하거나, 매 프레임 상태 업데이트를 남발하면 프레임이 떨어집니다. 필요 시 frameloop="demand"로 전환합니다.
- 반사/금속성 표현: 환경맵(HDR)을 로드해야 사실적인 금속 재질이 보입니다. drei의 Environment 컴포넌트를 검토합니다.

10. 배포 팁

- GLTF/GLB를 gltf-transform 등으로 최적화하고 텍스처는 WebP/KTX2로 변환합니다.
- 정적 파일 캐싱(Cache-Control)과 gzip/br 압축을 활성화합니다.
- 초기 로딩을 줄이려면 핵심 씬만 우선 렌더, 나머지 모델은 lazy-load와 Suspense로 후속 로딩합니다.

마무리

React와 Three.js(R3F)를 활용하면 3D 씬을 컴포넌트 단위로 관리하며 빠르게 구현할 수 있습니다. 위 예제를 시작점으로 조명, 머티리얼, 모델 최적화를 단계적으로 적용하면 실무 요구사항을 충분히 충족할 수 있습니다.