setTimeout
은
일정 시간 간격 이후에 함수가 한번 실행
setInterval
은
일정 시간 간격으로 함수가 주기적으로 실행
자바스크립트의 스펙 일부가 아니다. 하지만 대부분 환경은 내부적인 스케쥴러를 갖고있다.
구체적으로 Node.js와 모든 브라우저에 제공된다.
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
아무것도 리턴하지 않는 함수이다. 그러므로 아무것도 스케쥴되지 않는다.
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
메소드는 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 동안에도 시간이 간다. 과거의 지식이다.
무언가 정기적으로 실행시키기 위해서 두가지 방법이 있다.
한가지가 setInterval
이고 두번째가 재귀적인 setTimeout
이다.
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick,2000) // (*)
}
위의 setTimeout
은 현재 실행중인 것이 끝낼때 ((*)
) 다음 호출을 바로 스케쥴한다.
재귀적인 setTimeout
은 setInterval
보다 더울 유연하다. 이 방법에서는 다음 호출은 아마 때에 따라 다르게 스케쥴 될 것이다. 현재 실행하던 것의 결과에 따라 달라진다.
이를테면, 우리가 서버에 5초마다 데이터를 물어보는 요청을 보내는 서비스를 작성할 필요가 있는데, 서버에 요청이 너무 많을때는 계속해서 요청을 보내기보다는 우리가 주기를 10초, 20초, 40초 정도로 늘리는 것이 바람직하다.
let delay = 5000;
let timerId = setTimeout(function request() {
...요청전송
if( 서버 과부화 때문에 요청이 실패한다면...) {
delay *=2;
}
timerId = setTimeout(request,delay);
},delay);
그리고 주기적으로 CPU 사용량이 많은 작업이 있다면, 실행에 걸린 시간을 측정하고 다음 호출을 더 일찍할지 더 늦게할지 계획할 수 있다.
재귀적인 setTimeout
은 setInterval
이 보장하지 못하는 실행간 딜레이를 보장할 수 있다.
밑의 두개의 코드를 비교해보자.
첫번째 예제는 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(func,0)
또는 setTimeout(func)
이러한 특별한 용례가 있다.
앞의 코드는 func
의 실행을 가능한 빠르게 스케쥴합니다. 하지만 스케쥴러는 현재의 코드가 끝난 뒤에 호출할 것이다.
그래서 함수가 현재의 코드가 끝난 지후에 실행하도록 스케쥴 되는 것이다. 다른말로는 비동기적실행이다.
예를들면 다음 코드는 'Hello'를 출력한 후 즉시 'World'를 출력한다.
setTimeout(()=> alert('World'));
alert('Hello');
첫번째 줄은 달력에 0초후에 함수를 호출하라는 명령이다. 하지만 스케쥴러는 현재의 코드가 끝난 뒤에만 달력을 확인한다. 그래서 Hello
가 첫번째고 World
가 뒤따라 온다.
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가 완전히 동작한다.
우리는 작업의 부분을 수행한다.
i=1....10000000
i=1000001...2000000
while
에서 i
가 1000000
에 의해 나눠지는지 검사한다.그 후에 아직 작업이 완료되지 않았다면, 다음 호출은 (**)
에서 스케쥴된다.
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.nextTick과 setImmediate와 같은 것이 있다. 그래서 이런 개념은 브라우저에 한해 적용된다..
브러우저 내부에서 실행되는 스크립트의 또다른 이점은 유저에게 프로그래스바와 같은 것을 보여줄 수 있다는 것이다.
왜냐하면 브라우저는 주로 스크립트가 완료된 이후에 모든 '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>
이제 div
는 i
의 증가를 보여준다.
setInterval(func, delay, ... args)
와 setTimeout(func, delay, ... args)
2개의 메소드는 func
을 delay
ms 이후에 주기적으로 혹은 한번 실행하도록 허용해준다.
실행을 취소하기 위해, 우리는 setInterval
또는 setTimeout
에서 반환되는 값을 이용해 clearInterval
또는 clearTimeout
을 호출해야 한다.
중첩된 setTimeout
호출은 setInterval
을 이용하는 것보다 더욱 유연하다. 그리고 각 실행 사이에 최소한의 딜레이를 보장해준다.
타임아웃이 0인 스케쥴링 setTimeout(...,0)
는 '현재 코드가 끝난 이후에 바로 호출'을 스케쥴링하고 싶을때 사용한다.
setTimeout(...,0)
의 유즈케이스는 다음과 같다.
CPU가 많이 소모되는 작업들을 조각조각 나누기 위해, 스크립트는 더이상 그 작업에 매달려있지(hang) 않을 거이다.
프로세스가 진행되는 도중에 브라우저가 다른 것을 할 수 있도록 만들기 위해(이를테면 progress bar의 진행)
모든 스케쥴링 메소드는 정확한 딜레이를 보장하지 못하는 것을 알아둬야한다. 스케쥴된 코드에서 그것에 의존하지 않는 편이 좋다.
예를 들면, 브라우저 내부의 타이머는 많은 이유에 의해 지연될 수 있다.
모든것이 최소한의 타이머 딜레이를 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