본문 바로가기
Web/Frontend

[JS] setTimeout에서 delay는 무슨 의미일까?

by 콧등치기국수 2023. 7. 28.
console.log('1');
console.log('2');
setTimeout(()=> console.log('hi~'), 0)
console.log('3');
console.log('4');

위 코드는 콘솔에 [ 1 2 3 4 hi~ ] 으로 나타난다.  [ 1 2 hi~ 3 4 ] 일 것 같았지만 다르게 출력된다. 이유가 뭘까?

자바스크립트 동작 원리부터 알아보자.

 

🧊 자바스크립트 동작 원리

먼저 자바스크립트코드를 실행시키기 위해서는 자바스크립트 엔진이 필요하다.

자바스크립트 엔진은 Call stack(실행컨텍스트 스택) Heap으로 이루어져 있는데, 콜스택은 싱글스레드이므로 한 번에 하나의 작업만 처리하는 동기적 방식으로 처리하게 된다.

자바스크립트 엔진 구조(출처 : https://velog.io/@graphicnovel/JS-자바스크립트-동작-원리)

 

자바스크립트 코드에서 코드를 실행할 때,

함수를 호출하면 호출된 함수의 실행 컨텍스트(execution context)를 형성해 콜 스택에 push한다. 

콜스택은 LIFO(Last In First Out) 방식으로 시간 상으로 나중에 쌓인 실행컨텍스트부터 실행하며, 종료된 실행컨텍스트는
콜스택에서 pop된다.

 

그럼 아래 예시의 실행 순서를 생각해보자.

function add(a, b) {
    return a + b;
}

function average(a, b) {
    return add(a, b) / 2;
}

let x = average(10, 20);

위 코드의 실행 순서와 그에 따른 콜 스택의 상태는 아래 글과 이미지에서 확인할 수 있다.

 

  1. 전역코드 평가 후 전역 실행 컨텍스트 생성하여 콜스택에 push  | Call Stack = [main()]
  2. average() 함수 실행. ➡ 함수 실행 컨텍스트 생성하여 콜스택에 push    | Call Stack = [main(), average()]
  3. add() 함수 실행. ➡ 함수 실행 컨텍스트 생성하여 콜스택에 push  | Call Stack = [main(), average(), add()]
  4. add() 함수가 현재 실행 중인 실행컨텍스트가 되고 종료되면 콜스택에서 pop  | Call Stack = [main(), average()]
  5. average() 함수 종료. ➡ 콜스택에서 pop    | Call Stack = [main()]
  6. 전역 실행 컨텍스트 종료. ➡ 콜스택에서 pop    | Call Stack = []

실행 순서 (출처: https://www.javascripttutorial.net/javascript-call-stack/)

 

🧊 비동기 처리를 해야 하는 이유

지금까지 자바스크립트 동작 방식을 살펴보았다. 만약 sleep() 함수가 호출된다면 어떻게 처리될지 생각해보자.

function sleep(func, delayMs){
  var delayTime = Date.now() + delayMs
  while(Date.now() < delayTime){}
  
  func();
}

function foo(){
  console.log("foo");
}

function bar(){
  console.log("bar");
}

sleep(foo, 3000);
bar();

//foo
//bar

콘솔에 bar가 먼저 찍히고 foo가 3초 후에 찍히지 않는다. 3초를 기다린 후  foo 가 찍히고   bar 가 찍힌다.

즉 지정된 시간만큼 아무 함수도 실행할 수 없이 기다려야 한다. 만약 처리가 오래 걸리는 작업이 있다면 그 시간 동안 유저는 사이트가 느리다고 생각하며 불편을 겪을 수 밖에 없다.

이러한 블로킹(작업중단)을 막기 위해 자바스크립트에서 비동기 처리를 하고 있다. 

 

자바스크립트에서 비동기 처리를 하는 webApi로는 setTimeout(), setInterval(). HttpRequest, DomEvent 가 있다.

이번에는 setTimeout() 함수를 살펴보자. 

function foo(){
  console.log("foo");
}

function bar(){
  console.log("bar");
}

setTimeout(foo, 3000);
bar();

//bar
//foo

자바스크립트 엔진은 코드상 더 위인 setTimeout()부터 실행할테니 이번에도 3초 후에 foo가 찍히고 bar가 찍힐까?? 

아니다. 콘솔에는  bar 가 먼저 찍히고  foo 는 3초 후에 찍힌다.

이처럼 실행 중인 작업이 종료되지 않아도 다음 작업을 바로 실행하는 방식인 비동기 처리를 setTimeout()에서 확인할 수 있다. 그렇다면 자바스크립트에서는 어떻게 비동기 처리가 이뤄질까?

 

🧊 자바스크립트에서 비동기 처리 방식

위에서 간단히 자바스크립트의 동작 방식을 살펴본 결과, 자바스크립트 엔진은 단순 작업이 요청되면 콜 스택을 통해 작업을 순차적으로 실행할 뿐이라는 것을 알 수 있었다. 비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당한다.(ES6에 관련된 내용은 이번 포스트에서는 다루지 않겠다.)

 

싱글 스레드로 동작하는 자바스크립트 엔진과 달리 브라우저는 멀티 스레드로 동작한다. 그리고 브라우저에는 자바스크립트의 동시성을 지원하는 이벤트 루프태스크 큐가 내장되어 있다.

 

💧 이벤트 루프

MDN 에서 이벤트 루프를 표현하는 코드는 아래와 같다.

queue.waitForMessage() 는 현재 처리할 수 있는 메시지가 존재하지 않으면 새로운 메시지가 도착할 때까지 동기적으로 대기한다는 의미이다.

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

즉 이벤트 루프는 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 태스크 큐에 대기 중인 콜백함수가 있는지 반복적으로 확인한다. 이렇게 한 번 확인하는 것을 tick 이라고 한다.

💧 태스크 큐

콜 스택이 비었을 때까지 setTimeout()과 setInterval() 의 콜백함수가 임시로 대기하는 곳이다. 아래의 '자바스크립트 런타임 환경' 그림에서 Callback Queue 라고 적힌 부분이다. (태스크 큐는 콜백 큐의 종류 중 하나이다.)

 


자바스크립트 런타임 환경 (출처 : https://velog.io/@graphicnovel/JS-자바스크립트-동작-원리)

비동기 처리를 위해 비동기 함수의 평가와 실행은 자바스크립트 엔진에서 담당하지만, 호출 스케줄링을 위한 함수의 등록은 호스팅 환경이 담당한다. (호출 스케줄링 : 함수가 바로 호출되지 않고, 일정 시간이 지난 후 호출되도록 함수 호출을 예약하는 것으로 타이머 함수를 사용한다.)

 

위 그림과 아래 코드를 보며 태스크 큐이벤트 루프의 동작 순서를 자세히 살펴보자.

function foo(){
  console.log("foo");
}

function bar(){
  console.log("bar");
}

setTimeout(foo, 3000);
bar();

위 코드가 런타임 환경에서 실행되면 자바스크립트 엔진에 의해 setTimeout()이 먼저 실행될 것이다.

 

setTimeout() 이 실행되면 Call Stack에서 지워지고 웹 API로 하여금 타이머를 실행하게 한다.

 

웹 API의 타이머는 비동기 처리여서 콜 스택에서 pop되었으므로, bar() 함수가 실행되어 콘솔에  bar 가 출력된다.

 

setTimeout에 지정했던 시간이 끝나면 웹 API는 사용자가 등록한 콜백함수를 태스크 큐 집어넣는다.

그리고 이벤트 루프콜 스택에 실행 중인 실행 컨텍스트가 존재하는지, 태스크 큐에 콜백함수가 남아있는지를 반복적으로 확인한다.

그때 bar() 함수가 실행되어 콜스택에서 pop되었고, 태스크 큐에 콜백함수인 foo()가 남아있는 상황이라면 이벤트 루프는 콜백 foo()를 콜 스택에 push 한다.

그 후 foo()가 실행 컨텍스트가 되어 콘솔에  foo 가 출력되게 된다.

 

 

이제 맨 처음 예에서 콘솔에 [ 1 2 3 4 hi~ ] 로 찍힌 이유를 알 수 있을 것이다.

setTimeout()에 인자로 넘긴 지연 시간이 지켜지지 않는 이유를 방금 살펴보았기 때문이다. 지연 시간은 함수가 실행되기기까지 보장된 시간이 아니라, 요청을 처리하기 위해 필요한 최소의 시간이다.
이벤트 루프가 '현재 실행중인 태스크가 없는지', '태스크 큐에 태스크가 있는지' 확인하며 매번 Tick하면서 기회를 엿보고 있기 때문이다.

 

 

 

 

 참고

1. https://youngju-js.tistory.com/28 

2. https://ghost4551.tistory.com/225

3. https://velog.io/@edie_ko/javascript-eventloop

4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop

5. https://peach-milk.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%98%B8%EC%B6%9C-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81-setTimeout-setInterval