%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%8D%B8%EB%84%A4%EC%9D%BC-039


면접 과정 속에서 자각한 나의 지식 부족..

자바스크립트의 비동기 처리를 위한 이해가 부족했다

기초부터 정리하며 복기해보자!





01. 자바스크립트의 동시성?

자바스크립트는 ‘싱글 스레드’ 기반의 언어이다.

스레드가 하나라는 말은 곧, 동시에 하나의 작업만을 처리할 수 있다는 것이다.

하지만 실제로 자바스크립트가 사용되는 환경을 보면 많은 작업들이 동시에 처리된다.

예를 들어, HTML 요소가 애니메이션 효과를 통해 움직이면서 이벤트를 처리하기도 하고, HTTP 요청을 통해 서버로부터 데이터를 가지고 오면서 렌더링하기도 한다.


어떻게 싱글 스레드 언어인 자바스크립트는 이와 같은 동시성(Concurrency)를 지원할까?





02. 이벤트 루프

결론적으로, 자바스크립트는 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원한다.

동시성에 대한 처리는 자바스크립트 엔진을 구성하는 환경, 즉 브라우저나 Node.js가 담당한다.

브라우저 환경과 Node.js 환경에 대해 짚고 넘어가자.




브라우저 환경

811B097A-C43F-4279-BA46-AC227AFA4AF0

위 그림과 같이, 실제로 비동기 호출을 위해 사용하는 setTimeout이나 XMLHttpRequest와 같은 함수들은 자바스크립트 엔진이 아닌 Web API 영역에 따로 정의되어 있다.

또한 이벤트 루프, 태스크 큐 또한 자바스크립트 엔진 외부에 구현되어 있는 것을 확인할 수 있다.


자바스크립트 엔진은 크게 2개의 영역으로 구분할 수 있다.

  • 콜 스택 — 소스코드 평가 과정에서 생성된 실행 컨텍스트가 추가되고 제거되는 스택 자료구조
  • — 객체가 저장되는 메모리 공간 (데이터 저장)


함수를 호출하면 함수 실행 컨텍스트가 순차적으로 콜 스택에 푸쉬되어 실행된다. 자바스크립트 엔진은 단 하나의 콜 스택을 사용하기 때문에 최상위 실행 컨텍스트가 종료되어 콜 스택에서 제거되기 전까지는 다른 어떤 태스크도 실행되지 않는다.

힙의 경우, 어떠한 값과 변수를 저장한다. 가변값인 객체는 불변값인 원시 타입과 달리 크기가 정해져 있지 않으므로, 할당해야 할 메모리 공간의 크기를 런타임 시점에 결정해야한다. (동적 할당) 따라서 힙은 구조화되어 있지 않다는 특징이 있다.


이처럼 콜 스택과 힙으로 구성되어 있는 자바스크립트 엔진은 단순히 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행한다.

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




Node.js 환경

Untitled

https://chathuranga94.medium.com/nodejs-architecture-concurrency-model-f71da5f53d1d

Node.js는 비동기 IO를 지원하기 위해 libuv 라이브러리를 사용하며, 이 라이브러리가 이벤트 루프를 제공한다. 자바스크립트 엔진은 비동기 작업을 위해 Node.js의 API를 호출하며, 이때 넘겨진 콜백은 libuv의 이벤트 루프를 통해 스케쥴되고 실행된다.


즉 자바스크립트가 ‘싱글 스레드’ 기반 언어라는 말은, ‘자바스크립트 엔진이 단일 호출 스택을 사용한다’는 관점에서만 사실이다.

실제 자바스크립트가 구동되는 환경(브라우저, Node.js)에서는 여러개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바스크립트 엔진과 상호 연동하기 위해 사용되는 장치가 바로 ‘이벤트 루프’인 것이다.





03. 태스크 큐와 이벤트 루프

태스크 큐는 콜백 함수들이 대기하는 큐(FIFO)형태의 배열이며, 이벤트 루프는 호출 스택(Call Stack)이 비워질 때마다 콜백 함수를 꺼내와 실행해준다.


아래 예시를 보자.

function function01() {
  console.log("function 01");
}

function function02() {
  console.log("function 02");
}

setTimeout(function01, 500); // 500ms 후에 function01 함수 호출
function02();
  1. 전역 스코프에 존재하는 function01function02가 평가되어 전역 실행 컨텍스트 생성
  2. 코드를 읽어내려가다가, 상단에 위치한 setTimeout 함수 호출 → setTimeout 함수의 실행 컨텍스트가 생성되고 콜 스택에 푸쉬되어 가장 상단의 실행 컨텍스트가 된다.
  3. setTimeout 함수가 실행되며, 콜백 함수를 호출 스케쥴링하고 종료setTimeout 함수의 실행 컨텍스트가 콜 스택에서 팝
  4. 브라우저 — 호출 스케쥴링에 따라 타이머를 설정하고 타이머가 만료(500ms 후)되면, 콜백 함수 function01이 태스크 큐에 푸쉬 자바스크립트 엔진function02함수가 호출되어 실행 컨텍스트가 생성되고 콜 스택에 푸쉬, 이후 function02 함수가 종료되어 콜 스택에서 팝
  5. 실행되던 함수인 function02의 실행 컨텍스트가 콜 스택에서 제거되면, 태스크 큐에서 대기하고 있던 콜백 함수인 function02를 콜 스택에 푸쉬


따라서 코드의 실행 결과는 다음과 같다.

function 02
function 01

비동기로 진행되는 함수 (setTimeout/ setInterval), HTTP 요청, 이벤트 핸들러는 브라우저에 의해 테스크 큐로 이동하게 되는데, 동기적으로 실행하는 함수가 모두 실행이 완료되고 콜 스택에서 팝된 이후에 테스크 큐에 먼저 들어온 함수부터 차례로 콜 스택에 푸쉬된다.


정리하면 다음과 같다.
- 모든 비동기 API들은 작업이 완료되면, 콜백 함수를 태스크 큐에 추가한다.
- 이벤트 루프는 ‘현재 실행중인 태스크가 없을 때(주로 콜 스택이 비워졌을 때)’ 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다.

따라서 이벤트 루프는 ‘현재 실행중인 태스크가 없는지’와 ‘태스크 큐에 태스크가 있는지’를 반복적으로 확인하며 브라우저와 자바스크립트 엔진의 상호 작용을 돕는다.





04. 프로미스와 이벤트 루프

프로미스의 후속 처리 메서드가 포함된 아래 예시 코드를 보자.

setTimeout(() => console.log(1), 0);

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

프로미스의 후속 처리 메서드 또한 비동기적으로 동작하므로, 1 > 2 > 3의 순서로 출력될 것으로 보인다. 지금까지 살펴본 개념에 의하면, 태스크 큐에 콜백 함수가 차례로 쌓이고, 순차적으로 콜 스택에 추가되어 실행될 것이기 때문이다.

하지만 실제 결과는 2 > 3 > 1 순으로 출력되는 것을 확인할 수 있다.

이 이유는 프로미스는 마이크로테스크를 사용하기 때문이다.




마이크로 태스크 큐

마이크로 태스크는 쉽게 말해 일반 태스크보다 더 높은 우선순위를 갖는 태스크이다.

즉 태스크 큐에 대기중인 태스크가 있더라도, 마이크로 태스크 큐에 태스크가 존재하면 먼저 실행된다.


따라서 위의 예시 코드의 동작 방식은 다음과 같다.

  1. setTimeout 함수 실행 컨텍스트가 생성되어 콜스택에 푸쉬되고, 콜백 함수를 호출 스케쥴링한 후 종료된다. (콜 스택에서 팝)
  2. setTimeout 콜백 함수의 타이머가 종료되면, 콜백 함수가 태스크 큐에 추가된다. (console.log(1))
  3. 프로미스 then 메소드 실행 컨텍스트가 생성되고, 콜백 함수를 태스크 큐가 아닌 별도의 마이크로 태스크 큐에 추가한다. (console.log(2))
  4. then 메소드의 실행이 완료되면 이벤트 루프는 마이크로 태스크 큐가 먼저 비었는지 확인한다. 마이크로 테스크 큐에 콜백 함수가 있으므로 이(console.log(2))를 실행한다. > 2 출력
  5. 콜백 함수가 실행되고 나면, 두번째 then 메소드가 실행되어 콜백 함수(console.log(3))을 마이크로 태스크 큐에 추가한다.
  6. 두번째 then 메소드의 실행이 완료되면, 이벤트 루프는 또 다시 마이크로 태스크 큐가 먼저 비었는지 확인한다. 마이크로 태스크 큐에 콜백 함수가 있으므로 이(console.log(3))을 실행한다. > 3 출력
  7. 이후에 마이크로 태스크 큐가 비었는지 또 다시 확인하게 되고, 비어있음을 확인한 후 태스크 큐를 점검해 콜백 함수를 실행한다. (console.log(1)) > 1 출력


정리하자면 마이크로 태스크 큐는 태스크 큐보다 우선순위가 높다.
따라서 이벤트 루프는 마이크로 태스크 큐에 쌓인 태스크를 먼저 콜 스택에 올려준 뒤, 태스크 큐의 잔여 태스크를 콜 스택에 올린다.





05. 정리

자바스크립트

  • 자바스크립트는 싱글 스레드 기반의 언어이며, 동시성을 지원하기 위해 이벤트 루프를 이용한다.
  • 동시성에 대한 처리는 자바스크립트를 구성하는 환경, 즉 브라우저나 Node.js가 담당한다.


자바스크립트 엔진의 콜 스택과 힙

  • 콜 스택이란, 소스코드 평가 과정에서 생성된 실행 컨텍스트가 추가되고 제거되는 스택 자료구조이다.
  • 이란, 객체가 저장되는 메모리 공간으로 구조화되어 있지 않다는 특징이 있다.


이벤트 루프

  • 이벤트 루프란 콜 스택의 ‘현재 실행중인 태스크가 없는지’와 태스크 큐에 ‘대기중인 콜백 함수들이 있는지’반복적으로 확인하며 브라우저와 자바스크립트 엔진의 상호 작용을 돕는 장치이다.


태스크 큐 / 마이크로 태스크 큐

  • 태스크 큐는 콜백 함수들이 대기하는 큐(FIFO) 형태의 배열이다.
  • 마이크로 태스크 큐는 일반 태스크보다 우선순위가 높은 콜백 함수들이 대기하는 큐 형태의 배열이다. (프로미스 메서드)


이벤트 루프는 호출 스택(Call Stack)이 비워질 때마다 태스크 큐를 확인한다. 우선 마이크로 태스크 큐에 쌓인 태스크를 먼저 콜 스택에 올려준 뒤, 태스크 큐의 잔여 태스크를 콜 스택에 올린다.






출처

NHN Cloud 기술 블로그 - 자바스크립트와 이벤트 루프

prepare_frontend_interview-비동기-프로그래밍

댓글남기기