3D 모델 로딩 가이드: useGLTF와 useAnimations
glTF 포맷 이해부터 useGLTF 로딩, useAnimations 애니메이션 재생, PresentationControls 프레젠테이션까지
읽는 데 53분
- #useGLTF
- #glTF
- #3D-model
- #react-three-fiber
- #drei
- #useAnimations
이 문서의 목차
적용 환경: React 19+, Next.js 16+, three.js r182, @react-three/fiber 9+, @react-three/drei 10+
지금까지 R3F에서 만든 오브젝트는 전부 코드로 생성한 것이었습니다. boxGeometry, icosahedronGeometry, torusKnotGeometry — Three.js가 제공하는 내장 geometry를 JSX로 선언하는 방식이었습니다. 간단한 데모에는 충분하지만, 실제 3D 웹 사이트에서 볼 수 있는 캐릭터, 제품, 건축물 같은 복잡한 오브젝트를 코드만으로 만드는 것은 현실적이지 않습니다.
프로덕션 3D 사이트의 워크플로우는 다릅니다. 3D 아티스트가 Blender 같은 전문 도구에서 모델을 제작하고, 완성된 결과물을 glTF/GLB 형식으로 내보내면, 프론트엔드 개발자가 이를 로딩해서 웹에 렌더링합니다. 이 문서는 이 과정의 프론트엔드 쪽을 다루며, drei의 useGLTF 훅으로 모델을 로딩하고, useAnimations로 내장된 애니메이션을 재생하고, 프레젠테이션 헬퍼들로 프로덕트 쇼케이스 수준의 연출을 구성하는 방법을 단계별로 설명합니다.
3D 모델을 저장하는 파일 형식은 여러 가지가 있지만, 웹에서 사용할 형식을 고른다면 답은 glTF(GL Transmission Format)입니다. Khronos Group(OpenGL, Vulkan을 관리하는 표준화 단체)이 설계한 이 형식은 "3D의 JPEG"라고 불릴 만큼 웹 전송에 최적화되어 있습니다. 메시 데이터, PBR 머티리얼, 텍스처, 스켈레톤, 애니메이션 클립을 하나의 파일에 효율적으로 담을 수 있으며, Three.js와 R3F 생태계에서 가장 잘 지원되는 형식이기도 합니다.
glTF에는 두 가지 변형이 있는데, .gltf는 JSON 파일과 바이너리 데이터, 텍스처 이미지가 분리된 형태이고, .glb는 이 모든 것을 하나의 바이너리 파일로 합친 형태입니다. 웹 배포에는 네트워크 요청을 줄일 수 있는 .glb가 거의 항상 선호됩니다.
| 형식 | 확장자 | 웹 적합성 | 비고 |
|---|---|---|---|
| glTF/GLB | .gltf/.glb | 최적 | 웹 네이티브, Khronos 표준, PBR 지원 |
| FBX | .fbx | 보통 | Autodesk 독점 형식, 변환 필요 |
| OBJ | .obj | 낮음 | 애니메이션 미지원, PBR 미지원 |
| USDZ | .usdz | iOS AR 전용 | Apple Quick Look 용도 |
어디서 GLB 파일을 구할 수 있나
Blender는 glTF 2.0 내보내기를 기본으로 지원하며, Sketchfab과 Poly Haven 같은 에셋 사이트에서도 GLB 다운로드를 제공합니다. Mixamo에서 캐릭터 애니메이션을 FBX로 받은 뒤 Blender를 거쳐 GLB로 변환하는 것이 캐릭터 워크플로우의 표준적인 경로입니다.
drei의 useGLTF 훅은 GLB 파일을 로딩하고 파싱하는 과정을 한 줄로 추상화합니다. 내부적으로 Three.js의 GLTFLoader를 사용하지만, 결과를 캐싱하고 React의 Suspense와 통합하는 작업까지 자동으로 처리합니다.
가장 단순한 형태의 모델 로딩 코드는 다음과 같습니다.
import { useGLTF } from '@react-three/drei'
function Duck() {
const { scene } = useGLTF('/models/Duck.glb')
return <primitive object={scene} />
}<primitive object={...}>는 R3F에서 이미 존재하는 Three.js 오브젝트를 JSX 트리에 삽입하는 방법입니다. useGLTF가 반환하는 scene은 완성된 Three.js Scene 그래프이므로, 이를 <primitive>로 감싸면 모든 메시, 머티리얼, 텍스처가 그대로 렌더링됩니다.
같은 모델을 여러 곳에서 사용할 때: scene.clone()
useGLTF가 반환하는 scene은 단일 Three.js 오브젝트이므로, 한 번에 하나의 부모에만 속할 수 있습니다. 같은 모델을 여러 컴포넌트에서 렌더링하면 마지막으로 마운트된 컴포넌트만 모델을 표시하고 나머지는 빈 상태가 됩니다. 이 문제를 방지하려면 useMemo로 씬을 복제해야 합니다.
const { scene } = useGLTF('/models/Duck.glb')
const clone = useMemo(() => scene.clone(true), [scene])
return <primitive object={clone} />이 패턴은 한 페이지에 같은 모델의 데모가 여러 개 존재하거나, 리스트에서 동일 모델을 반복 렌더링할 때 필수적입니다.
모델 로딩은 네트워크 요청이므로 시간이 걸립니다. React의 Suspense와 drei의 Html을 결합하면 로딩 중 사용자에게 피드백을 제공할 수 있습니다.
import { Suspense } from 'react'
import { Html } from '@react-three/drei'
function LoadingFallback() {
return (
<Html center>
<p>모델 로딩 중...</p>
</Html>
)
}
// Canvas 내부:
<Suspense fallback={<LoadingFallback />}>
<Duck />
</Suspense>import { useGLTF } from '@react-three/drei'
function Model() {
const { scene } = useGLTF('/models/Duck.glb')
return <primitive object={scene} />
}
// 컴포넌트 마운트 전 프리로딩
useGLTF.preload('/models/Duck.glb')import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
const loader = new GLTFLoader()
loader.load(
'/models/Duck.glb',
(gltf) => { scene.add(gltf.scene) },
(progress) => { /* 로딩 진행률 */ },
(error) => { console.error(error) }
)vanilla 방식에서는 콜백 기반의 비동기 처리, 에러 핸들링, Scene에 수동 추가가 모두 개발자 몫이지만, useGLTF는 이 과정을 훅 호출 한 번으로 줄이고, Suspense를 통해 React의 선언적 로딩 패턴에 통합시킵니다.
useGLTF.preload로 프리로딩
useGLTF.preload('/models/Duck.glb')를 모듈 스코프에서 호출하면, 컴포넌트가 마운트되기 전에 모델 다운로드가 시작됩니다. 사용자가 해당 페이지에 도달했을 때 이미 로딩이 완료되어 있을 확률이 높아지므로, Suspense fallback이 보이는 시간을 줄일 수 있습니다.
useGLTF가 반환하는 객체에는 scene 외에도 nodes, materials, animations 같은 유용한 속성이 포함되어 있습니다. nodes는 모델 내부의 모든 명명된 오브젝트를 플랫한 딕셔너리로 제공하고, materials는 모든 머티리얼을 같은 방식으로 접근할 수 있게 합니다.
function Model() {
const gltf = useGLTF('/models/Duck.glb')
// 모델 내부 구조 확인
console.log('Nodes:', Object.keys(gltf.nodes))
// → ["LOD3sp", "LOD3spShape", ...]
console.log('Materials:', Object.keys(gltf.materials))
// → ["blinn3-fx", ...]
return <primitive object={gltf.scene} />
}<primitive object={scene}>으로 모델 전체를 한 번에 렌더링하는 것이 가장 간단하지만, nodes를 통해 개별 메시에 접근하면 특정 부품의 머티리얼을 교체하거나, 일부분만 렌더링하거나, 개별 파트에 이벤트 핸들러를 부착하는 것이 가능해집니다.
function CustomModel() {
const { nodes, materials } = useGLTF('/models/Duck.glb')
return (
<group>
<mesh
geometry={nodes['LOD3sp'].geometry}
material={materials['blinn3-fx']}
scale={0.01}
/>
</group>
)
}모델 스케일에 주의
3D 모델의 크기는 제작 환경에 따라 크게 다릅니다. Blender의 기본 단위는 미터이지만, 일부 에셋은 센티미터 단위로 제작되어 Three.js에서 100배 크게 렌더링될 수 있습니다. 모델이 보이지 않거나 화면을 가득 채운다면 scale prop을 먼저 조정해 보는 것이 좋습니다. <group scale={0.01}>처럼 그룹으로 감싸서 전체 스케일을 조절하는 것이 일반적인 방법입니다.
많은 GLB 모델에는 애니메이션 클립이 내장되어 있습니다. Mixamo에서 다운로드한 캐릭터라면 걷기, 뛰기, 인사 같은 모션이 포함되어 있고, Blender에서 직접 리깅한 모델이라면 제작자가 설정한 액션들이 들어 있습니다. useGLTF로 모델을 로딩하면 이 클립들이 animations 배열에 담겨 반환되지만, 이것만으로는 재생되지 않습니다. 애니메이션을 실제로 구동하려면 drei의 useAnimations 훅이 필요합니다.
import { useRef, useEffect } from 'react'
import { useGLTF, useAnimations } from '@react-three/drei'
import type { Group } from 'three'
function AnimatedRobot() {
const group = useRef<Group>(null)
const { scene, animations } = useGLTF('/models/RobotExpressive.glb')
const { actions, names } = useAnimations(animations, group)
useEffect(() => {
// 첫 번째 애니메이션 재생
actions[names[0]]?.reset().fadeIn(0.4).play()
return () => { actions[names[0]]?.fadeOut(0.4) }
}, [actions, names])
return <primitive ref={group} object={scene} />
}useAnimations는 animations 배열과 대상 오브젝트의 ref를 받아, 내부적으로 THREE.AnimationMixer를 생성하고 매 프레임 자동으로 업데이트합니다. 반환값의 names는 클립 이름 배열이고, actions는 이름을 키로 하는 THREE.AnimationAction 딕셔너리입니다.
React의 상태 관리와 결합하면 애니메이션 전환도 자연스럽게 구현됩니다. useState로 현재 클립 이름을 관리하고, useEffect에서 해당 클립을 fadeIn으로 시작하면서 이전 클립을 fadeOut으로 종료하면 부드러운 블렌딩이 적용됩니다.
const [current, setCurrent] = useState('Walking')
useEffect(() => {
const action = actions[current]
action?.reset().fadeIn(0.4).play()
return () => { action?.fadeOut(0.4) }
}, [current, actions])AnimationAction 주요 메서드
play() — 재생 시작. stop() — 즉시 정지. reset() — 시간을 0으로 되돌림. fadeIn(duration) — 지정된 시간에 걸쳐 weight를 0→1로 전환. fadeOut(duration) — weight를 1→0으로 전환. crossFadeFrom(other, duration) — 다른 액션에서 이 액션으로 크로스페이드.
fadeIn/fadeOut을 사용하면 두 애니메이션이 겹치는 구간에서 자연스러운 블렌딩이 일어납니다. 캐릭터가 걷다가 갑자기 춤추는 것이 아니라, 걷기 모션이 서서히 사라지면서 춤 모션이 서서히 나타나는 전환이 만들어집니다.
모델을 로딩하고 애니메이션을 재생하는 것은 기술적 기반이고, 사용자에게 전달되는 것은 최종적인 프레젠테이션입니다. 같은 모델이라도 조명, 그림자, 카메라 컨트롤, 미세한 모션이 더해지면 완성도가 크게 달라집니다. drei는 이러한 프레젠테이션 요소를 위한 전용 헬퍼들을 제공합니다.
PresentationControls — 제품 페이지형 드래그 회전
OrbitControls는 자유로운 궤도 회전이 가능하지만, 제품 페이지에서는 오히려 제어된 움직임이 필요합니다. PresentationControls는 드래그로 회전하되, 놓으면 원래 각도로 부드럽게 돌아오는(snap-back) 동작을 제공합니다. polar과 azimuth prop으로 회전 범위를 제한할 수 있어, 제품의 뒷면이나 밑면을 보여주고 싶지 않을 때 유용합니다.
import { PresentationControls } from '@react-three/drei'
<PresentationControls
speed={1.5}
global
polar={[-0.1, Math.PI / 4]}
azimuth={[-Math.PI / 4, Math.PI / 4]}
>
<Model />
</PresentationControls>Float — 부유 애니메이션
Float은 자식 오브젝트에 부드러운 상하 부유 효과를 적용합니다. 제품이 공중에 떠 있는 듯한 인상을 주며, speed로 속도를, floatIntensity로 움직임의 크기를, rotationIntensity로 미세한 회전량을 조절할 수 있습니다.
import { Float } from '@react-three/drei'
<Float speed={1.5} rotationIntensity={0.4} floatIntensity={0.8}>
<Model />
</Float>ContactShadows — 바닥 접촉 그림자
실시간 그림자 매핑은 설정이 복잡하고 성능 비용이 높습니다. ContactShadows는 바닥 평면에 부드러운 접촉 그림자만 렌더링하는 경량 대안으로, 오브젝트가 바닥에 "접지"되어 있다는 시각적 신호를 제공합니다.
import { ContactShadows } from '@react-three/drei'
<ContactShadows
position={[0, -1.2, 0]}
opacity={0.4}
blur={2.5}
far={4}
/>position은 그림자가 렌더링될 바닥 평면의 높이를 지정하고, blur는 그림자의 부드러운 정도, opacity는 투명도를 조절합니다.
Environment — 환경 조명 (복습)
R3F 입문 문서에서 다룬 Environment는 프레젠테이션 맥락에서도 핵심적인 역할을 합니다. HDRI 환경맵은 조명뿐만 아니라 금속 표면의 반사에도 영향을 미치므로, preset을 바꾸는 것만으로 제품의 분위기가 완전히 달라집니다.
import { Environment } from '@react-three/drei'
// "sunset" → 따뜻한 황금빛 | "city" → 차가운 도시 반사
// "studio" → 중립적 스튜디오 | "warehouse" → 산업적 분위기
<Environment preset="city" />이 헬퍼들을 결합하면, 외부 모델 하나를 프로덕트 페이지 수준의 인터랙티브 쇼케이스로 연출할 수 있습니다. 아래 데모는 DamagedHelmet 모델에 다크 배경, 3점 컬러 조명, PresentationControls + Float를 결합한 예시입니다.
<Canvas camera={{ position: [0, 0.2, 5.5], fov: 45 }}>
<color attach="background" args={['#080810']} />
<Environment preset="city" />
{/* 3점 조명: 키(보라) + 필(파랑) + 림(핑크) */}
<spotLight position={[4, 5, 3]} angle={0.4} penumbra={1} intensity={80} color="#a78bfa" />
<spotLight position={[-4, 3, -2]} angle={0.5} penumbra={1} intensity={40} color="#60a5fa" />
<pointLight position={[0, -3, -1]} intensity={15} color="#e879f9" />
<ContactShadows position={[0, -1.8, 0]} opacity={0.5} blur={3} far={5} color="#1e1b4b" />
<PresentationControls speed={1.5} global polar={[-Math.PI / 6, Math.PI / 4]} azimuth={[-Math.PI / 3, Math.PI / 3]}>
<Float speed={2} rotationIntensity={1.5} floatIntensity={2}>
<Suspense fallback={<LoadingFallback />}>
<Helmet />
</Suspense>
</Float>
</PresentationControls>
</Canvas><primitive object={scene}>은 빠르게 모델을 렌더링하는 데 유용하지만, 모델 내부의 구조가 코드에 드러나지 않는다는 한계가 있습니다. gltfjsx (새 창)는 이 문제를 해결하는 CLI 도구로, GLB 파일을 분석해서 타입이 지정된 React 컴포넌트를 자동 생성합니다.
npx gltfjsx Duck.glb --types위 명령은 이런 형태의 컴포넌트를 생성합니다 (간략화한 예시):
import { useGLTF } from '@react-three/drei'
import type { GLTF } from 'three-stdlib'
type GLTFResult = GLTF & {
nodes: { LOD3sp: THREE.Mesh }
materials: { 'blinn3-fx': THREE.MeshStandardMaterial }
}
export function Duck(props: JSX.IntrinsicElements['group']) {
const { nodes, materials } = useGLTF('/models/Duck.glb') as GLTFResult
return (
<group {...props}>
<mesh geometry={nodes.LOD3sp.geometry} material={materials['blinn3-fx']} />
</group>
)
}모든 노드와 머티리얼에 타입이 지정되어 있으므로, IDE의 자동 완성이 동작하고, 존재하지 않는 노드 이름을 참조하면 컴파일 타임에 에러가 발생합니다. --transform 플래그를 추가하면 Draco 압축, 텍스처 리사이즈, 중복 제거까지 자동으로 수행하여 70-90%의 용량 감소가 가능합니다.
Draco는 Google이 개발한 메시 압축 알고리즘으로, GLB 파일의 geometry 데이터를 60-90% 압축할 수 있습니다. drei의 useGLTF는 Draco 압축된 파일을 자동으로 감지하고, WASM 기반 디코더를 CDN에서 로딩하여 해제합니다. 별도의 DRACOLoader 설정이 필요 없습니다.
에셋 사이징 가이드라인
히어로 모델은 5MB 이하, 보조 모델은 1MB 이하를 목표로 합니다. 텍스처 해상도는 웹에서는 1K(1024x1024)로 충분한 경우가 많으며, 사용하지 않는 애니메이션 클립을 제거하는 것만으로도 상당한 용량 절감이 가능합니다. Blender의 GLB 내보내기 옵션에서 Draco 압축을 활성화하면 추가 도구 없이도 압축된 GLB를 생성할 수 있습니다.
| 소스 | 라이선스 | 특징 |
|---|---|---|
| Sketchfab (새 창) | CC0 / CC-BY 등 | 최대 규모 라이브러리, 브라우저 3D 미리보기 |
| Poly Haven (새 창) | CC0 | 고품질 스캔 모델, HDRI/텍스처도 함께 제공 |
| Mixamo (새 창) | 무료 사용 | 캐릭터 + 스켈레톤 애니메이션, FBX→GLB 변환 필요 |
| Kenney.nl (새 창) | CC0 | 로우폴리 게임 스타일 에셋 |
| Quaternius (새 창) | CC0 | 로우폴리 캐릭터·환경 에셋, 애니메이션 포함 |
| glTF Sample Assets (새 창) | CC0 / CC-BY | Khronos 공식 glTF 레퍼런스 모델 |
| Three.js 예제 모델 (새 창) | MIT | 테스트/학습용 소형 모델 |
라이선스를 반드시 확인할 것
CC0는 제한 없이 자유롭게 사용 가능하지만, CC-BY는 원작자 저작자 표시가 필수입니다. Sketchfab에서 모델을 다운로드할 때는 각 모델의 라이선스를 개별적으로 확인해야 합니다. 상업 프로젝트에서는 CC0 또는 명시적 상업 허용 라이선스만 사용하는 것이 안전합니다.
코드로 geometry를 만드는 단계를 넘어, 외부에서 제작된 3D 모델을 로딩하고 활용하는 전체 워크플로우를 다루었습니다.
이 문서에서 다룬 핵심 개념
glTF/GLB — Khronos Group이 설계한 웹 3D 표준 형식. 메시, 머티리얼, 텍스처, 스켈레톤, 애니메이션을 하나의 파일에 담으며, .glb(단일 바이너리)가 웹 배포에 선호됩니다.
useGLTF — GLB 파일을 로딩하고 캐싱하는 drei 훅. <primitive object={scene}>으로 모델 전체를 렌더링하거나, nodes/materials 딕셔너리로 개별 파트에 접근합니다. React Suspense와 통합되어 선언적 로딩 패턴을 지원합니다.
useAnimations — 모델에 내장된 애니메이션 클립을 재생하는 훅. actions 딕셔너리로 클립별 재생/정지/페이드를 제어하며, React 상태와 결합하면 자연스러운 애니메이션 전환이 가능합니다.
프레젠테이션 헬퍼 — PresentationControls(제한된 드래그 회전 + 스냅백), Float(부유 애니메이션), ContactShadows(바닥 그림자), Environment(환경 조명)를 결합하여 프로덕트 쇼케이스 수준의 연출을 구성합니다.
최적화 — gltfjsx로 타입 안전한 React 컴포넌트를 자동 생성하고, Draco 압축으로 에셋 크기를 60-90% 줄입니다.
여기서 더 나아가려면 @react-three/rapier로 물리 시뮬레이션을 추가하거나, @react-three/postprocessing으로 Bloom이나 Depth of Field 같은 후처리 효과를 적용하는 방향으로 확장할 수 있습니다. 로딩한 모델에 물리 충돌체를 부여하거나, 커스텀 셰이더 머티리얼로 교체하는 것도 가능하며, 이 모든 것이 R3F의 선언적 JSX 패턴 위에서 작동합니다.
- drei useGLTF 문서 (새 창)
- drei useAnimations 문서 (새 창)
- gltfjsx GitHub (새 창) — GLB → React 컴포넌트 변환 CLI
- glTF 명세 (Khronos Group) (새 창)
- Sketchfab (새 창) — 최대 규모 3D 모델 라이브러리
- React Three Fiber 공식 문서 (새 창)
관련 문서
글쓴이 mirunamu00



