3 분 소요

🚀 시작하며

프론트엔드 개발 신입으로서 업무를 보면서 직접 코드를 작성하는 시간보다 다른 사람이 작성한 코드를 보면서 흐름을 정리하거나 보는 경우가 많았습니다. 이때 들었던 생각이 있습니다.

“코드를 위에서부터 썼는데 왜 아래가 먼저 실행되지?”

이는 자바스크립트의 이벤트 루프(event loop)비동기 처리 방식 때문인데요,
오늘은 그 핵심 원리를 시각적으로, 그리고 실전 예제 중심으로 정리해보려고 합니다.


JavaScript는 왜 싱글 스레드일까?

자바스크립트(JavaScript)는 싱글 스레드 언어입니다.
즉, 한 번에 한 줄의 코드만 처리할 수 있다는 뜻입니다.
멀티 스레드가 아니라면 브라우저에서 여러 작업을 동시에 처리할 수 없을 텐데… 어떻게 가능할까요?

💡 정답은? 브라우저(Web API)가 비동기 작업을 대신 처리해주기 때문!

  • JavaScript는 코어 로직만 처리 (싱글 스레드)
  • 브라우저는 타이머, 이벤트 리스너 등 비동기 작업을 백그라운드에서 처리 후 다시 JS에게 넘김

비유하자면?
혼자 일하는 셰프가 요리만 하고, 손님 응대나 배달은 다른 직원이 맡는 구조인 것입니다.


동기 vs 비동기: 무슨 차이일까?

✅ 동기(Synchronous)

  • 코드를 한 줄씩 순차적으로 실행

  • 이전 작업이 끝나야 다음 줄 실행 가능

console.log('1');
console.log('2');
결과: 1, 2

✅ 비동기(Asynchronous)

  • 기다리지 않고 다음 코드를 먼저 실행함
  • 완료되면 나중에 “콜백(Callback)” 형태로 실행됨
console.log('1');
setTimeout(() => {
  console.log('2');
}, 1000);
console.log('3');
결과: 1, 3, 2

콜백(Callback)

콜백(Callback)

콜백함수는 다른 함수에 인자로 전달되어, 특정 이벤트나 조건이 발생했을 때 호출되는 함수를 말합니다. 👉 나중에 실행될 함수를 미리 넘겨주는 것

🔍 왜 사용하는가?

JavaScript는 싱글 스레드라서 서버에 파일을 요청하거나, 파일을 읽는 등의 시간이 오래걸리는 작업은 비동기로 처리하고, 그 결과가 나왔을 때 실행될 함수를 미리 정해줘야 합니다. 이때 사용되는 것이 바로 콜백입니다.

function greet(name, callback) {
  console.log("안녕하세요, " + name + "님!");
  callback(); // 나중에 실행됨
}

function finishGreeting() {
  console.log("인사 완료!");
}

greet("", finishGreeting);

>>> 
안녕하세요, 언님!
인사 완료!

✅ 비동기 상황에서 콜백

setTimeout(() => {
    console.log("1초 후에 실행!");
}, 1000);

위의 코드에서 ()=>{console.log("1초 후에 실행")}이것이 바로 콜백 함수 입니다. setTimeout은 1초 후에 이 함수를 실행해 달라는 의미이고, 그 함수를 콜백으로 전달한 것입니다.

콜백 큐와 마이크로태스크 큐

자바스크립트에는 두 가지 큐(Queue)가 존재합니다.

📦 콜백 큐 (Task Queue)

브라우저의 Web API에서 비동기 작업이 완료되었을 때, 해당 작업의 콜백 함수가 등록되는 큐를 의미합니다.

setTimeout, setInterval, DOM 이벤트, XHR(onload) 등이 여기에 해당됩니다.

Call Stack이 비었을 때, 이벤트 루프가 이 큐에서 하나씩 꺼내서 실행합니다.

setTimeout(() => {
  console.log('Callback Queue');
}, 0);

📦 마이크로태스크 큐 (Microtask Queue)

비동기 작업 중에서도 더 높은 우선순위를 가진 작업들이 들어가는 큐입니다.

Promise.then, async/await 의 await 이후, queueMicrotask()후등이 여기에 해당됩니다.

Call Stack이 비자마다 가장 먼저 처리됩니다.

하나라도 있으면, 모두 처리한 후에 콜백 큐로 넘어갑니다.

Promise.resolve().then(() => {
  console.log('Microtask Queue');
});

💥 차이점 정리

항목 마이크로태스크 큐 콜백 큐
우선순위 더 높음 낮음
실행 시점 Call Stack이 비자마자 바로 마이크로태스크가 모두 끝난 후
대표 작업 Promise.then, await, queueMicrotask setTimeout, setInterval, DOM 이벤트

이벤트 루프가 처리하는 순서

✅ 이벤트 루프 동작 순서

  1. JavaScript 코드 실행 (Call Stack)
  2. Stack이 비면
  3. Microtask Queue 처리
  4. 그 후에 Callback Queue 처리
console.log('A');

setTimeout(() => {
  console.log('B');
}, 0);

Promise.resolve().then(() => {
  console.log('C');
});

console.log('D');

실행 결과는?

A  
D  
C  
B

❗ 왜 이렇게 실행될까?

  • Aconsole.log 동기 처리
  • setTimeout: Callback Queue로 이동 (0초지만 최소 지연 있음)
  • Promise.then: Microtask Queue → Stack이 비면 바로 실행
  • D: 마지막 동기 코드 → 바로 실행됨
  • Stack이 비면 → Microtask(C) → Callback(B)

setTimeout, Promise, async/await 비교 실험

🔬 실험 1: setTimeout vs Promise

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));

결과: promisetimeout

➡️ Microtask Queue가 항상 우선!

실험 2: async/await 안의 흐름

async function test() {
  console.log('1');
  await Promise.resolve();
  console.log('2');
}

test();
console.log('3');

결과: 1, 3, 2

await함수 내부를 마이크로태스크로 분리해서, 먼저 바깥 코드(3)가 실행됨


실무에서 겪는 대표 이슈와 해결법

❌ Spinner가 안 뜨는 문제

setLoading(true);
fetchData();
setLoading(false);

이렇게 하면 fetch가 끝나기 전에 setLoading(false)가 실행됨 ➡️ 해결: await 또는 .then으로 분리 필요


❌ Promise 내부 비동기 로직 누락

function getData() {
  fetch('...')
    .then(res => res.json());
}
console.log(getData()); // undefined

Promise를 반환하지 않아서 await도 못 쓰고, 결과도 못 받음 ➡️ 해결: return fetch(...)


❌ setTimeout 안의 state 갱신이 늦게 반영

setTimeout(() => {
  setState(value + 1); // value가 최신값이 아닐 수 있음
}, 1000);

➡️ 해결: setState(prev => prev + 1) 형태 사용

카테고리:

업데이트:

댓글남기기