본문 바로가기

프로젝트/뉴스스탠드

[기능 구현] requestAnimationFrame을 활용한 애니메이션

목표

- 뉴스스탠드 프로젝트에서 화면 상단의 뉴스 헤드라인이 5초마다 무한으로 롤링되는 애니메이션의 구현이 필요했다.

- 즉, A, B, C, D, E라는 뉴스 헤드라인이 있을 때, 사용자에게 보여지는 헤드라인은 1개이며 처음에는 A가 보이고 5초가 있으면 B가 보이며 E 다음에는 다시 A가 보여야 한다.

- 무한으로 E 다음에 다시 A가 나타나는 로직 또한 고민이 필요한 부분이었지만, 이번 글에서는 5초마다 다음 뉴스의 헤드라인이 애니메이션과 함께 나타나는 기능에 대한 내용을 다뤄보려고 한다.

문제정의

- JavaScript를 공부하면 일정 시간마다 반복되는 기능을 사용할 때 가장 먼저 setInterval() 메서드를 생각할 수 있고 나 역시 setInterval() 메서드를 사용하여 5초마다 헤드라인이 변경되는 애니메이션을 구현했다.

- 하지만, 리뷰를 통해 setInterval() 메서드 대신 requestAnimaionFrame 함수의 사용과 setInterval() 메서드를 사용했을 때의 문제점 학습에 대한 피드백을 받았다.

 

먼저 requestAnimationFrame이란 무엇인가?

- requestAnimationFrame은 브라우저에서 제공하는 메서드로, 시스템이 프레임을 그릴 준비가 되면 애니메이션 프레임을 호출하여 애니메이션 웹페이지를 보다 원활하고 효율적으로 생성할 수 있도록 해준다.

- 즉, 실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에 자바스크립트가 프레임 시작 시 실행되도록 보장해준다.

- 프레임이란, 애니메이션을 구성하는 빠르게 연속된 이미지를 말하는데, 보통 눈의 잔상을 이용해서 표시하기 때문에 부드러운 애니메이션 표시를 위해 브라우저는 초당 60회의 프레임을 표시한다고 한다.

- 즉, 애니메이션을 구현하는 것은 프레임을 연속적으로 생성하는 것이라고 할 수 있는데, requestAnimationFrame은 브라우저에게 다음 프레임을 그리기 전에 애니메이션 관련 작업을 수행할 수 있도록 하여 브라우저가 화면을 갱신하기 전에 콜백 함수를 실행하여 애니메이션을 준비하는 데 사용된다.

- 즉, requestAnimationFrame을 사용하면 애니메이션 프레임이 그려지는 주기에 따라 적절한 타이밍에 코드를 실행할 수 있다.

- 또한, requestAnimationFrame은 setInterval()과 달리 브라우저의 다른 탭 화면을 보거나 브라우저가 최소화되어 있을 때에 일시 중지됨으로 CPU 리소스나 배터리 수명을 낭비하지 않을 수 있다고 한다.

setInterval() 메서드를 사용했을 때의 문제점은 다음과 같다.

- 먼저 requestAnimationFrame과 달리 setInterval() 혹은 setTimeout() 메서드는 주어진 시간내에 동작할 뿐 프레임을 신경쓰지 않고 동작한다.

- 만약 브라우저가 다른 작업 수행으로 인해 지연되어 프레임의 중간에 실행되는 경우에는 Layout(HTML 요소의 위치, 크기 등의 레이아웃 정보를 계산하는 단계)과 Paint(레이아웃 단계에서 계산된 요소들을 실제 화면에 픽셀 단위로 그리는 단계입니다) 과정이 다시 발생하기 때문에 해당 프레임이 누락되어 애니메이션이 부드럽게 진행되지 않고 끊김 현상이 발생할 수 있다.

- 즉, 브라우저는 약 16ms 간격으로 프레임 단위가 진행되는데, 프레임이 진행되는 중간에 setInterval()의 콜백이 실행되는 경우에 해당 프레임은 생성되지 못하고 누락될 수가 있다는 의미이다.

requestAnimationFrame 사용법 및 구현코드

- requestAnimationFrame 사용법은 setTimeout() 혹은 setInterval()처럼 콜백 함수 내부에서 재귀 호출하는 식으로 구성하면 된다.

- setTimeout() 혹은 setInterval()과의 차이점은 별도의 타이머를 등록할 필요는 없다는 것이다.

 

- requestAnimationFrame 기본 사용법

// 출처 : MDN(https://developer.mozilla.org/ko/docs/Web/API/window/requestAnimationFrame)

// DOM 요소를 가져와 element 변수에 할당한다.
const element = document.getElementById("some-element-you-want-to-animate");

// 애니메이션 시작 시간과 이전 타임스탬프를 초기화합니다.
let start, previousTimeStamp;

// 애니메이션이 완료되었는지를 나타내는 변수를 선언하고 초기화합니다.
let done = false;

// 애니메이션 스텝을 정의하는 함수를 선언합니다.
function step(timestamp) {
  // 시작 시간이 정의되지 않았으면 현재 타임스탬프를 시작 시간으로 설정합니다.
  if (start === undefined) {
    start = timestamp;
  }

  // 애니메이션 시작으로부터 경과된 시간을 계산합니다.
  const elapsed = timestamp - start;

  // 이전 타임스탬프와 현재 타임스탬프가 다른 경우에만 실행합니다.
  if (previousTimeStamp !== timestamp) {
    // 요소가 정확히 200px까지 이동하는지 확인하기 위해 카운터를 계산합니다.
    const count = Math.min(0.1 * elapsed, 200);

    // 요소의 위치를 업데이트합니다.
    element.style.transform = `translateX(${count}px)`;

    // 요소가 200px에 도달하면 애니메이션이 완료되었음을 표시합니다.
    if (count === 200) done = true;
  }

  // 경과된 시간이 2초 미만인 경우에만 애니메이션을 계속 실행합니다.
  if (elapsed < 2000) {
    // 이전 타임스탬프를 현재 타임스탬프로 업데이트합니다.
    previousTimeStamp = timestamp;

    // 애니메이션이 완료되지 않았다면 다음 프레임을 요청합니다.
    if (!done) {
      window.requestAnimationFrame(step);
    }
  }
}

// 애니메이션 시작을 요청합니다.
window.requestAnimationFrame(step);

 

- 실제 프로젝트에서 구현한 코드를 간략하게 표현하면 다음과 같다.

  // class로 구현되어 class 메서드 형태
  setHeadlineAnimation() {
    // this.setHeadlineSection() 메서드를 호출하여 헤드라인 섹션 DOM 요소를 headlineSection에 할당
    const headlineSection = this.setHeadlineSection();
    // requestAnimationFrame의 콜백으로 사용될 함수
    // requestAnimationFrame의 콜백 함수는 timestamp를 인자로 받는다.
    // timestamp는 requestAnimationFrame이 호출된 시점의 시간을 나타내며,
    // requestAnimationFrame이 호출될 때마다 새로운 값이 할당된다.
    const animateHeadline = (timestamp) => {
      // 만약 this._animationStartTime이 falsy하면 timestamp를 할당한다.
      if (!this._animationStartTime) this._animationStartTime = timestamp;
      // timestamp와 this._animationStartTime의 차이를 구하여 elapsedTime(경과시간)에 할당한다.
      const elapsedTime = timestamp - this._animationStartTime;

      // 만약 경과시간이 3000ms가 넘어가면 헤드라인을 위로 올리는 애니메이션을 실행하고
      // _animationStartTime을 null로 초기화한다.
      if (elapsedTime >= 3000) {
        // 애니메이션을 실행하는 메서드
        this.headlineRollingHandler(headlineSection);
        this._animationStartTime = null;
      }

      // 재귀적으로 requestAnimationFrame을 호출하여 애니메이션을 반복한다.
      requestAnimationFrame(animateHeadline);
    };

    requestAnimationFrame(animateHeadline);
  }

 

완성된 모습

배운점

- setInterval()을 사용하여 애니메이션을 구현할 수 있지만, 프레임이 밀릴 수 있는 부작용이 있어서 원활한 사용자 경험을 제공하는 데 고민했고, 이러한 고민 끝에 requestAnimationFrame을 활용하여 부드러운 애니메이션 효과를 구현할 수 있었고, 사용자가 더 나은 경험을 얻을 수 있게 되어 기뻤다.

- 앞으로도 단순히 화면에 보여지는 것이 전부가 아닌 사용자 경험 향상에 대해서 항상 고민할 줄 아는 개발자가 되어야겠다는 생각을 했다.


참고 : https://developer.mozilla.org/ko/docs/Web/API/window/requestAnimationFrame

https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C

https://velog.io/@younghwanjoe/requestAnimationFrame%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EC%83%81

https://www.jeong-min.com/36-RAF/

'프로젝트 > 뉴스스탠드' 카테고리의 다른 글

데이터 크롤링 및 json-server 활용  (2) 2024.01.25
Sass/Scss  (0) 2024.01.25
옵저버 패턴  (0) 2024.01.23