자바스크립트 이벤트 루프와 비동기 고급 활용
지난 8번째 글에서 비동기 기초를 다루며 콜백, 프로미스, async/await를 학습하였다면, 이번에는 비동기의 핵심 메커니즘인 이벤트 루프(Event Loop)를 탐구한다. 또한 이벤트 루프를 기반으로 비동기를 심화하여 활용하는 고급 기술을 소개한다. 이벤트 루프는 자바스크립트의 단일 스레드 실행 방식을 이해하는 데 필수적인 개념이며, 비동기 코드를 효율적으로 관리하는 데 중요한 역할을 한다.
이벤트 루프의 동작 원리와 태스크 큐, 마이크로태스크 큐의 차이를 설명하며, 비동기 패턴의 고급 응용과 실습 예제를 다룬다. 콘솔이나 HTML 파일에서 예제를 실행하며 비동기의 동작 방식을 체감할 수 있다.
이전 비동기 기초 내용을 확장하여 자바스크립트 엔진이 비동기 작업을 처리하는 방식을 심도 있게 분석한다. 이벤트 루프를 이해하면 코드 실행 순서를 예측하고 복잡한 비동기 상황을 효과적으로 다룰 수 있다.
이벤트 루프의 정의
이벤트 루프(Event Loop)는 자바스크립트가 비동기 작업을 처리하는 핵심 메커니즘이다. 자바스크립트는 단일 스레드(Single-Threaded) 언어로, 한 번에 하나의 작업만 수행한다. 그러나 setTimeout
이나 fetch
와 같은 비동기 작업은 동시에 처리되는 것처럼 보인다. 이는 이벤트 루프가 비동기 작업을 관리하기 때문이다.
자바스크립트 엔진(예: V8)에는 호출 스택(Call Stack)이 존재하며, 코드는 순차적으로 이 스택에 쌓여 실행된다. 비동기 작업은 호출 스택 외부에서 처리되며, 이벤트 루프가 이를 호출 스택으로 다시 가져오는 역할을 수행한다. 다음 예제를 통해 확인한다:
console.log('시작');
setTimeout(() => {
console.log('타이머 끝');
}, 1000);
console.log('끝');
// 출력: "시작" -> "끝" -> 1초 후 "타이머 끝"
위 코드에서 setTimeout
은 즉시 실행되지 않고 1초 후에 출력된다. 이는 이벤트 루프가 비동기 작업을 별도로 처리하여 호출 스택으로 적절히 전달하기 때문이다.
이벤트 루프는 호출 스택이 비었을 때 태스크 큐(Task Queue)에서 대기 중인 작업을 가져와 실행한다. 이 과정은 브라우저나 Node.js와 같은 런타임 환경이 제공하는 Web API와 협력하여 이루어진다. 예를 들어, setTimeout
은 Web API에서 타이머를 처리한 후, 완료 시 태스크 큐에 콜백을 추가한다. 이벤트 루프는 이를 확인하고 호출 스택으로 이동시켜 실행한다.
이벤트 루프의 동작 과정
이벤트 루프의 동작 과정을 단계별로 분석한다. 이를 이해하면 비동기 코드의 실행 순서를 명확히 파악할 수 있다.
1. 호출 스택(Call Stack)
호출 스택은 자바스크립트가 코드를 실행하는 공간이다. 함수가 호출되면 스택에 추가되고, 실행이 완료되면 제거된다:
function a() {
console.log('A');
}
function b() {
a();
console.log('B');
}
b();
// 출력: "A" -> "B"
2. Web API
비동기 작업(예: setTimeout
, fetch
)은 Web API로 전달된다. Web API는 브라우저가 제공하며, 자바스크립트 엔진 외부에서 작업을 처리한다:
setTimeout(() => {
console.log('타이머');
}, 0);
console.log('즉시');
// 출력: "즉시" -> "타이머"
위 예제에서 setTimeout
의 지연 시간이 0초임에도 즉시 실행되지 않는다. 이는 Web API에서 처리된 후 태스크 큐로 이동하기 때문이다.
3. 태스크 큐와 마이크로태스크 큐
비동기 작업이 완료되면 콜백이 큐에 저장된다. 큐에는 두 가지 종류가 존재한다:
- 태스크 큐(Task Queue): setTimeout
, setInterval
과 같은 일반 비동기 작업이 저장된다.
- 마이크로태스크 큐(Microtask Queue): Promise
, queueMicrotask
와 같은 우선순위가 높은 작업이 저장된다.
console.log('시작');
setTimeout(() => console.log('타이머'), 0);
Promise.resolve().then(() => console.log('프로미스'));
console.log('끝');
// 출력: "시작" -> "끝" -> "프로미스" -> "타이머"
마이크로태스크 큐의 작업은 태스크 큐보다 먼저 실행된다. 이는 우선순위의 차이 때문이다.
4. 이벤트 루프의 실행
이벤트 루프는 호출 스택이 비었을 때 마이크로태스크 큐의 모든 작업을 먼저 처리한 후, 태스크 큐의 작업을 처리한다. 이 과정은 지속적으로 반복된다:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
setTimeout(() => console.log('4'), 0);
console.log('5');
// 출력: "1" -> "5" -> "3" -> "2" -> "4"

비동기 패턴의 고급 활용
이벤트 루프를 이해한 후에는 비동기를 더욱 효과적으로 활용하는 방법을 탐구한다. 몇 가지 고급 패턴을 소개한다.
1. 프로미스 체이닝 개선
프로미스를 연속적으로 연결하여 코드를 깔끔하게 관리한다:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(1000)
.then(() => {
console.log('1초 후');
return delay(500);
})
.then(() => console.log('1.5초 후'));
이 방법은 콜백 중첩 문제를 해결하며 가독성을 높인다.
2. 병렬 프로미스 처리
Promise.all
을 활용하여 여러 비동기 작업을 병렬로 실행한다:
const p1 = delay(1000).then(() => '작업 1');
const p2 = delay(2000).then(() => '작업 2');
Promise.all([p1, p2]).then(results => {
console.log(results); // ["작업 1", "작업 2"]
});
이 패턴은 긴 작업 시간을 단축한다.
3. 비동기 에러 처리 강화
async/await를 사용하여 에러를 체계적으로 처리한다:
async function fetchData() {
try {
const result = await Promise.reject('에러!');
} catch (error) {
console.log('잡힌 에러:', error);
}
}
fetchData(); // "잡힌 에러: 에러!"
4. 순차 실행 제어
비동기 작업을 순차적으로 실행한다:
async function sequentialTasks() {
await delay(1000);
console.log('1초 후');
await delay(500);
console.log('1.5초 후');
}
sequentialTasks();
실습 예제
이벤트 루프와 비동기를 활용한 실습 예제를 제공한다. 버튼을 통해 순차적 및 병렬 작업을 시뮬레이션한다:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>비동기 실습</title>
<style>
#result {
margin-top: 20px;
}
button {
margin: 5px;
}
</style>
</head>
<body>
<button id="sequentialBtn">순차 실행</button>
<button id="parallelBtn">병렬 실행</button>
<div id="result"></div>
<script>
function delay(ms) {
return new Promise(resolve => setTimeout(() => resolve(ms), ms));
}
const sequentialBtn = document.getElementById('sequentialBtn');
const parallelBtn = document.getElementById('parallelBtn');
const result = document.getElementById('result');
sequentialBtn.addEventListener('click', async () => {
result.textContent = '순차 실행 시작...';
await delay(1000);
result.textContent += '\n1초 후';
await delay(500);
result.textContent += '\n1.5초 후';
});
parallelBtn.addEventListener('click', async () => {
result.textContent = '병렬 실행 시작...';
const tasks = [delay(1000), delay(500)];
const times = await Promise.all(tasks);
result.textContent += `\n${times.join(', ')}ms 후 완료`;
});
</script>
</body>
</html>
이 예제는 순차 실행과 병렬 실행의 차이를 시각적으로 확인할 수 있다.

비동기와 이벤트 루프의 실무 활용
실무에서는 API 호출, 데이터 로딩, 애니메이션 등 다양한 상황에서 이벤트 루프와 비동기를 활용한다:
async function loadData() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
console.log(data);
}
loadData();
이 예시는 API 데이터를 비동기로 가져오는 일반적인 사례이다.
주의할 점
마이크로태스크와 태스크의 우선순위를 정확히 이해한다. 비동기 작업이 많아질 경우 실행 순서를 예측한다. 에러 처리는 필수적으로 포함한다.
결론
이벤트 루프의 동작 과정, 큐의 차이, 고급 패턴을 학습함으로써 비동기 코드를 효과적으로 다룰 수 있다. 다음 포스팅에서는 뭐하지..
'코딩 공부 > 자바스크립트' 카테고리의 다른 글
11. 자바스크립트 이벤트 처리 (0) | 2025.02.26 |
---|---|
10. 자바스크립트 DOM 조작 (0) | 2025.02.26 |
8. 자바스크립트 비동기 기초 (1) | 2025.02.25 |
7. 자바스크립트 이벤트 기초 (0) | 2025.02.25 |
6. 자바스크립트 객체 기초 (1) | 2025.02.24 |