setTimeout & setInterval

y0ung·2020년 12월 16일
0

"호출 스케쥴링 하기(scheduling a call)"
함수를 당장 실행하지 않고 정확히 몇 초의 딜레이 후에 실행시키고 싶을때 사용하는것.

이것을 구현하기 위해 두가지 메소드가 존재한다

  • setTimeout : 일정한 시간 간격 이후에 함수가 한번 실행된다.
  • setInterval : 일정 시간 간격으로 함수가 주기적으로 실행된다.

setTimeout

문법

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...);

파라미터

func | code
실행을 위한 함수나 문자열이다. 주로 함수를 받는다. 문자열보다는 함수를 권장

delay
실행하기 전의 딜레이. ms단위로 이루어져 있다.
( 1000ms = 1s)

arg1,arg2...
함수에 대한 인자들이다.


function sayHi(phrase, who) {
  alert(`${phrase}, ${who}`);  
}

setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John

함수로 넘겨 줄때 실행하지 않는다!

// Wrong!!!!
setTimeout(sayHi(), 1000);

clearTimeout으로 취소하기

setTimeout을 호출했을 때, 반환 값으로 실행을 취소하기 위해 사용할수 있는 "timer identifier"인 timerId를 준다.

let timerId = setTimeout(...);
alert(timerId); // timer identifier

clearTimeout(timerId);
alert(timerId); // same identifier

취소를 했더라도 다시 호출 했을때 null 이 되진 않는다.

setInterval

문법

let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...);

파라미터

func | code
실행을 위한 함수나 문자열이다. 주로 함수를 받는다. 문자열보다는 함수를 권장

delay
실행하기 전의 딜레이. ms단위로 이루어져 있다.
( 1000ms = 1s)

arg1,arg2...
함수에 대한 인자들이다.


setTimeout과는 다르게 함수를 한번 실행하는 것이 아닌 부여된 시간 간격 이후로 주기적으로 실행된다.

호출을 중지하고 싶다면 clearInterval(timerId)를 호출하면 된다.

// 2초마다 반복
let timerId = setInterval(() => alert('tick'), 2000);

// 5초 후에 정지
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);

재귀적인 setTimeout

무언가를 정기적으로 실행시키기 위해서는 두가지 방법이 있는데

한가지가 setInterval 이고, 나머지가 재귀적인 setTimeout이다.

let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);

위의 setTimeout은 현재 실행중인 것이 끝날 때((*)) 다음 호출을 바로 스케쥴한다.

재귀적은 setTimeoutsetInterval보다 더욱 유연하다. 이방법은 때에 따라 다르게 스케쥴 될수 있다.

이를테면 서버에 5초마다 데이터를 물어보는 요청을 보내는 서비스를 작성할 필요가 있는데, 서버에 요청이 너무 많을 때는 계속해서 요청을 보내기 보단느 주기를 10초, 20초, 40초 정도로 늘리는 것이 바람직 하다.

let delay = 5000;

let timerId = setTimeout(function request() {
  ...요청 전송...
  
  if(서버 과부하 때문에 요청이 실패한다면...) {
    // 다음 실행까지 인터벌을 좀 늘리자..
    delay *= 2;
  }
  
  timerId = setTimeout(request, delay);
  
}, delay);

재귀적인 setTimeoutsetInterval이 보장하지 못하는 실행간 딜레이를 보장할 수 있다.

밑의 두개의 코드를 비교해보면서 이해를 해보자. 첫번째 예제는 setInterval 사용한것이다.

let i = 1;

setInterval(function () {
  func(i);
}, 100);

setTimeout을 사용한 예제

let i = 1;
setTimeout(function run() {
  func(i);
  setTimeout(run, 100);
}, 100);

setInterval에서는 내부적인 스케쥴러가 func(i)를 매 100ms마다 실행할 것입니다.

func 호출 사이의 진짜 딜레이는 코드에 기재된 것 보다 적다.

왜냐하면 func의 실행에 의해 소비되는 일부의 interval때문이다.

func가 예상한것보다 더 길게 실행되어 100ms의 시간보다 더 걸리는 것도 가능하다. 이 경우에는 엔진은 func의 실행 완료까지 기다리고 스케쥴러를 체크하고 시간이 됐다면, 다시 즉시 실행할 것이다. 만일 함수가 항상 delayms 보다 더 길게 실행된다면, 잠깐의 정지도 하지 않고 즉시 실행될 것이다.

아래는 재귀적인 setTimeout의 그림이다.

재귀적인 setTimeout고정된 딜레이를 보장한다.

새로운 호출이 이전 호출의 끝에 계획되기 때문이다.

Garbage Collection

함수가 setInterval 혹은 setTimeout에 넘겨졌을 때, 그것을 가리키는 내부적인 레퍼런스가 만들어지고 스케쥴러에 저장된다. 만일, 함수에 별다른 참조가 없더라도 함수가 garbage collect되는 것을 막아준다.

// 스케쥴러 호출 시까지, 함수는 메모리에 머무릅니다. 
setTimeout(function() {...}, 100);

setInterval의 경우에는, clearInterval이 호출될 때까지 함수는 메모리에 머문다.
side-effect도 있다. 한 함수가 lexical 환경 바깥을 참조한다. 그래서, 이 함수가 살아있는 동안, 바깥의 변수들도 마찬가지로 살아있다. 변수들은 아마 함수 자체보다 더 많은 메모리를 소비할 것이다. 그래서 스케쥴된 함수가 더이상 필요하지 않을 때는 아주 작은 함수라 할지라도, cancel 시켜주는 것이 더 좋다.

setTimeout(..., 0)

setTimeout(func, 0) 또는 setTimeout(func) 이러한 특별한 용례가 있다. 앞의 코드는 func의 실행을 가능한 빠르게 스케쥴 한다. 하지만 스큐줄러는 현재의 코드가 끝난 뒤에 호출할 것이다.

setTimeout(() => alert("World"));

alert("Hello");

위의 코드에서 setTimeou은 0초 후에 함수를 호출하라는 명령을 넣었다. 하지만 스케쥴러는 현재의 코드가 끝난뒤에 확인한다. 그래서 Hello가 첫번째고 World가 뒤따라 나온다.

CPU 소비가 많은 작업을 쪼개기(Splitting)

setTimeout을 이용해 CPU(중앙처리장치) 사용량이 많은 작업들을 나누는 트릭이 있다.

예를들면 syntax-highlighting을 하는 스크립트는 CPU를 꽤 많이 잡아 먹는다. 코드에 하이라이팅을 하기 위해, 엔진은 코드를 분석하고 많은 엘리먼트들을 만들어내고 document에 추가한다. 큰 텍스트의 경우에는 많은 시간이 소요되고, 브라우저를 잠시 먹통이 되게 만들기도 한다.

그래서 setTimeout(..., 0)을 이용해 긴 텍스트를 조각조각 나눌 수 있다.

명확한 이해를 위해 다음 예제를 한번 살펴보자. 이 예제는 1~ 1000000000까지 숫자를 세는 함수를 가지고 있다. 이 함수를 실행한다면, CPU는 잠시 멈출 것이다. 서버사이드에서 이용하는 자바스크립트의 경우, 이런 일은 상당히 크게 느껴진다. 그리고 만일 브라우저에서 실행시키고 다른 버튼을 클릭하려 할때, 모든 자바스크립트가 일시정지하는 현상을 볼 수 있을 것이다. 작업이 끝날 때까지 어떠한 액션도 작동하지 않을 것이다.

let i = 0;

let start = Date.now();

function count() {
  for(let j = 0; j < 1e9; j++){
    i++
  }
  
  alert(`Done in ${Date.now() - start}ms`)
}

count()

브라우저는 '스크립트가 너무 길다', 라는 경고 메시지를 보여줄 수 있지만 숫자가 크지 않으므로 그런결과는 나오지 않을 것이다.

이제부터 setTimeout을 이용하여 작업을 쪼개 보자!

let i = 0;
let start = Date.now(); 

function count() {
  do { // 무거운 작업 (*)
    i++;
  } while (i % 1e6 != 0);
  
  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count); // 호출을 스케쥴링 (**) 
  }
}

count();

"counting" 작업을 하는 동안, 브라우저 UI가 완전히 동작한다.

  1. 첫번째 실행에서: i=1...1000000
  2. 두번째 실행에서: i=1000001..2000000.
  3. 그리고 계속... while에서 i1000000에 의해 나눠지는지 검사.

그 후에 아직 작업이 완료되지 않았다면, 다음 호출은 (**) 에서 스케쥴 된다.

count실행중 일시정지는 다른 사용자의 액션에 반응할 시간을 준다.

브라우저가 렌더링 하도록 허락하기

브라우저 내부에서 실행되는 스크립트의 또다른 이점은 유저에게 프로그래스바와 같은 것을 보유줄 수 있다는 것이다. 브라우저는 주로 스크립트가 완료된 이후에 모든 "repainting"작업을 하기 때문이다.

<div id="progress"></div>

<script>
let i = 0;

function count() {
  for (let j = 0; j < 1e6; j++) {
    i++;
    // 현재의 i 값을 progress div에 넣는다.
    progress.innerHTML = i;
  }
}

count();
</script>

실행하게 되면, 모든 카운팅 작업이 끝난 뒤에 i의 변화가 반영된다.

위의 작업을 조각조각 나누기 위해 setTimeout을 사용한다면, 변화가 각 작업 도중에 반영될 것이다.

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // do a piece of the heavy job (*)
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e9) {
      setTimeout(count);
    }

  }

  count();
</script>

1e3 = 1000?=10^3 True.

요약

  • setInterval(func, delay, ...args)setTimeout(func, delay, ...args) 2개의 메소드는 funcdelayms 이후에 주기적으로 혹은 한번 실행하도록 허용.

  • 실행을 취소하기 위해, setInterval 또는 setTimeout에서 반환되는 값을 이용해 clearInterval 또는 clearTimeout을 호출.

  • 중첩된 setTimeout 호출setInterval을 이용하는 것보다 더욱 유연합니다. 그리고 각 실행 사이에 최소한의 딜레이를 보장.

  • 타임아웃이 0인 스케쥴링 setTimeout(..., 0)는 "현재 코드가 끝난 이후에 바로 호출"을 스케쥴링하고 싶을 때 사용.

setTimeout(..., 0)의 유즈케이스는 다음과 같다.

  • CPU가 많이 소모되는 작업들을 조각조각 나누기 위해, 스크립트는 더이상 그 작업에 매달려있지(hang) 않을 것이다.
  • 프로세스가 진행되는 도중에 브라우저가 다른 것을 할 수 있도록 만들기 위해. (이를테면 progress bar의 진행)

모든 스케쥴링 메소드는 정확한 딜레이를 보장하지 못하는 것을 명시ㅏㅁ해라. 스케쥴된 코드에서 그것에 의존하지 않는 편이 좋다.

브라우저 내부의 타이머는 많은 이유에 의해 지연될 수 있다.

  • CPU가 오버로드 됨
  • 브라우저 탭이 백그라운드 모드에 있음
  • 랩탑이 배터리를 사용 중임

모든 것이 최소한의 타이머 딜레이를 300ms 심하면 1000ms까지 증가시킬 수 있다. 어떤 브라우저를 쓰는지와 셋팅에 따라 달라진다.

연습문제

  • 매 초마다 증가하는 숫자를 출력하는 printNumbers(from, to) 함수를 작성해보세요.
- setInterval() 메소드를 이용하여 작성해본 뒤
- setTimeout() 메소드를 이용하여 재귀적으로 작성해보세요.

내 풀이

function printNumbers_setIn(from,to){
  let tiemId = setInterval(()=> {
    if(from >= to) clearInterval(tiemId);
    console.log(from);
    from++
  },1000)
}

printNumbers_setIn(1,2)
function printNumbers_setTime(from,to){
  setTimeout(
    function run(from,to){
      console.log(from,to);
      if(from < to) setTimeout(run, 1000, ++from, to)
    }, 1000,0,5);
}

printNumbers_setTime()

참고

profile
어제보다는 오늘 더 나은

0개의 댓글