6.9 KiB
6.9 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| frontend-three-r3f | Three.js / React Three Fiber — 3D 웹 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Three.js / R3F (React Three Fiber)
Web 3D 표준. Three.js (vanilla) / React Three Fiber (React) + drei (helpers) + Rapier (physics). WebGL → WebGPU 미래.
📖 핵심 개념
- Scene: 3D 공간.
- Mesh: geometry + material.
- Camera: 시점.
- Light: 조명.
- Renderer: WebGL / WebGPU.
💻 코드 패턴
기본 R3F
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Box } from '@react-three/drei';
import { useRef } from 'react';
function RotatingBox() {
const ref = useRef<THREE.Mesh>(null);
useFrame((_, delta) => {
ref.current!.rotation.y += delta;
});
return (
<Box ref={ref} args={[1, 1, 1]}>
<meshStandardMaterial color="orange" />
</Box>
);
}
export function Scene() {
return (
<Canvas camera={{ position: [3, 3, 5] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} />
<RotatingBox />
<OrbitControls />
</Canvas>
);
}
drei (helpers)
import {
OrbitControls, Environment, ContactShadows, Float,
Text, Html, useGLTF, useTexture, PerspectiveCamera, Stage,
} from '@react-three/drei';
<Canvas>
<Stage>
<Float speed={2} rotationIntensity={0.5}>
<mesh>...</mesh>
</Float>
</Stage>
<Text fontSize={1} color="white">Hello 3D</Text>
<Html position={[0, 2, 0]}>
<div>HTML overlay</div>
</Html>
</Canvas>
GLTF 모델 로드
function Model() {
const { scene, animations } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
// Optimization — useGLTF preload
useGLTF.preload('/model.glb');
→ DRACO compression 자동 (drei 가).
Animation (모델)
import { useAnimations } from '@react-three/drei';
function AnimatedModel() {
const ref = useRef<THREE.Group>(null);
const { scene, animations } = useGLTF('/dancer.glb');
const { actions } = useAnimations(animations, ref);
useEffect(() => {
actions.idle?.play();
}, []);
return <primitive ref={ref} object={scene} />;
}
Physics (Rapier)
import { Physics, RigidBody } from '@react-three/rapier';
<Canvas>
<Physics gravity={[0, -9.81, 0]}>
<RigidBody>
<Box position={[0, 5, 0]} />
</RigidBody>
<RigidBody type="fixed">
<Plane args={[10, 10]} rotation={[-Math.PI / 2, 0, 0]} />
</RigidBody>
</Physics>
</Canvas>
Material
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial
color="hotpink"
metalness={0.5}
roughness={0.3}
map={texture}
/>
</mesh>
// MeshPhysicalMaterial — 더 사실적
<meshPhysicalMaterial
clearcoat={1}
transmission={0.5} // 반투명 / glass
thickness={0.5}
ior={1.5}
/>
Shader (custom GLSL)
// vertex.glsl
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// fragment.glsl
varying vec2 vUv;
uniform float uTime;
void main() {
vec3 color = vec3(sin(vUv.x + uTime), sin(vUv.y + uTime), 1.0);
gl_FragColor = vec4(color, 1.0);
}
<mesh>
<planeGeometry args={[2, 2]} />
<shaderMaterial
vertexShader={vertexShader}
fragmentShader={fragmentShader}
uniforms={{ uTime: { value: 0 } }}
/>
</mesh>
Post-processing
import { EffectComposer, Bloom, ChromaticAberration } from '@react-three/postprocessing';
<Canvas>
<Scene />
<EffectComposer>
<Bloom intensity={1.5} luminanceThreshold={0.5} />
<ChromaticAberration offset={[0.001, 0.001]} />
</EffectComposer>
</Canvas>
Performance
// Instances (수천 mesh)
import { Instances, Instance } from '@react-three/drei';
<Instances limit={1000}>
<boxGeometry />
<meshStandardMaterial />
{data.map((d, i) => (
<Instance key={i} position={d.pos} color={d.color} />
))}
</Instances>
// LOD (Level of Detail)
import { Detailed } from '@react-three/drei';
<Detailed distances={[0, 10, 20]}>
<HighDetailMesh />
<MidDetailMesh />
<LowDetailMesh />
</Detailed>
// Frustum culling 자동
// Manual: ref.current.frustumCulled = true (default)
// Shadows — 비싸. 작은 set 만
<directionalLight castShadow shadow-mapSize={[1024, 1024]} />
<mesh receiveShadow castShadow>
Mobile / WebGL fallback
// 작은 화면 / GPU = quality down
const isMobile = useMediaQuery('(max-width: 768px)');
<Canvas dpr={isMobile ? 1 : 2} shadows={!isMobile}>
WebGPU (미래)
import { WebGPURenderer } from 'three/examples/jsm/renderers/webgpu/WebGPURenderer.js';
<Canvas
gl={(canvas) => new WebGPURenderer({ canvas })}
>
→ WebGL 보다 빠름. 2025+ stable.
React-spring + R3F
import { useSpring, animated } from '@react-spring/three';
const { position } = useSpring({ position: hovered ? [0, 1, 0] : [0, 0, 0] });
<animated.mesh position={position} ... />
화면 → 3D coordinates
import { useThree } from '@react-three/fiber';
function ClickToWorld() {
const { camera, raycaster, mouse } = useThree();
const handle = (e: MouseEvent) => {
raycaster.setFromCamera(mouse, camera);
// raycaster.ray 로 intersection
};
}
Spline (no-code 3D)
import Spline from '@splinetool/react-spline';
<Spline scene="https://prod.spline.design/xxx/scene.splinecode" />
→ Designer 가 Spline app 에서 만들고 R3F 가 import.
Bundle size 주의
Three.js: ~600KB (큰)
+ drei, postprocessing, rapier ...
→ Dynamic import + code split
const Scene = lazy(() => import('./Scene'));
<Suspense fallback={<Loader />}>
<Scene />
</Suspense>
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Product showcase | R3F + drei + GLTF |
| Game | R3F + Rapier physics |
| Data viz | R3F + custom shader |
| Designer 만든 scene | Spline |
| 매우 simple (1-2 model) | |
| 강력 / vanilla | Three.js direct |
❌ 안티패턴
- 모든 frame setState: re-render. useFrame 안 ref.
- Shadow 모든 light + 큰 mapSize: 60fps 깨짐.
- 너무 많은 mesh (1000+): Instances.
- 모델 압축 안 함 (GLB 100MB): load 느림. DRACO + Meshopt.
- WebGL fallback 없음 + GPU 약: blank 화면.
- Mobile 무 dpr 2: 발열.
- Memory leak (useEffect cleanup 없음): GPU resources 안 release.
🤖 LLM 활용 힌트
- R3F + drei + Rapier 가 표준 stack.
- GLTF + DRACO compression.
- Instances / LOD / shadow 절제.
- Suspense + lazy load.