🟨 2-27. 비동기 코드의 병목 해결 — Event Loop, Promise, Web API의 완전한 협력 구조
1. 비동기란 무엇인가?
비동기(Asynchronous)란
“코드가 순서대로 끝나지 않아도 다음 작업이 실행되는 구조” 를 말한다.
자바스크립트는 싱글 스레드(single thread) 언어이지만,
브라우저의 Web API, Task Queue, Microtask Queue 덕분에
마치 병렬처럼 작동한다.
🧠 핵심 개념
- JS는 하나의 실행 스레드만 가진다.
- 하지만 Web API(브라우저)가 백그라운드에서 일을 대신 처리한다.
- JS는 그 결과를 큐(Queue)로 받아 순서대로 실행한다.
2. 자바스크립트는 싱글 스레드, 브라우저는 멀티 스레드
많은 입문자들이 혼동하는 부분이다.
자바스크립트 자체는 싱글 스레드이지만,
브라우저는 멀티 스레드 구조로 동작한다 👇
구성 요소 역할
| JS 엔진(V8) | 코드 실행 (싱글 스레드) |
| Web API | 타이머, 이벤트, 네트워크, DOM 등 별도 스레드 |
| Task Queue | 완료된 작업의 콜백을 보관 |
| Event Loop | 큐를 감시하며 실행 순서 조율 |
즉,
“자바스크립트는 일을 시키고, 브라우저는 대신 처리하고, 결과만 다시 받는 구조.”
3. 비동기 코드 흐름의 실제 동작
console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
실행 결과 👇
A
C
B
이유를 순서대로 보면 이해가 된다.
1️⃣ JS 엔진이 console.log("A") 실행
2️⃣ setTimeout()은 Web API에게 콜백 위임
3️⃣ JS는 다음 코드(C)를 바로 실행
4️⃣ Web API는 0초 뒤 콜백을 Task Queue에 등록
5️⃣ Event Loop가 Stack이 비는 순간 Queue의 콜백(B) 실행
4. 브라우저 내부 흐름 시각화
JS Thread
├── Call Stack: 실행 중인 코드
├── Microtask Queue: Promise 등 우선순위 높은 작업
├── Task Queue: setTimeout, I/O 콜백
└── Event Loop: 스택 비면 큐에서 꺼내 실행
Web API Thread
├── Timer
├── Network Request
├── DOM Event Handler
└── Web Worker
✅ 브라우저는 JS의 요청을 Web API로 넘기고,
✅ JS는 그동안 멈추지 않고 다른 코드를 계속 실행한다.
5. Microtask vs Task의 차이
비동기 작업에도 “우선순위”가 존재한다.
구분 예시 실행 시점 특징
| Microtask | Promise.then, queueMicrotask | Stack 종료 직후 | 더 빠름 |
| Task | setTimeout, setInterval, fetch | 다음 Loop 주기 | 느림 |
예시 👇
console.log("1");
setTimeout(() => console.log("2 (Task)"), 0);
Promise.resolve().then(() => console.log("3 (Microtask)"));
console.log("4");
결과 👇
1
4
3 (Microtask)
2 (Task)
6. 병목이 생기는 이유
비동기 코드가 많아질수록 큐의 순서와 우선순위가 꼬이는 현상이 발생한다.
setTimeout(() => console.log("timeout 1"), 0);
Promise.resolve().then(() => console.log("promise 1"));
setTimeout(() => console.log("timeout 2"), 0);
Promise.resolve().then(() => console.log("promise 2"));
결과 👇
promise 1
promise 2
timeout 1
timeout 2
✅ Promise가 먼저 쌓이기 때문에 Task보다 먼저 실행
➡ 비동기 코드가 많을수록 순서 제어가 어려워짐
➡ “병목”이 아니라 “우선순위 혼잡”으로 느려짐
7. 병목을 해결하는 3가지 전략
(1) Web Worker 사용 — CPU 부하 분산
자바스크립트는 싱글 스레드이므로,
무거운 연산은 Web Worker로 분리해야 한다.
// main.js
const worker = new Worker("worker.js");
worker.postMessage({ n: 1000000000 });
worker.onmessage = e => console.log("결과:", e.data);
// worker.js
onmessage = e => {
let sum = 0;
for (let i = 0; i < e.data.n; i++) sum += i;
postMessage(sum);
};
✅ Worker는 별도의 스레드에서 동작하므로
UI 프리징 없이 백그라운드 계산 가능.
(2) requestIdleCallback 활용 — 브라우저가 한가할 때 실행
requestIdleCallback(() => {
console.log("브라우저가 한가할 때 실행됨");
});
✅ 렌더링 중단 없이 실행
✅ React 18의 Concurrent Mode가 내부적으로 이 메커니즘을 활용
(3) Debounce & Throttle — 이벤트 폭주 제어
마우스, 스크롤, 리사이즈 이벤트는 초당 수백 번 발생할 수 있다.
Debounce (마지막 이벤트만 실행)
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Throttle (일정 간격마다 실행)
function throttle(fn, delay) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn(...args);
}
};
}
✅ 브라우저의 Task Queue 부담을 획기적으로 줄이는 핵심 패턴이다.
8. async/await 내부의 진실
async/await은 문법적으로 깔끔하지만,
내부적으로는 Promise + Microtask Queue를 사용한다.
async function test() {
console.log("1");
await Promise.resolve();
console.log("2");
}
test();
console.log("3");
결과 👇
1
3
2
✅ await 뒤의 코드는 Microtask Queue에 등록되어
현재 Stack이 끝난 후 실행된다.
9. 병목 제거 실전 예시
async function fetchData() {
const res = await fetch("/api/data");
const json = await res.json();
render(json);
}
function render(data) {
data.forEach(item => {
const el = document.createElement("div");
el.textContent = item.name;
document.body.appendChild(el);
});
}
❌ 문제: render()에서 수천 개 DOM 추가 → Reflow 폭발
✅ 개선
function render(data) {
const frag = document.createDocumentFragment();
data.forEach(item => {
const el = document.createElement("div");
el.textContent = item.name;
frag.appendChild(el);
});
document.body.appendChild(frag);
}
✅ DOM 변경 횟수를 1회로 줄여
Task Queue와 Repaint 병목을 동시에 해결.
10. 병목 진단 도구
도구 용도
| Performance 탭 | 스크립트 실행 순서, FPS 확인 |
| Memory 탭 | 이벤트 루프 대기 중 객체 확인 |
| Network 탭 | fetch 대기 병목 탐지 |
| Web Vitals | LCP, FID, CLS 등 UX 지표 측정 |
✅ 특히 Performance 탭에서 “Main Thread” 구간이 길면,
JS 병목으로 인한 렌더링 지연 가능성이 높다.
11. 최적화 정리
문제 해결법
| 비동기 순서 꼬임 | async/await 통일 |
| 타이머 중첩 | clearInterval / clearTimeout |
| 이벤트 폭주 | debounce, throttle |
| 렌더링 지연 | DocumentFragment / requestIdleCallback |
| CPU 부하 | Web Worker 분리 |
| Promise 과다 | Microtask 정리 및 체인 최소화 |
12. 마무리
비동기 병목을 해결한다는 건,
단순히 “코드를 빠르게 만든다”가 아니라
JS 엔진, 브라우저, Web API가 서로 협력하는 리듬을 이해하는 것이다.
“비동기 최적화는 순서 제어가 아니라,
브라우저와 자바스크립트의 타이밍을 조율하는 일이다.”