Lesson 16

비동기와 콜백

3 min Views 1

자바스크립트의 싱글 스레드 동작 방식과 이벤트 루프를 이해한다. 콜백 함수의 구조와 콜백 헬이 발생하는 이유, 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 동작, 싱글 스레드 자바스크립트

채용정보
공지사항
인사이트
MY