들어가기 전
자바스크립트는 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드 방식으로 동작하는 특징을 가진다. 이 때 싱글 스레드 방식은 특정 태스크가 처리에 시간이 걸리는 경우 블로킹이 발생하게 되는데, 현재 실행 중인 태스크가 종료될 때까지 다음 태스크가 대기하는 동기 처리방식으로 동작한다. 그런데 자바스크립트를 사용하다보면 현재 실행 중인 태스크가 종료되지 않았음에도 다음 태스크가 실행되는 논블로킹 형태의 비동기 처리 방식으로 동작하는 것을 볼 수 있다. 자바스크립트는 단일 스레드 언어인데 어떻게 동시에 여러 작업을 처리할 수 있는 것일까? 같이 한 번 알아보도록 하자.
자바스크립트 엔진
자바스크립트 런타임 환경인 브라우저는 기본적으로 자바스크립트 엔진을 제공하는데, 자바스크립트 엔진은 두 가지 영역으로 구분가능하다.
1️⃣ 콜 스택: 실행컨텍스트 포스팅에서 언급했던 것처럼 소스코드가 평가되면 실행컨텍스트가 생성되는데, 이후 해당 실행컨텍스트는 새로운 프레임으로 콜 스택에 푸시된다. 예를 들어 특정 함수가 호출되면 해당 함수 실행 컨텍스트가 생성되고 순차적으로 콜 스택에 푸시되어 순차적으로 실행된다. 실행이 종료되어 콜 스택에서 제거되기 전까지는 다른 태스크는 실행되지 않는다.
2️⃣ 힙 메모리: 힙은 객체와 같은 참조타입 데이터가 저장되는 메모리 공간이다. (여담이지만 여기서 알 수 있는 사실은 원시 값은 각각의 변수에 직접 저장되기 때문에 콜 스택에 푸시되어있는 해당 실행컨텍스트에서 참조하여 사용하지만, 객체는 원시 값과 달리 크기가 정해져있지 않아 동적 할당되어 각각의 변수에 힙 메모리 주소를 가리키는 참조 값이 저장되고 해당 주소를 참조하여 객체를 사용한다.)
이벤트 루프와 태스크 큐
브라우저 환경은 자바스크립트 엔진 이외에도 여러 가지 기능을 제공한다.
1️⃣ Web APIs: 자바스크립트 런타임 환경인 브라우저에서 제공하는 인터페이스로 DOM, Ajax, 타이머등을 제공하는데, 해당 비동기 함수는 자바스크립트 엔진이 아닌 Web APIs에 의해 처리된다.
2️⃣ 태스크 큐: 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 장소로 콜 스택으로 가기위한 대기열이라고 할 수 있다. 태스크 큐는 또한 매크로 태스크 큐, 마이크로 태스크 큐가 존재한다. 이에 대해서는 뒤에서 알아보도록 하자.
3️⃣ 이벤트 루프: 이벤트 루프는 콜 스택과 태스크 큐를 반복해서 확인하는 작업을 진행하고, 태스크 큐에 대기 중인 함수가 존재하고 콜 스택에 현재 실행 중인 실행 컨텍스트가 존재하지 않는다면 이벤트 루프는 태스크 큐에 대기 중인 함수를 순차적으로 콜 스택으로 이동시킨다.
따라서 자바스크립트 런타임 환경인 브라우저 환경을 그림으로 나타내면 다음과 같은 그림으로 나타낼 수 있다.
마이크로 태스크 큐 vs 매크로 태스크 큐
마이크로 태스크 큐와 매크로 태스크 큐 모두 비동기 함수의 콜백을 처리하는 태스크 큐임은 동일하지만 몇가지 차이점이 존재한다.
매크로 태스크 큐는 setTimeout, setInterval, Ajax등의 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 보관되는 반면
마이크로 태스크 큐는 Promise의 콜백 함수 또는 MutationObserver의 콜백 함수를 보관한다.
위의 두 태스크 큐는 우선순위에도 차이를 보이는데, 마이크로 태스크 큐는 매크로 태스크 큐보다 높은 우선순위를 갖는다. 따라서 콜 스택이 비어있고 마이크로 태스크 큐와 매크로 태스크 큐에 모두 태스크가 존재한다면 마이크로 태스크 큐에 있는 함수를 이벤트 루프가 순차적으로 콜 스택으로 이동시켜 실행되고, 마이크로 태스크 큐가 비면 매크로 태스크 큐에 존재하는 함수가 순차적으로 이벤트 루프에 의해 콜 스택으로 이동되어 실행된다.
다음은 이해를 돕기위해 동작 과정을 나타내는 그림이다.
동작 과정
이제 예시를 통해 싱글스레드 언어인 자바스크립트가 비동기 방식으로 동작하는 과정에 대해서 알아보자.
console.log('Start');
// 매크로 태스크 큐에 등록된 비동기 작업 (setTimeout)
setTimeout(() => {
console.log('Macro Task');
}, 0);
// 마이크로 태스크 큐에 등록된 비동기 작업 (Promise)
Promise.resolve().then(() => {
console.log('Micro Task');
});
console.log('End');
위와 같은 코드가 있다고 가정했을 때, 콘솔에는 Start -> End -> Micro Task -> Macro Task 순으로 출력되는 것을 확인할 수 있다.
1️⃣ 전역 코드가 평가되어 전역 실행 컨텍스트가 생성되고 새로운 프레임으로 콜 스택에 푸시된다.
2️⃣ console.log('Start')가 실행되어 'Start'가 출력된다.
3️⃣ setTimeout 함수가 실행되면 setTimeout 함수의 실행 컨텍스트가 생성되고 새로운 프레임으로 콜 스택에 푸시된 후, 해당 비동기 함수의 콜백 함수를 호출 스케줄링하고 종료되어 콜 스택에서 제거된다. 호출 스케줄링을 통해 지정된 딜레이 시간이 지나면 Web APIs가 setTimeout 비동기 함수의 콜백 함수를 매크로 태스크 큐에 푸시한다.
4️⃣ Promise의 후속 처리 메서드는 매크로 태스크 큐가 아닌 마이크로 태스크 큐에 푸시된다.
5️⃣ console.log('End')가 실행되어 'End'가 출력된다.
6️⃣ 전역 코드 실행이 종료되어 콜 스택에서 전역 실행 컨텍스트 프레임이 제거된다.
7️⃣ 이벤트 루프에 의해 콜 스택이 비어있는 상태임을 확인하고 마이크로 태스크 큐와 매크로 태스크 큐를 확인한다.
8️⃣ 우선순위가 높은 마이크로 태스크 큐에 보관되어 있는 Promise의 후속 처리 메서드를 콜스택으로 이동시켜 실행한다.('Micro Task' 출력)
9️⃣ 마이크로 태스크 큐가 비어있음을 확인하고 이벤트 루프에 의해 매크로 태스크 큐에 보관되어 있는 setTimeout 콜백 함수를 콜 스택으로 이동시켜 실행한다.('Macro Task' 출력)
참고
'개발 지식 정리 > Javascript' 카테고리의 다른 글
Promise (0) | 2023.09.09 |
---|---|
requestAnimationFrame (0) | 2023.08.30 |
타이머 (0) | 2023.07.18 |
DOM (0) | 2023.07.14 |
이터러블 (0) | 2023.07.11 |