"호출 스케쥴링 하기(scheduling a call)"
함수를 당장 실행하지 않고 정확히 몇 초의 딜레이 후에 실행시키고 싶을때 사용하는것.
이것을 구현하기 위해 두가지 메소드가 존재한다
setTimeout
: 일정한 시간 간격 이후에 함수가 한번 실행된다.setInterval
: 일정 시간 간격으로 함수가 주기적으로 실행된다.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);
setTimeout
을 호출했을 때, 반환 값으로 실행을 취소하기 위해 사용할수 있는 "timer identifier"인 timerId
를 준다.
let timerId = setTimeout(...);
alert(timerId); // timer identifier
clearTimeout(timerId);
alert(timerId); // same identifier
취소를 했더라도 다시 호출 했을때 null 이 되진 않는다.
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);
무언가를 정기적으로 실행시키기 위해서는 두가지 방법이 있는데
한가지가 setInterval
이고, 나머지가 재귀적인 setTimeout
이다.
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
위의 setTimeout
은 현재 실행중인 것이 끝날 때((*)
) 다음 호출을 바로 스케쥴한다.
재귀적은 setTimeout
은 setInterval
보다 더욱 유연하다. 이방법은 때에 따라 다르게 스케쥴 될수 있다.
이를테면 서버에 5초마다 데이터를 물어보는 요청을 보내는 서비스를 작성할 필요가 있는데, 서버에 요청이 너무 많을 때는 계속해서 요청을 보내기 보단느 주기를 10초, 20초, 40초 정도로 늘리는 것이 바람직 하다.
let delay = 5000;
let timerId = setTimeout(function request() {
...요청 전송...
if(서버 과부하 때문에 요청이 실패한다면...) {
// 다음 실행까지 인터벌을 좀 늘리자..
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
재귀적인
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
의 그림이다.
재귀적인
setTimeout
은 고정된 딜레이를 보장한다.
새로운 호출이 이전 호출의 끝에 계획되기 때문이다.
Garbage Collection
함수가
setInterval
혹은setTimeout
에 넘겨졌을 때, 그것을 가리키는 내부적인 레퍼런스가 만들어지고 스케쥴러에 저장된다. 만일, 함수에 별다른 참조가 없더라도 함수가 garbage collect되는 것을 막아준다.// 스케쥴러 호출 시까지, 함수는 메모리에 머무릅니다. setTimeout(function() {...}, 100);
setInterval
의 경우에는,clearInterval
이 호출될 때까지 함수는 메모리에 머문다.
side-effect
도 있다. 한 함수가lexical
환경 바깥을 참조한다. 그래서, 이 함수가 살아있는 동안, 바깥의 변수들도 마찬가지로 살아있다. 변수들은 아마 함수 자체보다 더 많은 메모리를 소비할 것이다. 그래서 스케쥴된 함수가 더이상 필요하지 않을 때는 아주 작은 함수라 할지라도, cancel 시켜주는 것이 더 좋다.
setTimeout(func, 0)
또는 setTimeout(func)
이러한 특별한 용례가 있다. 앞의 코드는 func
의 실행을 가능한 빠르게 스케쥴 한다. 하지만 스큐줄러는 현재의 코드가 끝난 뒤에 호출할 것이다.
setTimeout(() => alert("World"));
alert("Hello");
위의 코드에서 setTimeou은
0초 후에 함수를 호출하라는 명령을 넣었다. 하지만 스케쥴러는 현재의 코드가 끝난뒤에 확인한다. 그래서 Hello
가 첫번째고 World
가 뒤따라 나온다.
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가 완전히 동작한다.
i=1...1000000
i=1000001..2000000
. while
에서 i
가 1000000
에 의해 나눠지는지 검사.그 후에 아직 작업이 완료되지 않았다면, 다음 호출은 (**)
에서 스케쥴 된다.
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개의 메소드는 func
을 delay
ms 이후에 주기적으로 혹은 한번 실행하도록 허용.
실행을 취소하기 위해, setInterval
또는 setTimeout
에서 반환되는 값을 이용해 clearInterval
또는 clearTimeout
을 호출.
중첩된 setTimeout
호출은 setInterval
을 이용하는 것보다 더욱 유연합니다. 그리고 각 실행 사이에 최소한의 딜레이를 보장.
타임아웃이 0
인 스케쥴링 setTimeout(..., 0)
는 "현재 코드가 끝난 이후에 바로 호출"을 스케쥴링하고 싶을 때 사용.
setTimeout(..., 0)
의 유즈케이스는 다음과 같다.
모든 스케쥴링 메소드는 정확한 딜레이를 보장하지 못하는 것을 명시ㅏㅁ해라. 스케쥴된 코드에서 그것에 의존하지 않는 편이 좋다.
브라우저 내부의 타이머는 많은 이유에 의해 지연될 수 있다.
모든 것이 최소한의 타이머 딜레이를 300ms
심하면 1000ms
까지 증가시킬 수 있다. 어떤 브라우저를 쓰는지와 셋팅에 따라 달라진다.
- 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()
참고