자바스크립트 콜 스택은 자바스크립트 엔진에서 코드 실행 흐름을 관리하는 메커니즘으로, 함수 호출과 실행을 추적하는 데 사용됩니다.
콜 스택은 LIFO(Last In, First Out)방식으로 작동하는 데이터 구조입니다. 즉, 가장 마지막에 추가된 작업이 가장 먼저 제거됩니다.
아래에 코드가 실행되면 콜 스택에는 어떤 변화가 생길까요?
function first() {
console.log("First function start");
second();
console.log("First function end");
}
function second() {
console.log("Second function start");
third();
console.log("Second function end");
}
function third() {
console.log("Third function");
}
first();
실행 흐름
1. first() 호출 → first 함수가 콜 스택에 푸시.
2. console.log 실행 → 메시지 출력.
3. second() 호출 → second 함수가 콜 스택에 푸시.
4. third() 호출 → third 함수가 콜 스택에 푸시.
5. console.log 실행 → 메시지 출력 후 third 함수가 스택에서 팝.
6. second 함수가 나머지 실행을 마친 후 팝.
7. first 함수도 실행을 완료한 뒤 팝.
콜 스택 변화
단계 | 콜 스택 내용 |
---|---|
초기 | (비어 있음) |
1 | first |
2 | first -> second |
3 | first + second -> third |
4 | first -> second |
5 | first |
6 | (비어 있음) |
여기서 확인했듯이 자바스크립트 엔진은 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드 방식으로 동작하기 때문에 단 하나의 실행 컨텍스트 스택(콜 스택)을 갖습니다.
싱글 스레드 방식은 한 번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행하는 경우 블로킹(작업중단)이 발생한니다.
아래의 코드를 실행시키면 two 함수는 sleep 함수의 실행이 종료된 이후 호출되므로 3초 이상 호출되지 못하고 블로킹됩니다.
이렇게 현재 실행 중인 태스크가 종료할 때까지 다음에 실행될 테스크가 대기하는 방식을 동기처리라고 합니다. 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료될 때까지 이후 태스크들이 블로킹되는 단점이 존재합니다.
function sleep(func, delay) {
const delayUntil = Date.now() + delay;
while (Date.now() < delayUntil);
func();
}
function one() {
console.log("one");
}
function two() {
console.log("two");
}
sleep(one, 3 * 1000);
two();
// 출력 결과
one
two
아래는 sleep 함수와 비슷한 기능을 수행하는 setTimeout 함수를 사용해보겠습니다.
출력 결과를 보면 setTimeout을 사용한 코드는 코드를 블로킹하지 않고 곧바로 실행합니다. 이러첨 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 비동기처리라고 합니다.
function one() {
console.log("one");
}
function two() {
console.log("two");
}
setTimeout(one, 3 * 1000);
two();
// 출력 결과
two
one
그런데 위에서 자바스크립트 엔진은 싱글 스레드 방식으로 동작한다고 했는데 어떻게 비동기처리가 가능한걸까요?
이어서 알아보겠습니다.
타이머 함수인 setTimeout과 setInterval, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작합니다. 비동기 처리는 이벤트 루프와 태스크 큐와 깊은 관계가 있습니다.
앞서 살펴본 것처럼 싱글 스레드 방식은 한 번에 하나의 태스크만 처리할 수 있다는 것을 의미합니다. 하지만 브라우저가 동작하는 것을 보면 많은 태스크가 동시에 처리되는 것처럼 느껴집니다. 프로그램을 다운 받으면서 웹툰을 본다던가 노래를 들으면서 친구와 대화를 나눌수도 있습니다. 이처럼 자바스크립트의 동시성을 지원하는 것이 바로 이벤트 루프입니다. 이벤트 루프는 브라우저에 내장되어 있는 기능 중 하나입니다.
위 그림에서 Js Engine(자바스크립트 엔진) 부분을 보겠습니다. 자바스크립트 엔진은 크게 2개의 영역으로 구분할 수 있습니다.
힙(Heap)
설명: 힙은 자바스크립트 엔진에서 사용하는 메모리 영역으로, 동적으로 할당된 객체와 데이터를 저장하는 공간입니다.
동적으로 생성된 변수나 객체(예: 배열, 객체 등)의 메모리 관리를 담당합니다.
힙에 저장된 데이터는 고정된 크기가 아니며, 필요에 따라 크기를 동적으로 조정할 수 있습니다.
가비지 컬렉터(Garbage Collector)에 의해 더 이상 참조되지 않는 데이터는 자동으로 제거됩니다.
콜 스택(Call Stack)
설명: 콜 스택은 자바스크립트 함수 호출과 실행 순서를 관리하는 LIFO(Last In, First Out) 방식의 데이터 구조입니다.
함수가 호출되면 해당 함수의 실행 컨텍스트가 스택에 쌓이고, 실행이 완료되면 스택에서 제거됩니다.
코드의 실행 흐름을 제어하며, 동기적으로 작동합니다.
콜 스택과 힙으로 구성되어 있는 자바스크립트 엔진은 단순히 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행할 뿐입니다. 비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당합니다. 예를 들어, 비동기 방식으로 동작하는 setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진이 담당하지만 호출 스케줄링을 위한 타이머 설정과 콜백 함수의 등록은 브라우저 또는 Node.js가 담당한다. 이를 위해 브라우저 환경은 태스크 큐와 이벤트 루프를 제공합니다.
태스크 큐(Task Queue)
태스크 큐는 브라우저에서 실행 대기 중인 비동기 작업의 콜백 함수를 저장하는 대기열입니다.
브라우저가 제공하는 Web APIs(ex. setTimeout
, setInterval
, DOM Events
등)를 통해 비동기로 처리해야 할 작업이 완료되면, 해당 작업의 콜백 함수가 태스크 큐에 추가됩니다.
동작 방식
브라우저는 특정 비동기 작업(ex. 타이머, 이벤트 리스너)의 실행이 완료되면 그 콜백을 태스크 큐에 넣습니다.
태스크 큐에 담긴 함수들은 콜 스택이 비었을 때 실행됩니다. (즉, 현재 실행 중인 코드가 끝난 후 실행)
console.log('1');
setTimeout(() => {
console.log('2'); // 태스크 큐에 들어감
}, 0);
console.log('3');
// 출력 결과
1
3
2
setTimeout
의 콜백은 태스크 큐에 추가되므로 1, 3
이 먼저 출력되고, 이후 콜 스택이 비었을 때 2
가 출력됩니다.
이벤트 루프(Event Loop)
이벤트 루프는 자바스크립트 런타임의 핵심 메커니즘으로, 콜 스택(Call Stack)과 태스크 큐(Task Queue)를 연결하여 비동기 작업을 관리합니다.
이벤트 루프는 계속 실행되면서 다음을 반복합니다:
콜 스택이 비어 있는지 확인.
비어 있다면, 태스크 큐에서 대기 중인 작업을 가져와 콜 스택에 추가하여 실행.
동작 방식
자바스크립트는 싱글 스레드로 작동하며, 콜 스택에 작업을 하나씩 처리합니다.
동기 코드는 즉시 실행되지만, 비동기 코드는 Web APIs에서 처리된 후 태스크 큐에 대기합니다.
이벤트 루프가 태스크 큐를 확인하여, 콜 스택이 비었을 경우 태스크 큐의 작업을 실행합니다.
마이크로 태스크 큐(Microtask Queue)는 프로그래밍, 특히 자바스크립트와 같은 이벤트 기반 언어에서 비동기 작업을 처리하는 데 중요한 역할을 하는 개념입니다. 이는 Event Loop의 일부로, 태스크를 관리하고 실행 순서를 제어하는 메커니즘입니다.
주요 특징
작업의 우선 순위
마이크로 태스크 큐는 콜 스택 큐보다 우선 실행됩니다.
처리 순서
아래 코드에 실행 결과를 스스로 예측해봅시다.
Promise.resolve().then(() => console.log(1));
setTimeout(() => console.log(2), 10);
queueMicrtask(() => {
console.log(3)
queueMicrotask(() => console.log(4))
});
console.log(5)
실행 과정
Promise.resolve()
→ 마이크로태스크 큐에 .then
추가setTimeout()
→ 10ms 후 실행될 콜백을 태스크 큐에 추가queueMicrotask()
→ 마이크로태스크 큐에 작업 추가console.log(5)
→ 즉시 실행콜 스택 실행 후: 메인 스택이 비면 마이크로태스크가 실행됩니다. 마이크로태스크는 큐에서 FIFO로 실행되며, 실행 중 새로운 마이크로태스크가 추가되면 즉시 큐에 들어갑니다.
매크로태스크 처리: 마이크로태스크가 모두 처리된 후, 매크로태스크가 실행됩니다.
실행 순서 (콜 스택 기준)
1단계: 초기 실행 (콜 스택에서 처리)
Promise.resolve().then(...)
→ 마이크로태스크 큐에 추가setTimeout(...)
→ 태스큐 큐에 추가queueMicrotask(...)
→ 마이크로태스크 큐에 추가console.log(5)
→ 출력: 52단계: 마이크로태스크 큐 처리
Promise.then(...)
실행 → 출력: 1
첫 번째 queueMicrotask(...)
실행 → 출력: 3
queueMicrotask(...)
가 또 마이크로태스크 큐에 추가됩니다.queueMicrotask(...)
실행 → 출력: 43단계: 태스크 큐 처리
최종 출력
5
1
3
4
2
이번 글을 통해 자바스크립트의 비동기 처리, 이벤트 루프, 그리고 콜 스택의 작동 원리에 대해 알아보았습니다. 이러한 기초 지식은 복잡한 비동기 로직을 작성하거나 디버깅할 때 큰 도움이 된다고 생각합니다. 자바스크립트 런타임 환경과 이벤트 루프의 작동 방식을 깊이 이해하면 더 효율적이고 안정적인 코드를 작성할 수 있습니다. 비동기 코드에서 발생할 수 있는 문제를 예방하려면 이 원리를 숙지해야 합니다!