스케쥴링: setTimeout 과 setInterval

fromzoo·2020년 12월 13일
0

setTimeout
일정 시간 간격 이후에 함수가 한번 실행

setInterval
일정 시간 간격으로 함수가 주기적으로 실행

자바스크립트의 스펙 일부가 아니다. 하지만 대부분 환경은 내부적인 스케쥴러를 갖고있다.

구체적으로 Node.js와 모든 브라우저에 제공된다.

setTimeout

  • 문법
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...);
  • 파라미터
    func|code

실행을 위한 함수나 문자열. 주로 함수를 받는다.

코드의 문자열도 넘겨질 수 있지만 권장되지는 않는다.

delay

실행하기 전의 딜레이. ms단위로 이루어져 있음. 1000ms = 1s, 디폴트 값은 0

arg1, arg2 ...
함수에 대한 인자들이다. (IE9미만 버전에는 지원되지 않는다)

예를들어 1초후에 sayHi()를 호출하는 코드를 작성해보자.

function sayHi() {
  alert('Hello');  
}

setTimeout(sayHi, 1000);

인자를 줘봅니다.

function sayHi(phrage,who) {
	alert(phrage + ',' + who)

}

setTimeout(sayHi,1000,'hello','John') // hello John

만일 첫번째 인자에 문자열이 들어온다면, 자바스크립트는 그로부터 함수를 만들어낸다.

무슨말이냐면

setTimeout('alert('Hello'))',1000)

하지만 문자열을 사용하는 것은 권장되지 않는다.
문자열 대신 함수를 사용하자.

setTimeout(() => alert('Hello'), 1000);

함수를 넘겨야하지만, 실행하면 안된다.

setTimeout(sayHi(), 1000);

위 코드는 동작하지 않는다. 왜냐면 setTimeout은 함수로의 참조를 받아올 것이라 예상했기 때문이다. 여기 sayHi()는 함수를 실행시킨다. 그리고 실행결과가 setTimeout으로 전달된다. 우리가 코딩했던 것들을 살펴봤을때, sayHi()의 결과는 undefined 아무것도 리턴하지 않는 함수이다. 그러므로 아무것도 스케쥴되지 않는다.

clearTimeout으로 취소하기

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

취소하기 위한 문법은 다음과 같다.

let timerId = setTimeout(...);
clearTimeout(timerId);

아래 코드에서 함수를 스케쥴링하고 그 후에 취소한다. 결과로 아무것도 일어나지 않는다.

let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier

clearTimeout(timerId);
alert(timerId); // same identifier (취소 이후에도 null이 되진 않습니다.)

우리가 alert 출력을 통해 알 수 있듯이, 브라우저 내부에서, timer identifier는 숫자이다. 다른환경에서는, timer identifier는 다른것이 될 수 있다. 예를들면, Node.JS는 추가적인 메소드와 함께 timer object를 리턴한다.

다시한번 말하면, 이러한 메소드에 대한 국제적은 스펙이 없다.

setInterval

setInterval 메소드는 setTimeout과 같은 문법이다.

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

모든 인자들은 같은 의미를 갖습니다. 하지만 setTimeout과는 다르게 함수를 한번만 실행하는 것이 아니라 부여된 시간 간격이후로 주기적으로 실행된다. 더이상 호출하는 것을 중지하고 싶다면, 우리는 clearInterval(timerId)을 호출해야 한다.

아래의 예제는 2초마다 메세지를 보여준다. 5초후에 출력이 중지된다.

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

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

Chrome/Opera/Safari에서는 Modal windows가 시간을 freeze한다.

IE와 Firefox에서는 alert/confirm/prompt를 보여주면서 내부적인 타이머가 계속해서 ticking을 하지만 크롬 오페라 사파리에서는 내부적인 타이머가 'frozen' 상태가 된다.

그래서 만일 위의 코드를 돌리고 alert 윈도우를 일정 시간 동안 없애지 않았다면, 그 후에 Firefox/IE는 다음 alert가 즉시 보여진다. (지난 호출에서 2초 경과후에) 그리고 chrome/opera/safari에서는 2초가 더 지난후에 보여지게 될 것이다. (타이머는 alert동안에 시간이 가지 않는다.)

역자 추가 설명 : 2019.04.23 일자 기준 테스트 결과 alert는 js 엔진의 스레드를 멈추지 않는다. alert 동안에도 시간이 간다. 과거의 지식이다.

재귀적인 setTimeout

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

한가지가 setInterval이고 두번째가 재귀적인 setTimeout이다.

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

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

재귀적인 setTimeoutsetInterval보다 더울 유연하다. 이 방법에서는 다음 호출은 아마 때에 따라 다르게 스케쥴 될 것이다. 현재 실행하던 것의 결과에 따라 달라진다.

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

let delay = 5000;

let timerId = setTimeout(function request() {
	...요청전송
	
	if( 서버 과부화 때문에 요청이 실패한다면...) {
		delay *=2;
	}
	timerId = setTimeout(request,delay);
},delay);

그리고 주기적으로 CPU 사용량이 많은 작업이 있다면, 실행에 걸린 시간을 측정하고 다음 호출을 더 일찍할지 더 늦게할지 계획할 수 있다.

재귀적인 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의 실행완료까지 기다리고 스케쥴러를 체크하고 시간이 됐다면 다시 즉시 실행할 것이다.

극단적인 경우에 만일 함수가 항상 delay ms보다 더 길게 실행된다면, 잠깐의 정지도 하지 않고 실행될 것이다.

재귀적인 setTimeout은 고정된 딜레이를 보장

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

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

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

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

setTimeout(...,0)

setTimeout(func,0) 또는 setTimeout(func) 이러한 특별한 용례가 있다.

앞의 코드는 func의 실행을 가능한 빠르게 스케쥴합니다. 하지만 스케쥴러는 현재의 코드가 끝난 뒤에 호출할 것이다.

그래서 함수가 현재의 코드가 끝난 지후에 실행하도록 스케쥴 되는 것이다. 다른말로는 비동기적실행이다.

예를들면 다음 코드는 'Hello'를 출력한 후 즉시 'World'를 출력한다.

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

alert('Hello');

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

CPU 소비가 많은 작업을 Splitting 하기

SetTimeout을 이용해 CPU 사용량이 많은 작업들을 나누는 트릭이 있다.

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

그래서 SetTimeout(...,0)을 이용해 긴 텍스트를 조각조각 나눌 수 있다. 처음 100줄 이후 또 다른 100줄 이러한 형식으로.

명쾌하게 이해하기 위해, 간단한 예제를 갖고 고려해보자.

우리는 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을 이용하여 작업을 Split 해보자.

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....10000000
  2. 두번째 실행에서 i=1000001...2000000
  3. 그리고 계속...while에서 i1000000에 의해 나눠지는지 검사한다.

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

count실행 중 일시정지는 자바스크립트 엔진에게 숨쉬며 다른 작업을 할 시간을 준다. 다른 액션에 반응할 시간이다.

주목해야 할 것은 작업을 setTimeout으로 나누든 나누지 않든, 속도는 거의 비슷하다는 것이다. 전체 카운팅 타임에는 큰 차이가 없다는 것

이 개념들에 더 익숙해지기 위해 소스코드를 개선해보자.

우리는 스케쥴링을 count()시작부분으로 옮길 것이다.

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

function count() {
  // move the scheduling at the beginning
  if ( i < 1e9 - 1e6) {
    setTimeout(count);  
  }
  
  do {
    i++;
  } while (i % 1e6 != 0);
  
  if ( i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  }

}

count();

이제, 우리는 count()부터 시작하고 우리가 count()를 더 할 필요가 있다는 것을 안다. 작업을 하기 전에 즉시 스케쥴을 걸어놓는다.

실행해보면, 이게 훨씬 적은 시간이 든다는 것을 알게된다.

첫타이머는 (스펙에 기재된대로) 즉시 실행된다. 그리고 그후에는 딜레이가 일어나기 시작해서 숫자값이 9, 15, 20, 24...이러한 형식으로 들어오게 된다.

서버사이드 자바스크립트의 경우 이러한 제한이 없다. 그리고 즉각적인 비동기 작업을 스케쥴하기 위해 다른방법이 존재한다. 이를테면 노드js에 process.nextTicksetImmediate와 같은 것이 있다. 그래서 이런 개념은 브라우저에 한해 적용된다..

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

브러우저 내부에서 실행되는 스크립트의 또다른 이점은 유저에게 프로그래스바와 같은 것을 보여줄 수 있다는 것이다.

왜냐하면 브라우저는 주로 스크립트가 완료된 이후에 모든 'repainting'작업을 하기 때문이다.

그래서 만일 우리가 하나의 큰 함수를 수행한다면, 만일 이것이 무언가를 변화시키더라도, 변화는 그 작업이 끝날때까지 반영되지 않는다.

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

<script>
let i = 0;

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

count();
</script>

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

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

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

<script>
let i = 0;

function count() {
	do {
		i++;
		progress.innerHTML = i;
	} while( i < 1e9) {
		setTimeout(count)
	}
}

count();
</script>

이제 divi의 증가를 보여준다.

요약

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

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

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

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

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

  • CPU가 많이 소모되는 작업들을 조각조각 나누기 위해, 스크립트는 더이상 그 작업에 매달려있지(hang) 않을 거이다.

  • 프로세스가 진행되는 도중에 브라우저가 다른 것을 할 수 있도록 만들기 위해(이를테면 progress bar의 진행)

모든 스케쥴링 메소드는 정확한 딜레이를 보장하지 못하는 것을 알아둬야한다. 스케쥴된 코드에서 그것에 의존하지 않는 편이 좋다.

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

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

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

연습문제

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

setInterval

function  printNumbers_setInterval(from, to) {
	let timer = setInterval(()=>{
		from++;
		timeIntervalElm.innerHTML = from

		if(from === to  ) {
			clearInterval(timer)
		}
	},1000);
}

setTimeout

// setTimeout
function  printNumbers_setTimeout(from, to) {
	setTimeout(
		function  run  (a,b) {
			console.log(a);
			if( from  <  to ) {
				timer  =  setTimeout(run,  1000,  ++a,b)
				timeOutElm.innerHTML  =  a
			}
		}
	,  1000,from,to);
}

setTimeout 재귀버전은 다시 풀어보기

역자의 풀이 코드

function printNumbers_setInterval(from, to){
	var timerId = setInterval(
		()=>{ 

			if(from>=to){
                clearInterval(timerId);
            }

            console.log(from); 			
			from ++;
        }, 1000);
}
function printNumbers_setTimeout(from, to){

    setTimeout(
        function run(from, to) {
            console.log(from);

            if(from<to){
                setTimeout(run, 1000, ++from, to);
            }
			
			
        }
    , 1000, 0, 5);

}

🔍 출처 블로그
자바스크립트 개발자라면 알아야 할 33가지 개념 #10 스케쥴링: setTimeout 과 setInterval

profile
프론트엔드 주니어 개발자 🚀

0개의 댓글