JavaScript/모던 자바스크립트 Deep Dive

31. 비동기 프로그래밍

sandwe 2022. 10. 31. 13:56

동기 처리와 비동기 처리

실행 컨텍스트 평가와 실행 과정

  1. 함수를 호출하면 함수 코드가 평가되어 함수 실행 컨텍스트가 생성된다.
  2. 생성된 함수 실행 컨텍스트는 실행 컨텍스트 스택(= call stack)에 push되고, 함수 코드가 실행된다. 즉, 함수의 실행이 시작된다.
  3. 함수 코드 실행이 종료되면 함수 실행 컨텍스트는 실행 컨텍스트 스택에서 pop되어 제거된다.

 

함수가 호출된 순서대로 순차적으로 실행되는 이유는 함수가 호출된 순서대로 함수 실행 컨텍스트가 실행 컨텍스트 스택에 push 되기 때문이다. 이처럼 함수의 실행 순서는 실행 컨텍스트 스택으로 관리한다.

 

자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 갖는다.

  • 2개 이상의 함수를 동시에 실행할 수 없다.
  • 실행 컨텍스트 스택의 최상위 '실행 중인 실행 컨텍스트'를 제외한 모든 실행 컨텍스트는 모두 실행 대기 중인 태스크들이다.

따라서, 자바스크립트 엔진은 싱글 스레드 방식으로 동작한다. 즉, 한번에 하나의 태스크만 실행할 수 있기 때문에 처리 시간이 긴 태스크를 실행하면 블로킹(작업 중단)이 발생한다.

 

동기 처리 방식

  • 현재 실행 중인 태스크가 종료할 때까지 다음에 실행될 태스크가 대기하는 방식
  • 장점) 태스크를 순서대로 하나씩 처리하여 실행 순서가 보장된다.
  • 단점) 현재 실행 중인 태스크가 종료할 때까지 이후 태스크들이 블로킹된다.

다음의 예제에서 구현한 sleep 함수는 setTimeout 함수와 유사학 일정 시간이 경과한 이후에 콜백 함수를 호출하는 함수이다.

function sleep(func, delay) {
    const delayUntil = Date.now() + delay;

	// sleep 함수는 3초 후에 foo 함수를 호출한다.
    while(Date.now() < delayUntil);
    func();
}

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

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

sleep(foo, 3 * 1000); // foo

bar(); // bar

bar 함수는 sleep 함수의 실행이 종료된 후 호출되므로 3초 이상(foo 함수의 실행 시간  + 3초) 호출되지 못하고 블로킹된다.

 

비동기 처리 방식

  • 현재 실행 중인 태스크가 종료되지 않은 상태여도 다음 태스크를 곧바로 실행하는 방식
  • 장점) 현재 실행 중인 태스크가 종료되지 않은 상태여도 다음 태스크를 곧바로 실행하여 블로킹이 발생하지 않는다.
  • 단점) 태스크의 실행 순서가 보장되지 않는다.

setTimeout으로 일정 시간 경과후 함수를 실행해보자.

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

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

setTimeout(foo, 3 * 1000);
bar();
// bar 호출 -> 3초 -> foo 호출

setTimeout 함수는 delay 되는 시간 동안 setTimeout 함수 이후의 태스크를 블로킹하지 않고 곧바로 bar 함수가 실행된다.

 

이벤트 루프(Event Loop)와 태스크 큐(Task Queue)

비동기 처리는 이벤트 루프와 태스크 큐와 깊은 관계가 있다.

자바스크립트는 싱글 스레드로 동작하지만 브라우저가 동작하는 것을 살펴보면 많은 태스크가 동시 처리되는 것처럼 느껴진다.

예1) HTML 요소가 애니메이션 효과를 통해 움직이면서 이벤트를 처리

예2) HTTP 요청을 통해 서버로부터 데이터를 가지고 오면서 렌더링

 

이러한 자바스크립트의 동시성(concurrency)을 지원하는 것이 바로 이벤트 루프다.

 

이벤트 루프와 브라우저 환경

자바스크립트 엔진은 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행할 뿐이다. 즉, 소스코드의 평가와 실행을 자바스크립트 엔진이 담당한다. 자바스크립트의 엔진은 크게 콜 스택과 힙 두 개 영역으로 구분된다.

 

1️⃣ 콜 스택 (Call Stack)

  • 실행 컨텍스트 스택을 말한다.
  • 소스 코드 평가 과정에서 생성된 실행 컨텍스트가 추가/제거되는 스택 자료구조

2️⃣ 힙(Heap)

  • 객체가 저장되는 메모리 공간이다. 콜 스택의 실행 컨텍스트 요소는 힙에 저장된 객체를 참조한다.
  • 메모리에 값을 저장하려면 먼저 값을 저장할 메모리 공간의 크기를 결정해야 한다.
  • 객체는 크기가 정해져 있지 않아 할당해야 할 메모리 공간의 크기를 런타임에 결정한다(동적 할당). 따라서 힙은 구조화되어 있지 않다.

 

비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당한다. 브라우저 환경은 태스크 큐와 이벤트 루프를 제공한다.

 

1️⃣ 태스크 큐 (Task Queue)

  • setTimeout/setInterval 과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역
  • 추가) 태스크 큐와 별도로 프로미스의 후속 처리 메서드의 콜백 함수가 일시적으로 보관되는 마이크로태스크 큐도 존재한다.

2️⃣ 이벤트 루프 (Event Loop)

  • 콜 스택에 현재 실행중인 실행 컨텍스트가 있는지, 그리고 태스크 큐에 대기중인 함수(콜백 함수, 이벤트 핸들러 등)가 있는지 반복해 확인한다.
  • 만약 콜 스택이 비어 있고 태스크 큐에 대기중인 함수가 있다면 이벤트 루프는 순차적(FIFO, First in First out)으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다. 이때, 콜 스택으로 이동한 함수는 실행된다. 즉, 태스크 큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작한다.

 

브라우저 환경에서 다음 예제가 동작하는 방식을 살펴보자.

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

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

setTimeout(foo, 0); // 0초(실제는 4ms) 후에 foo 함수가 호출된다.
bar();
  1. 전역 코드가 평가되어 전역 실행 컨텍스트가 생성되고 콜 스택에 push 된다.
  2. 전역 코드가 실행되기 시작해 setTimeout 함수가 호출된다. 이때, setTimeout 함수 실행 컨텍스트가 생성되고, 콜 스택에 푸시되어 현재 실행 중인 실행 컨텍스트가 된다. 브라우저의 Web API인 타이머 함수도 함수이므로 함수 실행 컨텍스트를 생성한다.
  3. setTimeout 함수가 실행되면 콜백 함수를 호출 스케줄링하고 종료되어 콜 스택에서 팝된다. 이때 호출 스케줄링(타이머 설정과 타이머가 만료)하고 나면 콜백 함수를 태스크 큐에 푸시하는 것은 브라우저의 역할이다.
  4. 브라우저가 수행하는 4-1과 자바스크립트 엔진이 수행하는 4-2는 병행 처리된다.
    1. 브라우저는 타이머를 설정하고 타이머의 만료를 기다린다. 타이머가 만료되면 콜백 함수 foo가 태스크 큐에 푸시되어 대기한다. 이때, 콜백 함수는 정확히 지연 시간 후에 호출될 것이라고 보장할 수 없다. 콜백 함수는 콜 스택이 비어야 태스크 큐에서 콜 스택으로 이동하여 호출되믜로 시간차가 발생할 수 있기 때문이다.
    2. bar 함수가 호출되어 bar 함수의 함수 실행 컨텍스트가 생성되고 콜 스택에 푸시되어 현재 실행 중인 실행 컨텍스트가 된다. 이후 bar 함수가 종료되어 콜 스택에서 팝된다. 이때, 브라우저가 타이머를 설정한지 4ms가 지났다면 foo 함수는 아직 태스크 큐에서 대기 중이다.
  5. 전역 코드 실행이 종료되고 전역 실행 컨텍스트가 콜 스택에서 pop 된다. 따라서 콜 스택에는 아무런 실행 컨텍스트도 존재하지 않게 된다.
  6. 이벤트 루프가 콜 스택이 비어있음을 감지하고, 태스크 큐에서 대기중인 콜백함수 foo가 이벤트 루프에 의해 콜 스택에 push 된다. 이때, 함수 실행 컨텍스트가 생성되어 콜 스택에 push 되고, 현재 실행 중인 실행 컨텍스트가 된다.
  7. foo 함수의 실행이 종료되어 콜 스택에서 pop된다.

 

자바스크립트 엔진은 싱글 스레드 방식으로 동작하지만, 브라우저는 멀티 스레드로 동작하여 비동기 처리가 가능하다.