Javascript 비동기 프로그래밍 진화기

망7H·2021년 5월 4일
1

비동기 처리는 javascript의 XHR (XMLHttpRequest)이나 jquery를 사용한 ajax등을 사용해서 서버에서 데이터를 비동기적으로 처리하도록 요청할 수 있습니다.
이번 포스팅에서는 XHR이나 AJAX가 아닌, 이러한 비동기 처리를 가독성 있게 다루기 위한 비동기 프로그래밍의 진화기를 다뤄보겠습니다.

1. Call back

과거 javascript에서 비동기 프로그래밍을 하기 위해 콜백(Call Back) 패턴을 많이 사용하였습니다.
하지만 콜백 패턴은 콜백이 중첩되면 코드가 복잡해지는 단점이 있었습니다.
아래의 예제 코드를 보겠습니다.

function getData1(callback) {
  console.log("getData1");
  ...
  callback(data);
}

function getData2(callback) {
  console.log("getData2");
  ...
  callback(data);
}

function onSuccess1(data) {
  console.log("onSuccess1");
  getData2(onSuccess2);
}

function onSuccess2(data) {
  console.log("onSuccess2");
}

getData1(onSuccess1);


// 출력 결과
getData1
onSuccess1
getData2
onSuccess2

콜백이 겨우 2개임에도 읽기가 무척 불편한 소스가 만들어졌다.
실제 실서비스를 개발하는 과정에서도
상태 확인 → 데이터 처리 → 후처리를 하는 등의 콜백들이 순서대로 호출되어야 할 때가 있다.

이러한 콜백 지옥을 해결한 비동기 프로그래밍의 혁신은 ES6에서 Promise가 나오면서 시작되었다.

2. Promise

프로미즈(Promise)는 비동기 상태를 값으로 다룰 수 있는 객체이다. 프로미즈를 사용하면 비동기 프로그래밍을 작성 할 때, 동기 프로그래밍 방식으로 코드를 작성할 수 있다.

프로미즈는 3가지 상태를 가질 수 있다.

1) Promise의 3가지 상태

  • Pending(대기중): 결과를 기다리는 중
  • Fulfilled(이행됨): 수행이 정상적으로 끝났고 결과값을 가지고 있음.
    (※ Settled(처리됨) 상태)
  • Rejected(거부됨): 수행이 비정상적으로 끝났음.
    (※ Settled(처리됨) 상태)

프로미즈의 상태가 Settled(처리됨) 상태가 되면 더 이상 다른 상태로 변경될 수 없다.
오로지 Pending(대기중) 상태일 때만 Fulfilled(이행됨) 또는 Rejected(거부됨) 상태가 될 수 있다.

2) Promise를 생성하는 3가지 방법

  • 방법 1 (가장 일반적)
    비동기적으로 유저의 목록을 조회하는 RESTful API를 사용하여 성공적으로 데이터를 조회한 경우에는 resolve 메서드가 동작하고, 에러가 발생한 경우에는 reject 메서드가 동작하도록 하였다.
    ※ 이번 예제에서 Jquery의 Ajax를 사용하였지만, 어떤 비동기 함수를 사용하던 상관은 없다.
const promise1 = new Promise(function(resolve, reject) {
  ...
  $.ajax({
    url: '/User',
    data: {staff_no: 1022},
    type: 'get',
    dataType: 'json',
    error: function(err) {
      reject(err);
    },
    success: function(data) {
      resolve(data);
    }
  });
  ...
});  
  • 방법 2
const promise2 = Promise.resolve(data);

비동기로 어떤 작업을 수행한 후 성공했을 때는 resolve를 호출한다.
resolve를 호출하면서 반환된 Promise 객체는 Fulfilled(이행됨) 상태가 된다.

  • 방법 3
const promise3 = Promise.reject(err);

비동기로 어떤 작업을 수행한 후 실패했을 때는 reject를 호출한다.
reject를 호출하면서 반환된 Promise 객체는 Rejected(거부됨) 상태가 된다.

3) Promise를 이용하기

(1) then(onResolve, onReject)

then은 Settled(처리됨) 상태가 된 프로미스를 처리할 때 사용되는 메서드입니다.
즉, then은 Fulfilled(이행됨)Rejected(거부됨)의 상태가 된 프로미즈를 둘 다 처리할 수 있다는 것.

then 메서드는 전달 받은 프로미즈의 상태가
Fulfilled(이행됨)인 경우에는 onResolve 메서드가 동작하고,
Rejected(거부됨)인 경우에는 onReject 메서드가 동작한다.

그리고 then 메서드가 동작한 이후에 then은 항상 프로미즈 객체를 반환합니다.
만약 then 메서드 내부에서 프로미즈가 아닌 값을 반환하는 경우에는 then 메서드는 Fulfilled(이행됨) 상태의 프로미즈를 반환합니다.

아래 예제를 통해 정확히 이해할 수 있습니다.

Promise.reject('err')
  .then(() => console.log('then 1'))
  .then(() => console.log('then 2'))
  .then(() => console.log('then 3'), () => console.log('then 4'))
  .then(() => console.log('then 5'), () => console.log('then 6'))
  .then(() => console.log('then 7'), () => console.log('then 8'))

// 출력 결과
then 4
then 5
then 7

reject 메서드에 프로미즈는 Rejected(거부됨) 상태를 반환하고
onReject가 있는 then 메서드로 쭉 내려가면서
'then 4'를 출력하는 then 메서드의 onReject 콜백 호출.
이후, 반환하는 데이터가 없기 때문에 프로미즈는 Fulfilled(이행됨) 상태로
onResolve가 있는 다음 then 메서드로 내려가면서
'then 5'를 출력한다.
이후, 반환하는 데이터가 없기 때문에 프로미즈는 Fulfilled(이행됨) 상태로
onResolve가 있는 다음 then 메서드로 내려가면서
'then 7'을 출력한다.

(2) catch(onReject)

catch 메서드는 then의 두번째 파라미터인 onReject가 동작하는 것과 동일한 동작을 한다.
catch 메서드를 호출한 프로미즈 객체의 상태가 Rejected(거부됨)인 경우에 동작한다.
then으로도 catch와 동일한 기능을 할 수 있지만, 가독성 측면에서 catch를 사용하는 것이 더 좋다.

아래의 예시 코드는 then과 catch를 사용하여 만든 동일한 기능이다.

// then(null, onReject) 사용
Promise.reject('err').then(null, err => {
  console.log(err);
});

// catch(onReject) 사용
Promise.reject('err').catch(err => {
  console.log(err);
});

catch 메서드도 then 메서드와 마찬가지로 반환하는 프로미즈가 없는 경우에는 Fulfilled(이행됨) 상태의 프로미즈를 반환한다.
즉, catch 다음의 메서드로 then을 또 사용할 수 있다는 것!

4) Promise 활용하기

(1) Promise.all 을 사용한 병렬처리

then 메서드를 체인으로 연결하면 각각의 비동기 처리는 병렬로 진행되지 않는다는 단점이 있다.
아래의 예시 코드를 보도록 하자.

callPromiseData1()
  .then(data => {
    console.log('function 1');
    return callPromiseData2();
  })
  .then(data => console.log('function 2'))

만약, 첫번째 then 메서드와 두번째 then 메서드 사이에 의존성이 있다면 위와 같이 순차적으로 연결하는게 합당하다.
ex) 첫번째 then 메서드에서 반환하는 데이터를 두번째 then 메서드에서 사용해야 하는 경우.

하지만, 첫번째 then 메서드와 두번째 then 메서드가 의존성이 없다면?
아래와 같이 구현해도 무방하다.

callPromiseData1().then(data => console.log('function 1'));
callPromiseData2().then(data => console.log('function 2'));

하지만 이렇게 쓰면 병렬처리가 되더라도 가독성 측면에서 썩 좋지는 않아보인다.
그래서 Promise.all 을 사용한다.

// Promise.all을 사용한 병렬처리
Promise.all([callPromiseData1(), callPromiseData2()])
  .then(([data1, data2] => {
    console.log(data1, data2);
  })

Promise.all이 반환하는 프로미즈 객체는
내부에서 반환되는 프로미즈 객체가 하나라도 Rejected(거부됨) 상태가 되었다면
Rejected(거부됨) 상태의 프로미즈가 반환된다.

5) Promise를 사용할 때의 주의점

then 메서드 내부 함수에서 return 키워드를 반드시 입력하라.
then 메서드가 반환하는 프로미즈 객체의 데이터는 내부 함수가 반환한 값이다.
return 키워드를 사용하지 않으면 프로미즈 객체의 상태는 이행됨 일지라도 데이터는 undefined이다.


3. Async/Await

프로미즈가 비동기 상태를 객체로 다루는 기능이라는 관점에서
AsyncAwait는 프로미즈를 활용하는 방법 정도로 이해하면 된다.

1) Async

프로미즈가 비동기 상태를 다루는 객체 상태로 존재하는 객체에 적용되는 개념이라면,
Async은 함수에 적용되는 개념이다.

async가 붙은 함수는 프로미즈를 반환한다.
아래의 예시 코드는 동일한 기능을 하는 소스 코드이다.

async function getData() {
  return 123;
}

async function getData() {
  return Promise.resolve(123);
}

이전의 then이나 catch와 마찬가지로 async 키워드가 붙은 함수도 프로미즈 객체를 반환한다.
이때, 내부 메서드에서 reject를 사용하였거나 예외가 발생한 경우에는 Rejected(거부됨) 상태의 프로미즈를 반환하고 이 밖의 정상적인 경우에는 Fulfilled(이행됨) 상태의 프로미즈를 반환한다.

2) Await

await 키워드는 async 함수 내부에서 사용된다.
await 키워드의 우측에 프로미즈를 반환하는 코드를 작성하면 반환하는 프로미즈가 Settled(처리됨) 상태가 될 때까지 기다린다.

쇼핑몰에서의 동작을 가정하여 예제코드를 작성해보겠다.
비동기적으로 주문 목록을 가져오는 getOrderList와
가져온 주문 목록을 사용하여 조건에 맞는 주문에 사은품을 매칭 시키는 비동기 setGift 메서드를
await를 사용하여 프로미즈를 반환하기를 기다리도록 한다.

async function getData() {
  ...
  const orders = await getOrderList();
  const ordersAndGift = await setGift(orders);
  ...
}

3) Promise와 Async/Await 코드 비교

아래의 예제 코드는 동일한 기능에 대한 비동기 프로그래밍을 Promise와 Async/Await로 구현한 것이다.

// Promise 방식
function getData() {
  callPromiseData1()
  .then((data) => {
    console.log(data);
    return callPromiseData2();
  })
  .then((data) => console.log(data);
}

// Async, Await 방식
async function getData() {
  const data1 = await callPromiseData1();
  console.log(data1);
  const data2 = await callPromiseData2(); 
  console.log(data2);
}


이번 포스팅에서는 가장 기본적인 비동기 프로그래밍들에 대해 다루어보았습니다.
Promise만 잘 다루어도 충분히 가독성 있는 코드가 만들어진다고 생각하지만,
더욱 잘 확장되고 만들어진 기능인 async/await를 다루는 것도 중요하다 생각됩니다.

해당 글 작성에 참고한 링크

[도서] 실전 리액트 프로그래밍

profile
망한 개발자의 개발 기록입니다. 저를 타산지석으로 삼으시고 공부하세요.

0개의 댓글