2022. 10. 20. 23:30ㆍJavaScript
일정 시간이 지난 후에 원하는 함수를 예약 실행할 수 있게 하는 것을 ‘ 호출 스케줄링(scheduling a call)이라고 한다.
호출 스케줄링을 구현하는 방법은 두 가지가 있다.
- setTimeout을 이용해 일정 시간이 지난 후에 함수를 실행하는 방법
- setInterval을 이용해 일정 시간 간격을 두고 함수를 실행하는 방법
setTimeout
문법:
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
매개변수:
func | code
- 실행하고자 하는 코드로, 함수 또는 문자열 형태이다. 대개는 이 자리에 함수가 들어간다. 하위 호환성을 위해 문자열도 받을 수 있게 해 놓았지만 추천하진 않는다.
delay
- 실행 전 대기 시간으로, 단위는 밀리초(millisecond, 1000밀리초 = 1초)이며 기본값은 0이다.
arg1, arg2…
- 함수에 전달할 인수들로, IE9 이하에선 지원하지 않는다.
예시를 통해 seTimeout을 어떻게 쓰는지 알아보자
function sayHi() {
alert('안녕하세요.');
}
setTimeout(sayHi, 1000);
함수에 인수를 넘겨줄수도 있다.
function sayHi(who, phrase) {
alert(who + '님,' + phrase);
}
setTimeout(sayHi, 1000, "홍길동","안녕하세요"); // 홍길동 님, 안녕하세요.
setTimeout의 첫 번째 인수가 문자열이면 자바스크립트는 이 문자열을 이용해 함수를 만든다.
아래 예시가 정상적으로 동작하는 이유이다.
setTimeout('alert("안녕하세요")',1000);
그런데 이렇게 문자열을 사용하는 방법은 추천하지 않는다. 되도록 익명 화살표 함수를 사용하자
setTimeout(() => alert('안녕하세요.'), 1000);
함수를 실행하지 말고 넘기시오.
초보 개발자는 setTimeout에 함수를 넘길 때, 함수 뒤에 ()을 붙이는 실수를 하곤 한다.
//잘못된 코드
setTimeout(sayHi(), 1000);
//참된 코드
setTimeout(sayHi, 1000);
setTimeout은 함수의 참조 값을 받도록 정의되어 있는데 sayHi()를 인수로 전달하면 함수 실행 결과가 전달되어 버린다. 그런데 sayHi()엔 반환 문이 없다. 호출 결과는 undefined가 되는 것이다. 따라서 setTimeout은 스케줄링할 대상을 찾지 못해, 원하는 대로 코드가 동작하지 않는다.
clearTimeout으로 스케줄링 취소하기
setTimeout을 호출하면 ‘타이머 식별자(timer identifier)가 반환된다. 스케줄링을 취소하고 싶을 땐 이 식별자 를 사용하면 된다.
스케줄링 취소하기
let timerId = setTimeout(...);
clearTimeout(timerId);
함수 실행을 계획해 놓았다가 중간에 마음이 바뀌어 계획해 놓았던 것을 취소한 상황을 코드로 표현하고 있다. 예시를 실행해도 스케줄링이 취소되었기 때문에 아무런 변화가 없는 것을 확인할 수 있다.
let timerId = setTimeout(() => alert("아무일도 없었다."), 1000);
alert(timerId); // 타이머 식별자
clearTimeout(timerId);
alert(timerId); // 위 타이머 식별자와 동일함 (취소 후에도 식별자의 값은 null이되지 않는다.
예시를 실행하면 alert 창이 2개가 뜨는데, 이 얼럿 창을 통해 브라우저 환경에선 타이머 식별자가 숫자라는 걸 알수 있다. 다른 호스트 환경에선 타이머 식별자가 숫자형 이외의 자료형일 수 있다. 참고로 Node.js에서 setTimeout을 실행하면 타이머 객체가 반환된다.
setInterval
setInterval 메서드는 setTimeout과 동일한 문법을 사용한다.
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
setInterval은 함수를 주기적으로 실행하게 만든다.
clearInterval(timerId)을 사용하면 호출을 중단 할 수 있다.
다음 예시를 실행하면 2초 간격 으로 보이다가 5초 이후에는 더 이상 메시지가 보이지 않는다.
let timerId = setInterval(() => alert("2초~~"), 2000);
setTimeout(() => { clearInterval(timerId); alert("5초 지났어"); },5000);
재귀적인 setTimeout
무언가를 정기적으로 실행시키기 위해서는 두 가지 방법이 있다.
- 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);
그리고 주기적으로 CPU 사용량이 많은 작업(CPU-hungry tasks)이 있다면, 실행에 걸린 시간을 측정하고 다음 호출을 더 일찍 할지 더 늦게 할지 계획할 수 있다.
재귀적인 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의 실행 완료까지 기다리고 스케쥴러를 체크하고 시간이 됐다면, 다시 즉시 실행할 것이다.
극단적인 경우에(In the edge case), 만일 함수가 항상 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) 은 특별한 경우로 사용될 수도 있다.
대기 시간을 0으로 설정하면 func을 ‘가능한 한’ 빨리 실행할 수 있다.
하지만 스케줄러는 현재의 코드가 종료된 이후에 스케줄링한 함수를 실행한다.
그래서 함수가 현재의 코드가 끝난 지후에 실행하도록 스케줄 되는 것이다.
다른 말로는, 비동기적으로 실행된다.
예시는 “Hello”를 출력한 후 즉시 “World”를 출력한다.
setTimeout(() => alert("World"));
alert("Hello");
스케줄러는 현재 스크립트(alert 함수)의 실행이 종료되고 나서야 '어떤 할 일이 있는지 확인’하므로, Hello가 먼저, World은 그다음에 출력된다.
⚠️브라우저 환경에서 실제 대기 시간은 0이 아니다.
브라우저는 HTML5 표준에서 정한 중첩 타이머 실행 간격 관련 제약을 준수합니다. 해당 표준엔 "다섯 번째 중첩 타이머 이후엔 대기 시간을 최소 4밀리 초 이상으로 강제해야 한다."라는 제약이 명시되어있습니다.
예시를 보며 이 제약 사항을 이해해봅시다. 예시 내 setTimeout은 지연 없이 함수 run을 다시 호출할 수 있게 스케줄링되어 있습니다. 배열 times에는 실제 지연 간격에 대한 정보가 기록되도록 해놓았는데, 배열 times에 어떤 값이 저장되는지 알아봅시다.
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start); // 이전 호출이 끝난 시점과 현재 호출이 시작된 시점의 시차를 기록
if (start + 100 < Date.now()) alert(times); // 지연 간격이 100ms를 넘어가면, array를 얼럿창에 띄워줌
else setTimeout(run); // 지연 간격이 100ms를 넘어가지 않으면 재스케줄링함
});
// 출력창 예시:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
앞쪽 타이머들은 스펙에 적힌 것처럼 지연 없이 바로 실행됩니다. 그런데 다섯 번째 중첩 타이머 이후엔 지연 간격이 4밀리 초 이상이 되어 9, 15, 20, 24...와 같은 값이 저장되는 것을 확인할 수 있습니다.
이런 제약은 setTimeout뿐만 아니라 setInterval에도 적용됩니다. setInterval(f)도 처음 몇 번은 함수 f를 지연 없이 실행하지만, 나중엔 지연 간격을 4밀리초 이상으로 늘려버립니다.
이는 오래전부터 있던 제약인데, 구식 스크립트 중 일부는 아직 이 제약에 의존하는 경우가 있어서 명세서를 변경하지 못하고 있는 상황입니다.
한편, 서버 측엔 이런 제약이 없습니다. Node.js의 process.nextTick과 setImmediate를 이용하면 비동기 작업을 지연 없이 실행할 수 있습니다. 위에서 언급된 제약은 브라우저에 한정됩니다.
Reference
https://velog.io/@jakeseo_me/자바스크립트-개발자라면-알아야-할-33가지-개념-10-스케쥴링-setTimeout-과-setInterval-y6 juukjsey
'JavaScript' 카테고리의 다른 글
자바스크립트 개발자가 알아야할 33가지 #12 비트연산 실제로 활용하기 !! (1) | 2022.10.24 |
---|---|
자바스크립트 개발자가 알아야할 33가지 #11 V8엔진이 JS를 기계코드로 바꾸는 방법 (2) | 2022.10.23 |
자바스크립트 개발자가 알아야할 33가지 #8 IIFE마스터하기 (0) | 2022.10.19 |
자바스크립트 개발자가 알아야할 33가지 #표현식(Expression)과 문장(Statement) (0) | 2022.10.19 |
자바스크립트 개발자라면 알아야할 33가지 개념 #6 함수와 블록 스코프 (0) | 2022.10.19 |