(JS) 동기와 비동기 (3/3) : Async / Await

호두파파·2021년 3월 15일
1

JavaScript

목록 보기
18/25


2023년 2월 13일 내용이 추가 개편되었습니다 :-)

Promise 개념에 대해서 다시 짚고 넘어가기
Promise는 비동기 작업의 단위를 말합니다. 그리고 async/await 문법에서 반환하는 Promisepending, fulfilled, reject라는 각각의 상태에 따른 값을 가지는 Promise 객체를 말합니다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

Async / Await

비동기 코드를 동기식으로 표현해서 간단하게 표현하기 위해 사용하는 async / await는 가장 최근에 나온 문법입니다. 콜백 헬, 프러미스 헬이 생길 수 있는 기존 방식의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있게 도와줍니다.

Async Function : 비동기 작업을 만드는 손쉬운 방법

async 키워드는 함수를 선언할 때 붙여주 수 있습니다. 함수 선언시 앞에 async키워드를 붙인 함수를 Async 함수라고 하고, 이 Async 함수 내부에서는 비동기 처리할 로직(함수) 앞에await키워드 사용하는 방식으로 비동기 작업을 손쉽게 구현할 수 있습니다.

기본문법

async function asyncFunc() {
  // await 비동기_처리_메서드_명();
}

// or 

const asyncFunc = async () => {
 // await 비동기 처리할 함수 or 메서드 명 
}

1) 먼저 함수의 앞에 async라는 예약어를 붙인다.
2) 함수의 내부 로직중 HTTP 통신을 하는 비동기 처리 코드 앞에 await를 붙인다.

🔥 일반적으로 await의 대상이 되는 비동기 처리 코드는 프로미스를 반환하는 API 호출함수입니다.

코드 살펴보기

function fetchDoodles() {
  return new Promise(function(resolve, reject) {
    const dogs = ["위로","호두","바오"];
    resolve(dogs)
  });
}

async function logDoodles() {
  const result = await fetchDoodles();
  console.log(result); // 
}
  • fetchDoodles()함수는 프러미스 객체를 반환하는 함수이다.
  • fetchDoodles()함수를 실행하면 프러미스가 resolved되며 결과 값은
  • dogs 배열이 된다.

logDoodles() 함수를 실행하면 fetchDoodles() 함수의 결과값인 dogs배열이 result 변수에 담긴다. 콘솔에는 ["위로","호두","바오"]가 출력된다.

반드시 기억해두세요!

  • async함수의 리턴값은 무조건 Promise 객체입니다. 따라서, async 함수 내부에서 thencatch를 활용해 제어된 값이 Promise에 담겨 반환됩니다.

  • 모든 비동기 동작을 async 함수로 만들 수 있는게 아닙니다.async 함수를 만들때 resolve(value);return value;로 변경할 수 있습니다. 따라서, Promise 를 반환하는 것에 한해 해당 키워드를 사용할 수 있습니다. Promise가 아닌 것은 Promise로 감싸 반환하도록 방식으로 사용할 수 있습니다.

async func foo() {
  return Promise.resove(1);
}
foo().then(alert) // 1

Await : Promise가 끝날 때까지 기다립시다.

awaitPromise를 받아 처리하는 키워드입니다. wait라는 문맥 상 기다리라는 의미를 가지고 있는 것 같는데, 얼추 맞습니다. await는 Promise가 fulfilled(이행)되든지, rejected(거부)되든지, 비동기 처리 로직이 끝 날때까지 기다리도록 요청하는 함수입니다.

await 키워드를 사용하려면 함수가 async 함수로 선언되어야 합니다. async 함수는 화살표 함수로도 가능하고, 함수 표현식으로도 가능합니다.

const functionExpression = async function() {
  console.log('함수 표현식');
};

const arrowFunction = async () => {
  console.log('화살표 함수'); // 전 이 방식을 선호합니다.
};
const IIFE = (async () => {
  console.log('즉시 실행 함수 표현식');
})();

코드 예제로 살펴보기

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

async function add1(x) {
  console.log('start')
  const a = await resolveAfter2Seconds(20)
  console.log('a', a);
  return x + a
}

add1(10).then(v => {
  console.log(v)
})

console.log('finish')

다음과 같이 async function 내부에서 await키워드를 사용하였고,
이는 resolveAfter2Seconds(20)으로 리턴받는 Promise객체가 fulfilled(이행) 된 상태가 될 때까지 기다렸다가 다음 코드로 넘어가게 됩니다.

코드 흐름 보기

  • add1(10)함수 호출
    .then()은 async 함수의 리턴값이 프로미스 객체이다. 따라서, async 함수가 return되어야 then()의 콜백함수가 실행됩니다.
  • 콘솔에 start 출력
  • await 키워드로 인해 프로미스 객체가 resolve 될 때까지 기다렸다가, 다음으로 넘어감
  • 콘솔에 finish 출력
  • 2초 후 pending 상태이던 프로미스 객체 resolve 되어 해당 시점에서 다시 진행
    a에 최종 값 20할당
  • 콘솔에 'a', 20 출력
  • add1 함수 x+a 값이 30 리턴
  • add1 함수가 리턴되었기 대문에, .then()의 콜백함수가 실행되면서 콘솔에 10 출력된다.

await 프로미스 객체는 완료된 값을 반환한다.

async 함수는 return 또는 예외처리되어 throw 된 값이 담긴 Promise 객체를 반환합니다.


async/await 문의 에러 예외처리

Promise 객체를 리턴하기 때문에 코드의 then(성공) 또는 catch(실패)여부에 따라 다시 await로 연결할 수 있습니다. Promise에서 reject가 발생한다면 예외가 발생해, 예외 처리를 위해 try-catch구문을 사용할 수 있습니다. 에러는 catch절로 넘어가, 에러를 처리하게 됩니다.

async fuction another() {
  try {
    let result = await returnPromise();
  } catch (err) {
    console.error(err);
  }
}

직렬, 병렬 처리

코드를 async/await 문을 사용해 비동기 프로그래밍을 동기적으로 작동하는 것처럼 보이게 한다는 점에서 코드의 흐름을 잘 보여준다는 점에서 나름의 의미를 갖습니다. 하지만 중요한 것은 async/await 문을 사용해해 코드의 흐름을 분절해서 실행하려는 습관에 갇히지 않기를 바랍니다.

비동기 작업의 의의는 어떤 함수를 실행하는 동안, 여러가지 처리를 동시에 할 수 있다는데 의의가 있습니다. 따라서 실제 작업이 끝난 다음(분절) 그 후속조치를 수행한다가 아니라, 실제 작업이 끝나는걸 기다린 다음 코드를 수행한다라는 느낌을 가지는 것이 중요합니다.

직렬 처리

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

async function add1(x) {
  const a = await resolveAfter2Seconds(20)
  console.log('a', a) // 2초 뒤 a 20 출력
  const b = await resolveAfter2Seconds(30)
  console.log('b', b) // 4초 뒤 a 30 출력
  return x + a + b
}

add1(10).then(v => {
  console.log(v) // 50 출력
})

console.log('finish')

병렬 처리

await키워드를 호출하기 전에 Promise 객체를 생성해서 미리 비동기 작업을 pending 시켜놓고 이 pending 상태의 프로미스 객체를 await하면 병렬적으로 처리할 수 있습니다.

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

// Async/Await Example #3
async function add2(x) {
  const a = resolveAfter2Seconds(20)
  const b = resolveAfter2Seconds(30)
  return x + (await a) + (await b)
}

add2(10).then(v => {
  console.log(v)
})

async await 문은 Promise 객체를 다루는 최종병기일까?

Promise가 도입되었음에도 여전히 콜백을 사용하는 것처럼, async/await 모두 Promisecallback의 완벽한 대체품이 아닙니다. 경우에 따라 사용할 수 있는 것이죠.

비동기 작업은 동작 특성상 실제 작업과 그 후속조치를 따로 분리시킬 수 밖에 없습니다. 하여, thencatch키워드를 사용하기도 합니다.

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}
async function startAsync() {
  await setTimeoutPromise(1000).then(() => {
    console.log("1초 지났습니다.");
  });
}
console.log("시작입니다.");
startAsync();

다만, 위의 방식대로 awaitthen을 동시에 사용하면 문법적으로는 틀리지 않았지만,
코드의 가독성 및 의도가 불분명해지는 문제가 있습니다.

await문을 사용하는 이유 중 가장 주된 이유는 코드를 조금 더 직관적으로 볼 수 있게 하기 위함입니다.

비동기 처리를 할 때, 일관된 스타일을 사용합시다

가능하다면, async/await를 사용하고, 불가피하다면 어쩔 수 없이 resolve/reject -then-catch를 사용하는 것을 추천합니다. 위 예제에서 then이하는 불필요해보입니다. 따라서, 아래와 같이 수정해볼 수 있겠죠

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

async function startAsync() {
  await setTimeoutPromise(1000);
  console.log("1초 지났습니다.");
}

console.log("시작입니다.");

startAsync();

.then()async 함수를 넣는다면?

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

async function startAsync() {
  await setTimeoutPromise(1000).then( async () => {
    await setTimeoutPromise(1000);
    console.log("A");
  });
  console.log("B");
}

startAsync();

코드는 의도한 대로 순차적으로 잘 작동하겠지만, 코드가 상당히 난잡합니다.
따라서, 다음과 같이 작성해볼 수 있습니다.

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

async function startAsync() {
  await setTimeoutPromise(1000);
  await setTimeoutPromise(1000);
  console.log("A");
  console.log("B");
}

startAsync();

.then으로 구현하고 싶다면 다음과 같이 작성할 수 있겠네요

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

function startAsync() {
  setTimeoutPromise(1000)
    .then(() => setTimeoutPromise(1000))
    .then(() => console.log("A"))
    .then(() => console.log("B"));
}

startAsync();

요약 정리

  • Promise를 생성할 때는 resolve, reject 함수를 사용합니다. 이후 작업은 try/catch문 블록 안에서 후속 작업 로직을 작성할 수 있습니다.
  • new Promise(...)async 키워드를 사용해 async/await문으로 변환할 수 있습니다.
  • async함수 내에서 Promise에 대해 await를 걸어 작업을 기다릴 수 있다.
  • 비동기 작업 스타일은 일관되게 작성하는 것이 좋습니다. async/await 혹은 resolve-reject-then-catch를 사용합시다.

출처

zerocho 블로그
캡틴 판교 블로그
[Javascript] 비동기, Promise, async, await 확실하게 이해하기

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

0개의 댓글