자바스크립트 비동기 처리

jeong_hyeok·2025년 7월 28일

https://www.jsv9000.app/

자바스크립트는 싱글 스레드 언어로, 한 번에 하나의 작업만 수행 가능하다.

대신, 네트워크 요청이나 이벤트 처리, 타이머와 같은 오래 걸리고 반복적인 작업들은 자바스크립트 엔진이 아닌 브라우저 내부의 멀티 스레드인 Web APIs에서 비동기 + 논블로킹으로 처리된다.

아래 예시가 실행될 때의 흐름을 살펴보자.

console.log('Hi');

setTimeout(function cb() {
	console.log('there');
}, 5000);

console.log('Bye');
// Console
Hi
Bye
there
  1. 콘솔에 'Hi' 출력.
  2. setTimeout 호출. (setTimeout은 브라우저에서 제공하는 API)
  3. 브라우저가 Web API에서 타이머를 실행시키고 카운트 다운을 시작.
  4. 콘솔에 'Bye' 출력.
  5. 타이머 종료 -> 콜백(cb())을 task queue에 넣음.
  6. Event loop가 콜 스택이 빈 것을 확인하고 콜백(cb())을 콜 스택에 넣음.
  7. 콜백(cb()) 실행, 콘솔에 'there' 출력.
  • Event loop란?
    콜 스택과 콜백 큐를 주시하는 역할을 함. 스택이 비어있으면, 큐의 첫번째 콜백을 스택에 쌓아 효과적으로 실행할 수 있게 해줌.

  • Callback Queue
    Task Queue와 Microtask Queue를 묶어서 지칭하는 용어.
    - Task Queue: setTimeout, setInterval, fetch, addEventListener 와 같이 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐
    - Microtask Queue: promise.then, process.nextTick, MutationObserver 와 같이 우선적으로 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐 (처리 우선순위가 높음)

setTimeout( , 0)는 0초동안 기다리므로 의미가 없어 보일 수도 있지만, task queue에 들어가서 event loop를 기다리기 때문에 바로 실행되지 않음.

기억할 점은, 자바스크립트의 비동기가 완벽한 멀티 스레딩은 아니라는 점이다. setTimeout의 예를 들면, 타이머만 병렬적으로 처리되고 그 안의 콜백 함수 실행 코드는 추후에 이벤트 루프에 의해 콜 스택에 들어가 싱글 스레드로 처리되기 때문이다.

자바스크립트의 멀티 스레딩은 웹 워커(web workers)를 사용해 구현할 수 있다. Node.js의 경우 Worker_Threads 모듈로 멀티 스레드를 구현할 수 있다.

Node.js에서의 이벤트 루프

Node.js 환경에서도 브라우저와 거의 비슷한 구조를 볼 수 있는데, 차이점은 다음과 같다:

  • libuv 라이브러리를 사용하여 비동기 I/O 지원.

  • Web API 대신 Node.js API 사용.

  • V8 (JavaScript 엔진) : Node.js에서 사용하는 JavaScript 엔진으로 코드를 컴파일하고 실행한다

  • Bindings (Node API) : Node.js 시스템과 V8 엔진 간의 상호작용을 가능하게 하는 C++ 라이브러리

  • Libuv 라이브러리 : Node.js에서 비동기 I/O 작업을 처리하기 위한 C 라이브러리

  • Event Queue : 비동기 I/O 작업 결과를 저장하고 처리하기 위한 자료구조 (웹브라우저의 Task Queue와 비슷하다)

  • Event Loop : Event Queue에 저장된 I/O 작업 결과를 처리하고, 다음 작업을 수행하도록 하는 관리자

  • Worker Threads : CPU 집약적인 작업을 처리하기 위해 Node.js 10 버전부터 추가된 멀티 스레드. worker threads는 메인 스레드와 독립적인 V8 엔진 인스턴스를 가진다.

자바스크립트 비동기 처리 문법

콜백 함수

콜백 함수는 다른 함수의 인자로 전달되어, 그 함수 내부에서 호출되는 함수이다. 비동기 작업이 완료된 후 실행되도록 사용되는 경우가 많다.


Promise

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과를 나타낸다.

Promise를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다. 다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 'promise'를 반환한다.

Promise는 다음 중 하나의 상태를 가진다.

  • 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.

인스턴스 메서드

  • catch()
    - promise가 거부되었을 때 실행할 콜백 함수를 등록하고, 새로운 promise 반환.
  • finally()
    - promise의 이행과 거부 여부에 상관없이 실행할 콜백 함수를 등록하고, 새로운 promise 반환.
  • then()
    - promise에 이행과 거부 처리기 콜백을 등록하고, 새로운 promise 반환.

.then() 메서드는 최대 두 개의 인수를 받는다. 첫 번째 인수는 비동기 작업이 이행된 경우에 대한 콜백 함수이고, 두 번째 인수는 거부된 경우에 대한 콜백 함수이다. 각 .then()은 새로 생성된 promise 객체를 반환하며, 선택적으로 연쇄에 사용할 수 있다.

아래는 예제이다.

function doSomething() {
  return new Promise((resolve, reject) => {
      resolve(100)
  });
}

doSomething()
    .then((value1) => {
        const data1 = value1 + 50;
        return data1 // 150
    })
    .then((value2) => {
        const data2 = value2 + 50;
        return data2 // 200
    })
    .then((value3) => {
        const data3 = value3 + 50;
        return data3 // 250
    })
    .then((value4) => {
        console.log(value4); // 250 출력
    })

Promise의 콜백은 Microtask Queue에 담긴다.


Async/Await

async/await는 Promise 로직을 더 쉽고 간결하게 사용할 수 있게 해준다.


async function

Async function 선언은 AsyncFunction 객체를 반환하는 비동기 함수를 정의한다. Async function은 암시적으로 Promise 객체를 사용하여 결과를 반환한다.

async function에서는 await 키워드를 사용할 수 있다. await 키워드는 async function 안에서만 사용할 수 있다.


await

await 연산자는 Promise를 기다리기 위해 사용된다. Async function 내부에서만 사용할 수 있다.

[rv] = await expression;
  • expression
    - Promise 혹은 기다릴 어떤 값
  • rv
    - Promise에 의해 만족되는 값이 반환된다. Promise가 아닌 경우에는 그 값 자체가 반환된다.

await 문은 Promise가 이행되거나 거부될 때까지 async 함수의 실행을 일시 정지하고, Promise가 이행되면 async 함수를 일시 정지한 부분부터 실행합니다. 이때 await 문의 반환값은 Promise 에서 이행된 값이 됩니다.

function resolveAfter2Seconds(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function f1() {
  var x = await resolveAfter2Seconds(10);
  console.log(x); // 10
}

f1();

0개의 댓글