패턴 1) 호버 중에만 60fps로 회전/렌더
import { Canvas, useThree } from '@react-three/fiber'
import React from 'react'
function InteractiveBox(): JSX.Element {
const { invalidate } = useThree() // 역할: 한 프레임만 다시 그리기
const ref = React.useRef<THREE.Mesh>(null)
// 상태: 호버 여부
const [hovered, setHovered] = React.useState(false)
// 호버 중에만 60fps 루프 구동
React.useEffect(() => {
if (!hovered) return
let raf = 0
let last = 0
const fps = 60
const interval = 1000 / fps
const loop = (now: number) => {
raf = requestAnimationFrame(loop)
if (now - last < interval) return
last = now - ((now - last) % interval)
// 회전 업데이트 (예시)
if (ref.current) ref.current.rotation.y += 0.03
invalidate() // 이 순간에만 한 프레임 렌더
}
raf = requestAnimationFrame(loop)
return () => cancelAnimationFrame(raf)
}, [hovered, invalidate])
return (
<mesh
ref={ref}
onPointerOver={() => setHovered(true)} // 호버 시작 → 루프 시작
onPointerOut={() => setHovered(false)} // 호버 끝 → 루프 종료
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? '#2F9E44' : '#0B5FFF'} />
</mesh>
)
}
export default function App() {
return (
<Canvas frameloop="demand" camera={{ position: [2.5, 2.5, 4], fov: 50 }}>
<ambientLight intensity={0.6} />
<directionalLight position={[3, 5, 2]} intensity={1.0} />
<InteractiveBox />
</Canvas>
)
}
핵심:
- frameloop="demand" → 기본적으로 그리지 않음.
- 호버 중에만 requestAnimationFrame 루프를 돌리고, 매 틱마다 invalidate()로 한 프레임만 렌더.
- interval로 60fps 제한.
패턴 2) 이벤트 단발 렌더 (애니메이션 없이 “상태만 바뀜”)
“호버 시 색만 토글” 같은 건 루프가 필요 없고, 이벤트에서 한 번만 invalidate 하면 됩니다.
function HoverColorBox(): JSX.Element {
const { invalidate } = useThree()
const [hovered, setHovered] = React.useState(false)
return (
<mesh
onPointerOver={() => { setHovered(true); invalidate() }}
onPointerOut={() => { setHovered(false); invalidate() }}
>
<boxGeometry />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
- 변화가 발생한 시점에만 invalidate() 호출 → 그 프레임만 그려지고 다시 정지.
패턴 3) 화면에 보일 때만 갱신 (IntersectionObserver)
리스트/대시보드처럼 뷰포트에 보일 때만 렌더링:
function VisibleOnlyRenderer({ children }: { children: React.ReactNode }) {
const { invalidate, set } = useThree()
const ref = React.useRef<HTMLDivElement>(null)
const [visible, setVisible] = React.useState(false)
React.useEffect(() => {
const el = ref.current!
const io = new IntersectionObserver(([e]) => {
setVisible(e.isIntersecting)
if (e.isIntersecting) invalidate() // 처음 보일 때 1프레임
})
io.observe(el)
return () => io.disconnect()
}, [invalidate])
React.useEffect(() => {
if (!visible) return
// 필요하다면 여기에 패턴1과 같은 60fps 루프를 붙여도 됨
}, [visible])
// R3F Canvas 바깥 래퍼
return <div ref={ref} style={{ width: 400, height: 300 }}>{children}</div>
}
구성: Canvas frameloop="demand" 안에 넣고, 보일 때만 invalidate() + 필요 시 패턴1 루프를 시작/종료.
언제 어떤 패턴을 사용해야할까?
- 패턴 1: “호버 중에만 애니메이션/회전/물리”가 돌아야 함. (60fps 제한도 같이)
- 패턴 2: “상태만 바뀜” (색/머티리얼/텍스트 등) → 이벤트에서 invalidate() 1회.
- 패턴 3: “보일 때만” 갱신 → IntersectionObserver + 필요 시 루프.
useFrame은?
frameloop="demand"에서는 useFrame 콜백이 있더라도 자동 렌더는 안 돕니다.
즉, useFrame에서 뭔가 업데이트했다면 내부에서 invalidate()를 호출해줘야 실제로 그려져요.
useFrame(({ invalidate }, delta) => {
// 업데이트
ref.current.rotation.y += 0.03
// 이 프레임을 그려라
invalidate()
})
단, 이러면 사실상 “매 프레임”이 되므로, 60fps로 제한하고 싶으면 패턴1처럼 rAF + interval 로 직접 제어하세요.
보너스: 입력 반응성/전력 절약 팁
- 짧은 상호작용(hover, click)만 있을 땐 패턴2로 충분 — GPU가 대부분 쉼.
- 긴 애니메이션은 패턴1로 명시적 60fps 캡 → 발열/소모 줄임.
- 가능하면 위치/회전 변경은 transform(행렬) 위주로, 머티리얼 변경은 최소화.
'Graphic > R3F' 카테고리의 다른 글
| 59 Load models(GLTF 모델 로딩과 애니메이션) (2) | 2025.08.18 |
|---|---|
| 58 R3F Environment and Staging (5) | 2025.08.16 |
| 57 R3F Debug (7) | 2025.08.15 |
| 56 Drei (2) | 2025.08.15 |
| 55 R3F Application 기초 (5) | 2025.08.15 |