React Three Fiber(R3F)
1. 개요
Three.js를 다뤄보신 분들이라면 아시겠지만, 순수 Three.js로 프로젝트를 구성할 때는 다음과 같은 흐름을 직접 작성해야 합니다.
// Three.js 기본 구성 예시
import * as THREE from 'three'
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
R3F는 이 모든 흐름을 React 컴포넌트 안에서 자동으로 처리해 줍니다.
비교 요약
기능 | Three.js | R3F
| Scene 생성 | new THREE.Scene() 직접 호출 | <Canvas> 컴포넌트 자동 생성 |
| Camera 설정 | new THREE.PerspectiveCamera(...) | <Canvas camera={{ ... }}> props |
| Renderer 생성 | new THREE.WebGLRenderer() | Canvas 내부에서 자동 생성 및 React 생명주기에 맞게 관리 |
| 렌더 루프 | requestAnimationFrame 직접 작성 | useFrame 훅 제공 |
| 이벤트 처리 | Raycaster 직접 구현 | onPointerDown, onClick 등 DOM 이벤트처럼 사용 |
| 상태 관리 | 전역 변수 또는 수동 관리 | React state, props, context 사용 가능 |
2. 프로젝트 준비
2.1 React 프로젝트 생성
권장: Vite
npm create vite@latest my-r3f-app
cd my-r3f-app
npm install
2.2 R3F 설치
npm install three @react-three/fiber
3. R3F의 핵심 컴포넌트 — <Canvas>
R3F에서 <Canvas>는 Three.js의 Renderer + Scene + Camera 역할을 한 번에 담당합니다.
import { Canvas } from '@react-three/fiber'
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
{/* Scene 안에 들어갈 3D 객체 */}
</Canvas>
)
}
Canvas Props 예시
- camera: 초기 카메라 설정 { position: [x,y,z], fov: number }
- shadows: 그림자 활성화
- gl: WebGLRenderer 옵션 { antialias: true, powerPreference: 'high-performance' }
- frameloop: 'always' | 'demand' | 'never'
4. 첫 번째 Mesh 생성하기
4.1 Three.js에서 Mesh 생성
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({ color: 'orange' })
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
4.2 R3F에서 Mesh 생성
function Box() {
return (
<mesh>
<boxGeometry args={[1, 1, 1]} /> {/* args는 생성자 매개변수 */}
<meshStandardMaterial color="orange" />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<Box />
</Canvas>
)
}
차이점
- Three.js: scene.add(mesh) 직접 호출
- R3F: JSX 태그로 Scene 트리에 자동 추가
- args: Three.js 생성자 인자를 배열 형태로 전달
5. 애니메이션 — useFrame
5.1 Three.js 방식
function animate() {
requestAnimationFrame(animate)
mesh.rotation.x += 0.01
renderer.render(scene, camera)
}
animate()
5.2 R3F 방식
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function RotatingBox() {
const ref = useRef()
useFrame((state, delta) => {
ref.current.rotation.x += delta
ref.current.rotation.y += delta
})
return (
<mesh ref={ref}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
차이점
- Three.js: 렌더 루프 전체를 직접 제어
- R3F: useFrame 훅으로 프레임별 업데이트만 제어, 렌더링 자체는 Canvas가 관리
6. 이벤트 처리 — Raycaster vs Pointer Events
6.1 Three.js
// 마우스 위치 계산 + Raycaster 사용
직접 Raycaster 인스턴스를 만들고, 마우스 좌표를 정규화해 객체 교차를 계산해야 함.
6.2 R3F
function InteractiveBox() {
const [hovered, setHovered] = useState(false)
return (
<mesh
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? 'skyblue' : 'orange'} />
</mesh>
)
}
- DOM 이벤트처럼 사용 가능 (onPointerOver, onPointerOut, onClick 등)
- 터치 디바이스까지 동일하게 처리됨
7. 카메라 컨트롤 — OrbitControls
Three.js에서는:
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const controls = new OrbitControls(camera, renderer.domElement)
R3F에서는:
import { useThree, extend, useFrame } from '@react-three/fiber';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
extend({ OrbitControls }); // extend는 three.js 클래스를 JSX에서 사용할 수 있도록 해줌.
export default function Experience() {
const { camera, gl } = useThree();
// const three = useThree();
// three.camera: PerspectiveCamera
// three.gl: WebGLRenderer, domElement 등 <canvas> 요소
// three.clock: 시계 인스턴스
// 등 three js 요소를 가지고 있음.
return (
<>
{/* orbitControls는 camera와 domElement 필요 */}
<orbitControls args={[camera, gl.domElement]} />
</>
);
}
또는:
npm install @react-three/drei
import { OrbitControls } from '@react-three/drei'
<Canvas>
<OrbitControls enableZoom={true} />
<mesh>...</mesh>
</Canvas>
- Drei는 R3F 생태계에서 필수적인 유틸 라이브러리
- 자동으로 camera와 renderer에 연결
8. 성능 최적화 팁
- frameloop="demand": 장면이 바뀔 때만 렌더링
- <mesh castShadow receiveShadow>: 그림자 최적화
- useMemo: 지오메트리/머티리얼 캐싱
- <Suspense>: 모델 로딩 시 비동기 처리
9. 확장 방향
- Drei의 <Environment>로 HDRI 환경 조명 적용
- <Html>로 3D 위치에 HTML 오버레이 배치
- useGLTF로 glTF 모델 로딩
- @react-three/postprocessing으로 후처리 효과 추가
10. 전체 템플릿 예시
import { Canvas, useFrame } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import { useRef, useState } from 'react'
function RotatingBox() {
const ref = useRef()
const [hovered, setHovered] = useState(false)
useFrame((_, delta) => {
ref.current.rotation.x += delta
ref.current.rotation.y += delta
})
return (
<mesh
ref={ref}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
castShadow
receiveShadow
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? 'skyblue' : 'orange'} />
</mesh>
)
}
export default function App() {
return (
<Canvas
shadows
camera={{ position: [3, 3, 3], fov: 60 }}
style={{ height: '100vh', background: '#272730' }}
>
<ambientLight intensity={0.5} />
<directionalLight
position={[5, 5, 5]}
castShadow
shadow-mapSize-width={1024}
shadow-mapSize-height={1024}
/>
<OrbitControls />
<RotatingBox />
</Canvas>
)
}
💡 정리
- R3F는 Three.js의 기본 객체 생성, 렌더링 루프, 이벤트 처리를 React 컴포넌트로 추상화하여 생산성과 유지보수성을 크게 높여줍니다.
- 기존 Three.js 지식을 그대로 활용하면서도, React의 장점(컴포넌트화, 상태 관리, props)을 결합할 수 있습니다.
- 특히 Pointer 이벤트, useFrame 훅, Drei 유틸은 Three.js 순수 구현보다 훨씬 짧고 명확한 코드 작성이 가능하게 합니다.

1) index.jsx (엔트리 + <Canvas> 설정)
import './style.css';
import ReactDOM from 'react-dom/client';
import { Canvas } from '@react-three/fiber';
import Experience from './Experience';
import * as THREE from 'three';
const root = ReactDOM.createRoot(document.querySelector('#root'));
// ------------------------------
// 아래 블록은 "전통적인 three.js(명령형, imperative) 방식"을 JSX로 치환하기 전의 참고 예시입니다.
// R3F는 이 imperative 코드를 "선언형(declarative) JSX"로 바꿔주어, 상태/라이프사이클 관리를 React로 위임합니다.
// const mesh = new THREE.Mesh();
// mesh.position.set(1, 2, 3);
// mesh.rotation.x = 0.5;
// mesh.geometry = new THREE.BoxGeometry(1, 1, 1);
// mesh.material = new THREE.MeshBasicMaterial({ color: 'red' });
// scene.add(mesh);
// root.render(
// <>
// <mesh position={[1, 2, 3]} rotation-x={0.5}>
// <boxGeometry />
// <meshBasicMaterial color={'red'} />
// </mesh>
// </>,
// );
// -------------------------------
// 그룹도 동일한 개념으로, R3F에서는 <group> / <mesh>로 트리 구조를 "선언"하면
// 내부적으로 create + add + set 등을 자동으로 수행합니다.
// const group = new THREE.Group();
// scene.add(group);
// const mesh1 = new THREE.Mesh();
// mesh1.geometry = new THREE.BoxGeometry(1, 1, 1);
// mesh1.material = new THREE.MeshBasicMaterial({ color: 'red' });
// const mesh2 = new THREE.Mesh();
// mesh2.geometry = new THREE.SphereGeometry(0.5);
// mesh2.material = new THREE.MeshBasicMaterial({ color: 'orange' });
// group.add(mesh1, mesh2);
// root.render(
// <>
// <group>
// <mesh>
// {/* attach를 사용하여 부모의 특정 속성을 구성 요소에 할당할 수 있지만,
// r3f가 이름으로 구분하므로 생략할 수 있음. */}
// <boxGeometry attach="geometry" />
// <meshBasicMaterial attach="material" color="red" />
// </mesh>
// <mesh>
// <sphereGeometry attach="geometry" />
// <meshBasicMaterial attach="material" color="orange" />
// </mesh>
// </group>
// </>,
// );
// -------------------------------
const cameraSetting = {
// OrthographicCamera에서는 fov 대신 zoom이 핵심
// (type: number) 화면에서의 '픽셀/단위' 배율 → 값이 클수록 크게 보임
// zoom: 100,
// (type: number) 클리핑 평면 — 너무 작은 near는 z-fighting을 유발할 수 있음
near: 0.1,
far: 200,
// (type: [number, number, number]) 카메라 위치; 정사영은 거리로 스케일이 변하지 않음
position: [3, 2, 6],
};
root.render(
<Canvas
// dpr={[1, 2]} // default
// - (type: number | [min, max]) 렌더링 해상도를 장치 픽셀 비율에 맞춰 제한
// - 고해상도 디스플레이에서 성능/선명도 균형을 맞추기 좋음
// flat // tone mapping 없이 "있는 그대로(flat)" 렌더링 = 색이 왜곡되면 안 될 때 사용
// - flat이 켜지면 toneMapping=NoToneMapping, outputColorSpace=Linear로 강제되어
// PBR 톤매핑/감마 보정을 끕니다. (UI/HUD, 포스트 프로세싱 직접 제어 시 유용)
gl={{
antialias: false, // 안티앨리어싱 - 기본값 true
// - 캔버스 컨텍스트 생성 옵션. WebGL의 MSAA는 컨텍스트 특성이라 post-생성 변경 불가.
// - post-processing(SMAA/TAA 등)으로 대체할 계획이면 false가 성능상 유리할 수 있음.
toneMapping: THREE.ACESFilmicToneMapping, // toneMapping
// - flat을 사용하지 않는 경우에만 의미가 있습니다.
// - 물리기반 렌더링(PBR)에서 HDR 값을 SDR 디스플레이로 자연스럽게 압축.
outputColorSpace: THREE.SRGBColorSpace, // outputEncoding에서 outputColorSpace로 변경됨.
// - 세 줄 요약: Linear(연산) → sRGB(출력)로 감마 보정 적용.
// - flat 모드에서는 Linear로 강제되므로 여기는 무시됩니다.
}}
// orthographic // (type: boolean) 정사영 카메라 사용 플래그, perspective가 기본값
// - 정사영은 거리와 무관하게 동일 크기로 보이며, 기본 zoom=1이면 "1월드단위 ≈ 1픽셀"로
// 체감상 매우 작게(멀리) 보입니다. 이때 zoom을 올려 프레이밍합니다.
camera={cameraSetting} // (type: object) 카메라 초기 파라미터
>
<Experience />
</Canvas>,
);
✅ Canvas 주요 파라미터 정리 (역할/타입)
- dpr: number | [number, number] — 디바이스 픽셀 비율 범위. 성능/선명도 절충.
- flat: boolean — tone mapping/감마 보정 비활성화(Linear 출력).
- gl: WebGLRenderer 생성 옵션 객체
- antialias: boolean — MSAA 사용 여부(컨텍스트 생성 시 결정).
- toneMapping: THREE.ToneMapping — flat이 꺼져 있을 때만 유효.
- outputColorSpace: THREE.ColorSpace — 출력 색 공간(sRGB 권장).
- orthographic: boolean — 정사영 카메라 사용 플래그.
- camera: { fov?, near, far, position, zoom? } — 초기 카메라 설정.
- PerspectiveCamera: fov 사용.
- OrthographicCamera: zoom 사용(정사영에서 스케일 조절 핵심).
2) Experience.jsx (씬 구성 + 컨트롤 + 애니메이션)
import { useThree, extend, useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import CustomObject from './CustomObject';
extend({ OrbitControls }); // extend는 three.js 클래스를 JSX에서 사용할 수 있도록 해줌.
// - 등록 후에는 <orbitControls />처럼 소문자 태그로 사용할 수 있습니다.
// - 대안: @react-three/drei의 <OrbitControls makeDefault /> 사용(자동 등록 + 편의 props)
export default function Experience() {
const { camera, gl } = useThree();
// const three = useThree();
// three.camera: PerspectiveCamera
// three.gl: WebGLRenderera를 포함하는 것 domElement, 즉<canvas>
// three.clock: 시계 인스턴스
// 등 three js 요소를 가지고 있음.
// + 추가 설명:
// - R3F의 커스텀 렌더러가 관리하는 씬/카메라/렌더러에 접근할 수 있어,
// 컨트롤러 초기화나, 사이즈/FOV 동기화 같은 imperative 작업을 최소한으로 수행 가능.
const cubeRef = useRef(); // (type: React.MutableRefObject<THREE.Mesh | undefined>)
const groupRef = useRef(); // (type: React.MutableRefObject<THREE.Group | undefined>)
useFrame((state, delta) => {
// useFrame은 렌더 루프에 매 프레임 호출되며, (state, delta)를 제공합니다.
// - state: clock, pointer, size, camera 등 R3F 상태 및 헬퍼 모음
// - delta: 이전 프레임 대비 경과 시간(초) → 프레임레이트와 무관한 애니메이션에 사용
// const angle = state.clock.elapsedTime;
// state.camera.position.x = Math.sin(angle) * 8;
// state.camera.position.z = Math.cos(angle) * 8;
// state.camera.lookAt(0, 0, 0); // 중앙 지정
// - 위 패턴은 "원형 궤도" 카메라를 직접 구현할 때 유용.
// - OrbitControls를 쓰는 경우 충돌하므로 한쪽만 사용.
cubeRef.current.rotation.y += delta; // 회전 속도는 delta(초) 기반이라 FPS와 무관(일관된 속도)
// groupRef.current.rotation.y += delta; // 그룹 단위 회전도 가능(자식 전체가 같이 회전)
});
return (
<>
{/* camera와 domElement 필요 */}
{/* args의 의미: new OrbitControls(camera, gl.domElement)와 동일 */}
<orbitControls args={[camera, gl.domElement]} />
{/* 라이트: MeshStandardMaterial(PBR)를 쓰므로 직접광 + 환경광 조합 */}
<directionalLight position={[1, 2, 3]} intensity={4.5} />
<ambientLight intensity={1.5} />
<group ref={groupRef}>
<mesh position-x={-2}>
<sphereGeometry />
<meshStandardMaterial color="orange" />
{/* Standard는 PBR 재질. 라이트 영향 받음(메탈/러프니스 등 추가 가능). */}
</mesh>
{/* scale={[x, y, z]} == mesh.scale.set(x, y, z) */}
<mesh
ref={cubeRef}
rotation-y={Math.PI * 0.23}
// position={[2, 0, 0]}
position-x={2}
scale={1.5}>
{/* args = {[radius, widthSegments, heightSegment]} */}
{/* <sphereGeometry args={[1.5, 32, 32]} /> */}
{/* ⚠️ 주의: <boxGeometry scale={1.5} /> 는 geometry의 "필드"가 아니라 "메서드(geometry.scale)"라
R3F가 자동으로 호출해주지 않습니다. 실제 스케일링은 mesh의 scale={1.5}가 담당합니다.
(geometry 자체를 스케일하고 싶다면 ref로 geometry.scale(x,y,z) 호출) */}
<boxGeometry />
<meshStandardMaterial
// args={[{ color: 'red', wireframe: true }]}
color="mediumpurple"
wireframe={false}
/>
</mesh>
</group>
{/* 바닥 플레인: 시각적 기준면. backface culled 방지 위해 회전 */}
<mesh position-y={-1} rotation-x={-Math.PI * 0.5} scale={10}>
<planeGeometry />
<meshStandardMaterial color="greenyellow" />
</mesh>
<CustomObject />
</>
);
}
✅ Experience 주요 파라미터 정리 (역할/타입)
- useThree() 반환값
- camera: THREE.Camera — 현재 활성 카메라(기본: PerspectiveCamera 또는 <Canvas orthographic />면 OrthographicCamera).
- gl: THREE.WebGLRenderer — .domElement는 <canvas> 노드.
- useFrame((state, delta))
- state: { clock, size, pointer, camera, scene, ... } — 렌더 루프 컨텍스트.
- delta: number — 초 단위 프레임 간격.
- <orbitControls args={[camera, gl.domElement]} />
- args[0]: THREE.Camera — 제어할 카메라.
- args[1]: HTMLCanvasElement — 입력을 받을 DOM 엘리먼트.
- <directionalLight position intensity />
- position: [number, number, number] — 광원 위치.
- intensity: number — 광 강도(기본 1).
- <mesh rotation-y position-x scale />
- rotation-y: number(rad) — Y축 회전(대시 케이스로 부분 접근).
- position-x: number — X축 위치(부분 접근).
- scale: number | [x,y,z] — 메쉬 스케일. number면 setScalar.
3) CustomObject.jsx (커스텀 BufferGeometry)
import * as THREE from 'three';
import { useRef, useMemo, useEffect } from 'react';
export default function CustomObject() {
const geometryRef = useRef(); // (type: React.MutableRefObject<THREE.BufferGeometry | undefined>)
const verticesCount = 10 * 3;
// 의미: 삼각형 10개 × 버텍스 3 = 총 30 버텍스
// - 아래 positions 배열 길이는 '버텍스 수 × itemSize(=3)' = 90 float로 생성됩니다.
// 리렌더링 시 최적화를 위해 useMemo로 관리하는 게 좋다.
const positions = useMemo(() => {
const positions = new Float32Array(verticesCount * 3);
for (let i = 0; i < verticesCount * 3; i++) {
positions[i] = (Math.random() - 0.5) * 3; // [-1.5, 1.5] 범위 랜덤 위치
}
return positions;
}, []);
// - 메모이제이션으로 배열 재할당을 피함(불필요한 GPU 업로드 방지)
// - positions 내용을 "동적"으로 바꿔야 한다면 .needsUpdate=true 또는 setAttribute를 사용
useEffect(() => {
geometryRef.current.computeVertexNormals(); // 최초 렌더링 시 동작하지 않으므로 useEffect 사용
// - 표면 법선(normal)이 없으면 StandardMaterial가 어둡게 보이거나 조명이 비정상.
// - ref는 초기 마운트 후에만 안전하므로 effect에서 호출.
}, []);
return (
<mesh>
<bufferGeometry ref={geometryRef}>
<bufferAttribute
attach="attributes-position"
count={verticesCount} // 버텍스 개수(= array.length / itemSize)
itemSize={3} // 한 버텍스를 이루는 구성요소 개수(x,y,z)
array={positions} // Float32Array(길이: verticesCount * 3)
/>
</bufferGeometry>
<meshStandardMaterial
color="red"
side={THREE.DoubleSide} // double side 설정
/>
</mesh>
);
}
✅ CustomObject 파라미터 정리 (역할/타입)
- bufferAttribute
- attach: "attributes-position" — 부모 BufferGeometry의 attributes.position에 바인딩.
- (attach는 명시적 바인딩용. R3F가 타입 이름으로 자동 추론할 수도 있으나, 명시하면 명확)
- count: number — 버텍스 개수(= array.length / itemSize).
- itemSize: number — 한 버텍스를 구성하는 요소 수(좌표 3개 → 3).
- array: TypedArray — GPU로 업로드될 원시 데이터.
- attach: "attributes-position" — 부모 BufferGeometry의 attributes.position에 바인딩.
- computeVertexNormals(): BufferGeometry의 법선 재계산.
- 동적으로 포지션을 갱신했다면, 다시 호출하여 조명 결과를 갱신해야 함.
- 포지션을 자주 바꿀 경우 geometry.attributes.position.needsUpdate = true 설정도 필요.
보충 개념
1) Declarative R3F vs Imperative three.js
- Imperative(명령형): new Mesh() → set → add 순서대로 호출.
- Declarative(선언형): <mesh><boxGeometry /><meshStandardMaterial/></mesh>처럼 트리 구조를 선언하면,
R3F가 생성/속성 적용/관계 설정을 자동으로 reconcile합니다.
2) Dash-case property piercing
- position-x={2}, rotation-y={...} 처럼 중첩 객체의 특정 축/필드만 부분 업데이트.
내부적으로 object.position.x = 2에 대응. - position={[x,y,z]}와 동시에 쓰면 나중에 적용되는 값이 최종적용됩니다.
3) Orthographic vs Perspective
- 기본은 PerspectiveCamera.
- <Canvas orthographic /> 시 OrthographicCamera: 거리와 무관하게 같은 크기 →
zoom=1이면 화면 픽셀 스케일에 종속되어 매우 작게 보일 수 있음 → zoom으로 프레이밍.
4) flat vs Tone Mapping/Color Space
- flat: NoToneMapping + Linear 출력 → 색 "있는 그대로".
- PBR로 현실감(빛의 범위/하이라이트)을 원하면 ACES + sRGB 조합 권장.
- 하나의 씬에서 3D는 PBR, UI는 flat처럼 혼용이 필요하면 후처리 단계에서 제어하는 편이 안전.
5) OrbitControls 등록 방식
- extend({ OrbitControls }) + <orbitControls args={[camera, gl.domElement]} />
- 또는 @react-three/drei의 <OrbitControls makeDefault /> (권장: 간단하고 충돌 적음).
6) BufferGeometry 성능 팁
- 정적 데이터 → useMemo로 배열 재사용.
- 동적 데이터 → setAttribute + .needsUpdate = true, 변경 시에만 GPU 업로드.
- 매우 큰 버퍼는 DynamicDrawUsage 설정 고려:
geometryRef.current.attributes.position.setUsage(THREE.DynamicDrawUsage).
자주 겪는 함정 & 팁
- <boxGeometry scale={1.5} />: geometry에는 scale 메서드만 있고 필드가 아님 → JSX prop으로는 호출되지 않음.
→ **메쉬의 scale**로 처리하거나, geometry ref로 geometry.scale(x,y,z) 호출. - OrbitControls vs 수동 카메라 이동: 둘을 동시에 쓰면 충돌 → 한쪽만 사용.
- toneMapping/flat 혼용: flat을 켜면 gl.toneMapping/outputColorSpace 설정이 사실상 무력화.
- 정사영에서 “멀어 보임”: zoom으로 프레이밍하는 게 정상 패턴.
- 라이트/재질 매칭: MeshStandardMaterial는 조명 필요. MeshBasicMaterial는 조명 무시.
'Graphic > R3F' 카테고리의 다른 글
| 57 R3F Debug (7) | 2025.08.15 |
|---|---|
| 56 Drei (2) | 2025.08.15 |
| 53, 54 React와 React Three Fiber (9) | 2025.08.11 |
| react-three/drei (1) | 2025.06.09 |
| 무료로 R3F 아바타 생성하기 (1) | 2025.05.26 |