λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
πŸ‘¨‍πŸ’» web.dev/fe

rAF ν”„λ ˆμž„ λΉ„μœ¨ μ‘°μ ˆν•˜κΈ°

by HandHand 2024. 1. 30.

 

 

πŸ“Œ requestAnimationFrame의 μ΄ˆλ‹Ή ν”„λ ˆμž„ μ‘°μ ˆν•˜κΈ°

requestAnimationFrame(μ΄ν•˜ rAF) 을 μ΄μš©ν•΄μ„œ JavaScript μ• λ‹ˆλ©”μ΄μ…˜μ„ κ΅¬ν˜„ν•˜λŠ”λ°, μ„±λŠ₯μƒμ˜ 이유둜 μ΄ˆλ‹Ή ν”„λ ˆμž„(FPS) 을 μ‘°μ ˆν•΄μ•Ό ν•˜λŠ” 상황이 μƒκ²ΌμŠ΅λ‹ˆλ‹€.

λ‹€λ₯Έ 타이머 ν•¨μˆ˜λ“€(setInterval, setTimeout)을 μ‚¬μš©ν•˜μ§€ μ•Šκ³  rAF μ—μ„œ μ΄ˆλ‹Ή ν”„λ ˆμž„μ„ μ‘°μ ˆν•˜λŠ” 방법을 κ΅¬ν˜„ν•΄ λ΄€μŠ΅λ‹ˆλ‹€.

 

animation tick

κ°„λ‹¨ν•˜κ²Œ FPS 에 λŒ€ν•΄μ„œ μ•Œμ•„λ³΄κ³  μ›ν•˜λŠ” FPS λ₯Ό μœ„ν•œ tick 을 κ΅¬ν•΄λ΄…μ‹œλ‹€.

 

const tick = 1000 / fps

 

1μ΄ˆλ‹Ή 60 ν”„λ ˆμž„μ„ μ›ν•œλ‹€λ©΄ 1000 / 60 = 16.6ms λ§ˆλ‹€ ν•˜λ‚˜μ˜ ν”„λ ˆμž„μ„ κ·Έλ €μ•Ό 함을 μ˜λ―Έν•©λ‹ˆλ‹€.

μ—¬κΈ°μ„œ λͺ©ν‘œν•œ ν”„λ ˆμž„ λΉ„μœ¨μ„ μœ„ν•΄μ„œ ν•˜λ‚˜μ˜ ν”„λ ˆμž„μ„ κ·Έλ¦¬λŠ”λ° ν•„μš”ν•œ μ‹œκ°„μ„ tick 이라고 ν•˜κ² μŠ΅λ‹ˆλ‹€.

이제 루프 μ‹€ν–‰ 뢀뢄을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.

60fps(16.6ms) 둜 μ‹€ν–‰λ˜λŠ” rAF λ£¨ν”„μ—μ„œ tick 에 λ„λ‹¬ν•œ κ²½μš°μ—λ§Œ drawFn 을 ν˜ΈμΆœν•©λ‹ˆλ‹€.

 

let startTime = Date.now() // βœ… 루프 μ‹œμž‘ μ‹œκ°„μ„ μ„€μ •ν•©λ‹ˆλ‹€.
loop()

function loop() {
  animationId = requestAnimationFrame(_loop)

  const now = Date.now()
  const elapsed = now - startTime

  if (elapsed > tick) {
    startTime = now - (elapsed % tick) // βœ… rAF의 λ°°μˆ˜κ°€ λ˜μ§€ μ•Šλ„λ‘ 값을 보정해쀀닀.
    drawFn()
  }
}

 

그리고 elapsed % tick 에 ν•΄λ‹Ήν•˜λŠ” 값을 λΉΌμ€€ κ°’μœΌλ‘œ ν˜„μž¬ μ‹œκ°„μ„ κ°±μ‹ ν•©λ‹ˆλ‹€.

이λ₯Ό 톡해 κΈ°μ‘΄ νƒ€μ΄λ¨Έμ˜ λ°°μˆ˜κ°€ λ˜μ§€ μ•Šλ„λ‘ 보정해 주게 λ©λ‹ˆλ‹€.

 

타이머 μ‹œμž‘, 쀑지, μ΄ˆκΈ°ν™”

루프λ₯Ό μ‹œμž‘, μ’…λ£Œν•  수 μžˆλŠ” ν•¨μˆ˜λ₯Ό ν¬ν•¨ν•˜λŠ” 객체λ₯Ό λ°˜ν™˜ν•˜λ„λ‘ λ³€κ²½ν•΄ λ΄…μ‹œλ‹€.

이λ₯Ό μœ„ν•œ ν•¨μˆ˜ 본체 κ΅¬ν˜„μ€ λ‚΄λΆ€ ν•¨μˆ˜λ‘œ μ œκ³΅ν•©λ‹ˆλ‹€.

 


function createAnimationLoop({
  drawFn,
  immediate,
}: {
  drawFn: () => void
  immediate?: boolean
}): AnimationLoop {
  const fps = 1  // βœ… 1μ΄ˆλ‹Ή 1ν”„λ ˆμž„μ„ κ·Έλ¦°λ‹€.
  const tick = 1000 / fps

  let animationId: number
  let startTime: number

  function _init() {
    startTime = Date.now()

    if (immediate) {
      drawFn()
    }
  }

  function _loop() {
    animationId = requestAnimationFrame(_loop)

    const now = Date.now()
    const elapsed = now - startTime

    if (elapsed > tick) {
      startTime = now - (elapsed % tick) // βœ… rAF의 λ°°μˆ˜κ°€ λ˜μ§€ μ•Šλ„λ‘ 값을 보정해쀀닀.
      drawFn()
    }
  }

  return {
    start: function _start() {
      _init()
      _loop()
    },
    stop: function _stop() {
      if (animationId !== -1) {
        cancelAnimationFrame(animationId)
        animationId = -1
      }
    },
  }
}

 

loop μ‹œμž‘ μ‹œ μ΄ˆκΈ°μ— ν•œλ²ˆ μ‹€ν–‰ν•  수 μžˆλ„λ‘ immediate μ˜΅μ…˜μ„ μΆ”κ°€ν•΄ μ€¬μŠ΅λ‹ˆλ‹€.

 

μ‚¬μš© μ˜ˆμ‹œ

React 와 ν•¨κ»˜ μ‚¬μš©ν•œλ‹€λ©΄ λ‹€μŒκ³Ό 같이 μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

function useAnimation() {
  const loop = useRef()

    useEffect(() => {
    loop.current = createAnimationLoop(animate)

    if (/** 쑰건에 λ„λ‹¬ν•œλ‹€λ©΄ */) {
      loop.current.start()
    } else {
      loop.current.stop()
    }
  }, [])
}

 

πŸ“Œ 참고자료

Controlling fps with requestAnimationFrame?

λ°˜μ‘ν˜•

πŸ’¬ λŒ“κΈ€