비동기(Asyncronous)

dr7204·2025년 4월 23일

비동기(Asynchronous)

특정 코드가 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 실행하는 것

콜백 기반(Callback-based)

무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 한다.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(script);
  document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  alert(`${script.src}가 로드되었습니다.`);
  alert( _ ); // 스크립트에 정의된 함수
});

콜백 지옥

언뜻 봤을 때는 꽤 쓸만해보이지만, 꼬리에 꼬리를 무는 비동기 동작의 경우 문제가 될 수 있다.

  • 콜백 지옥 예시
    loadScript('1.js', function(error, script) {
    
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('2.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...
            loadScript('3.js', function(error, script) {
              if (error) {
                handleError(error);
              } else {
                // 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
              }
            });
    
          }
        })
      }
    });

위의 예시와 같이 계속된 호출로 인해 깊은 중첩 코드가 만들어지는 패턴을 콜백 지옥 혹은 멸망의 피라미드라고 부른다.

이를 해결하기위해 독립적 함수들로 분리할 수는 있지만, 코드의 진행 순서를 읽기가 불편해진다는 점과 재사용하지 않을 함수들을 각각 선언하면서 네임스페이스가 복잡해져버린다.

  • 독립적 함수로 분리 예시
    loadScript('1.js', step1);
    
    function step1(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('2.js', step2);
      }
    }
    
    function step2(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', step3);
      }
    }
    
    function step3(error, script) {
      if (error) {
        handleError(error);
      } else {
        // 모든 스크립트가 로딩되면 다른 동작을 수행합니다. (*)
      }
    };

프로미스(Promise)

자바스크립트의 비동기 작업을 처리하기 위한 객체

state

  • pending(보류) : 지정한 작업을 수행중인 상태
  • fulfilled(이행) : 작업이 성공적으로 마무리된 상태
  • rejected(거절) : 작업에 문제가 생겨 오류가 발생한 상태

producer

원하는 기능을 비동기적으로 실행하는 Promise

const promise = new Promise((resolve, reject) => {
	console.log("promise.execute!");
});

프로미스 생성자를 통해 promise 객체를 생성하며, 매개변수로는 executor라는 callback 함수를 전달해줘야한다.

executor 함수는 프로미스가 생성되는 동시에 바로 실행된다.

executor 함수는 두 가지 매개변수를 갖는다.

  • resolve(value) : 일이 성공적으로 끝난 경우, 그 결과를 나타내는 value와 함께 호출
    • resolve가 호출되면 프로미스 객체의 상태(state)는 fulfilled로 변경된다.
  • reject(error) : 에러 발생 시, 에러 객체를 나타내는 error와 함께 호출
    • reject가 호출되면 프로미스 객체의 상태는 rejected로 변경된다.

consumer

producer가 제공한 데이터를 소비하는 것

then, catch, finally 를 이용하여 값을 받아올 수 있다.

  • then : 프로미스에서 가장 중요하고 기본이 되는 메서드다.

    promise.then(
      result => alert(result),
      error => alert(error) 
    );
    • 첫번째 매개변수는 프로미스가 이행되었을 때 실행되는 함수이고,
    • 두번째 매개변수는 프로미스가 거부되었을 때 실행되는 함수이다.
  • catch : 에러가 발생한 경우만 다루고 싶을 때 사용되는 메서드다.

    let promise = new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error("에러 발생!")), 1000);
    });
    
    // .catch(f)는 promise.then(null, f)과 동일하게 작동한다.
    promise.catch(alert); //바로 아래 코드와 같은 의미
    //promise.catch((error) => alert(error));
  • finally : 프로미스를 처리한 후(이행되거나, 거부된 후) 호출할 함수를 예약하는 메서드다.
    new Promise((resolve, reject) => {
      /* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve, reject를 호출함 */
    })
      // 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
      .finally(() => 로딩 인디케이터 중지)
      .then(result => result와 err 보여줌 => error 보여줌)

프로미스 API

Promise.all

배열 안의 프로미스가 모두 처리되면 새로운 프로미스가 이행되는데, 배열 안 프로미스 결과값을 담은 배열이 새로운 프로미스의 result가 된다.

let promise = Promise.all([...promises...]);
  • 배열 result의 요소 순서는 이행 순서가 아닌, 매개변수로 전달된 프라미스의 순서에 상응한다.
  • 전달되는 프로미스 중 하나라도 거부되면, Promise.all이 반환하는 프로미스는 에러와 함께 거부된다.

Promise.allSettled

모든 프로미스가 처리될 때까지 기다렸다가 이행(거부) 여부와 상관없이 그 결과(객체)를 담은 배열을 반환한다.

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/Violet-Bora-Lee',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

결과

[
  {status: 'fulfilled', value: ...응답...},
  {status: 'fulfilled', value: ...응답...},
  {status: 'rejected', reason: ...에러 객체...}
]

Promise.race

Promise.all과 비슷하지만, 가장 먼저 처리되는 프라미스의 결과를 반환한다는 점이 다르다.

let promise = Promise.race(iterable);

전달된 프라미스 중 하나가 먼저 이행(거부)된다면, 나머지 프로미스의 이행(거부) 여부의 상관 없이 첫번째 프로미스의 결과만 반환한다.

async/await

프로미스를 조금 더 편하게 사용할 수 있는 문법이다.

async

함수 앞에 async 키워드를 붙이면 해당 함수는 항상 프로미스를 반환한다.

async function f() {
  return 1;
}
f().then(alert); // 1

await

async 함수 안에서만 동작한다.

자바스크립트는 await 키워드를 만나면 프라미스가 처리될 때까지 기다린다. 결과는 그 이후 반환된다.

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)

  alert(result); // "완료!"
}

f();

awiat은 최상위 레벨 코드에서 작동하지 않는다.

프로미스가 정상적으로 이행되면 프라미스 객체의 result에 저장된 값을 반환한다.

반면 프로미스가 거부되면 마치 throw문처럼 에러가 던져진다.

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소');
    let user = await response.json();
  } catch(err) {
    // fetch와 response.json에서 발행한 에러 모두를 여기서 잡습니다.
    alert(err);
  }
}

f();

위와 같이 코드를 작성할 경우,

여러 줄의 코드에서도 오류를 감지 할 수 있다.

0개의 댓글