본문 바로가기
Javascript

호출 스케줄링 setInterval, setTimeout | setInterval 함수 즉시 종료

by memeseo 2021. 11. 10.

일정 간격으로 데이터를 불러와 그래프를 그려낼 때 '일정 시간 간격을 두고 함수를 실행하는 방법'이라는 설명을 읽고 아무 생각없이 setInterval로 구현했다가 일정 간격으로 데이터를 불러오지 못하고 clearInterval을 사용해도 setInterval 함수 호출이 중단되지 않는 현상을 발견했다. 나와 같은 사람이 없기를 바라며 .. 호출 스케줄링에 대해 제대로 알아보도록 하자.

 

🔎 호출 스케줄링 (Scheduling a call)

일정 시간이 지난 후에 함수를 예약 실행(호출)할 수 있게 하는 것을 '호출 스케줄링'이라고 한다. 호출 스케줄링을 구현하는 방법은 두 가지가 있다.

 

setTimeout

: 일정 시간이 지난 후에 함수를 실행하는 방법

 

setInterval

: 일정 시간 간격을 두고 함수를 실행하는 방법

 

 

🔎 setTimeout?

 

문법

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

 

매개변수 [func | code]

: 실행하고자 하는 코드, 함수 또는 문자열 형태다. 대개는 이 자리에 함수가 들어가지만 하위 호환성을 위해 문자열도 받을 수 있게 해놨다.

 

delay

: 실행 전 대기 시간(지연 시간)으로 단위는 밀리초 (millisecond, 100밀리초 = 1초)이다. 기본값은 0.

 

➕ arg1, arg2 ...

: 함수에 전달할 인수들이다. IE9 이하에서는 지원하지 않는다.

 

➕ 예제

아래 코드를 실행하면 1초 후 message()가 호출된다.

function message(){
	alert('hello');
}

setTimeout(message, 1000);

 

아래와 같이 함수에 인수를 넘겨줄 수도 있다.

function message(message1, message2){
	alert(message1 + " , " + message2);
}

setTimeout(message, 1000, 'hello', 'stranger');

 

setTimeout의 첫번째 인수가 문자열이면, 자바스크립트는 이 문자열을 이용해 함수를 만든다.

setTimeout('alert("hello")', 1000);

 

하지만 문자열은 추천하지 않으므로 아래와 같이 사용하도록 하자.

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

 

🚫 함수는 실행하지말고 넘기자.

setTimeout(message(), 1000); // 잘못된 코드

setTimeout은 함수의 참조값을 받는다. 그런데 message()를 인수로 전달하면 함수 실행 결과가 전달되어 버린다. message()는 반환문이 없기 때문에 실행 결과는 undefined로 나온다. setTimeout은 스케줄링 할 대상을 찾기 못해 원하는 대로 코드가 작동하지 않는다.

 

🔎 clearTimeout으로 스케줄링 취소

setTimeout을 호출하면 '타이머 식별자(Timer identifier)'가 반환된다. 스케줄링을 취소하고 싶을 땐 식별자 (아래 예시에서 timeout)를 사용하면 된다.

 

let timeout = setTimeout(()=>alert('아무일도 일어나지 않음.'), 1000);
alert(timeout); // 타이머 식별자

clearTimeout(timeout);
alert(timeout); // 취소 후에도 식별자의 값은 null이 되지 않는다.

예시를 실행하면 alert창이 두 개 뜨는데 브라우저 환경에서는 숫자지만, 다른 호스트 환경에서는 숫자형 외의 다른 자료형일 수 있다. (브라우저는 HTML5의 timers section을 준수하고 있다.)

 

🔎 setInterval

setInterval method와 setTimeout은 동일한 문법을 사용한다.

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

함수를 한 번만 실행하는 setTimeout과 달리 serInterval은 함수를 주기적으로 실행한다. 함수 호출을 중단하려면 clearInterval(interval)을 사용한다.

 

아래 예제를 실행하면 메세지가 2초 간격으로 보여지다가 5초 이후에는 더 이상 메세지가 보이지 않는다.

let interval = setInterval(()=> alert('tick'), 2000);

setTimeout(()=>{
	clearInterval(interval);
    alert('tick stop');
}, 5000);

 

🚫 alert창이 떠 있더라도 타이머는 멈추지 않는다.

: 크롬과 파이어폭스를 포함한 대부분의 브라우저는 alert / confirm / prompt 창이 떠 있는 동안에도 내부 타이머를 멈추지 않는다. 위 예제들을 실행하고 첫 번째 alert 창이 떴을 때 몇 초간 기다렸다가 창을 닫으면 두 번째 alert창이 2초 뒤에 나타나는 게 아니라 바로 alert창이 나타나는 것을 확인할 수 있다. 명시한 지연 시간 2초보다 alert창 간격이 짧아진다.

 

 

❔ 무언가를 일정 간격을 두고 실행하고 싶을때 setInterval을 사용하면 되겠네

네니요. 무언가를 일정 간격을 두고 실행하는 방법은 두 가지 방법이 있다. 하나는 위 설명과 같이 일정 간격을 두고 연속적으로 실행하는 함수인 setInterval이 있고, 다른 하나는 중첩 setTimeout을 이용하는 방법이다.

 

🔎 중첩 setTimeout?

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

// result : 2초마다 발생

setTimeout은 (*)으로 표시한 줄의 실행이 종료되면 다름 호출을 스케줄링한다. 중첩 setTimeout을 이용하는 방법은 setInterval을 사용하는 방법보다 유연하다. 호출 결과에 따라 호출을 원하는 방식으로 조정해 스케줄링 할 수 있기 때문이다.

 

5초 간격으로 서버에 요청을 보내 데이터를 얻고 있을 때, 서버가 과부하 상태라면 요청 간격을 늘려주는 것이 좋을 것이다. 아래는 이를 setTimeout으로 유연하게 구현한 코드이다.

let delay = 5000;

let timer = setTimeout(function request(){
	if(/* 서버 과부하로 인한 요청 실패일 경우*/) {
    	delay *= 2;
    }
    
    timer = setTimeout(request, delay);
}, delay);

CPU 소모가 많은 작업을 주기적으로 실행하는 경우에도 setTimeout을 재귀 실행하는 방법이 유용하다. 작업에 걸리는 시간에 따라 다음 작업을 유동적으로 계획할 수 있기 때문이다.

 

❗ 중첩 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++)를 100밀리초마다 실행한다. 

 

setInterval을 사용하면 func호출 사이의 지연 간격이 실제 명시한 간격 (100ms)보다 짧아진다. func을 실행하는데 '소모되는' 시간이 지연 간격에 포함되기 때문이다. 그럼, 'func을 실행하는데 걸리는 시간'이 '명시해 놓은 지연 시간' 보다 길면 어떻게 될까? 이런 경우 엔진이 func의 실행이 종료될 때 까지 기다려준다. func의 실행이 종료되면 엔진은 스케줄러를 확인하고, 이때 지연 시간이 지났으면 다음 호출을 바로 시작한다. 즉, 모든 함수가 쉼 없이 계속 연속 호출된다 .. ! (delay 의미 無)

 

 

🚩 좀 더 자세히 알아보자.

setInterval은 지정된 시간 간격만큼 무조건 지정된 코드를 호출하고자 한다. 하지만 지정된 시간 간격에 도달했음에도 불구하고 지정된 코드를 실행할 수 없는 상태라면 이벤트를 큐(Queue)에 저장한다. setInterval에서 큐의 크기는 1이다. 즉, 하나의 실행만을 저장할 수 있다. 그리고 큐에 저장된 것이 있다면 실행해야할 시간 간격과는 관계 없이 실행 가능한 상태일 때, 즉시 큐에서 이벤트를 꺼내 실행하게 되어 있다. (위에 모든 함수가 쉼 없이 계속 연속 호출되는 이유) 

조금 더 풀어서 설명을 하자면, 정해진 시간이 100ms라고 가정할 때, setInterval이 처음 실행할 때 100ms 시간대에서 지정된 코드(A)를 실행할 것이다. 그리고 200ms의 시간대가 되면 지정된 코드(B)를 실행하려 할 것이다. 하지만 이전 코드(A)가 지연 등의 이유로 실행할 수 없는 상태라면 setInterval은 현재 실행할 코드(B)를 큐에 저장한다. 그리고 300ms의 시간대가 되어 다시 지정된 코드(C)를 실행하려 한다. 하지만 이전코드(A)가 아직 끝나지 않은 상태에서 큐에 이미 실행할 코드(B)가 저장되어 있다면 setInterval은 현재 실행 코드(C)를 무시한다.

▫ 정리 : setInterval은 정해진 시간 간격내에 지정된 코드가 실행되는 경우 깔끔하게 처리할 수 있지만, 지정된 코드가 지연되어 다음 이벤트를 발생에 영향을 끼치는 경우 '무시 당하는 이벤트'가 발생할 수 있다.

 

이에 반해 중첩 setTimeout을 이용하면 아래와 같이 실행흐름이 이어진다.

중첩 setTimeout을 사용하면 명시한 지연(여기서는 100ms)이 보장된다. 이렇게 지연 간격이 보장되는 이유는 이전 함수의 실행이 종료된 이후에 다음 함수 호출에 대한 계획이 세워지기 때문이다.

 

 

🔎 setTimeout 대기시간이 0일때

setTimeout(func, 0)이나, setTimeout(func)을 사용하면 setTimeout의 대기시간을 0으로 설정할 수 있다. 이렇게 대기 시간을 0으로 설정하면 func을 '가능한 빨리' 실행할 수 있다. 왜 '가능한'일까?

 

1. 호출 스케줄러는 현재 스크립트의 실행이 종료된 직후에 실행된다.

// 예시

setTimeout(()=> alert('world'));
alert('hello');

위 예시에서 첫 번째 줄은 "'0밀리초 후에 함수 호출하기'라는 할 일을 계획표에 기록"해주는 역할을 한다. 그런데 스케줄러는 스크립트 (alert함수)의 실행이 종료되고 나서야 '계획표에 어던 할일이 적혀 있는지 확인'하므로, 'Hello'가 먼저 그리고 'world'가 출력된다.

 

2. 브라우저 환경에서 실제 대기 시간은 0이 아니다.

브라우저는 HTML5표준에서 정한 중첩 타이머 실행 간격 관련 제약사항을 준수한다. 해당 표준엔 "다섯 번째 중첩 타이머 이후엔 대기 시간을 최소 4밀리초 이상을 강제해야 한다."라고 사항이 명시되어 있다.

 

let start = Date.now();
let times = [];

setTimeout(function run(){
	// 이전 호출이 끝난 시점과 현재 호출이 시작된 시점의 시차를 기록
	times.push(Date.now() - start);
    
    // 지연 간격이 100ms를 넘어가면 array를 띄워줌
    if(start + 100 < Date.now()) alert(times);
    
    // 지연 간격이 100ms를 넘어가지 않으면 재스케줄링
    else setTimeout(run);
});

// 출력창 result
// 1,1,1,1,5,10,14,19,23,29 ....

초기 타이머들은 지연 없이 바로 실행된다. 그런데 다섯번 째 중첩 타이머 이후엔 지연 간격이 4밀리초이상 되어 값이 저장되는 것을 볼 수 있다. 이런 제약 사항은 setTimeout뿐만 아니라 setInterval에도 적용된다. 이 제약은 구식스트립트 중 일부가 아직 이 제약에 의존하는 경우가 있어 스펙에서도 이 제약사항을 변경하지 않고 명시하고 있다. 한편, 서버에는 이런 제약이 없으며 Node.js의 process.nextTick과 setImmediate를 이용하면 비동기 작업을 지연 없이 실행할 수 있다. 위에 언급된 제약은 브라우저에 한정된다.

 

 

🔎 모든 스케줄링은 메서드가 명시한 지연 간격을 보장하지 않는다.

아래와 같은 상황에서 브라우저 내 타이머가 느려지면 지연 간격이 보장되지 않는다.

- CPU가 과부하 상태인 경우
- 브라우저 탭이 백그라운드 모드인 경우
- 노트북이 배터리에 의존해서 구동중인 경우

이런 상황에서 타이머의 최소 지연 시간은 300밀리초에서 심하면 1000밀리초까지 늘어난다. 연장 시간은 브라우저나 구동중인 운영체제의 성능 설정에 따라 다르다.

 

 

📝요약

1. setInterval과 setTimeout은 delay 밀리초 후에 func을 규칙적으로, 또는 한 번 실행하도록 해준다.

2. setInterval / setTimeout을 호출하고 반환받은 값을 clearInterval / clearTimeout에 넘겨주면 스케줄링을 취소할 수 있다.

3. 중첩 setTimeout을 사용하면 setInterval을 사용한 것보다 유연한 코드를 작성할 수 있다. (지연 간격 보장..!)

4. 대기시간이 0인 setTimeout을 사용하면 현재 스크립트의 실행이 완료된 후에 가능한 빠르게 원하는 함수를 호출할 수 있다.

5. 지연없이 중첩 setTimeout에서 5회 이상 호출하거나 지연없는 setInterval에서 호출이 5회 이상 진행되면, 4밀리초 이상의 지연 간격을 강제적으로 더해준다. 이는 브라우저에서만 적용되는 사항이며, 하위 호환성을 위해 유지되고 있다.

 


 

즉, setInterval을 사용했을 때 clearInterval을 사용해도 setInterval 함수가 계속 호출되고 내가 지정한 delay시간 간격으로 함수가 호출되지 않았던 이유는 func이 실행되는데 소모되는 시간이 내가 설정한 delay시간보다 길어 queue에 저장되어 있던 코드를 우선적으로 실행하기 바빴기 때문이다 .. 😀 ..

 

이미 setInterval로 구현한 코드를 그럼 다시 setTimeout으로 다 변경해야 할까? 사실 그게 더 좋긴 하지만 차선책으로 queue에 코드가 남아있더라도 중지시키는 방법을 공유하고자한다..

 

🔎 setInterval 함수 즉시 종료

 

원하지 않게 함수가 실행되는 결과를 방지하기 위해 flag를 사용한다.

let isPuase = false;
let timer;

function startTimer(){
	
    timer = setInterval(function(){
    	if(isPuase===true){
        	stopTimer();
        }else{
        	//실행하고자 하는 func
        }
    }, 50000);
}

function stopTimer(){
	isPuase = true;
	clearInterval(timer);
}

이렇게 구현했을 때 사용하고자 하는 func에서 stopTimer()를 부르면 queue에 특정 함수 실행 요청이 있어도 조건문에 의해 종료된다.