본문 바로가기
Graphic/R3F

정적 렌더링을 통해 원하는 시기에만 frame 갱신하기 (frameloop="demand")

by curious week 2025. 8. 16.

 

패턴 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