[JavaScript] JS의 동작원리 - 3 (Event Loop)

coderH·2022년 5월 17일
1

JavaScript 연대기

목록 보기
9/11
post-thumbnail

JS의 동작원리 3편 - 이벤트 루프

오늘은 JS의 동작원리 세번째 편으로 이벤트루프를 중심으로 다뤄보려고 합니다.

주의 ❗
이 글은 NodeJS가 아닌 브라우저 환경에서 동작하는 JavaScript를 기준으로 다루고 있습니다.

먼저 JS는 싱글쓰레드 언어로 싱글쓰레드란 콜 스택(Execution Stack)이 하나만 존재하는것을 의미합니다.

지난편에서 다루었듯이 콜 스택은 코드를 읽으면서 실행 순서에 맞게 함수를 보관하는 역할을 하며
스택 형태로 하나씩 쌓이기 때문에 작업을 처리할 때도 한개씩만 처리할 수 있습니다.

반대로 멀티쓰레드의 경우 콜스택이 여러개이기 때문에 동시에 여러개의 작업을 처리할 수 있습니다.

여기서 프로그램과 프로세스, 쓰레드에 대한 정의를 먼저 짚고 넘어가보겠습니다.

  • 프로그램 : 보통 exe확장자를 가지며 특정 목적을 위해 작성된 코드의 집합으로 SSD, 디스크와 같은 보조기억장치에 저장되는 실행가능한 파일을 말합니다.

  • 프로세스 : 프로그램의 인스턴스이며 OS로부터 자원을 할당받아 프로그램이 동적으로 실행되는것을 말합니다.
    RAM과 같은 주기억장치에 올라가게 되며 인스턴스이기 때문에 JS에서 클래스를 통해 여러개의 객체를 생성할 수 있는것처럼 하나의 프로그램은 여러개의 프로세스가 될 수 있습니다.

  • 쓰레드 : 프로세스 내부의 실행을 위한 작은 단위모든 프로세스는 최소 한개의 쓰레드를 가지고 있으며 만약 쓰레드가 여러개라면 한 프로세스에서 여러 작업을 동시에 수행할 수 있습니다.

예를 들어, 하드에 설치되어 있는 chrome.exe라는 정적인 상태의 프로그램

사용자가 더블클릭하여 실행하면 OS로부터 자원을 할당받으며 내부의 코드를 실행하여 동적인 상태가 됩니다. (프로세스)

이후 사용자의 동작에 따라 알맞은 작업을 처리합니다.(쓰레드)

JS는 싱글쓰레드 언어이기 때문에 코드를 한줄한줄 차례대로 읽고 작업을 수행하는 동기적인 형태로 동작을 해야합니다.

만약, 중간에 시간이 오래 소요되는 작업이 있다면 해당 작업이 완료될 때까지 기다려야만 이후 작업을 진행할 수 있습니다.

console.log("one");

setTimeout(() => {
	console.log("two");
}, 1000);

console.log("three");

위의 코드가 동기적으로 동작한다면 one -> 1초 후 two -> three 순서로 출력됩니다.

하지만 JS는 one -> three -> two 순서로 출력되어 비동기적으로 실행된 결과값을 보여주는데
싱글쓰레드인 JS가 이처럼 비동기적으로 동작할 수 있는 이유는 브라우저에서 제공하는 Web API를 사용하기 때문입니다.

위 코드의 setTimeout은 Web API로 JS엔진이 직접 처리하지 않고 브라우저에게 작업을 넘겨주게 됩니다.

그럼 브라우저에서는 자신이 가지고 있는 타이머를 이용해 1000ms를 측정하게 되고 브라우저가 해당 작업을 처리하는 동안 JS는 그 밑에 있던 코드인 console.log(three)를 먼저 실행하게 되는것입니다.

브라우저는 멀티쓰레드를 사용하기 때문에 여러개의 작업을 동시에 처리할 수 있고
JS는 브라우저의 쓰레드를 같이 사용함으로써 싱글쓰레드임에도 마치 멀티쓰레드처럼 비동기적으로 동작할 수 있게 되는것입니다.

그럼 이 과정이 일어나는 JS의 런타임 환경(실행환경)에서 생성되는 구성요소들은 어떤 것들이 있고 각각의 요소들이 무슨 역할을 하는지 알아보겠습니다.

JS의 런타임 환경 구성요소

위 사진은 JS엔진이 브라우저 환경에서 실행될 때 구성되는 요소들입니다.

이 중 콜스택만 JS의 영역이고 이 외 요소들은 브라우저의 영역입니다.

JS 영역

  • 힙 (Heap)
    힙은 변수와 함수들을 저장하는 공간으로 비정형 메모리입니다.
    비정형 메모리란 데이터가 순서대로 쌓이는 형태가 아니라 메모리 공간에 순서없이 저장되는 형태를 의미하며 힙에 저장된 변수 또는 함수를 찾기 위해서는 Reference type을 가진 변수 데이터에 접근할 때처럼 주소를 통해 접근 합니다.

위와 같이 데이터가 정해진 규칙없이 메모리 힙이라는 공간안에 놓여집니다.

  • 콜 스택
    이전 편에서 다루었듯이 Execution Context및 함수를 스택형태로 저장하는 공간을 말합니다.

브라우저 영역

  • Web API
    JS로부터 넘겨 받은 Web API를 수행하는 공간입니다.

  • 콜백 큐
    이벤트 큐, 태스크 큐, 매크로태스크 큐라고도 불리며
    Web API로 넘겨진 함수가 작업이 완료된 후 해당 콜백함수를 잠시 저장하는 공간입니다.
    콜백 함수는 콜 스택이 비었을 때 이벤트 루프에 의해 콜 스택으로 이동됩니다.

  • 이벤트 루프
    콜 스택과 콜백 큐를 돌면서 만약 콜백 큐에 작업이 있다면 콜 스택을 수시로 확인하며 비어있을 때 콜백함수를 넘겨줌으로써 콜백함수가 실행될 수 있도록 도와주는 역할을 합니다.

위에서 보았던 코드는 내부적으로 아래와 같이 동작합니다.

여기서 유의 할 점은 setTimeout의 딜레이 시간을 0초로 설정해도 가장 마지막에 실행되며
이는 비동기적으로 동작하는 Promise의 메소드들 또한 같은 결과값을 보여줍니다.

const promise = new Promise(function(resolve, reject) {
    resolve("b");
});

console.log("a");

promise.then(res => console.log(res));

console.log("c");

// a -> c -> b 순서로 출력됩니다.

이렇게 동작하는 이유는 JS엔진이 Web API를 만났을 때 실행에 소요되는 시간과는 상관없이 일단 해당 작업을 브라우저에게 넘기기 때문입니다.

Web API 작업에 대한 판단은 JS가 아닌 브라우저가 하기 때문에 브라우저는 해당 작업을 넘겨받고 delay가 0인것을 확인한 후 콜백함수를 콜백 큐에 전달하고 콜 스택이 빈 상태가 되기 전까지 해당 콜백함수는 콜백 큐에서 대기하게 됩니다.

그래서 setTimeout의 delay는 정확히 n초 뒤에 실행되는것이 아닌
최소 n초 이후에 실행되는 최소 지연시간으로 생각해야 합니다.

예를 들어, 1000ms로 설정했을 경우 1초 뒤에 콜 스택이 비어있지 않다면
빈 상태가 되기전까지 콜백함수는 콜백 큐에서 대기하게 되어 실제 콜백함수의 출력시간은 1초가 넘을 수 있습니다.

* TIP
setTimeout의 경우 4ms의 최소지연시간이 있기 때문에 0으로 설정하더라도 콜백함수는 최소 4ms이후에 실행됩니다.

마이크로태스크 큐

JS의 런타임 환경 구성요소에는 사실 콜백 큐뿐만 아니라
렌더 시퀀스, 마이크로태스크 큐 등의 요소들도 있습니다.

이 중 마이크로태스크 큐는 잡큐(Job Queue)라고도 불리는데 이 요소는 콜백 큐처럼 Web API에서 넘겨주는 콜백함수가 잠시 대기하는 장소입니다.

Promise의 then, catch, finallyMutationObserver같은 특정 문법의 콜백함수만 콜백 큐가 아닌 마이크로태스크 큐에 들어가게 됩니다.

그렇다면 왜 JS엔진은 이미 콜백 큐가 있는데 왜 굳이 마이크로태스크 큐라는것을 만들었을까요?

우리가 Promise의 then, catch, finally를 사용하는 이유는 Promise의 결과값이 반환되었을 때 해당 결과값을 기반으로 동기적으로 실행하기 위해서입니다.

콜백 큐의 경우 이벤트 루프가 콜 스택을 확인하며 비어있을때 마다 콜백 함수를 한개씩 콜 스택으로 넘겨주는데 만약 그 사이에 콜 스택에 새로운 작업이 추가되면 추가된 작업을 먼저 마무리한 뒤 다시 콜백함수를 가져오기 때문에 콜백 큐에 있는 콜백함수는 동기적으로 수행되지 않을 수 있습니다.

반면 마이크로태스크 큐의 경우 콜백 큐처럼 콜 스택이 비어있을 때 콜백함수를 가져온다는 점은 같지만
한번 콜백함수를 옮기기 시작하면 마이크로태스크 큐에 콜백함수가 존재하지 않을 때까지 연속적으로 콜백함수를 가져옵니다.

또한 마이크로태스크 큐가 콜백 큐보다 우선순위가 높기 때문에 만약 두 개의 큐에 모두 작업이 들어있다면 마이크로태스크 큐의 작업부터 먼저 처리하게 됩니다.

출처 : https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke

위 내용을 통해 아래 코드의 실행 순서를 유추해 볼 수 있습니다.

  1. Web API가 아닌 a와 b 먼저 출력
  2. 마이크로태스크 큐에 속하는 1, 2, 3이 출력
  3. delay는 0이지만 우선권이 가장 낮은 h가 마지막에 출력

마지막으로 이벤트 루프를 이해하는데 도움이 되는 사이트와 영상을 알려드리고 마무리하려고 합니다.

코드에 따라 이벤트 루프가 어떻게 비동기적으로 동작하는지 시각적으로 볼 수 있는 사이트입니다.

2014년 JS 컨퍼런스에서 Philip Roberts라는 분이 이벤트 루프에 대해서 설명해주는 영상입니다.
이벤트 루프 관련 영상 중 가장 유명한 영상으로 한글 자막도 지원되고 있으니 시청해보시는걸 추천드립니다.

참고 사이트

Javascript 동작원리 - vincent | Medium

JavaScript Visualized: Event Loop - Lydia Hallie | dev.to

자바스크립트와 이벤트 루프 | NHN meetup

EventLoop | MDN

0개의 댓글