들어가며
자바스크립트와 브라우저 환경에서의 비동기 처리 구조를 전반적으로 정리한 내용입니다. 단일 스레드 언어인 자바스크립트가 어떻게 비동기 처리를 구현하는지, 그리고 브라우저가 제공하는 Web API와 이벤트 루프를 통해 어떤 식으로 콜백이 실행되는지 통합적으로 이해할 수 있도록 합니다.
비동기 프로그래밍(Asynchromous Programming)
동기(Synchronous) vs 비동기(Asynchronous)
동기
- 어떤 작업 A를 요청하면 그 결과가 나올 때까지 멈춰서 대기한 뒤, 다음 작업을 처리하는 방식
- 작업이 순차적으로만 진행되므로, 하나가 지연되면 전체 흐름에 블로킹이 발생
비동기
- 어떤 작업 A를 요청해도 그 결과를 기다리지 않고 다음 작업을 진행
- 비동기 작업이 완료되면 콜백(Callback) 함수나 Promise 등을 통해 결과를 나중에 받아 처리
비동기의 장점
블로킹 없이 여러 일을 동시에 처리 가능 > 성능 및 사용자 경험(UX) 향상
예시: 파일 I/O, Ajax 통신, 타이머 함수 등
비동기 처리를 위한 자바스크립트의 동작 방식
자바스크립트의 단일 스레드 구조
자바스크립트는 기본적으로 단일 스레드(single-thread) 언어입니다. 즉, 한 번에 하나의 Call Stack만 사용할 수 있습니다. 그럼에도 불구하고 비동기가 가능한 이유는 이벤트 루프(Event Loop)와 브라우저(Web APIs)의 조합으로 메인 스레드가 아닌 별도의 영역(브라우저나 Node.js 런타임)이 비동기 작업을 처리하고, 완료된 작업을 다시 메인 스레드가 실행하도록 만들어주기 때문입니다.
이벤트 루프(Event Loop)와 호출 스택(Call Stack)
호출 스택
- 현재 실행 중인 함수(Execution Context)들의 모임(스택 구조)
- 자바스크립트 코드는 모두 이 스택에 올라와 순차적으로 실행됩니다.
- 함수가 끝나면 스택에서 빠져나옵니다.
이벤트 루프
- 콜 스택이 비어 있는지와 콜백 큐(또는 태스크 큐)에 실행할 작업이 있는지를 확인
- 스택이 비어 있으면 큐에서 콜백 함수를 꺼내 스택에 올려 실행(즉, 비동기 처리를 가능케 하는 핵심 메커니즘)
브라우저 Web API
- 자바스크립트 엔진이 아닌 브라우저(또는 Node.js 런타임)가 제공하는 API
- `setTimeout`, `fetch`, 이벤트 리스너(클릭, 스크롤, 키보드 입력 등) 등의 기능을 별도 스레드/영역에서 처리
- 작업이 완료되면 등록된 콜백을 콜백 큐(메시지 큐)로 전달
이벤트 루프와 큐의 구조
메시지 큐(Message Queue)
비동기 함수의 콜백(예: `setTimeout`, `setInterval`, `fetch`의 응답처리, DOM 이벤트 콜백 등)이 끝나면, 이 콜백들이 '메시지 큐(또는 매크로 태스크 큐)로 들어가 대기합니다.
이벤트 루프는 호출 스택이 비어 있을 때, 메시지 큐에서 대기 중인 콜백을 하나씩 꺼내 Call Stack 에 올려 실행합니다.
매크로 태스크(Macro Task) 예시
- `setTimeout`, `setInterval` 콜백
- DOM 이벤트 콜백(클릭, 스크롤, 키보드 입력 등)
- AJAX/XHR, `fetch` 콜백(성공/실패 시점에 실행되는 콜백)
- I/O 처리가 완료된 후 실행되는 콜백 등
특징
- 메시지 큐의 태스크들은 한 번의 이벤트 루프 틱(tick)에서 하나씩 처리됩니다.
- 즉, 이벤트 루프가 한 바퀴를 돌 때, 메시지 큐에 여러 콜백이 쌓여 있더라도 한 콜백을 처리하고 나면 그 다음 이벤트 루프 틱에 다음 콜백을 처리합니다.(실제 구현에 따라 조금씩 다를 수 있으나, 개념적으로는 이런 식으로 구분해서 이해하는 것이 보통입니다.)
마이크로태스크 큐(Microtask Queue)
메시지 큐에 들어가는 매크로 태스크보다 더 높은 우선순위를 갖는 비동기 작업입니다. `Promise.then`, `async/await` 후속 처리, `MutationObserver` 등이 이에 해당합니다.
실행 시점
- 이벤트 루프가 한 번 반복(tick)을 마치기 직전, 콜 스택이 비어 있을 때 마이크로태스크 큐에 있는 모든 콜백을 한꺼번에 실행합니다.
- 즉, 매크로 태스크가 끝난 후(콜 스택이 다시 비어있을 때), 우선적으로 마이크로태스크가 전부 처리된 뒤에야 다음 매크로 태스크가 실행되는 구조입니다.
우선순위
- 마이크로태스크 큐가 메시지 큐보다 우선 처리됩니다.
- 따라서 `Promise.then` 내 콜백이 `setTimeout` 콜백보다 먼저 실행됩니다.
메시지 큐와 마이크로태스크 큐의 실행 순서 예시
console.log("시작");
setTimeout(() => {
console.log("타이머 콜백");
}, 0);
Promise.resolve()
.then(() => {
console.log("프로미스 콜백 1");
})
.then(() => {
console.log("프로미스 콜백 2");
});
console.log("끝");
- `console.log("시작")` → Call Stack에서 바로 실행
- `setTimeout()` → 브라우저가 Web API 영역에서 타이머를 동작시키고, 콜백이 준비되면 메시지 큐(매크로 태스크 큐)에 넣는다.
- `Promise.then(...)` → Promise가 이미 ‘이행(fulfilled)’ 상태이므로, .then에 등록된 콜백은 마이크로태스크 큐에 들어간다.
- `console.log("끝")` → Call Stack에서 바로 실행
- 메인 스레드(콜 스택)에 남은 작업이 없으므로, 이벤트 루프가 마이크로태스크 큐를 먼저 확인.
- 프로미스 콜백 1 실행 → 또 다음 .then 콜백(프로미스 콜백 2)이 마이크로태스크 큐에 등록
- 바로 이어서 프로미스 콜백 2 실행
- 마이크로태스크가 모두 끝난 뒤, 메시지 큐(매크로 태스크 큐)를 확인.
- 이제 setTimeout 콜백(타이머 콜백)이 실행된다.
따라서 출력 순서는 다음과 같이 결정됩니다.
시작
끝
프로미스 콜백 1
프로미스 콜백 2
타이머 콜백
브라우저와 Node.js에서늬 비동기 처리
브라우저 비동기 처리 구조
Web API
- 브라우저가 제공하는 API (DOM 조작, 타이머, Ajax, Fetch 등)
- 자바스크립트 엔진이 아닌 별도 영역에서 동작하며, 비동기 작업이 끝나면 해당 콜백을 큐에 전달
DOM 이벤트 처리
- `button.addEventListener('click', callback)
- 클릭 발생 시 Web API가 이벤트를 감지
- 콜백을 매크로 태스크 큐에 등록
- 스택이 비었을 때 이벤트 루프가 실행
Web Worker, Service Worker
- Web Worker : 별도 스레드에서 자바스크립트 코드를 동작시켜, 메인 스레드와 메시지를 주고받음
- Service Worker : 브라우저 백그라운드 스레드에서 네트워크 요청 가로채기, 알림, 오프라인 캐싱 등 수행
Node.js 비동기 구조와의 비교
Node.js도 자바스크립트 엔진(V8) 위에서 동작하며, libuv 라이브러리를 통해 비동기 I/O를 처리합니다.
- 이벤트 루프, 콜백 큐 구조 자체는 브라우저와 유사
- 하지만 Web API 대신 Node.js에서 제공하는 내장 모듈, C++ 스레드 풀(libuv) 등이 비동기 처리를 담당함
결국 "브라우저냐, Node.js냐"에 따라 비동기 API가 다를 뿐, 단일 스레드 + 이벤트 루프 + (매크로/마이크로)태스크 큐라는 큰 그림은 거의 동일합니다.
요점 정리
- 단일 스레드 환경에서 비동기를 제공하기 위해 자바스크립트는 이벤트 루프와 큐(콜백 큐)를 사용
- 매크로 태스크 큐(메시지 큐)와 마이크로태스크 큐가 별도로 존재
- 마이크로태스크(Promise.then, async/await)는 우선순위가 높아, 이벤트 루프 한 틱마다 전부 먼저 실행된 후, 매크로 태스크(setTimeout, DOM 이벤트 등)가 하나씩 실행됨
- 브라우저 환경에서는 타이머, DOM 이벤트, 네트워크 요청 등 각종 Web API가 비동기 처리를 담당
- 작업 완료 후 콜백(또는 이벤트) → 매크로 태스크 큐로 전달 → 이벤트 루프가 스택이 빌 때마다 하나씩 실행
- Node.js 환경에서도 큰 틀은 동일하나, 비동기 API(파일 I/O, 네트워크 I/O 등)를 처리하는 방식(libuv 스레드 풀 등)이 다름
- 비동기 패턴
- 콜백 → Promise → async/await
- 각각 장단점이 있으나, “단일 스레드 + 이벤트 루프 + 큐”라는 구조는 동일
'JAVASCRIPT' 카테고리의 다른 글
[React] 리액트적으로 사고하기(번역) (1) | 2025.01.01 |
---|---|
자바스크립트 문자열(String)의 모든 것 (1) | 2024.12.29 |
배열 생성 심화: Array / Array.of / Array.from (0) | 2024.12.24 |
자바스크립트 배열(Array)의 모든 것 (0) | 2024.12.21 |
자바스크립트의 스코프 규칙: Lexical Scope (1) | 2024.12.20 |