setInterval과 싱글 스레드 그리고 시간 보정

setInterval과 싱글 스레드 그리고 시간 보정

Front-end developer WONISM
Interested in ReactJS, RxJS and ReasonML.

setInterval

setInterval을 사용하면, 일정한 주기로 반복적인 함수를 실행할 수 있다.
사용 방법은 setInterval(function callback () {}, interval.ms)과 같이 사용하며, 두번째 인자는 ms단위이다.
예를 들어 다음과 같은 코드는 1초에 한 번씩 Hello, Interval!이란 문자열을 출력한다.

const hello = setInterval(() => { console.log('Hello, Interval'); }, 1000);

setInterval을 실행함으로써 반환되는 값은 자연수(Number 타입)이며, 일종(一種)의 프로세스 아이디(pid)라고 봐도 무방하다.
이 값은 clearInterval(pid)로 인터벌을 중지하고자 할 때 사용된다.

setInterval과 싱글 스레드

자바스크립트는 싱글 스레드를 사용하기 때문에 이벤트 루프 기반의 동시성 지원을 한다.
이로 인해 setInterval은 (setTimeout도 마찬가지이다.) 약간의 지연이 발생한다.

다음 예제 코드를 실행하고, 그 결과를 보겠다.
(크롬 브라우저(65.0.3325.181 사용 중)에서는 내부적으로 보정이 들어가기 때문에 파이어폭스에서 실행을 한 결과를 첨부했다.)

const start = Date.now();

setInterval(() => {
  console.log(`${Date.now() - start}ms`);
}, 1000);

// 1001ms
// 2002ms
// 3003ms
// 4004ms
// 5005ms
// 6009ms
// 7009ms
// 8010ms
// 9013ms
// 10016ms

setInterval 보정하기

const setCorrectedInterval = ((callback, delay) => {
  let startTime;
  let count = 0;

  const tick = (cb, dl) => {
    if (!count) {
      startTime = Date.now();
      isStarted = true;
      count++;

      setTimeout(() => { tick(cb, dl); }, dl);
    } else {
      const delayed = (Date.now() - startTime);
      const correction = (delay * count) - delayed;

      callback();
      count++;

      setTimeout(() => { tick(cb, dl + correction); }, dl + correction);
    }
  }

  return tick(callback, delay);
});

const now = Date.now();
setCorrectedInterval(() => { console.log(Date.now() - now); }, 1000);

시간 보정을 위해 클로저 구문 안에 startTime, count 등의 지역 변수를 선언한다.

함수가 처음 시작되면 첫 번째 인자인 콜백 함수를 즉시 시작하는게 아니라, setTimeout에 내부 함수와 인터벌값을 인자로 전달한다.
두 번째 부터는 지연된 시간과 콜백 함수를 실행하고자 하는 시간의 갭을 구한 뒤, 내부 함수에서 전달받은 2번째 인자와 더하여 시간을 보정하며, setInterval을 호출하며 콜백함수를 인터벌로 반복하도록 한다.

setCorrectedInterval을 비우기 위한 clearCorrectedInterval

setInterval이나 setTimeout처럼 id값을 반환하도록하며, 조건문을 통해 clear되지 않은 인터벌들만 실행하도록 한다.
아이디들을 저장할 객체 intervalssetCorrectedInterval과 같은 스코프에 생성하며, 각 아이디들은 setTimeout의 아이디와 맵핑된다.

let intervalId = 0;
const intervals = {};

const setCorrectedInterval = ((callback, delay) => {
  let startTime;
  let count = 0;
  let id = intervalId++;

  const tick = (cb, dl) => {
    if (!count) {
      startTime = Date.now();
      isStarted = true;
      count++;

      intervals[id] = setTimeout(() => { tick(cb, dl); }, dl);
    } else {
      const delayed = (Date.now() - startTime);
      const correction = (delay * count) - delayed;

      callback();
      count++;

      if (intervals[id]) {
        intervals[id] = setTimeout(() => { tick(cb, dl + correction); }, dl + correction);
      }
    }
  }

  tick(callback, delay);

  return id;
});

const clearCorrectedInterval = (id) => {
  clearTimeout(intervals[id]);
  delete intervals[id];
};