Promise, Async-await, 그리고 병렬적 비동기 처리

ToastEggsToast·2021년 8월 8일
0

TIL

목록 보기
14/15

동기, 비동기?

  • 동기: 한 코드의 실행이 완료될 때 까지 완전히 기다렸다가 그 이후 다음 코드를 실행하는 것. 즉, 순차적인 코드 실행
  • 비동기: 한 코드의 실행이 완료될 떄 까지 기다리지 않고, 그 다음 코드를 먼저 실행시키는 것.

자바스크립트의 setTimeOut 메소드를 생각해보면 가장 이해가 빨리 될 것이라고 생각합니다.

console.log('first');

setTimeout(function() {
	console.log('second');
}, 5000);

console.log('third');

우리는 흔히 코드는 위에서부터 아래로 읽히고, 진행방향 또한 마찬가지로 위에서 아래로 진행된다고 생각합니다.
위의 코드도 그러한 맥락에서 바라보면 콘솔이 찍히는 순서가 'first', (3초 후)'second', 그리고 그 후 'third'가 찍힐 것이라고 생각할 수 있지만, 실제로는 'first', 'third', (3초 후)'secod' 순서로 출력됩니다.

이렇듯 자바스크립트는 오래 걸릴 것 같은 특정 로직의 실행이 완전히 끝날 때 까지 기다리지 않고, 그 뒤의 함수를 먼저 실행시키는 비동기적인 특징을 가지고 있습니다.

이러한 비동기적인 특징은 fetch 등을 이용해 api 통신을 할 때, 답신을 기다리고 그 답신에서 받아온 데이터를 가공하거나, 순서가 보장되어야하는 함수의 처리에 있어서 꽤 골칫거리가 될 것입니다.

자바스크립트는 이러한 비동기적 특성을 처리할 수 있도록 여러가지 방안을 조언하고 있는데요, 가장 대표적인 것이 바로 Promise 객체를 사용해 순서를 보장하도록 하는 것입니다.

Promise

위에서 이야기했듯 Promise는 비동기 상태를 값으로 다룰 수 있는 객체입니다.
Promise가 널리 보급되기 전에는 콜백 패턴이 많이 사용되었고, 그 이후 ES6에서 Promise가 자바스크립트 언어에 포함되었습니다.

프로미스는 다음 세 가지 상태 중 하나의 상태로 존재합니다.

  1. 대기중 (Pending): 결과를 기다리는 중
  2. 이행됨 (Fulfilled): 수행이 정상적으로 끝났고, 결과값을 가지고 있음
  3. 거부됨 (Rejected): 수행이 비정상적으로 끝났음
// Promise를 생성하는 방법
const p1 = new Promise((resolve, rejected) => {
  // ...
  // resolve(data)
  // or reject('error message');
});
const p2 = Promise.reject('error message');
const p3 = Promise.resolve(param);

then

then은 처리됨 상태가 된 프로미스를 처리할 때 사용하는 메서드입니다.
then메서드로 프로미스의 결과값이 인수로 전달되며, then은 항상 프로미스를 반환합니다. 따라서 하나의 프로미스에서 연속적으로 then 메서드를 호출할 수 있습니다.
then의 두 번째 인자로 onReject 함수를 받을 수 있습니다. Promise가 거부됨 상태인 경우 onReject함수가 존재하는 then을 만날 때 까지 이동합니다.

requestData1()
  .then(data => {
	return requestData2()
  })
  .then(data => {
	return data + 1;
  })
  .then(data => {
	throw new Error('some error');
  })

catch

catch는 프로미스 수행 중 발생한 예외를 처리하는 메서드입니다.
then 메서드의 onReject와 같은 역할을 하지만, 가독성 면에서는 onReject보다 catch 메서드를 사용하는 편이 더 좋습니다.
또한 then 메서드의 onResolve 함수에서 발생한 예의는 같은 then 메서드의 onReject함수에서 처리되지 않으므로 catch를 같이 사용해주는 것이 좋습니다.

Promise.reject(10)
.then(data => {
  console.log('then1: ', data);
  return 20;
})
.catch(err => {
  console.log('catch: ', err);
  return 30;
})
.then(data => {
  console.log('then2: ', data);
});

finally

finally는 프로미스가 이행됨 또는 거부됨 상태일 때 호출되는 메서드로, 프로미스의 가장 마지막 단계에서 사용됩니다.
finally 메서드는 이전에 사용된 프로미스를 그대로 반환하기 때문에 처리됨 상태인 프로미스의 데이터를 건드리지 않고 추가 작업을 할 때 유용하게 사용될 수 있습니다.
실패 여부와 관계 없이 서버에 로그를 보내거나 할 때 유용합니다.

function requestData() {
  return fetch()
    .catch(err => { ... })
    .finally(()=>{sendLogToServer('requestData finished');}
};

async - await

async await은 비동기 프로그램을 동기 프로그래밍처럼 작성할 수 있도록 함수에 추가된 기능입니다.
프로미스를 완전히 대체하는 것은 아니지만, 프로미스의 then 메서드를 체인 형식으로 호출하는 것보다 가독성이 좋아진다는 장점이 있습니다.

asnync await 함수는 프로미스를 반환하며, async는 함수를 정의할 때, await는 async로 정의 된 함수 내부에서 사용됩니다.
await 키워드 오른쪽에 프로미스를 입력하면, 그 프로미스가 처리됨 상태가 될 때 까지 기다립니다.

function requestData(value) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('requestData' : value);
      resolve(value);
    }, 100),
  );
}
                     
async function getData() {
  const data1 = await requestData(10);
  const data2 = await requestData(20);
  console.log(data1, data2);
  return [data1, data2];
}
  
getData();

// getData: 10
// getData: 20
// 10 20

async await 함수 내부에서 발생하는 예외는 다음과 같이 try catch문으로 처리하는게 좋습니다.

async function getData() {
  try {
    await doAsync();
    return doSync();
  } catch (error) {
    console.log(error);
  }
}

병렬적 비동기 처리

Promise

Promise.all을 이용하면 여러 개의 프로미스를 병렬로 처리할 때 사용하는 함수입니다.
비동기 함수 간 서로 의존성이 없다면, then으로 순차 실행을 진행하는 것 보다 all을 이용해 각각 호출되어 처리되도록 진행하면 좀 더 효율적으로 비동기 처리를 진행할 수 있습니다.

Promise.all함수는 프로미스를 반환하는데, 하나라도 거부됨 상태가 된다면 Promise.all에서 반환하는 프로미스도 거부됨(rejected)상태가 됩니다.

Promise.all([requestData1(), requestData2()])
.then([data1, data2] => { ... });

async - await

프로미스는 생성과 동시에 비동기 코드가 실행되므로, 두 개의 프로미스를 먼저 생성하고 await 키워드를 나중에 사용하면 병렬로 실행되는 코드가 됩니다.

async function getData() {
  const p1 = asyncFunc1();
  const p2 = asyncFunc2();
  const data1 = await p1();
  const data2 = await p2();
  //...
};

Promise.all을 async await과 함께 사용하면 위 코드를 더 간략하게 줄일 수 있습니다.

async function getData() {
  const [data1, data2] = await Promise.all([asyncFunc1(), asyncFunc2()]);

Summary

Promise를 사용하는 것과, async await을 사용하는 것 어떤 것이 더 좋은 것인지는 알 수 없지만,
늘 공부 할 때 마다 비동기 동기 단어가 늘 헷갈리는 것 같습니다.
여러가지 api를 동시에 호출해 연관되지 않은 데이터를 받아올 때에는 병렬 호출을 통해 좀 더 효율을 높일 수 있는 코드를 짤 수 있도록 해야겠습니다. :)

참고 서적: 실전 리액트 프로그래밍 개정판 (인상트 / 이재승 지음)

profile
개발하는 반숙계란 / 하고싶은 공부를 합니다. 목적은 흥미입니다.

0개의 댓글