Front/Node.js

자바스크립트 비동기 처리

oodada 2024. 12. 25. 18:11

콜 스택(Call Stack) & 이벤트 루프(Event Loop) & 콜백 큐(Callback Queue)

콜 스택(Call Stack)

  • 자바스크립트는 싱글 스레드 언어이기 때문에, 콜 스택이 하나만 존재한다.
  • 콜 스택은 함수가 호출되면, 해당 함수를 콜 스택에 쌓아놓고 실행한다.
  • 함수가 실행이 끝나면, 콜 스택에서 해당 함수를 제거한다.
function first() {
  console.log('첫번째 함수');
  second();
}

function second() {
  console.log('두번째 함수');
}

first();

// 실행 순서
// 첫번째 함수
// 두번째 함수

이벤트 루프(Event Loop)

  • 자바스크립트는 이벤트 중심 언어이기 때문에, 이벤트 루프가 존재한다.
  • 이벤트 루프는 콜 스택과 콜백 큐를 감시하면서, 콜 스택이 비어있을 때 콜백 큐에 있는 함수를 콜 스택에 넣어 실행한다.

콜백 큐(Callback Queue)

  • 콜백 큐는 비동기 함수의 콜백 함수를 담아두는 큐이다.
  • 콜백 큐에 있는 함수는 콜 스택이 비어있을 때, 이벤트 루프에 의해 콜 스택에 넣어 실행된다.
console.log('시작');
setTimeout(() => {
    console.log('타이머 완료');
}, 0);
console.log('끝');

// 출력: 시작 -> 끝 -> 타이머 완료

실제 동작 과정

  1. 동기 코드는 즉시 콜 스택에서 실행됩니다
  2. setTimeout 같은 비동기 작업은 Web API로 보내집니다
  3. Web API에서 작업이 완료되면 콜백은 콜백 큐로 이동합니다
  4. 이벤트 루프는 콜 스택이 비었는지 확인하고, 비었다면 콜백 큐의 첫 번째 작업을 콜 스택으로 가져옵니다

"자바스크립트는 마치 멀티태스킹을 하는 것처럼 여러 작업을 처리할 수 있습니다. 시간이 오래 걸리는 작업(예: 서버에서 데이터 가져오기)을 실행하더라도, 그동안 다른 작업들을 계속할 수 있습니다. 마치 라면을 끓이면서 그동안 핸드폰을 하는 것처럼..."

콜백 함수

콜백(Callback) 함수는 영문 그대로, 나중에 실행되는 함수를 뜻한다.

콜백 함수의 비동기 처리

  • 자바스크립트는 이벤트 중심 언어이기 때문에
  • 특정 이벤트가 발생하고, 그에 대한 결과가 나올 때까지 기다리지 않고 다음 코드를 실행한다.
// 먼저 호출했지만 3초 뒤에 실행되기 때문에 2초 뒤에 실행되는 함수보다 나중에 실행된다.
setTimeout(() => {
  console.log('첫번째 실행');
}, 3000);

setTimeout(() => {
  console.log('두번째 실행');
}, 2000);

// 실행 순서
// 두번째 실행
// 첫번째 실행

콜백 함수의 동기 처리

  • 첫번째를 먼저 실행하고 두번째를 실행하고 싶으면
  • '콜백 함수'를 이용해 비동기 작업을 동기적으로 처리해주어야 한다.
setTimeout(() => {
  setTimeout(() => {
    console.log('두번째 실행');
  }, 2000);
  console.log('첫번째 실행');
}, 3000);


// 실행 순서
// 첫번째 실행
// 두번째 실행

사용자 정의 함수의 동기 처리

  • 아래 예제를 실행해보면, 첫번째 실행 -> 두번째 실행 -> 세번째 실행 순으로 실행되고
  • 동기적으로 처리된다.
function faker(callback) {
  callback();
}

console.log('첫번째 실행');

faker(() => {
  console.log('두번째 실행');
});

console.log('세번째 실행');

// 실행 순서
// 첫번째 실행
// 두번째 실행
// 세번째 실행
  • 위 세 가지 실행부는 모두 동기적이기 때문에, 콜백 큐를 사용하지 않고 모두 콜 스택을 거쳐 실행된다.

API 비동기 처리

console.log('첫번째 실행');

setTimeout(() => {
  console.log('세번째 실행');
}, 0);

console.log('두번째 실행');

// 실행 순서
// 첫번째 실행
// 두번째 실행
// 세번째 실행

  1. console.log('첫번째 실행')이 콜 스택에 들어가서 실행됩니다.
  2. setTimeout이 콜 스택에 들어가면서
    • 콜백 함수는 Web APIs로 보내집니다
    • 타이머가 완료되면 콜백 함수는 콜백 큐로 이동합니다
  3. console.log('두번째 실행')이 콜 스택에 들어가서 실행됩니다.
  4. 콜 스택이 비워지면, 이벤트 루프가 콜백 큐의 콜백 함수를 콜 스택으로 가져옵니다.
  5. console.log('세번째 실행')이 마지막으로 실행됩니다.

콜백 지옥(Callback Hell)

  • 콜백 함수를 중첩해서 사용하다 보면, 코드가 복잡해지고 가독성이 떨어지는 현상을 말한다.
setTimeout(() => {
  console.log('첫번째 실행');
  setTimeout(() => {
    console.log('두번째 실행');
    setTimeout(() => {
      console.log('세번째 실행');
    }, 1000);
  }, 1000);
}, 1000);

  • 이러한 콜백 지옥을 해결하기 위해, Promiseasync/await을 사용한다.

Promise

  • Promise는 코드의 중첩이 발생하는 콜백 지옥을 해결하기 위한 객체이다.
  • Promise는 단어 그대로 '약속'을 의미하며, 비동기 작업이 완료되었을 때, 성공했는지 실패했는지 알려주는 객체이다.

커피 주문 시나리오로 보는 Promise

1. 커피 주문

function order(sec, callback) {
  setTimeout(() => {
    callback(new Date().toISOString());
  }, sec * 1000);
}

order(1, (time) => {
  console.log(`커피 주문`, time);
});

order(2, (time) => {
  console.log(`시럽 추가 주문`, time);
});

order(3, (time) => {
  console.log(`휘핑 추가 주문`, time);
});

// 실행 순서
// 동시에 실행
  • 위 코드는 동시에 실행되기 때문에, 순서가 보장되지 않는다.
  • 실행 순서를 보장하기 위해 비동기 처리를 해야 한다.
function order(sec, callback) {
  setTimeout(() => {
    callback(new Date().toISOString());
  }, sec * 1000);
}

order(1, (time) => {
  console.log(`커피 주문`, time);
  order(2, (time) => {
    console.log(`시럽 추가 주문`, time);
    order(3, (time) => {
      console.log(`휘핑 추가 주문`, time);
    });
  });
});

// 실행 순서
// 커피 주문 -> 시럽 추가 주문 -> 휘핑 추가 주문
  • 이런 콜백 지옥을 해결하기 위해 Promise를 사용한다.

1. 커피 주문 = Promise 생성

const orderCoffee = new Promise((resolve, reject) => {
  // 바리스타가 커피 만드는 과정
});
  • 커피를 주문하면 진동벨(Promise)을 줍니다
  • 이 진동벨로 커피가 완성되었는지 알 수 있어요

2. Promise의 3가지 상태

  • 대기중(Pending): "커피 제조중입니다" (진동벨 대기중)
  • 성공(Fulfilled): "삐삐! 커피가 준비되었습니다" (진동벨 울림)
  • 실패(Rejected): "죄송합니다. 머신 고장으로 제조가 불가능합니다" (진동벨 오류)

3. 실제 코드로 보는 커피 주문

const orderCoffee = new Promise((resolve, reject) => {
  console.log("바리스타가 커피를 만들기 시작합니다!");

  setTimeout(() => {
    const isSuccess = true;  // 커피가 잘 만들어졌다고 가정

    if(isSuccess) {
      resolve("주문하신 아메리카노 나왔습니다! ☕");
    } else {
      reject("죄송합니다. 머신 고장으로 제조가 불가능합니다 😢");
    }
  }, 3000);  // 3초 동안 커피 제조중
});

// 커피 주문 결과 처리
orderCoffee
  .then((result) => {
    console.log(result);  // "주문하신 아메리카노 나왔습니다! ☕"
  })
  .catch((error) => {
    console.log(error);  // "죄송합니다. 머신 고장으로 제조가 불가능합니다 😢"
  });

4. Promise의 장점

  • 콜백: "커피 주문하고, 시럽 추가하고, 휘핑 추가하고..." (복잡)
  • Promise: 진동벨 하나로 모든 과정을 깔끔하게 처리 (.then 체이닝)

5. Promise 체이닝

  • Promise.then을 이용해 여러 개의 비동기 작업을 순차적으로 처리할 수 있다.
// 콜백 지옥 버전
orderCoffee(function(coffee) {
  addSyrup(coffee, function(withSyrup) {
    addWhippedCream(withSyrup, function(complete) {
      console.log("주문 완료!");
    });
  });
});

// Promise 버전 (깔끔!)
orderCoffee()
  .then(coffee => addSyrup(coffee))
  .then(withSyrup => addWhippedCream(withSyrup))
  .then(() => console.log("주문 완료!"))
  .catch(error => console.log("주문 실패:", error));

async/await

  • async/awaitPromise를 더 쉽게 사용할 수 있도록 ES8에서 도입된 문법이다.

무인 카페 주문 시나리오로 보는 async/await

1. 기본 개념

// 기존 Promise 방식 (진동벨)
orderCoffee()
  .then(coffee => console.log("커피 완성!"))
  .catch(error => console.log("주문 실패"));

// async/await 방식 (셀프 주문기)
async function orderCoffee() {
  try {
    const coffee = await makeCoffee();  // 커피가 완성될 때까지 기다림
    console.log("커피 완성!");
  } catch(error) {
    console.log("주문 실패");
  }
}

2. Promise vs async/await 주문 비교

// 진동벨(Promise) 방식
const orderByBell = () => {
  getCoffee()
    .then(coffee => addSyrup(coffee))
    .then(withSyrup => addWhippedCream(withSyrup))
    .then(() => console.log("주문 완성!"))
    .catch(error => console.log("주문 실패"));
}

// 셀프 주문기(async/await) 방식
const orderBySelf = async () => {
  try {
    const coffee = await getCoffee();        // 1. 커피 추출 기다리기
    const withSyrup = await addSyrup(coffee);     // 2. 시럽 추가 기다리기
    const completed = await addWhippedCream(withSyrup); // 3. 휘핑 추가 기다리기
    console.log("주문 완성!");
  } catch(error) {
    console.log("주문 실패");
  }
}

// 실행
orderByBell();  // 진동벨 주문
orderBySelf();  // 셀프 주문기 주문

3. async/await 장점

쉽고 직관적인 코드 작성이 가능하다.

  • async: "셀프 주문기를 사용하겠습니다"
  • await: "이 작업이 끝날 때까지 기다립니다"
  • try-catch: "주문 실패시 환불해드립니다"
티스토리 친구하기