본문 바로가기

Web/HTML, CSS, Javascript

[Javascript] requestAnimationFrame

requestAnimationFrame은(이하 raf) 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출하게 합니다.

이전에는 setInterval을 사용했지만, 프레임 유실이 있을 수 있고 모바일에서 배터리 절약이 안되는 문제가 있다고 합니다.

raf는 초당 60번을 수행하지만, 컴퓨터 상황 등에 따라 유동적이라고 합니다. 그만큼 최적화해서 애니메이션을 부드럽게 해줄 수 있는 기술이라 애니메이션에서 매우 자주쓰이고 canvas에 많이 활용된다고 합니다.

[사용방법]

우선, 아래 코드처럼 재귀형식으로 활용하면 됩니다.
pageYOffset은 스크롤된 값을 의미하는데, 스크롤 값만큼 left를 움직여주는 코드입니다.
하지만 아래 gif처럼 스크롤 정도에 따라 조금 딱딱한 느낌이 있고
무한대로 함수가 반복하게 되서 그만큼 부하가 걸릴수밖에 없습니다.

function loop(){
      circle.style.left = `${pageYOffset}px`;
      rafId = requestAnimationFrame(loop);
}
loop();

[부드러운 액션]

여기에서는 딱딱한 느낌을 없애고 스크롤이 완료되면 loop를 중지시켜보겠습니다.
우선 acc는 누적치인데 이 수치 조절에 따라 부드러운 정도의 차이를 조절할 수 있습니다.
delayedYOffset은 한번에 갈 스크롤위치(pageYOffset)까지 지연시켜 가는 위치를 나타냅니다.
위 2개는 아래에서 다시 설명하도록 하겠습니다.
rafId는 requestAnimationFrame의 id로, console을 찍어보면 숫자가 찍힙니다. 그를 바탕으로 raf를 중지시킬 수 있습니다.
rafState는 중단된 상태인지 아닌지를 나타내고, false이면 중단된 것입니다.
해당 부분은 아래 코드의 주석을 통해 확인 가능합니다.


delay 부분을 보면, 이전 yOffset부터 현재 yOffset 까지를 1/10씩 계속 쪼개나가는 것입니다. 그러다보면 결국 현재 yOffset까지 도달하게되는 일종의 수학에서의 극한(?) 개념이라고 보면 됩니다.
delayedYOffset = 0, pageYOffset = 100 이라고 했을 때, 최초의 경우부터 보도록 하겠습니다.
delayedYOffset = 0 + (100 - 0) * 0.1 = 10
=> 10+ (100 - 10) * 0.1 = 19
=> 19+ (100 - 19) * 0.1 = 27.1
....
=> 이렇게 하면 최종적으로 100까지는 아니더라도 그 근사값까지는 가겠죠??
그렇기 때문에 아래 코드에서 종료 조건이 == 0 이 아니고 < 1로 설정되어 있는 것입니다.

let acc = 0.1;
let delayedYOffset = 0;
let rafId;
let rafState;

window.addEventListener('scroll', ()=>{
    // scroll 할때, raf가 중지상태이면 raf 시작
    if(!rafState){
        rafId = requestAnimationFrame(loop);
        rafState = true;
    }
});
function loop(){
    // 부드러운 액션
    delayedYOffset = delayedYOffset + (pageYOffset - delayedYOffset) * acc;
    circle.style.left = `${delayedYOffset}px`;
    rafId = requestAnimationFrame(loop);

    // delayed가 yOffset과 같아질 때 raf 중지
    if(Math.abs(pageYOffset - delayedYOffset) < 1){
        cancelAnimationFrame(rafId);
        rafState = false;
    }
}
loop();

아래는 전체 예시 코드 입니다. (아래 링크의 유튜브 영상에서 설명을 너무 잘해주셨습니다 감사합니다 :)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <style>
      body{
        height:500vh;
      }
      .circle {
        position: fixed;
        margin-top:30vh;
        background: orange;
        width:50px;
        height:50px;
        left:0px;
        right:0;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <div class="circle"></div>
    <script>
      const circle = document.querySelector('.circle');
      let acc = 0.1;
      let delayedYOffset = 0;
      let rafId;
      let rafState;

      window.addEventListener('scroll', ()=>{
        if(!rafState){
          rafId = requestAnimationFrame(loop);
          rafState = true;
        }
      });
      function loop(){
        // 부드러운 액션
        delayedYOffset = delayedYOffset + (pageYOffset - delayedYOffset) * acc;
        circle.style.left = `${delayedYOffset}px`;
        // 딱딱한(?) 액션
        // circle.style.left = `${pageYOffset}px`;

        rafId = requestAnimationFrame(loop);
        console.log('loop');
        if(Math.abs(pageYOffset - delayedYOffset) < 1){
          console.log('done')
          cancelAnimationFrame(rafId);
          rafState = false;
        }
      }
      loop();
    </script>
  </body>
</html>

 

ref) mozilla DOC
1분코딩 requestAnimationFrame 사용법