4 분 소요

비동기 처리 (Asynchronous Processing) : 프로세스의 완료를 기다리지 않고 다른 작업을 진행

  • 싱글 쓰레드 (Single Thread) : 한번에 하나의 함수만 실행 → 동기 처리 (Synchronous Processing)과 동일
    • 자바스크립트는 콜 스택이 하나 → 콜 스택에 쌓인 함수나 코드를 위에서 아래로 차례대로 실행
  • 논블로킹 (Non-Blocking) : I/O를 수행하는 비동기 함수는 백그라운드에 넘김
  • 멀티 프로세스 (Multi Processes) : 백그라운드는 OS 프로세스에 의존

비동기 처리의 순서?

  1. 런타임 (Runtime) → 실행 컨텍스트 (Execution Context) → 콜 스택 (CallStack)
  2. 백그라운드 (Background) → 운영체제 (OS)
  3. 테스크 큐 (Task Queue) → 콜 스택 (Call Stack)

자바스크립트 런타임 (JavaScript Runtime) : 자바스크립트가 실행되는 환경

  • Web API : 브라우저에서 제공하는 API (setTimeout, HTTP 요청 메소드, DOM 이벤트)
  • 테스트 큐 (Task Queue) : 이벤트가 발생한 뒤에 호출되어야 할 콜백 함수들이 대기하는 공간
  • 이벤트 루프 (Event Loop) : 이벤트 발생 시 콜백 함수들을 관리, 호출된 콜백 함수의 실행 순서 결정

비동기 콜백 패턴 (Asynchronous Callback Pattern) : 비동기 작업의 완료를 다루는 전통적인 방식

  • 현재 실행되고 있는 함수가 끝난 뒤에 실행되는 콜백 함수를 통해 실행 순서를 지정
setTimeout(function() {
  console.log('task1', new Date());
  setTimeout(function() {
    console.log('task2', new Date());
    setTimeout(function() {
      console.log('task3', new Date());
      console.log('END>>', new Date());
    }, 1000 );
  }, 2000);
}, 3000);
console.log('START', new Date());

콜백 함수를 여러 개 중첩하면, 코드의 가독성이 떨어지는 콜백 지옥 (Callback Hell)이 발생!

프로미스 (Promise) : 비동기 작업의 성공 및 실패에 대한 완료 결과를 처리하는 객체

  • 콜백 지옥을 피하고 비동기 처리를 쉽게 처리할 수 있도록 ES6부터 then 도입
  • 프로미스를 호출하면 프로미스 인스턴스 (Promise Instance)를 반환한 후 Resolve & Reject

프로미스의 3가지 상태 (Status)

  • 대기 (Pending) : 비동기 처리 로직이 미완료된 초기 상태, 성공 또는 실패할 때까지 대기
  • 이행 (Fulfilled ): 비동기 처리가 완료되어, 프로미스가 결과 값을 반환한 상태
  • 거부 (Rejected) : 비동기 처리가 실패하거나 오류가 발생한 상태
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const now = Date.now();
        
        if (now % 2 === 0) {
            resolve(console.log('[Fulfilled]'), now);    
        } else reject('Rejected');
    }, 1000);

    setTimeout(() => {
        reject(new Error('[TimeExceeded]'),);
    }, 1001);
});

promise.then(
    success => console.log('[Resolved]'),
    fail => console.log('[Rejected]')
)
// 프로미스를 클래스로 표현한다면?
class Promise {
  constructor(callback) {
    console.log('[Promise 생성자]')
    callback (this.resolve.bind(this), this.reject.bind(this));
}

  then(resolve) {
    console.log('[then 메소드 실행]')
    this.success = callback;
  }

  catch(x) {
    console.log('[catch 메소드 실행]')
    this.failure = callback;
  }

  success(x) {
    console.log('[success 메소드 실행] ' + x)
  }

  failure(x) {
    console.log('[failure 메소드 실행] ' + x)
  }

  resolve(x) { 
    console.log('[resolve 메소드 실행]')
    return this.success(x);
  }

  reject(x) {
    console.log('[reject 메소드 실행]')
    return this.failure(x);
  }

  callback(resolve, reject) {
    console.log('[callback 메소드 실행]')
  }
};

let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            const now = Date.now();
            console.log('이행 :', now)

            if (now % 2 === 0)
                resolve(now)
            else
                reject(new Error("실패"))

            console.log("[setTimeout 메소드 실행]")
        }, 1000)
    }
);

프로미스 클래스 메소드 (Promise Class Method) : 프로미스에서 비동기 작업을 다루기 위해 제공

  • Promise.resolve : 주어진 값을 성공 상태의 프로미스로 반환
    • Promise.resolve(x).then(val => console.log(val));
  • Promise.reject : 주어진 값을 실패 상태의 프로미스로 반환
    • Promise.reject(new Error('...')).catch(console.error);
  • Promise.all : 여러 프로미스가 모두 성공 시 시간과 무관하게 순서를 보장하여 프로미스들을 모두 반환, 하나라도 실패하면 첫번째로 실패한 프로미스 반환
    • Promise.all(iterables).then().catch(...)
  • Promise.race : 여러 프로미스 중에서 가장 빠른 것을 반환, 하나라도 실패하면 첫번째로 실패한 프로미스 반환
    • Promise.race(iterables).then().catch(...)
  • Promise.any : 여러 프로미스 중에서 제일 빨리 성공한 것을 반환
    • Promise.any(iterables).then().catch(...)

Node.js 모듈의 util.promisify : 콜백 함수 기반의 비동기 함수를 프로미스 기반으로 변환

function promisify(fn) {
  return new Promise( (resolve, reject) => {
    try {
      const ret = fn();
      resolve(ret);
    } catch(err) {
      reject(err);
    }
  })
}

const exec = util.promisify(execute);
exec.then(...).catch(...)

async, await : 프로미스를 생성하고 소비하기 위한 문법적 설탕

문법적 설탕 (Syntax Sugar) : 문법적 기능은 그대로인데, 사람이 직관적으로 읽을 수 있게끔 만드는 것

  • 비동기 함수에서 콜백을 사용하는 대신에, 단순한 논리적 흐름을 작성
    • 프로미스의 then, catch, finally를 사용할 필요 없음
  • async는 프로미스를 반환하고, awaitresolvereject와 매핑
    • 성공 : returnresolveresult
    • 실패 : errorrejectthrow
// const fn = async() => {...}
async function fn() {   // Promise 반환
  ...
  result = await fetch(url);  // fetch.then().catch()
}
console.log(await fn());
  • Promise & then : 각각이 별도의 쓰레드로 실행되므로 병렬
  • async, await : 단일 쓰레드를 차례로 실행하므로 직렬

→ 연관이 없는 비동기 함수 실행에 async, await을 남발하지 말자!

// promise, async, await을 활용하여 페치한 뒤에 2초간 sleep 구현
const f = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users/1");  
  if (!res.ok) throw new Error("Fail to Fetch!!");
  console.log(Date.now())
  await new Promise((resolve) => {setTimeout(resolve, 2000)});
  const data = await res.json();
  return data.name;
};

console.log(await f());
console.log(Date.now())
// promise, async, await을 활용하여 1초 간격으로 3번 출력하는 depthTimer 구현
let depthTimer = async (str) => {
  console.log(str, new Date());
  await new Promise((resolve) => setTimeout(resolve, 1000));
};
  
(async function () {
  await depthTimer('START!');
  await depthTimer('depth1');
  await depthTimer('depth2');
  await depthTimer('depth3');
  console.log('Already 3-depth!!');
})();

for-await-of : 비동기적으로 이터레이터 (Iterator)를 반복하는 문법

  • async 함수에서 비동기적으로 값을 가져와 처리할 때 활용
const afterTime = sec => new Promise(
    resolve => setTimeout(resolve, sec * 1000, sec
  ));
  console.time('for-await-of ');
  const arr = [afterTime(1), afterTime(2)];
  
  for (const fo of arr.values()) {
    console.log('fo =', fo);
  }
  
  for await (const fao of arr.values()) {
    console.log('fao =', fao);
  }
  
  console.timeEnd('for-await-of ');

// > fo = Promise { <pending> }
// > fo = Promise { <pending> }
// > fao = 1
// > fao = 2
// > for-await-of : 2.003s

태그:

업데이트: