자바스크립트는 비동기를 어떻게 처리할까?

💛 nalsae·2022년 7월 8일
1
post-thumbnail

🤔 자바스크립트의 동작 원리

 자바스크립트는 보편적으로 싱글 스레드 언어라고 알려져 있다. '싱글'이라는 용어에서 뭔가 하나에 관련되어 있다는 느낌이 든다. 그렇다면 여기서 잠깐, 스레드는 무엇일까? 스레드에 대해 알기 전에 스레드의 상위 개념인 프로세스를 먼저 간단하게 살펴보고자 한다.

💡 프로세스란?
: 메모리가 할당되어 실행 중에 있는 프로그램
: CPU가 할당 순서 및 방법을 정하는 스케줄링의 대상

💡 스레드란?
: 프로세스 내에서 실행되는 작업의 단위

 위의 개념을 바탕으로 자바스크립트의 원리를 다시 정리하면, 자바스크립트는 근본적으로 프로세스 내에서 한 번에 하나의 스레드만 실행된다고 이해할 수 있다. 즉 원래 자바스크립트는 동기적인 처리만 가능하다.

 그러나 우리가 알고 있는 자바스크립트는 한 번에 한 가지 일만 처리하지 않는다. 자바스크립트 파일을 브라우저에서 동작시키면 동시에 많은 작업을 실행할 수 있다. setTimeout 함수의 사용 예제만 봐도 자바스크립트가 동시적으로 작업을 처리한다는 데 이견이 없을 것이다.

🔮 브라우저에 비밀이!?

 그렇다면 어떻게 자바스크립트는 동시성을 처리할 수 있는 것일까? 바로 그 비밀은 브라우저 환경 안에 있다. 특히 브라우저 환경 안에 있는 이벤트 루프가 자바스크립트의 동시성을 지원하는 장치라고 볼 수 있다. 지금부터 그 숨겨진 장치를 한 번 파헤쳐보려고 한다.


⛺ 브라우저 환경이란?

 이벤트 루프에 대해 살펴보기 전에, 이벤트 루프가 포함되어 있는 브라우저 환경에 대해 자세히 알아볼 필요가 있을 것이다. 브라우저 환경에서는 여러 개의 스레드가 사용되고, 이를 통해 자바스크립트의 동시성을 지원한다. 더 자세히 살펴보면 브라우저 환경에는 크게 4가지의 구성 요소가 있다.

(1) 자바스크립트 엔진
(2) Web API
(3) 태스크 큐
(4) 이벤트 루프


🔩 자바스크립트의 심장, 자바스크립트 엔진

 먼저 자바스크립트 엔진은 말 그대로 브라우저에서 자바스크립트 파일을 읽기 위한 장치라고 보면 된다. 자바스크립트로 작성한 코드를 해석하고 실행시켜주는 역할을 한다.

 위의 사진처럼 자바스크립트 엔진 안에는 메모리 힙(Memory Heap)콜 스택(Call Stack)이라는 것이 존재한다.

💾 변수 담당, 메모리 힙

 메모리 힙은 자바스크립트 엔진의 구성 요소 중 하나로, 메모리를 할당 받아서 자바스크립트에서 선언한 변수와 함수, 객체를 저장하는 공간이다.

🧮 함수 담당, 콜 스택

 콜 스택은 자바스크립트로 실행한 함수가 저장되는 공간이다. 자바스크립트가 싱글 스레드 기반의 언어라는 개념은 콜 스택 때문이라고 봐도 무방하다. 자바스크립트 엔진 안에 콜 스택이 하나밖에 없기 때문에 한 번에 하나의 함수만 실행할 수 있는 것이다.

 콜 스택의 함수 실행 순서는 LIFO(Last In First Out), 즉 후입선출의 방식을 따른다. 쉽게 말하면 나중에 들어온 것부터 빠져 나간다는 말이다. 예를 들어 '1 > 2 > 3 > 4'의 순으로 함수가 콜 스택에 쌓였다고 가정하면, '4 > 3 > 2 > 1'의 순으로 함수가 실행된다.

 간단한 예제 코드를 통해 콜 스택의 실제 동작 순서를 자세히 살펴보고자 한다.

function delay() {
   for (var i = 0; i < 100000; i++);
}
function foo() {
   delay();
   bar();
   console.log('foo!'); // (3)
}
function bar() {
   delay();
   console.log('bar!'); // (2)
}
function baz() {
   console.log('baz!'); // (4)
}

setTimeout(baz, 10); // (1)
foo();

💡 실행 순서

(1) setTimeout 함수는 브라우저에게 타이머 이벤트를 요청한 후 스택에서 제거됨
(2) foo 함수가 스택에 추가되고, foo 함수가 내부적으로 실행하는 함수들이 스택에 추가되었다가 제거됨
(3) foo 함수가 실행을 마치면서 스택에서 제거됨
(4) 마지막으로 baz 함수가 스택에 추가되어 실행됨


🌍 날 도와줘, Web API!

 브라우저 환경의 또 다른 구성 요소 중 하나인 Web API는 말 그대로 브라우저에서 제공하는 API를 의미한다. 그렇다면 API란 무엇일까?

💡 API란?

: 운영체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 마드는 인터페이스
: 프로그램들이 서로 상호작용하는 것을 도와주는 매개체
: 애플리케이션과 기기가 원활하게 통신할 수 있도록 돕는 역할

 즉 Web API는 웹 서버와 웹 브라우저 간의 위한 애플리케이션 처리 인터페이스이자, 브라우저 환경에서 쉽게 개발할 수 있도록 돕는 객체 모음들이다.

 쉽게 말하면 Web API는 브라우저의 무언가를 조작할 때 도와주는 역할을 한다. 예를 들면 브라우저 문서를 조작하는 데 쓰이는 DOM 메서드, 웹 페이지 일부를 업데이트할 때 쓰이는 XMLHttpRequest, setTimeout과 같은 타이머 API가 해당된다.

 이러한 Web API들은 전부 비동기 메서드이기 때문에 자바스크립트가 기본적으로 동기적임에도 불구하고 비동기적으로 사용할 수 있게 된다. 또한 각각의 API는 콜백 함수를 가지고 있는데, 콜 스택에서 Web API 메서드 실행이 끝나면 가지고 있는 콜백 함수는 브라우저 환경의 태스크 큐라는 곳으로 넘어가게 된다.


📂 장기 투숙객은 이쪽으로, 태스크 큐

 호출된 Web API가 가지고 있는 콜백 함수들은 태스크 큐라는 곳에 저장된다. 태스크 큐는 자바스크립트 엔진 안의 콜 스택과 달리 FIFO(First In, First Out)의 선입선출 방식으로 함수를 실행한다. 쉽게 말하면 먼저 들어온 것이 먼저 빠져나가는 방식이다. 예를 들어 '1 > 2 > 3 > 4' 순으로 함수가 쌓였다면, 태스크 큐에서는 '1 > 2 > 3 > 4' 순으로 함수를 실행시킨다.

 그렇다면 함수들을 굳이 왜 태스크 큐에 저장하는 것일까? 그 이유는 동시성 처리에 있다. 태스크 큐에 들어오는 함수들은 Ajax 요청, addEventListener, setTimeout처럼 처리가 오래 걸리는 함수들이고, 이를 따로 처리해야만 동시성을 지원할 수 있다.

 이러한 함수들을 왜 따로 처리해야만 할까? addEventListener 함수로 사용자가 클릭하면 동작하는 버튼을 하나 생성했다고 가정해보자. 태스크 큐에서 따로 관리하지 않는다면 자바스크립트 엔진은 함수를 콜 스택에 쌓아둔 채로 사용자의 클릭을 기다릴 것이다. 콜 스택은 한 번에 하나의 함수만 처리할 수 있기 때문에, 결국 아래에 쌓여 있는 다른 함수들은 사용자가 클릭하여 addEventListener 함수가 실행될 때까지 영겁의 시간 동안 기다려야 한다. 즉 동시성을 지원할 수 없게 되는 것이다.


💫 빙빙 돌아가는 회전목마, 이벤트 루프!

 드디어 브라우저 환경의 구성 요소 마지막, 이벤트 루프다. 이벤트 루프는 태스크 큐에서 대기 중인 콜백 함수들을 자바스크립트 엔진의 콜 스택으로 올려 보내는 역할을 한다.

 근데 여기서 아주 중요한 조건이 있다. 바로 콜 스택이 비어 있어야 한다는 점이다. 콜 스택에 실행 중인 함수가 있다면 태스크 큐의 함수들은 실행될 수 없다. 즉 이벤트 루프는 콜 스택이 비어 있는지 그 여부를 우선적으로 확인하고, 만약 콜 스택이 비어 있다면 FIFO 방식으로 태스크 큐의 함수들을 콜 스택에 올려보낸다.

 그리고 이벤트 '루프'라는 이름에서도 유추할 수 있듯이, 이벤트 루프는 위의 과정을 태스크 큐가 텅 비게 될 때까지 계속해서 반복함으로써 자바스크립트의 동시성 지원을 책임진다.


💼 태스크 큐? 다 비켜, 마이크로 태스크!

 이벤트 루프에서 끝나면 정말 좋겠지만, 불행하게도 마이크로 태스크라는 추가적인 개념이 존재한다. 마이크로 태스크는 쉽게 말하면 태스크 큐보다 우선 순위가 높은 큐라고 할 수 있다. 즉 마이크로 태스크 안의 함수들은 태스크 큐 안에 대기중인 함수가 있더라도 먼저 처리된다.

 그렇다면 마이크로 태스크에 들어갈 만큼 중요한 함수들은 뭐가 있을까? 태스크 큐 안에 들어가는 함수들과 비교해보면 다음과 같다.

💡 태스크 큐에 들어가는 함수들
: setTimeout, addEventListener

💡 마이크로태스크에 들어가는 함수들
: Promise, Object.observe, MutationObserver

 위의 함수들 중에서 가장 많이 사용되는 함수는 아무래도 Promise 객체의 콜백 함수들일 것이다. 이를 간단한 예제 코드와 함께 살펴보고자 한다.

setTimeout(function() { // (A)
   console.log('A');
}, 0);
Promise.resolve().then(function() { // (B)
   console.log('B');
}).then(function() { // (C)
   console.log('C');
});

💡 실행 순서

(1) setTimeout 함수는 콜백 (A)를 태스크 큐에 추가함
(2) Promise 객체의 then 메서드는 콜백 (B)를 태스크 큐가 아니라 별도의 마이크로 태스크 큐에 추가함
(3) 이벤트 루프가 일반 태스크 큐가 아닌, 마이크로 태스크가 비었는지를 먼저 확인하고, 콜백 (B) 실행
(4) 콜백 (B) 실행 후 두 번째 then 메서드가 콜백 (C)를 마이크로 태스크에 추가함
(5) 이벤트 루프가 다시 마이크로 태스크를 확인하고 콜백 (C) 실행
(6) 마이크로 태스크가 비었음을 확인한 이벤트 루프가 일반 태스크 큐의 콜백 (A) 실행


🎨 그림과 함께 이해하자!

 지금까지 살펴본 자바스크립트의 동작 원리를 완벽히 이해하기가 쉽지는 않을 것이다. 이 글을 작성하고 있는 나도 완벽히 이해했다고 생각하지 않는다. 아마 추후에 공부하면서 추가적으로 작성할 부분이 분명히 있을 것이라고 생각한다.

 동작 원리를 이해하는 과정이 왜 어려울까 생각해본다면, 아마 추상성에 그 이유가 있을 것이다. 자바스크립트가 동작하는 브라우저 환경이 눈에 직접적으로 보이지 않기 때문에, 이해가 더 어렵게 느껴질 수밖에 없는 것 같다. 그러므로 그림이나 영상을 통해서 이해를 보충하는 것이 효과적이라고 생각한다. 나 역시도 개념을 이해하면서 그림과 영상의 도움을 많이 받았다.

 특히 위의 유튜브 영상이 이해하는 데 아주 큰 도움이 되었다. 그래픽 요소를 통해 실제로 코드가 실행되는 과정을 보여주기 때문에 추상적인 개념을 쉽게 이해할 수 있었다. 이 게시글을 보시는 분들께도 위의 영상이 도움이 되었으면 한다.


🙏 출처
https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop
https://ko.javascript.info/event-loop
https://meetup.toast.com/posts/89
https://developer.mozilla.org/ko/docs/Web/API

profile
𝙸'𝚖 𝚊 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 𝚝𝚛𝚢𝚒𝚗𝚐 𝚝𝚘 𝚜𝚝𝚞𝚍𝚢 𝚊𝚕𝚠𝚊𝚢𝚜. 🤔

0개의 댓글