게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌19 - ECMAScript - 7년 전 등록 / 6년 전 수정

ES2017(ES8) async/await

안녕하세요. 이번 시간에는 async/await에 대해 알아보겠습니다.

async/await이 왜 도입되었는지를 알기 위해서는 콜백프로미스 에 대한 이해가 우선입니다. 해당 개념을 잘 모르신다면 먼저 위의 링크를 통해 간단히 알아보고 오시는 것을 추천합니다.

콜백 지옥에서 벗어나기 위해 프로미스 패턴을 쓰는 것처럼, 프로미스 패턴의 단점을 극복하기 위해 async/await를 쓰는 겁니다.

프로미스 패턴의 단점이 뭐가 있을까요? 콜백 지옥보다는 낫지만, 여전히 코드가 너무 장황합니다. 뭔가 코드를 줄일 수 있으면 좋겠는데요. 그래서 async/await 이전에 한 가지 시도가 있었습니다. ES2015의 generator을 사용해서 비동기 코드를 동기 형태로 만드는 건데요. 이것도 꼭 보고 오세요. 보고 오셔야 왜 이런 형식의 문법을 도입했는지 이해할 수 있습니다.

결국 이런 시도가 async/await을 채택하게 되는 계기가 되었습니다. 비동기 코드를 동기식으로 표현해서 간단하게 만드는 거죠. 제가 generator 시간에 보여드렸던 async 함수가 자바스크립트에 내장된 셈입니다.

generator의 예시와 비교하자면 generator의 async 함수를 async 키워드로 바꾸고, yield를 await으로 바꾸면 됩니다. (하지만 그렇다고 generator와 async 함수가 같은 건 아닙니다!) 두 코드를 동시에 띄워놓고 비교해보세요!

async function findUser() {
  try {
    let user = await Users.findOne({}).exec();
    user.name = 'zero';
    user = await user.save();
    user = await Users.findOne({ gender: 'm' }).exec();
    ...
  } catch (err) {
    console.error(err);
  }
}

await이 Promise를 받아 처리하는 키워드입니다. async 함수는 Promise가 없으면 의미가 없습니다. 그리고 await 키워드를 사용하려면 함수가 async 함수로 선언되어야 하는 거고요. async 함수는 ES2015의 화살표 함수로도 가능하고, 함수 표현식으로도 가능합니다. 즉시 실행도 가능하고요. async만 앞에 붙여주면 됩니다.

const functionExpression = async function() {
  console.log('함수 표현식');
};
const arrowFunction = async () => {
  console.log('화살표 함수');
};
const IIFE = (async () => {
  console.log('즉시 실행 함수 표현식');
})();

한 가지 주의할 점은 await은 반드시 async 함수 바로 안에서만 쓰여야한다는 점입니다. async 함수 안에 또 다른 일반 함수가 있고, 그 안에 await이 있다면 안 됩니다.

async function a() {
  (function b() {
    await Promise.resolve(true); // async 함수 바로 안이 아니라서 에러. 아래와 같이 수정
  })();
}
function a() {
  (async function b() {
    await Promise.resolve(true); // 이제 정상 작동. 차이점이 보이시죠?
  })();
}

async 코드의 문제는 에러가 발생했을 때입니다. 에러가 나면 어떻게 될까요? 아무 일도 일어나지 않습니다. 에러가 났는데 에러가 났다고 말을 안 하면 안 되겠죠? 그래서 generator 시간 때처럼 try catch문으로 감싸서 명시적으로 에러 처리를 해줍니다.

async 함수는 return 또는 throw 값이 담긴 Promise를 리턴합니다. 위의 코드에서는 따라서 then(성공) 또는 catch(실패) 하면 됩니다.

const returnPromise = async () => {
  return 'zero';
};
returnPromise().then((res) => {
  console.log(res); // 'zero'
});

근데 눈치 채셨나요? Promise를 리턴하기 때문에 이 코드를 다시 await으로 연결할 수 있습니다.

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

이렇게 할 수 있는거죠. 어떻게 보면 그냥 Promise를 동기식으로 작성하는 것과 다를 바가 없네요. async 함수 안에 여러 await이 있을 때 앞의 await이 완료된 후에야 뒤의 await이 실행됩니다. 동시에 실행하려면 await Promise.all([프로미스들])해야 합니다. (예전에 이것을 위한 await*이라는 키워드가 있었는데 사라졌습니다) Promise를 쓰지 않으려고 async 함수를 쓰지만 이 상황에서는 결국 Promise를 쓸 수 밖에 없습니다.

프로미스가 도입되었음에도 여전히 콜백을 사용하는 것처럼, async/await이 도입되었다고 해서 프로미스나 콜백을 사용하지 않아야 하는 것은 아닙니다. async/await이 그것들의 완벽한 대체품이 아닙니다.(generator의 대체품은 맞습니다) 여전히 콜백이 쓰여야하는 경우도 존재하고, 프로미스가 쓰여야 하는 경우도 존재합니다.

콜백은 문법이 간단하기 때문에 콜백 지옥으로 여겨지지 않는 한 여전히 유용합니다. 보통 코드가 적은 게 좋죠? 프로미스와 async/await이 나왔음에도 콜백이 계속 쓰이는 것은 간단함에 있습니다. 콜백을 async 함수로 전환하려면 Promise를 거쳐야 합니다. 두 단계를 거쳐 async/await을 사용하느니 그냥 콜백을 사용하는 게 나은 경우가 많습니다.

조금 복잡한 예시를 들어봅시다. 세 가지 일이 있다고 칩시다. 각각 3초, 6초, 9초가 걸립니다. 6초 걸리는 일은 3초 걸리는 일 다음에 해야 합니다. 9초 걸리는 일은 동시에 할 수 있고요. 코드로 짜볼까요?

const job = (x) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(x);
      resolve(x);
    }, x * 1000);
  });
};

당연한 것이지만 await을 세 번 연달아 쓰는 건 안 됩니다. 앞의 await이 끝나야 뒤의 await이 실행되기 때문에 총 18초가 걸리게 됩니다. 다음과 같이 Promise.all로 job(3)과 job(6)을 동시에 처리해봅시다.

(async function() {
  const [a, b] = await Promise.all([job(3), job(9)]);
  const c = await job(6);
  return a + b + c;
})().then(res => console.log(res));

하지만 이렇게 해도 15초가 걸립니다. Promise.all이 한 묶음이라서 [a, b]가 나오는 데 9초가 걸리고 그 후에야 job(6)를 처리하기 때문이죠. 그럼 어떻게 해야 할까요? job(3) 다음에 job(6)을 하도록 async/await으로 묶고, job9와 동시에 실행하면 됩니다.

ES2018에 for await of 문법이 추가되어 Promise.all을 for await of로 대체할 수 있게 되었습니다.

다음 시간에는 잠시 ES2015로 되돌아가서 Symbol이라는 새로운 자료형에 대해 알아보도록 하겠습니다!

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

6개의 댓글이 있습니다.
4년 전
job3,job9 가 같이 시작하고, job6 는 job 3가 끝난 뒤 실행된다면 전체실행시간을 9초로 만들 수 있을텐데요.. 이런 코드는 없을까요? Promise.all 로는 구현이 안되겠지만
4년 전
밑에 댓글에 답이나와있습니다~
6년 전
const h = await hello; 이렇게 하게되면 hello의 resolve()된 값이 h에 대입 되는건가요?
6년 전
네 hello가 promise라면 resolve된 값이 h에 들어가고요. hello가 일반 값(문자열, 불린, 숫자, 객체 등등)이라면 그 값이 그대로 들어갑니다.
6년 전
Promise.all 도 await로 구현이 가능합니다. 대신 for-loop 구문이 사용될 뿐입니다.

마지막 내용을 Promise의 명시적 사용없이 구현한 코드를 여기에 붙이면 알아보기 어려워지니 코드펜 주소를 대신합니다.

https://codepen.io/Isitea/pen/erdepa
6년 전
for loop 구문이 사용된다고 하셨는데 코드펜에는 해당 내용이 없네요? for await of나 보여주신 방법대로 하면 Promise.all 없이도 됩니다.
6년 전
job(3) 다음에 job(6)를 실행하면 된다고 말씀하신 부분은 다음과 같이 하면 되는건가요>?
const job = (x) => {
return new Promsie( (resolve, reject) => {
setTimeout( () => {
console.log(x);
resolve(x);
}, x * 1000);
});
};

(async function(){
const [a, b] = await Promise.all( [job(3).then( () => { job(6) }), job(9) ]);
return a+b;
})().then(result => console.log(result));
6년 전
네 맞습니다!
7년 전
말씀하신 문제는 promise 중첩을 사용안하고 아래와 같이 하면 되겠네요
(async function() {
const j3 = job(3);
const j9 = job(9);

await j3;
await job(6);
await j9;
})()
4년 전
? 이렇게 하면 18초 걸리지 않나요
4년 전
아뇨 9초 걸립니다.
4년 전
아~ promise가 만들자마자 실행되고 있군요 이미. 9초네요!
7년 전
C# 만세!
7년 전
자바스크립트 만세!