자바스크립트의 싱글 스레드 동작 방식과 이벤트 루프를 이해한다. 콜백 함수의 구조와 콜백 헬이 발생하는 이유, setTimeout/setInterval의 정확한 동작을 익힌다.
자바스크립트는 왜 비동기가 필요한가
자바스크립트는 싱글 스레드(Single Thread)입니다. 한 번에 하나의 작업만 처리할 수 있습니다. 만약 서버에서 데이터를 받아오는 동안 모든 코드가 멈춘다면 화면이 굳어버립니다. 비동기 처리는 이 문제를 해결하기 위한 핵심 메커니즘입니다.
이벤트 루프 — 비동기의 핵심 원리
자바스크립트 런타임 구성:
[Call Stack] 코드 실행 공간 (LIFO)
[Web APIs] 브라우저 제공 (setTimeout, fetch, DOM 이벤트 등)
[Task Queue] Web API 완료 후 대기하는 콜백들
[Microtask Queue] Promise 콜백 (Task Queue보다 우선)
[Event Loop] Call Stack이 비면 Queue에서 하나씩 가져옴
실행 순서:
1. 동기 코드 모두 실행 (Call Stack 소진)
2. Microtask Queue 모두 처리 (Promise 콜백)
3. Task Queue에서 하나 실행
4. 반복
// 실행 순서 예측
console.log("1"); // ① 동기
setTimeout(() => {
console.log("2"); // ④ Task Queue
}, 0);
Promise.resolve().then(() => {
console.log("3"); // ③ Microtask Queue
});
console.log("4"); // ② 동기
// 출력: 1, 4, 3, 2
setTimeout과 setInterval
// setTimeout — 일정 시간 후 한 번 실행
const timerId = setTimeout(() => {
console.log("3초 후 실행");
}, 3000);
// 취소
clearTimeout(timerId);
// setInterval — 일정 간격으로 반복 실행
const intervalId = setInterval(() => {
console.log("1초마다 실행");
}, 1000);
// 5번 후 중단
let count = 0;
const id = setInterval(() => {
count++;
console.log(count);
if (count >= 5) clearInterval(id);
}, 1000);
// 주의: delay 0은 "즉시"가 아니라 "현재 Call Stack 비운 후"
console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
// A, C, B
콜백 함수 — 비동기의 첫 번째 해법
콜백(Callback)은 나중에 호출될 함수를 인수로 전달하는 패턴입니다. Node.js의 fs, 구식 Ajax 등에서 광범위하게 쓰였습니다.
// 동기 콜백 (배열 메서드에서 흔함)
[1, 2, 3].forEach(n => console.log(n));
// 비동기 콜백
function loadUser(userId, callback) {
setTimeout(() => {
const user = { id: userId, name: "Kim" }; // 시뮬레이션
callback(null, user); // Node.js 스타일: (error, result)
}, 1000);
}
loadUser(1, (error, user) => {
if (error) {
console.error("에러:", error);
return;
}
console.log("유저:", user);
});
콜백 헬 — 콜백의 한계
비동기 작업이 순서대로 이어지면 콜백이 중첩되어 읽기 불가능한 코드가 됩니다.
// 콜백 헬 예시 — 로그인 → 유저 정보 → 주문 목록 → 최근 주문 상세
loginUser(username, password, (err, user) => {
if (err) return handleError(err);
getUserProfile(user.id, (err, profile) => {
if (err) return handleError(err);
getOrders(profile.id, (err, orders) => {
if (err) return handleError(err);
getOrderDetail(orders[0].id, (err, detail) => {
if (err) return handleError(err);
console.log(detail); // 드디어 원하는 결과
// 더 중첩될 수 있음...
});
});
});
});
이 코드의 문제점:
- 가독성: 피라미드 모양의 들여쓰기 (Pyramid of Doom)
- 에러 처리: 각 단계마다 개별 처리 필요
- 유지보수: 단계 추가/변경이 매우 어려움
콜백 헬 완화 — 함수 분리
// 중첩 대신 함수로 분리
function handleDetail(err, detail) {
if (err) return handleError(err);
console.log(detail);
}
function handleOrders(err, orders) {
if (err) return handleError(err);
getOrderDetail(orders[0].id, handleDetail);
}
function handleProfile(err, profile) {
if (err) return handleError(err);
getOrders(profile.id, handleOrders);
}
function handleUser(err, user) {
if (err) return handleError(err);
getUserProfile(user.id, handleProfile);
}
loginUser(username, password, handleUser);
// 들여쓰기는 줄었지만 여전히 흐름 파악이 어려움
이 방법도 근본적인 해결책은 아닙니다. 다음 강에서 배울 Promise가 콜백 헬의 진짜 해결책입니다.
requestAnimationFrame — 애니메이션에 최적화된 비동기
// setTimeout 대신 rAF 사용 — 브라우저 렌더링 주기에 맞춤
function animate(timestamp) {
// timestamp: 페이지 로드 후 경과 밀리초
element.style.left = `${Math.sin(timestamp / 1000) * 100}px`;
requestAnimationFrame(animate); // 다음 프레임 예약
}
requestAnimationFrame(animate); // 시작
관련 검색어 · 자바스크립트 비동기, 이벤트 루프 설명, 콜백 함수란, 콜백 헬, setTimeout 동작, 싱글 스레드 자바스크립트