마이크로 태스크 큐, 태스크 큐

쏘쏘임·2021년 11월 2일
1
post-thumbnail

작성 이유 : 자바스크립트 엔진은 단일 스레드로 동작한다. 즉, 현재 실행 중인 태스크가 완료 되어야 다음 태스크들이 순차적으로 실행될 수 있다. 그런데 바로 호출되지 않고 특정 조건에 따라 병렬적으로 호출되는 것 같은 이벤트 핸들러, setTimeout, 비동기 통신은 어떻게 동작하는걸까?

자바스크립트 엔진이 비동기를 처리하는 방법

자바스크립트는 단일 스레드로 동작하며 콜스택에 실행 컨텍스트를 푸시, 팝해가며 실행 순서를 제어한다. 즉, 병렬적으로 진행되는 듯한 작업들은 모두 멀티 스레드를 가진 브라우저 엔진의 도움으로 작동한다.

타임 스케줄링 함수나 이벤트 핸들러는 고차함수가 콜백함수를 호출하지 않는다. 브라우저에 특정 조건과 콜백함수를 주며 해당 조건에 호출해 달라는 위임을 하고 스레드에서 종료된다.

브라우저는 태스크 큐와 이벤트 루프를 사용해 위임받은 업무를 실행한다. 콜백 함수가 호출되는 조건을 감지하면 해당 함수를 태스크 큐에 삽입하고, 이벤트 루프를 통해 콜 스택과 태스크 큐를 반복해서 체크하다가 '콜 스택이 모두 비었고 태스크 큐에 대기 중인 함수가 있다'면 이를 콜 스택으로 푸쉬해준다.

따라서 콜 스택이 비지 않았거나 태스크 큐에 많은 함수들이 쌓여있을 경우 등에는 정확히 지정한 조건에 호출된다는 보장은 없다. 그저 지정된 조건에 태스크 큐에 삽입해주는 것이다.

이들과 동일한 원리로 작동하지만 우선순위가 더 높은 다른 큐를 사용하는 것이 프로미스 객체를 이용한 비동기 통신이다. 이 경우 콜백함수는 '마이크로 태스크 큐'라는 곳에 삽입되며 태스크 큐보다 우선순위가 높기 때문에 이 곳에 푸쉬된 것이 있다면 콜스택이 비는 순간 태스크큐를 무시하고 먼저 콜스택으로 푸쉬한다.

예제

이를 더 구체적으로 이해하기 위해 다음 예제를 살펴보자.

function foo() {
  console.log('foo');
}
function bar() {
  console.log('bar');
}
function foobar() {
  console.log('foobar');
}

// 함수 호출 1
setTimeout(foo, 0);

// 함수 호출 2
Promise.resolve(console.log('1st Promise'))
  .then(() => {
    console.log('1st resolve 1');
    setTimeout(foobar, 0);
  })
  .then(() => console.log('1st resolve 2'));

// 함수 호출 3
Promise.resolve(console.log('2nd Promise'))
  .then(() => console.log('2nd resolve 1'))
  .then(() => console.log('2nd resolve 2'));

// 함수 호출 4
bar();

설명

일단 상단의 함수 선언문들은 코드 평가 단계에서 실행 컨텍스트의 환경 레코드에 등록될 것이다.
평가가 끝난 후 실행 단계에서 가장 처음 만나는 것은 setTimeout함수다. 이 함수는 foo 함수를 0 딜레이 후에(최소 단위인 4ms 가량 후에 실행될 것이다) 호출해달라는 명령을 브라우저에 위임하고 pop된다.

이 후 리졸브한 값을 가진 프로미스 객체를 반환하는 프로미스의 내장 메서드 resolve가 호출된다. 이 때 console.log('1st Promise')가 실행되고 pop된다.
그 후로 두 번째 resolve 함수가 호출되어 '2nd Promise'를 출력한다.

마지막으로 bar 일반 함수가 호출되어 'bar'를 출력한다.

이로서 콜스택은 비게된다. 사실 이 콜 스택이 비워지기 이전에 이미 태스크 큐에는 4ms가 지난 시점에서 foo 함수가 들어가 있을 확률이 높다.
마이크로 태스크 큐에도 console.log('1st resolve 1'); 와 console.log('2nd resolve 1'); 가 이미 들어있을 수 있다.
태스크 큐와 마이크로 태스크 큐, 각각의 프로미스 객체들을 병렬적으로 동작하기 때문이다.

우선 순위가 높은 1번째, 2번째 프로미스 객체의 콜백 함수가 각각 콜스택을 거쳐 실행된 후, 각각이 다시 반환한 두 번째 콜백 함수가 실행된다.

마지막으로 태스크 큐에 들어가 있는 foo 콜백 함수와 foobar 콜백함수가 차례로 실행되며 모든 함수 실행을 마치게 된다.

// 1st Promise
// 2nd Promise
// bar
// 1st resolve 1
// 2nd resolve 1
// 1st resolve 2
// 2nd resolve 2
// foo
// foobar
profile
무럭무럭 자라는 주니어 프론트엔드 개발자입니다.

0개의 댓글