
JavaScript는 싱글 스레드 언어이지만 비동기 처리가 가능하다.
이는 JavaScript 엔진과 브라우저(또는 Node.js) 런타임 환경의 협력으로 이루어진다.
1단계: 코드 실행
2단계: 비동기 작업 발견
setTimeout, fetch 같은 비동기 코드를 만나면 → Web API로 넘겨서 백그라운드에서 처리3단계: 작업 완료 대기
4단계: 실행 준비
핵심: JavaScript는 한 번에 하나씩만 처리하지만, 시간이 걸리는 작업은 다른 곳에서 처리하고 나중에 결과만 받아온다!
콜 스택은 JavaScript 엔진이 함수 호출을 추적하는 LIFO(Last In, First Out) 구조의 자료구조다.
function first() {
console.log('첫 번째');
second();
console.log('첫 번째 종료');
}
function second() {
console.log('두 번째');
third();
console.log('두 번째 종료');
}
function third() {
console.log('세 번째');
}
first();
콜 스택 변화:
1. [first] ← first() 호출
2. [first, second] ← second() 호출
3. [first, second, third] ← third() 호출
4. [first, second] ← third() 완료, 팝
5. [first] ← second() 완료, 팝
6. [] ← first() 완료, 팝
콜 스택이 허용된 크기를 초과하면 "Maximum call stack size exceeded" 에러가 발생한다.
function infiniteRecursion() {
infiniteRecursion(); // Stack Overflow 발생
}
Web API는 브라우저가 제공하는 비동기 작업 처리 환경이다. JavaScript 엔진과는 별개의 독립적인 스레드에서 동작한다.
setTimeout, setIntervaladdEventListener, DOM 조작fetch, XMLHttpRequest localStorage, sessionStoragenavigator.geolocationconsole.log('1');
setTimeout(() => {
console.log('2 - 타이머 완료');
}, 1000);
console.log('3');
처리 과정:
1. console.log('1') → 콜 스택에서 즉시 실행
2. setTimeout → Web API의 Timer로 위임 (1초 타이머 시작)
3. console.log('3') → 콜 스택에서 즉시 실행
4. Web API에서 콜백을 콜백 큐에 추가
콜백 큐는 Web API에서 완료된 비동기 작업의 콜백 함수들이 대기하는 FIFO(First In, First Out) 구조의 큐다.
JavaScript 환경에는 여러 종류의 큐가 있다:
setTimeout, setIntervalsetImmediate (Node.js)Promise.then/catch/finallyqueueMicrotask()MutationObserverMicro Task Queue > Macro Task Queue
마이크로 태스크 큐가 완전히 비워져야 매크로 태스크 큐의 작업이 실행된다.
console.log('1');
setTimeout(() => console.log('2 - setTimeout'), 0);
Promise.resolve().then(() => console.log('3 - Promise'));
console.log('4');
실행 순서:
1
4
3 - Promise
2 - setTimeout
Event Loop는 콜 스택과 콜백 큐들을 지속적으로 모니터링하며, 콜 스택이 비어있을 때 콜백 큐의 작업을 콜 스택으로 이동시키는 메커니즘이다.
while (true) {
if (콜 스택이 비어있음) {
if (마이크로 태스크 큐에 작업이 있음) {
마이크로 태스크를 콜 스택으로 이동
} else if (매크로 태스크 큐에 작업이 있음) {
매크로 태스크를 콜 스택으로 이동
}
}
}
console.log('시작');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => console.log('Promise 3'));
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => console.log('setTimeout 2'), 0);
});
Promise.resolve().then(() => console.log('Promise 2'));
console.log('끝');
1단계 - 동기 코드 실행:
콜 스택: [console.log('시작')]
출력: '시작'
2단계 - setTimeout 처리:
setTimeout → Web API로 위임
콜 스택: []
Web API: [Timer(0ms, callback1)]
3단계 - 첫 번째 Promise:
Promise.resolve().then() → 마이크로 태스크 큐에 추가
마이크로 태스크 큐: [callback2]
4단계 - 두 번째 Promise:
Promise.resolve().then() → 마이크로 태스크 큐에 추가
마이크로 태스크 큐: [callback2, callback3]
5단계 - 동기 코드 완료:
콜 스택: [console.log('끝')]
출력: '끝'
콜 스택: [] ← 비워짐
6단계 - 이벤트 루프 동작:
1. 마이크로 태스크 우선 처리
출력: 'Promise 1'
새로운 setTimeout → Web API로 위임
2. 다음 마이크로 태스크 처리
출력: 'Promise 2'
3. 마이크로 태스크 큐 비워짐, 매크로 태스크 처리
출력: 'setTimeout 1'
새로운 Promise → 마이크로 태스크 큐에 추가
4. 마이크로 태스크 다시 우선 처리
출력: 'Promise 3'
5. 마지막 매크로 태스크 처리
출력: 'setTimeout 2'
최종 출력:
시작
끝
Promise 1
Promise 2
setTimeout 1
Promise 3
setTimeout 2
동기 코드 → 마이크로 태스크 → 매크로 태스크
콜 스택이 비어있을 때만 콜백 큐의 작업이 실행된다.
무거운 콜 스택 작업이 있으면 콜백 큐에 쌓여있는 비동기 콜백들이 지연된다.
마이크로 태스크 큐가 완전히 비워질 때까지 매크로 태스크는 대기한다.
비동기 작업은 메인 스레드를 블로킹하지 않고 백그라운드에서 처리된다.
이러한 메커니즘을 통해 JavaScript는 싱글 스레드임에도 불구하고 효율적인 비동기 처리가 가능하다.