42장 비동기 프로그래밍

niyu·2021년 7월 23일
0
post-thumbnail

동기 처리와 비동기 처리

23장 실행 컨텍스트에서 보았듯 함수의 실행 순서는 실행 컨텍스트 스택으로 관리하고, 함수가 실행되려면 함수 실행 컨텍스트가 실행 컨텍스트 스택에 푸시되어야 한다.

자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 갖기 때문에, 함수를 실행할 수 있는 창구가 단 하나밖에 없다.

자바스크립트 엔진은 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드 방식으로 동작한다. 싱글 스레드 방식은 한 번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행하는 경우 블로킹이 발생한다.

동기 처리

// 일정 시간이 경과한 이후에 콜백 함수를 호출
function sleep(func, delay) {
  const delayUntil = Date.now() + delay;
  while(Date.now() < delayUntil);
  func();
}

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

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

sleep(foo, 3000);

bar();

sleep는 3초 후에 foo 함수를 호출한다. 이때 bar 함수는 sleep 함수의 실행이 종료된 이후에 호출되기 때문에 3초이상 호출되지 못하고 블로킹된다.

동기 처리

이처럼 현재 실행 중인 태스트가 종료할 때까지 다음에 실행될 태스크가 대기하는 방식을 동기(synchronous) 처리라 한다. 태스크를 순서대로 하나씩 처리해서 실행 순서가 보장된다는 장점이 있지만 앞선 태스크가 종료할 때까지 이후 태스크들이 블로킹되는 단점이 있다.

비동기 처리

위 예제를 타이머 함수를 사용해 수정해보자.

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

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

setTimeout(foo, 3000);

bar();

setTimeout 함수는 앞선 sleep 함수와 달리, setTimeout 함수 이후의 태스크를 블로킹하지 않고 곧바로 실행한다.

비동기 처리

이처럼 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 비동기(asynchronous) 처리라 한다. 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하기 때문에 블로킹이 발생하지 않는다는 장점이 있지만, 태스크의 실행 순서가 보장되지 않는 단점이 있다.

자바스크립트가 싱글 스레드로 동작한다면 비동기로서 동작할 수 없을 텐데, 어떻게 비동기로 처리되는 것일까?

태스크 큐와 이벤트 루프

위 예제의setTimeout 함수가 호출되면 자바스크립트 엔진의 콜 스택에 푸시되어 실행된다. 이때 setTimeout 함수는 브라우저의 Web API이다.

브라우저는 자바스크립트 엔진 외에도 렌더링 엔진과 Web API를 제공하는데, Web API에는 DOM API와 타이머 함수, HTTP 요청(Ajax)과 같은 비동기 처리를 포함한다.

비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저/Node.js가 담당한다. 브라우저는 멀티 스레드 방식으로 동작한다.

setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진이 담당하지만, 호출 스케줄링을 위한 타이머 설정과 콜백 함수의 등록은 브라우저/Node.js가 담당한다. 이를 위해 브라우저 환경은 태스크 큐와 이벤트 루프를 제공한다.

🧩 태스크 큐(task queue/event queue/callstack queue)
비동기 함수의 콜백 함수나 이벤트 핸들러가 일시적으로 보관되는 영역이다.

🧩 이벤트 루프(event loop)
콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인한다. 만약 콜 스택이 비어 있고 태스크 큐에 대기 중인 함수가 있다면, 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택에 이동시킨다.

즉, setTimeout 함수가 타이머가 완료하면 콜백 함수를 태스크 큐에 등록하는 처리는 자바스크립트 엔진이 아니라 브라우저가 실행한다. 자바스크립트 엔진은 단순히 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행할 뿐이다.

setTimeout의 콜백 함수는 태스크 큐에 푸시되어 대기하다가 콜 스택이 비게 되면, 콜 스택에 푸시되어 실행하게 된다.

다음의 예제를 보자.

function func1() {
  console.log('func1');
  func2();
}

function func2() {
  setTimeout(function() {
    console.log('func2');
  }, 0); // 0초로 설정

  func3();
}

function func3() {
  console.log('func3');
}

func1(); // ?

setTimeout 함수는 비동기 함수이기 때문에, 지연 시간(delay)을 0초로 설정해도 func1 func2 func3의 순서로 출력되지 않는다.

지연 시간은 0이지만 콜 스택이 비어야 호출되므로 약간의 시간차가 발생할 수 있기 때문이다. 즉, 정확히 지연 시간 후에 호출된다는 보장은 없다.

func1 함수가 호출되면 콜 스택에 쌓인다. func1 함수는 func2 함수를 호출해 func2 함수가 콜 스택에 쌓이고 setTimeout 함수가 호출된다. setTimeout의 콜백 함수는 즉시 실행되지 않고 지정 대기 시간만큼 기다리다가, 시간이 끝나면 태스크 큐로 이동한 후 콜 스택이 비어졌을 때 콜 스택으로 이동되어 실행된다. 즉, 위의 예제의 결과는 func1 func3 func2 순으로 출력된다.

그림(?)으로 보면 다음과 같다.

이벤트 루프에 의한 setTimeout 콜백 함수 실행
이벤트 루프에 의한 setTimeout 콜백 함수의 실행 / 그림출처: poiemaweb


이렇게 브라우저와 자바스크립트 엔진이 협력해 비동기 함수를 실행한다.

0개의 댓글