본문 바로가기

개발공부/Javascript

[JS] 이벤트 루프

싱글 스레드 언어인 자바스크립트가 비동기가 가능한 이유

이벤트 루프와 같은 비동기 코드 실행 원리를 알고 개발을 해야 한다. 효율적이고 전문적인 코드 작성을 가능하게 한다. 어떻게 코드가 실행되는지를 너머서 어떤 왜 비동기 방식이 필요한지, 어떤 상황에서 비동기 코드가 우선되어야 하는지를 구분할 수 있다.

싱글 스레드란?

한번에 하나의 작업만 수행할 수 있는 프로그래밍 언어이다. 해당 언어의 실행 환경이 단일 실행 흐름을 사용한다는 것을 뜻한다. JS는 대표적인 싱글 스레드 언어이다. 싱글 스레드 모델에서는 코드가 순차적으로 실행이 되는데, 한 작업이 완료된 후에 다른 작업이 시작된다. 이 모델의 주요 이점은 프로그램의 흐름을 이해하고 디버깅하기가 쉽다는 것이다. 한 작업이 많은 시간을 소요할 경우, 다른 작업이 이전 작업의 종료를 기다려야 하기 때문에, 애플리케이션의 반응성이 다소 저하될 수 있다. JS는 이벤트 루프와 비동기 프로그래밍 모델을 사용하여 싱글 스레드의 한계를 극복한다. 비동기 콜백, 프로미스, async, await를 통해 동시성을 관리한다. 또한, 네트워크 요청과 같은 시간이 많이 소요되는 작업을 백그라운드에서 처리할 수 있도록 한다. 이를 통해, 메인스레드가 차단되지 않고, ui가 계속 반응할 수 있게 한다. 결론적으로 JS는 싱글스레드 언어이지만, 비동기 프로그래밍 모델과 이벤트 루프를 통해 복잡한 앱과 높은 사용자 상호작용을 필요로 하는 웹앱을 효과적으로 구현할 수 있다.

JS가 싱글스레드 기반으로 설계된 배경은 초기에 JS는 웹페이지의 동적요소를 추가하는 간단한 스크립팅 언어로 개발이 되었다. 당시에는 멀티 코어 프로세서가 일반적이지 않았고, 웹애플리케이션의 복잡성도 오늘날에 비해서 상대적으로 낮았다. 이런 환경에서 JS의 주사용 사례를 지원하기 위해서 단일 스레드 모델만으로도 충분했다. 단일 스레드 모델은 메모리 사용량을 최소화 하고, 언어의 복잡성을 줄이며 개발기간을 단축 시키는데 장점이 있었다. 하지만, 단일 스레드 모델은 동시성을 처리하는 데 있어 제한적이고, 이로인해 응답성이 저하될 수 있는 단점이 있다. 이를 해결하기 위해, JS는 브라우저가 제공하는 멀티 스레딩 기능을 활용하여 비동기 프로그래밍 모델을 채택하였다. 이 모델의 핵심이 이벤트 루프이다. 이벤트 루프는 단일 스레드 환경에서도 비동기 이벤트를 효과적으로 처리할 수 있게 하며, JS 앱은 높은 수준의 응답성을 유지할 수 있다. JS에서 웹워커스 라는 기능을 지원해서 메인 스레드와는 별도로 백그라운드에서 스크립트를 실행할 수 있는 방법을 제공한다. 이를 통해 무거운 작업을 별도의 스레드로 넘김으로써, ui의 버벅이는 현상없이 복잡하고 시간 소요가 많이 되는 작업을 수행할 수 있다.

싱글 스레드를 콘솔에 입력하여 실행

```jsx
const func1 = () => {
    const func2 = () => {
        const func3 = () => {
            const func4 = () => {
                const func5 = () => {
                    debugger;
                }
                func5();
                debugger;
            }
            func4();
            debugger;
        }
        func3();
        debugger;
    }
    func2();
    debugger;
}
func1();
debugger;
```

![Untitled](https://prod-files-secure.s3.us-west-2.amazonaws.com/7009f0c0-ae47-4d0d-bc73-5bd6a1950c98/09264d29-341b-4fc4-a48c-e68ed8a62ae3/Untitled.png)

JS는 싱글스레드 방식으로 작동하고, 함수 호출이 LIFO 방식으로 콜스택에 관리되는 것을 확인할 수 있다. 만약, 반복문을 잘못 작성해서 콜스택이 너무 많이 쌓이면 어떻게 될까?

```jsx
const loop = () => {
    loop();
}
loop();
```

![Untitled](https://prod-files-secure.s3.us-west-2.amazonaws.com/7009f0c0-ae47-4d0d-bc73-5bd6a1950c98/993f0983-e8f4-4a9e-9808-a1dd033ab2a0/Untitled.png)

함수가 자기 자신을 계속 호출하기 때문에, 콜 스택의 최대 크기를 초과하기 때문에 이 오류를 발생 시킨다. JS 엔진은 함수 호출을 관리하기 위해 콜스택을 사용한다. 이 스택에는 실행 중인 함수 호출 기록이 저장된다. 각 함수 호출은 스택의 프레임을 추가해서 함수 실행이 완료되면, 해당 프레임이 스택에서 제거된다. 이 콜 스택의 크기가 제한되어 있기 때문에, 크기는 브라우저나 JS 엔진에 따라서 다르긴하지만, 너무 많이 중첩되서 최대 크기를 초과하면 JS 엔진은 에러를 발생시키고 해당 스크립트의 실행을 중지 시킨다.

싱글스레드에서 비동기가 가능한 이유는 무엇일까?

자바스크립트 런타임, Web API, 콜백큐, 이벤트 루프의 상호작용을 통해서 이루어진다. 이러한 구성요소들이 결합하여, 동시성을 관리하고 효율적으로 비동기 작업을 처리한다. JS 런타임 환경은 JS 코드를 실행할 수 있는 환경을 제공한다. 이 환경은 JS 엔진이 포함되어있고, JS 엔진은 코드를 해석하고 실행하는 역할을 한다. 런타임 환경은 Web API, 이벤트 루프, 콜백 큐와 같은 추가적인 구성 요소를 포함하여, 비동기작업을 지원한다. Web API는 브라우저가 제공하는 비동기 기능의 집합이다. 콘솔에 윈도우를 입력하면 브라우저에 내장된 JS 객체가 나오는데, 이것이 바로 Web API 이다. JS 엔진에 있는 메서드가 아닌 브라우저의 메서드이다. JS의 V8 엔진에는 WebAPI 메서드인 setTimeout이나 http 관련된 비동기 메서드가 없다. setTimeout 같은 Web API는 JS 엔진 외부에 구현되어 있다고 볼 수 있다. 이러한 API는 비동기 작업을 수행하고 싱글 스레드와는 독립적으로 작동한다고 볼 수  있다. 콜백 큐는 비동기 작업에 대한 콜백 함수를 임시로 저장하는 일의 대기열이다. Web API에 의해 시작된 비동기 작업이 완료되면, 해당 작업의 콜백함수는 콜백 큐에 추가가 된다. 콜백 큐는 FIFO 방식으로 관리가 된다. 이벤트 루프는 JS 런타임 환경의 중요한 구성요소로, 콜스택과 콜백 큐의 상태를 지속적으로 확인하여, 콜스택이 비어있고 콜백큐에 대기 중인 콜백 함수가 있을 경우, 이벤트 루프는 콜백큐에서 콜스택으로 이동 시키고 실행한다. 이런 과정을 통해 비동기 작업의 결과가 적잘한 시점에 처리된다. 이벤트 루프와 WebAPI 의 상호작용은 JS가 싱글스레드 임에도 불구하고 비동기 작업을 효율적으로 처리할 수 있게 해주고, JS 엔진은 코드를 순차적으로 실행하는 동안에 비동기 작업은 병렬로 진행이 되고, 그 결과는 콜백 큐를 통해 순차적으로 처리가 된다. 이러한 메커니즘 때문에 JS 애플리케이션은 동시에 여러 작업을 진행할 수 있고, UI가 멈추지 않고 반응할 수 있다.

예제 코드

```jsx
const function1 = () => {console.log('hi')};
const function2 = () => fetch('http://jsonplaceholder.typicode.com/posts/1').then(response => response.json().then(console.log));
const function3 = () => console.log('bye');

function1();
function2();
function3();
```

`result console`

```
hi
bye
{
  userId: 1,
  id: 1,
  title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
  body: 'quia et suscipit\n' +
    'suscipit recusandae consequuntur expedita et cum\n' +
    'reprehenderit molestiae ut ut quas totam\n' +
    'nostrum rerum est autem sunt rem eveniet architecto'
}

```

```
hi
bye
{
  userId: 1,
  id: 1,
  title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
  body: 'quia et suscipit\n' +
    'suscipit recusandae consequuntur expedita et cum\n' +
    'reprehenderit molestiae ut ut quas totam\n' +
    'nostrum rerum est autem sunt rem eveniet architecto'
}

```

function1과 function3는 동기적으로 처리가 되고, function2는 비동기적으로 처리된다.

중간 정리

싱글스레드: 프로그램이나 프로세스가 한 번에 하나의 명령이나 작업만 처리하는 구성이다. JS 엔진이 싱글 스레드로 작동한다는 것은 JS코드의 실행이 단일 실행 컨텍스트, 단일 콜스택으로 순차적으로 이루어진다는 것을 의미한다. 이러한 특성은 코드의 예측 가능성과 단순성을 높여준다는 장점이 있지만, 한 번에 하나의 작업만 처리할 수 있다는 제한이 따른다. 싱글 스레드에서 비동기 처리는 JS에서 런타임환경, 브라우저 또는 Node.js 와 같은 환경이 제공하는 비동기 API를 통해 실행이 된다. 이런 환경은 JS 엔진 외부에서 작동하는 이벤트 루프나, 기타 메커니즘을 포함한다. 이벤트 루프는 동시성을 모방한다. 비동기 이벤트 예를 들어, setTimeout이나 네트워크 요청이 발생하면, 해당 이벤트에 대한 콜백을 이벤트 큐에 넣는다. 그리고 JS 엔진의 콜스택이 비게 되면, 이벤트 루프는 이벤트 큐에서 대기 중인 콜백을 콜스택으로 이동시켜 실행을 해서 싱글 스레드 환경에서도 비동기가 가능하도록 한다.

싱글 스레드 환경에서 비동기 코드는 런타임 환경에서 어떻게 실행이 되는 것일까? 비동기 코드의 실행 메커니즘은 JS의 런타임 환경, 특히 이벤트 루프와 밀접하게 연관되어 있다. 비동기 코드의 실행 과정을 이해하기 위해서는 콜스택, WebAPI, 콜백 큐, 이벤트 루프에 대해 잘 알아야 한다. 함수를 호출할 때 콜스택에 push가 되고 작업이 완료되면 pop이 된다. 콜스택은 LIFO 규칙을 따른다. WebAPI는 비동기 작업이 발생하면, JS엔진이 아닌 브라우저나 Node.js 환경에서 제공이 된다. 이 JS 실행 컨텍스트의 콜스택 밖에서 비동기 작업을 처리한다고 볼 수 있다. WebAPI에서 처리된 비동기 작업이 끝나는 시점에 콜백함수와 함께 콜백 큐에 추가가 된다. 이 큐는 FIFO 규칙을 따른다. 이벤트 루프는 콜스택과 콜백큐를 지속적으로 확인한다. 콜스택의 작업이 비어있을 떄 이벤트 루프는 콜백 큐에서 대기중인 작업을 콜 스택으로 이동 시킨다. 이로 인해 콜백함수가  실행된다. 이 과정을 통해 JS는 싱글 스레드 환경에서도 비동기 작업을 효과적으로 처리할 수 있다. 이러한 Web API나 이벤트 루프의 도움을 통해 비동기작업이 메인스레드를 차단하지 않으면서도 효율적으로 수행될 수 있다. 이는 JS 앱의 반응성과 성능에 중요한 역할을 한다.

Q. setTimeout에 5초를 할당하면, 정확히 5초 후에 실행이 될까? 최소 지연 시간의 옵션에 가깝다. 최소한 5초 이후에 실행이 된다. 정확히 시간이 보장되지 않는 이유는 무엇일까? 런타임 환경에서 살펴보자.

![setTimeout 최소시간.png](https://prod-files-secure.s3.us-west-2.amazonaws.com/7009f0c0-ae47-4d0d-bc73-5bd6a1950c98/8f5a13b0-26b5-4e4c-a5eb-f5f63df14623/setTimeout_%EC%B5%9C%EC%86%8C%EC%8B%9C%EA%B0%84.png)

setTimeout을 호출하는 function1이라는 함수가 실행되면 콜스택에서 실행될 때 setTimeout은 Web API이기 때문에 브라우저의 WebAPI로 이동이 되어 실행이 된다. (모든 윈도우 객체의 API가 무조건 브라우저 WebAPI에서 실행이 되는 것이 아니라, 윈도우 객체의 비동기 API 만 WebAPI에서 실행이 된다.) WebAPI 영역에서 setTimeout이 실행되면, setTimeout에 지정된 시간이 경과한 후에 함수가 실행이 된다. 지연시간이 경과하면 콜백함수는 콜백 큐로 이동을 하고, 콜스택이 비어있을 때, 이벤트 루프에 의해서 콜백큐에서 대기 중인 함수를 콜스택으로 이동시켜서 실행이 된다. 실행이 되고 나서는 제거가 된다. 따라서 전체적인 맥락에서 setTimeout에 지정된 지연 시간은 WebAPI영역에서의 대기 시간을 의미한다. 콜백함수가 콜백 큐로 이동하고 그 다음에 콜스택으로 이동되기 때문에, 그 과정에서 추가적인 시간이 소요될 수 있다. setTimeout에 의한 콜백 실행이 예정된 시간보다 늦어질 수밖에 없다. 이러한 구조 때문에 setTimeout은 지연시간을 완벽히 보장할 수 없다.

콜백큐

콜백큐는 비동기 작업의 결과로 발생하는 콜백함수들을 관리하는 대기열이다. 콜백큐는 이벤트 루프에 의해 관리가 되고, 콜스택이 비어있을 때, 콜백함수들을 순서대로 실행하기 위해 사용된다. 단순한 FIFO 구조의 콜백큐 외에도 비동기 처리를 더 세밀하게 관리하기 위한 다양한 종류의 콜백 큐가 있다.

MicroTask Queue: Promise와 같은 콜백을 처리한다. 현재 실행 중인 스크립트가 종료되고, 콜스택이 비어있을 때, 이벤트 루프는 MicoTask Queue를 먼저 확인한다. MicoTask Queue에 대기 중인 작업이 있다면, 이벤트 루프는 모든 MicroTask가 완료될 때까지 순차적으로 콜스택으로 이동시켜 실행하고 MicroTask 처리가 완료되면, 그 이후에 MacroTask Queue 처리를 한다. (setTimeout, setInterval 이벤트 등) MacroTask Queue 에서는 하나의 Task가 콜스택으로 이동해 실행한 후에, 다시 MicroTask Queue를 확인하고, MicroTask Queue처리가 된 이후에 다시에 MacroTask Queue 로 넘어와서 처리를 한다. 이러한 구조 때문에, JS는 Promise와 같은 MicoTask Queue를 더 높은 우선 순위로 처리할 수 있다. 이를 통해, 더 반응성 높은 비동기 코드 실행을 가능하게 한다. MicroTask Queue의 작업들은 같은 이벤트 루프 사이클 내에서 가능한 한 빨리 처리가 되고, MacroTask Queue 사이에서 실행이 된다. 이러한 세분화된 큐 관리는 JS의 비동기 작업의 성능과 반응성을 최적화 하는데 중요한 역할을 한다.

코드 실습

```jsx
setTimeout(function () {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    console.log('Promise');
})
```

`result console`

```
Promise
setTimeout
```

이벤트 루프는 MicroTask Queue를 먼저 확인 후 모든 작업을 처리하고

MicroTask가 처리된 이후에 이벤트 루프는 setTimeout과 같은 MacroTask Queue를 확인한다. 따라서, Promise 콘솔이 먼저 출력되고, 이후에 setTimeout이 출력된다. 따라서 같은 비동기 코드라고 하더라도 순서가 다르게 처리되는 것을 볼 수 있다. MicroTask가 먼저 처리되는 이유는 Promise같은 경우에는 비동기 연산을 다루는 주요 메커니즘 중 하나 이기 때문에, Promise의 결과 값이나 거부 값을 다루는 콜백은 MicroTask Queue에서 먼저 처리가 된다. 비동기 연산의 결과가 가능한 한 빠르게 반영될 수 있도록 하여, 앱의 반응성을 향상 시키기 위함이다.

MicroTask와 MacroTask 구분을 통해서 이벤트 루프는 비동기 작업들을 효율적으로 관리할 수 있다. MicroTask는 작업의 양이 많아도 빠르게 처리될 수 있도록 설계가 되어있어서, 스크립트의 실행, 사용자의 상호작용, 렌더링 사이클과 같은 중요한 작업들 사이에서 빠르게 중간 결과를 처리할 수 있다.

**최종 정리**

이벤트 루프는 JS의 비동기 동작을 관리하는 핵심 메커니즘이다. 브라우저와 Node.js 와 같은 JS 런타임 환경 모두에서 중요한 역할을 수행하고, 이 메커니즘은 싱글 스레드 언어임에도 비동기 작업을 효율적으로 처리할 수 있도록 한다. 이벤트 루프는 코드의 실행, 이벤트 수집 및 처리, 콜백함수의 실행을 순환적으로 관리한다. 이 과정에서 주요 구성 요소는 콜스택, WebAPI, 콜백큐이다. 콜스택은 js 함수의 호출을 기록한다. 함수가 실행을 마치면, 콜스택에서 제거가 되고, 콜스택이 비어있어야 새로운 작업이 시작될 수 있다. WebAPI는 JS 엔진 외부에서 실행되는 비동기 작업을 뜻한다. Promise나 setTimeout은 WebAPI를 통해서 처리가 된다. 이 작업이 완료되면, 콜백함수를 통해 콜백 큐로 전달이 되고, 콜백큐는 실행을 대기하고 있다가 콜스택이 비어져있을 때, 콜백큐에서 함수를 하나씩 가져와서 콜스택으로 옮기고 실행을 한다.

 

[참고]

패스트캠퍼스 강의 (내 생에 마지막 JavaScript : 기초 문법부터 실무 웹 개발까지 한 번에 끝내기 초격차 패키지 Online) 를 기반으로 작성하였고, 추가 서치 내용을 정리하였습니다.

'개발공부 > Javascript' 카테고리의 다른 글

[JS]프로토타입  (1) 2024.04.28
[JS] 비동기  (0) 2024.04.10
[JS] 이벤트 버블링과 캡처링  (0) 2024.04.04
[JS] 호이스팅  (0) 2024.03.28
[JS] 기본형 데이터와 참조형 데이터  (0) 2024.03.24