JavaScript - 동기와 비동기, Promise (async, await)

3 분 소요

동기와 비동기

보통의 자바스크립트 코드는 위에서 아래로, 실행한 순서대로 진행됩니다.

console.log(1);
console.log(2);
console.log(3);
console.log(4);
/*
1
2
3
4
*/

동기적인 처리 방식으로 인해 위의 코드의 연산이 끝날 때까지 기다렸다가, 순차적으로 다음 코드를 실행하게 됩니다.

하지만 클라이언트에서 서버로 데이터를 요청했을 때, 서버가 요청에 대한 응답을 받을 때까지 10초가 걸린다면(..)

다음 코드는 실행되기 까지 10초나 기다려야 한다니, 너무나 비효율적이겠죠!

console.log(1);
setTimeout(() => console.log(2),1000);
console.log(3)
/*
1
3
2
*/

보통 우리는 그런 흐름에서 비동기적으로 처리하게 됩니다.

JavaScript에서 비동기 처리를 위한 Promise에 대해서 알아보도록 합시다!


Promise

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.

Promise 객체는 3가지의 상태를 가지고 있습니다.

Promise 객체를 사용하면서 우리가 지정한 operation을 수행 중일 때는 pending 상태가 되고,

우리가 지정한 operation을 완전히 수행하고 나서는 성공적으로 수행이 됐다면 fulfilled,

중간에 문제가 있었다면 rejected 상태가 됩니다!

실제로 Promise를 사용해보면서 좀 더 알아보도록 합시다

Promise 생성

Promise는 함수에 콜백을 전달하는 대신에, 콜백을 첨부하는 방식의 객체입니다.

const mypromise = new Promise((resolve, reject) => {
  console.log('뭔가 헤비한 일');
  setTimeout(() => resolve('mengkki'), 2000);
});

비동기적으로 실행되는 동작이 성공적으로 완료됐을 때 resolve()를 호출하고, 실패했을때는 reject()를 실행합니다.

위 예제에서는 비동기적인 코드의 예를 들기 위해 setTimeout을 사용했습니다. 실제로는 API콜과 같은 동작이 들어가겠죠!

Promise 사용

mypromise.then((msg) => console.log(msg));

/*
뭔가 헤비한 일
mengkki
*/

promise 뒤에 then을 사용해서 msg라는 인자를 불러왔는데요,

여기서 msg에는 위 Promise에서 resolve에 전달한 값이 들어가게 됩니다.

따라서 위 코드를 실행하게 되면

뭔가 헤비한 일 « 이 가장 먼저 출력되고, 2초 후 mengkki가 출력됩니다.

reject를 사용한 경우

const mypromise = new Promise((resolve, reject) => {
  console.log('뭔가 헤비한 일');
  setTimeout(() => reject(new Error('에러났다')), 2000);
});
mypromise.then((msg) => console.log(msg));

위 코드를 실행하면 콘솔에는 아래와 같이 나타납니다.

Uncaught 에러가 발생했습니다.

mypromise
  .then((msg) => console.log(msg))
  .catch((error) => console.log(error))

위 코드를 이렇게 수정해주면 콘솔에 이렇게 나타납니다.

catch는 promise 실행 중 오류를 잡아내는 역할을 합니다.

error인자로 reject에 전달된 에러를 받아와서 콘솔에 찍어준 것이죠!

번외) finally

mypromise
  .then((msg) => console.log(msg))
  .catch((error) => console.log(error))
  .finally(() => console.log('파이널리'))

finally는 promise 가 성공적으로 수행되던 에러가 발생하던 상관없이 실행됩니다!

Promise chaining

then 다음에 catch가 .으로 쭉쭉 이어지고 있죠? 이것을 promise chain이라고 하는데요,

보통 하나나 두개 이상의 비동기 작업을 순차적으로 진행해야 할 때 사용합니다.

const getNumber = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 1000);
});

getNumber.then((num) => num * 2)
         .then((num) => num * 3)
         .then((num) => new Promise((resolve, reject) => {
          setTimeout(() => resolve(num - 3), 1000)
        }))
         .then((num) => console.log(num)); // 3

처음에 resolve로 1이 들어가기 때문에 num이 1이 되고,

그 후 2, 그 후 6이 되었다가 3을 빼면서 최종적으로 3이 콘솔에 찍히게 됩니다.

doSomeThing()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

then 함수는 새로운 promise를 리턴합니다. 그렇기 때문에 뒤에 .으로 then이나 catch같은 것을 쭉쭉 이을 수 있습니다!

이 예제에서는 doSomeThing이 리턴되면 그 값으로 doSomethingElse를 호출하고 또 그 결과로 doThirdThing, 마지막으로 finulResult를 콘솔에 찍게 됩니다.

만약 도중에 에러가 발생한다면 failureCallback을 실행하게 되겠네요!

중요: 반환값이 반드시 있어야 합니다, 만약 없다면 콜백 함수가 이전의 promise의 결과를 받지 못합니다. (화살표 함수 () => x는 () => {return x;}와 같습니다).


async, await

promise chaining이 연속적으로 이어지다보면 코드가 난잡하게 보일 수 있습니다.

async와 await은 promise를 좀더 간편하게 사용할 수 있도록 해주는 syntatic sugar입니다!

async

function getUser(){
  return new Promise((resolve, reject) => {
    resolve('mengkki');
  })
}

const user = getUser();
user.then((user) => console.log(user));

기존의 promise를 리턴하는 함수입니다. 이걸 async를 활용하게 된다면

async function getUser(){
  return 'mengkki';
}

const user = getUser();
user.then((user) => console.log(user));

짜잔! 정말 간편해졌죠!

async가 붙어있는 함수의 블럭 안은 자동적으로 promise가 됩니다.

await

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

async function getApple(){
  await delay(3000);
  return '🍎';
}

async function getBanana(){
  await delay(3000);
  return '🍌';
}

await은 async함수 내에서만 사용할 수 있습니다.

await는 async 함수의 실행을 일시 중지하고 전달된 promise가 풀릴때까지 기다린 후 async함수의 실행을 다시 시작하고 완료후 값을 반환합니다.

async function getBanana(){
  await delay(3000);
  return '🍌';
}

function getBanana2(){
  return delay(3000)
  .then(() => '🍌');
}

위 코드와 아래의 코드는 같은 일을 합니다.

이렇게만 봐서는 await이 뭐가 좋은지 모르겠죠..?하지만… chaining이 복잡해진다면 이야기는 달라집니다.

function pickFruits(){
  return getApple().then((apple) => {
    return getBanana().then(banana => `${apple} + ${banana}`);
  });
}

async function pickFruits(){
  const apple = await getApple();
  const banana = await getBanana();
  return `${apple} + ${banana}`;
}

위 코드와 아래 코드는 하는 일은 같지만 바로 차이가 드러나죠!

모양새가 간단해지긴 했지만 위 코드는 사과를 먼저 받고 바나나를 받게 됩니다.

사과를 받는데 바나나가 필요없고, 바나나를 받는 데 사과가 필요없다면 아래와 같이 병렬적으로 수정해볼 수 있겠습니다.

function pickFruits(){
  return Promise.all([getApple(), getBanana()])
  .then(fruits => fruits.join(' + '))
}

pickFruits().then(console.log); // 🍎 + 🍌

Promise의 all API를 이용해서 Promise들을 배열의 형태로 넣어주면 병렬적으로 실행됩니다!

function pickFruits(){
  return Promise.race([getApple(), getBanana()]);
}

pickFruits().then(console.log); // 🍎

promise의 race API를 사용하면 주어진 promise 중 가장 먼저 끝나는 것을 리턴하게 됩니다.

참조

MDN Promise

드림코딩 유튜브

댓글남기기