setTimeout()과 requestAnimationFrame()

woolee의 기록보관소·2022년 12월 8일
1

FE개념정리

목록 보기
25/35

setTimeout()

setTimeout() 메서드는 타이머를 설정한다.
지정한 시간 이후에 함수나 특정 코드가 실행된다.

Web API이다.

setTimeout()은 한번만 실행된다. 여러번 사용하려면 setInterval()이 권장된다.

setTimeout(code, delay)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay(milliseconds), param1, param2, /* … ,*/ paramN)

보안에 취약하기에 code는 사용하지 않고 function을 사용한다. 문자 리터럴을 첫번째 인자로 작성하면 eval()과 같은 문제를 일으킨다고 한다.
문자 리터럴을 사용하고 싶다면 함수로 감싸면 된다.

// Don't do this
setTimeout("console.log('Hello World!');", 500);

// Do this instead
setTimeout(() => {
  console.log('Hello World!');
}, 500);

뒤에 붙는 delay나 param들은 생략이 가능하다.

param은 함수에 전달할 추가 매개변수들이다. 여러 개를 넣을 수 있다.

delay는 주어진 함수를 실행하기 전에 기다릴 시간(milliseconds 단위)을 의미한다. 생략하거나 0을 지정할 경우 함수는 즉시 실행된다(정확히는 다음 이벤트 사이클에 실행된다).

setTimeout()은 timeoutID라는 양의 정수를 반환한다. 이 ID 반환값은 타이머를 식별할 때 사용되며, 이 값을 clearTimeout()에 전달해 타이머를 취소할 수 있다. (clearTimeout()으로 주기적으로 타이머를 청소해줘야 메모리 누수를 막을 수 있다.)

setTimeout()은 비동기 메서드이다. 함수 스택의 다른 함수 호출을 막지 않는다. 즉, delay 값으로 5초를 준다고 해서 5초 간 다음 함수 호출이 정지되지 않는다. 그 시간 안에 다른 함수를 호출할 수 있다.

setTimeout(() => {console.log("this is the first message")}, 5000);
  setTimeout(() => {console.log("this is the second message")}, 3000);
  setTimeout(() => {console.log("this is the third message")}, 1000);

  // Output:

  // this is the third message
  // this is the second message
  // this is the first message

함수의 실행이 완료된 후에 다른 함수를 호출하는 구조(즉, 동기적인 구조)를 갖추려면 promise 같은 구조를 사용해야 한다. setTimeout()은 비동기 메서드이기 때문이다.

setTimeout()에서의 "this" problem

setTimeout()에 의해 실행되는 코드는 setTimeout()을 호출했던 함수와는 다른 실행 컨텍스트(execution context)를 갖는다.

호출 시에 this를 따로 지정하거나 bind로 바인딩하지 않는 한,
this는 window 객체를 가리키게 된다. Array 메서드와 달리, call을 사용해 this를 설정할 수도 없다.

해결 방법은

  1. 다른 함수(화살표 함수도 가능하다)로 감싸거나
setTimeout(function(){myArray.myMethod()}, 2.0*1000);
setTimeout(() => {myArray.myMethod()}, 2.0*1000);
  1. bind()를 사용하면 된다.
const myArray = ['zero', 'one', 'two'];
const myBoundMethod = (function (sProperty) {
    console.log(arguments.length > 0 ? this[sProperty] : this);
}).bind(myArray);

myBoundMethod(); // prints "zero,one,two" because 'this' is bound to myArray in the function
myBoundMethod(1); // prints "one"
setTimeout(myBoundMethod, 1.0*1000); // still prints "zero,one,two" after 1 second because of the binding
setTimeout(myBoundMethod, 1.5*1000, "1"); // prints "one" after 1.5 seconds

setTimeout()의 delay 시간은 의도했던 것보다 더 길 수도 있다.

Reasons for delays longer than specified
⇒ 브라우저는 setTimeout 호출을 5번 이상 중첩할 경우 4ms의 최소 타임아웃을 강제한다.

⇒ 브라우저는 백그라운드 탭으로 인한 부하를 줄이기 위해, 비활성 탭에서의 최소 딜레이에 최소 값을 강제한다.

⇒ 파이어폭스 같은 브라우저는 추적 스크립트로 인식한 스크립트에 대해 추가적인 Throttling을 적용한다. 전역 탭에서 Throttling의 최소 딜레이가 여전히 4ms라 할지라도, 백그라운드 탭에서는 페이지 첫 로드 후 30초가 지나면 추가적인 Throttling을 적용한다.
그리고 파이어폭스는 현재 탭이 로딩 중이면 setTimeout() 타이머 실행을 지연시킨다. 실제 실행은 메인 스레드가 대기 상태에 들어가기 전까지 혹은 load 이벤트가 발생하기 전까지 미뤄진다.

⇒ 페이지, 운영체제, 브라우저가 다른 작업으로 바쁘다면 timeout이 예상보다 늦게 실행될 수 있다. setTimeout()을 호출한 스레드가 종료되기 전까지는 setTimeout()에서 지정한 함수(혹은 코드)를 실행할 수 없다.

setTimeout()에서의 delay는 정확히 그 delay 시간 후에 콜백함수가 실행되는 걸 보장하는 게 아니다. delay 시간 후에 콜백 큐에 들어가는 것을 보장하는 것이다.

requestAnimationFrame

자바스크립트에는 사용할 수 있는 여러가지 타이머 function이 존재한다.

  • setTimeout() : 지정한 delay 시간 후에 함수를 호출하거나 식을 평가한다.
  • setInterval() : 지정된 간격으로 함수를 호출하거나 식을 평가한다. setInterval()은 clearInterval()이 호출 될 때까지 함수를 계속 호출한다.
  • requestAnimationFrame() : setTimeout처럼 동작하지만 mozilla에 의해 개선된 function이다.

requestAnimationFrame은 콜백함수를 언제 실행하냐면, 브라우저가 다음 repaint task를 시작할 준비가 되었을 때 콜백함수를 실행한다. setTimeout()이 가진 성능 문제를 해결할 수 있다.

window.requestAnimationFrame(callback);

콜백함수로는 다음 repaint를 위한 애니메이션을 업데이트할 때 호출할 함수를 받는다.

모든 자바스크립트 타이머들은, 이들이 생성될 때 timestamp를 반환한다. 이 timestamp는 list of active timeouts에서 콜백함수를 찾기 위한 key이다.

예를 들어 setTimeout()을 생성하는 순간, Execute this in 100 milliseconds 같은 메모를 작성해서 list of active timeouts에 콜백함수를 저장하고 timestamp를 반환한다.

자바스크립트는 기본적으로 단일 스레드 기반 언어이다. 즉 한번에 하나의 작업만 수행할 수 있으므로 바쁘다면 콜백함수 실행이 설정한 delay 시간보다 더 늦게 실행될 수 있는 것이다.

내 웹사이트를 빠르게 하려면 FPS(Frame Per Second) 개념을 알아야 한다.
FPS가 웹 개념은 아니지만 애니메이션, 영화 등 사용자에게 지속적으로 무언가를 보여줘야 하는 상황에서 초당 가장 이상적인 프레임 수는 60이라고 한다. 그래야 사용자는 느리다고 느끼지 않는 것이다.

⇒ 애니메이션의 매끄러움은 애니메이션의 프레임 속도에 의해 결정된다. 프레임 속도는 초당 프레임수(FPS)로 측정한다. 영화는 보통 24fps, 비디오는 30fps로 실행된다. 이 숫자가 높을 수록 애니메이션이 부드러워지지만 너무 많은 프레임은 오히려 너무 많은 처리로 인해 끊기거나 건너뛰어질 수도 있다(frame drop). 대부분의 screens은 60Hz의 refresh rate을 가지고 있으므로 60FPS가 가장 이상적인 프레임 수라고 여겨지는 것이다.

60FPS는 초당 60프레임을 의미한다.
그리고 60FPS ⇒ 초당 60프레임 ⇒ 프레임당 16ms 라는 결과를 도출할 수 있다.

즉, 프레임에 가장 적합한 시간은 16ms인데, setTimeout은 이 조건을 만족할 수 없다.
1. setTimeout은 브라우저에서 일어나는 일을 고려하지 않는다.
페이지는 페이지가 탭 뒤에 숨겨져 필요하지 않을 때도 CPU를 독차지하고 있을 수 있다. 또는 애니메이션 자체가 페이지 밖으로 스크롤되어 업데이트 호출이 필요하지 않은 상황일 수도 있다.
2. setTimeout은 원할 때 화면을 업데이트한다. 열악한 브라우저의 경우, repaint하는 동안 setTimeout으로 인해 또 repaint하는 경우가 생길 수 있다. CPU 사용량 등이 낭비될 수 있다.

여기에서 requestAnimationFrame과 setTimeout을 비교해볼 수 있다. 두 함수 간, 실제 callbacks과 기대 callbacks 간 차이 등을 비교해볼 수 있다.

requestAnimationFrame이 프레임당 16ms를 보장할 수 있는 이유

반면 requestAnimationFrame은 setTimeout과 달리 미리 지정된 간격에 애니메이션을 다시 그리는 게 아니라 브라우저가 다음에 repaint 할 때 예약한다. 미리 지정된 간격을 지키기 위해 막무가내로 동작하지 않고, 브라우저의 상황에 맞게 동작을 예약할 수 있다.

그리고 이 예약 동작을 통해 브라우저는 load, element visibility (being scrolled out of view) and battery status 등을 고려해 성능을 최적화할 수 있는 기회를 제공한다.

requestAnimationFrame은

  • 브라우저에서 애니메이션을 최적화할 수 있게 해준다.
  • 비활성 탭의 애니메이션이 중지되므로 CPU를 효율적으로 쓸 수 있게 된다.
  • 배터리 친화적이다.

requestAnimationFrame과 자바스크립트 실행 최적화

자바스크립트는 시각적 변화가 종종 발생한다. 애니메이션 구현을 위해 시각적 변화를 의도하거나 데이터 변동에 의해 시각적 변화가 강제되기도 한다.

Optimize JavaScript execution에서는 자바스크립트 실행 최적화를 위해 아래와 같이 권장한다.

  1. 시각적 업데이트를 위해 setTimeout 또는 setInterval을 사용하지 말 것. 대신 항상 requestAnimationFrame을 사용할 것.
  2. 오래 실행되는 JavaScript를 기본 스레드에서 Web Workers로 이동할 것.
  3. 마이크로 작업을 사용하여 여러 프레임에 걸쳐 DOM을 변경할 것.
  4. Chrome DevTools의 타임라인 및 JavaScript 프로파일러를 사용하여 JavaScript의 영향을 평가할 것.

참고

setTimeout() - Web APIs - MDN Web Docs
Window setTimeout()

Better Understanding of Timers in JavaScript: SetTimeout vs RequestAnimationFrame

requestAnimationFrame API

window.requestAnimationFrame() - Web API | MDN
requestAnimationFrame
Optimize JavaScript execution

profile
https://medium.com/@wooleejaan

1개의 댓글

comment-user-thumbnail
2023년 11월 10일

감사히 읽었습니다.

답글 달기