본문 바로가기

개발

비동기(Asynchronous) 처리에 대한 이론

‘비동기(Asynchronous)'란?

  • 서버로 보낸 요청에 서버가 응답할 때까지 기다리지 않고, 다른 처리를 할 수 있는 행위
  • 콜백 지옥을 Promise의 등장으로 해결할 수 있게 되었다.

1-1. Promise

  • 미래의 어떤 시점에 결과를 제공하겠다는 ‘약속’ 반환
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});

myPromise.then(n => {
  console.log(n);
});

작업이 끝나고 나서 또 다른 작업을 해야 할 때에는 Promise 뒤에 .then(...) 을 붙여서 사용하면 된다.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error());
  }, 1000);
});

myPromise
  .then(n => {
    console.log(n);
  })
  .catch(error => {
    console.log(error);
  });
  
  // 1초 뒤에 실패되는 함수
  • 실패하는 상황에서는 reject 를 사용하고, .catch 를 통하여 실패했을시 수행 할 작업을 설정한다.
  • then 내부에 넣은 함수에서 또 Promise 를 리턴하게 된다면, 연달아서 사용 할 수 있다.
function increaseAndPrint(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const value = n + 1;
      if (value === 5) {
        const error = new Error();
        error.name = 'ValueIsFiveError';
        reject(error);
        return;
      }
      console.log(value);
      resolve(value);
    }, 1000);
  });
}

increaseAndPrint(0)
  .then(n => {
    return increaseAndPrint(n);
  })
  .then(n => {
    return increaseAndPrint(n);
  })
  .then(n => {
    return increaseAndPrint(n);
  })
  .then(n => {
    return increaseAndPrint(n);
  })
  .then(n => {
    return increaseAndPrint(n);
  })
  .catch(e => {
    console.error(e);
  });
  
  
// 혹은 다음과 같은 표현으로 가능함  
//  increaseAndPrint(0)
//  .then(increaseAndPrint)
//  .then(increaseAndPrint)
//  .then(increaseAndPrint)
//  .then(increaseAndPrint)
//  .then(increaseAndPrint)
//  .catch(e => {
//    console.error(e);
  • create().then(성공 시 실행 함수, 실패 시 실행 함수)
  • create() 함수에서 Promise 객체 인스턴스 생성 → 인스턴스 + executer(실행자)
    • then은 코드 끝까지 확인한 후 실행한다.
    • 소스 코드 끝까지 내려갔다 다시 올라온다.
 
  • [[promiseState]] : 코드 실행시 파라미터로 반환된 값 저장
    • Pending(대기) → settled(이행) || rejected(거부)
      • Pending
        • Promise 인스턴스 생성 → execute 설정 + 성공, 실패 핸들러 함수 바인딩 (함수 호출 X 환경 셋팅만)
      • Settled
        • 성공 및 실패 여부를 알 수 있다.
        • 성공(fulfill)
        • 실패(reject)

 

Promise 인스턴스 특징

형태new Promise

파라미터 executer, function(resolve, reject) {}
반환 Promise (생성한 promise 인스턴스)
  • then() 성공/실패 핸들러 함수의 파라미터는 각 1개
    • 여러 개일 경우 배열 사용
  • 성공, 실패 핸들러 함수
    • return 문의 존재와 무관하게 promise 인스턴스 반환
      • [[promiseValue]]에 설정
        • return문 작성 O : Promise Value
        • return문 작성 X : undefined

.then() 연속

  • then(one)의 [[PromiseValue]] 값이 .then(two)의 two 인자값으로 설정

Catch() 실패 핸들러

  • catch() 다음에도 then() 연결 가능
    • catch()의 [[PromiseValue]]값을 then 인자값으로 여전히 보낸다.
  • create() 작성시 (Promise 객체 인스턴스 만드는 함수)
    • Promise의 2번째 인자값인 reject() 없으면 catch() 핸들러 함수 호출하지 않는다.

All()

  • 모두 성공이었을 때 resolve() 성공 핸들러 함수 실행
    • all() 파라미터에 iterable Object
  • Promise 인스턴스 하나라도 실패하면 then() 핸들러 실행 X

Promise 단점 → async/await이 ES8에 등장

  • 에러를 잡을 때 몇번째에서 발생했는지 알아내기 어렵다.
  • 특정 조건에 따라 분기를 나누는 작업도 어렵다.
  • 특정 값을 공유해가면서 작업을 처리하기도 까다롭다.

1-2. async-await

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

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // Expected output: "resolved"
}

asyncCall();
  • 목적
    • 여러 promise의 동작을 동기스럽게 사용할 수 있게 하고, 어떠한 동작을 여러 promise의 그룹에서 간단하게 동작하게 하는 것
    • promise가 구조화된 callback과 유사한 것 처럼 async/await 또한 제네레이터(generator)와 프로미스(promise)를 묶는것과 유사함.
  • 장점
    • awaitpromise.then보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법
async function foo() {
  return 1;
}
function foo() {
  return Promise.resolve(1);
}
  • 상기 두 코드는 같은 결과를 수행하는 코드

async를 활용한 promise chain

  • Promise를 사용한 chain
function getProcessedData(url) {
  return downloadData(url) // returns a promise
    .catch((e) => {
      return downloadFallbackData(url); // returns a promise
    })
    .then((v) => {
      return processDataInWorker(v); // returns a promise
    });
}
  • async 함수를 사용한 chain
async function getProcessedData(url) {
  let v;
  try {
    v = await downloadData(url);
  } catch (e) {
    v = await downloadFallbackData(url);
  }
  return processDataInWorker(v);
}

→ return 구문에 await 구문이 없다. async function 반환값이 암묵적으로 Promise.resolve로 감싸지기 때문.

await 연산자

  • Promise를 기다리기 위해 사용
  • async function 내부에서만 사용 가능

await 문은 Promisefulfill되거나 reject 될 때까지 async 함수의 실행을 일시 정지하고, Promisefulfill되면 async 함수를 일시 정지한 부분부터 실행한다.

이때 await 문의 반환값은 Promise 에서 fulfill된 값이 된다.

Promisereject되면, await 문은 reject된 값을 throw한다.

  • await 특징
    • 최상위 레벨 코드에서 작동하지 않음
    • 만일 작동을 원한다면 익명 async 함수로 감싸면 가능
function resolveAfter2Seconds(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

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

f1();
  • 만약 Promiseawait에 넘겨지면, awaitPromise가 fulfill되기를 기다렸다가, 해당 값을 리턴

await 에러 핸들링

  1. 단순 구조
async function f() {
  await Promise.reject(new Error("에러 발생!"));
}

// 혹은
async function f() {
  throw new Error("에러 발생!");
}
  1. 복수개의 흐름을 제어해야 할 때
async function f() {

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

f();

async-await 사용 예시

  1. 사용 방법
 
  • async가 붙은 함수는 결과값으로 Promise 를 반환
  • async가 붙은 함수 내에서만 await 연산을 사용할 수 있다.
  • Promise.resolve(1) 대신return 1; 만 해도 된다.
  1. 기본 예시
async function f() {

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

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

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

f();
  1. async 함수에서 에러를 발생 시킬 때와 에러를 잡아낼 때
    1. 에러 발생 시킬 때는 throw를 사용하고, 에러를 잡을 때에는 try/catch 를 사용한다.
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function makeError() {
  await sleep(1000);
  const error = new Error();
  throw error;
}

async function process() {
  try {
    await makeError();
  } catch (e) {
    console.error(e);
  }
}

process();
  1. 배열 비구조화 할당 (구조 분해 할당) 문법 사용도 가능
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const getDog = async () => {
  await sleep(1000);
  return '멍멍이';
};

const getRabbit = async () => {
  await sleep(500);
  return '토끼';
};
const getTurtle = async () => {
  await sleep(3000);
  return '거북이';
};

async function process() {
  const [dog, rabbit, turtle] = await Promise.all([
    getDog(),
    getRabbit(),
    getTurtle()
  ]);
  console.log(dog);
  console.log(rabbit);
  console.log(turtle);
}

process();

Promise Chaining

  1. Promise를 활용한 loadJson
function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new Error(response.status);
      }
    })
}

loadJson('no-such-user.json')
  .catch(alert); // Error: 404
  1. async/await을 사용한 loadJson
async function loadJson(url) { // (1)
  let response = await fetch(url); // (2)

  if (response.status == 200) {
    let json = await response.json(); // (3)
    return json;
  }
  
  /* 혹은 */
  if (response.status == 200) {
    return response.json(); // (3)
  } // 대신, 프라미스가 이행되는 것을 await을 통해 바깥 코드에서 기다려야 한다.
  

  throw new Error(response.status);
}

loadJson('no-such-user.json')
  .catch(alert); // Error: 404 (4)

async/await vs promise.then/catch

async/await의 장단점

async/await을 사용하면 await가 대기를 처리해주기 때문에 .then이 거의 필요하지 않다.

여기에 더하여 .catch 대신 일반 try..catch를 사용할 수 있다는 장점도 생긴다.

항상 그러한 것은 아니지만, promise.then을 사용하는 것보다 async/await를 사용하는 것이 대개 더 편리.

그런데 문법 제약 때문에 async함수 바깥의 최상위 레벨 코드에선 await를 사용할 수 없다. 그렇기 때문에 관행처럼 .then/catch를 추가해 최종 결과나 처리되지 못한 에러를 다룬다.

Promiss.all()

async/awaitPromise.all과도 함께 쓸 수 있다.

여러 개의 프라미스가 모두 처리되길 기다려야 하는 상황이라면 이 프라미스들을 Promise.all로 감싸고 여기에 await를 붙여 사용할 수 있습니다.

  • // 프라미스 처리 결과가 담긴 배열을 기다립니다.
    let results = await Promise.all([
      fetch(url1),
      fetch(url2),
      ...
    ]);

실패한 프라미스에서 발생한 에러는 보통 에러와 마찬가지로 Promise.all로 전파됩니다. 에러 때문에 생긴 예외는 try..catch로 감싸 잡을 수 있습니다.


간단한 Timer 예제.tsx

  • Promise와 Async-await의 문법 차이 및 혼용해서 사용하는 경우
"use client";

export function promiseTimer(time: number) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(time);
    }, time);
  });
}

export default function Asyncawait() {
  const onPromiseTimer = () => {
    console.log("start");
    promiseTimer(1000)
      .then(function (time: any) {
        console.log("time:" + time);
        return promiseTimer(time + 1000);
      })
      .then(function (time: any) {
        console.log("time:" + time);
        return promiseTimer(time + 1000);
      })
      .then(function (time: any) {
        console.log("time:" + time);
        //return promiseTimer(time + 1000);
        console.log("end");
      });
    //console.log("end"); // start, end가 바로 나온다. timer는 비동기기 때문에 보내고 자기 일 하는 것.
  };

  const onAsyncTimer = async () => {
    console.log("start");
    let time: any = await promiseTimer(1000);
    console.log("time:" + time);
    time = await promiseTimer(time + 1000);
    console.log("time:" + time);
    time = await promiseTimer(time + 1000);
    console.log("time:" + time);
    console.log("end");
  };
  //   console.log('parent start');
  //   onAsyncTimer()     -> 현재에는 button 클릭시 실행이지만 자동 실행하려면 함수 호출
  //   console.log(onAsyncTimer());       -> Promise를 암시적으로 리턴
  //   console.log('parent end'); -> 위 케이스와 마찬가지로 parent start, end를 먼저 보내고 자신의 할 일을 한다.

  const onAsync2Timer = async () => {
    console.log("parent start");
    await onAsyncTimer();
    console.log("parent end");
  };

  const onAsync3Timer = async () => {
    console.log("parent parent start");
    onAsync2Timer().then(() => {
      console.log("parent parent end");
    });
  };

  return (
    <div>
      <button className="bg-blue-100 p-10" onClick={onPromiseTimer}>
        Promise Timer
      </button>
      <button className="bg-blue-300 p-10" onClick={onAsyncTimer}>
        Async-await Timer
      </button>
      <button className="bg-blue-400 p-10" onClick={onAsync2Timer}>
        Async-await2 Timer
      </button>
      <button className="bg-blue-500 p-10" onClick={onAsync3Timer}>
        Async-await3 Timer
      </button>
    </div>
  );
}

참고

https://ko.javascript.info/async-await

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function

https://learnjs.vlpt.us/async/02-async-await.html