본문 바로가기

Web

Javascript 비동기 처리 방식의 변화 및 효율적인 처리

2022년 기준으로 비동기 처리에는 async & await 함수가 가장 많이 사용되고 있습니다. async & await은 ES8에 해당하는 문법을 통해 우리는 손쉽게 비동기를 처리할 수 있게 됩니다. 과거에는 어떻게 비동기를 처리하고 있었을까요?

callback 함수를 통하여 처리

jQuery를 사용한 경험이 있다면 Ajax를 통하여 서버에 request를 전송한 경험이 있을 것입니다. 이때 우리는 response 값을 받기 위해 success에 함수를 넣어 데이터를 callback 함수의 인자를 통하여 전달받았습니다. 

$.ajax({
  url: 'https://www.examples.com/post/1',
  success: function (payload) {
    ...
  }
})

 

콜백으로 여러가지의 비동기를 처리하게 될 때 우리는 여러 가지 문제를 접하게 되는데 대표적으로 콜백 지옥 (Callback hell)을 접하게 될 수도 있습니다.

콜백 지옥(Callback hell) 이란?

$.ajax({
  url: 'https://www.examples.com/post/1',
  success: function (payload) {
    $.ajax({
      url: 'https://www.examples.com/user/'+payload.userId,
      success: function (userPayload) {
        ...
      }
    })
  }
})

연속된 비동기 처리를 위해 callback 함수 안에 callback 함수가 들어가 점점 들여 쓰기 뎁스가 높아지며 가독성이 떨어지게 됩니다. 이를 콜백 지옥(Callback hell)이라고 합니다. 위 문제는 callback 함수를 따로 선언하여 간결하게 만들 수 있습니다. 

function fetchPost() {
  $.ajax({
    url: 'https://www.examples.com/post/1',
    success: fetchUser
  })
}
function fetchUser(payload) {
$.ajax({
  url: 'https://www.examples.com/user/'+payload.userId,
    success: finish
  })
}
function finish() {
  console.log("finish")
}
function run() {
  fetchPost()
}
run()

들여 쓰기 뎁스를 줄이는 것은 성공했지만 한눈에 함수의 호출 순서를 파악하는데 어려움은 해결하지 못했습니다. 하지만 Promise의 등장으로 callback 함수를 통한 비동기 처리를 지양하게 됩니다.

Promise를 통하여 처리

Promise는 자바스크립트 비동기 처리를 위해 만들어진 객체로 callback 함수보다 직관적이고 깔끔하게 비동기 처리가 가능해집니다. 비동기를 처리하기 전에 Promise의 3가지 상태에 대해 먼저 알아야 합니다.

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

위 Ajax를 Promise를 통해 비동기 처리를 하면 다음과 같습니다. 우선 Promise 객체로 Ajax를 wrapping한 함수를 하나 생성합니다.

function ajaxWithPromise(ajaxConfig) {
  return new Promise((resolve, reject) => {
    $.ajax({
      ...ajaxConfig,
      success: resolve,
      error: reject
    })
  })
}

Promise는 생성 인자로 함수를 하나 받게 되는데요. 그 함수의 인자로 resolve와 reject를 받게 됩니다. 각 인자는 함수로 호출 시 Promise의 상태를 변경시킵니다.

  • resolve: Fulfilled(이행) 완료 상태로 변경 시킵니다. 정삭적으로 비동기 처리가 되었을 때 사용합니다
  • reject: Rejected(실패) 실패 상태로 변경 시킵니다. 정삭적으로 비동기 처리에 실패했을 때 사용합니다

아래와 같이 사용 가능하며, 요청의 response 값에 따라 then 이나 catch로 분기하여 요청 결과 값이나 에러코드를 출력하고 있습니다.

ajaxWithPromise({
  url: 'https://www.examples.com/post/1'
}).then((payload) => {
  console.log(payload)
}).catch((error) => {
  console.log(error)
})

 

2개 이상의 비동기 처리

Promise 객체는 then 메소드를 실행 후 또 다른 Promise 객체를 반환하게 됩니다. 그러므로 쉽게 체이닝을 통하여 처리가 가능합니다. 만약 then 메서드에서 Promise 객체를 반환하게 하는 경우, 다음 then에 반환한 Promise에 대한 결과가 나오게 됩니다. 아래와 같이 높은 가독성을 유지하며 여러 개의 비동기 처리가 가능해집니다.

ajaxWithPromise({
  url: 'https://www.examples.com/post/1'
}).then((payload) => {
  return ajaxWithPromise({
    url: `https://www.examples.com/user/${payload.userId}`,
  })
}).then((payload) => {
  ...
}).catch((error) => {
  console.log(error)
})

효율적인 2개 이상의 독립적인 비동기 처리 (Promise.all, Promise.allSettled)

위 예제들은 user/{userid} 요청이 post/1 요청에 결괏값에 의존하고 있습니다. 하지만 만약 서로 독립적인 요청이라면 위 방식으로 처리하게 된다면 불필요한 지연이 발생하게 됩니다. 예를 하나 들어봅시다.

위 코드를 그림으로 표현하면 위 그림과 같습니다. 총 4초의 시간이 소요하고 비동기 처리가 종료됩니다. 하지만 가정을 하나 해봅시다. 우리가 글 ID와 작성자 ID를 가지고 있다면, 순차적으로 처리하는 것은 배우 비효율적입니다. 우리는 아래 그림과 같이 효율적이게 게시판 정보와 작성자 정보를 한 번에 불러오기를 원하게 됩니다.

우리는 Promise.all 또는 Promise.allSettled을 통하여 위와 같이 구현이 가능합니다. 앞 두 메서드의 주요 차이점은 다음과 같아 상황에 맞게 사용하면 됩니다.

  • all: 모든 Promise 객체가 Fulfilled 즉 성공해야 then 구문이 실행되게 됩니다.
  • allSettled: 모든 Promise 객체가 Fulfilled 하지 않아도 then 구문이 실행됩니다.

모든 Promise 객체가 성공해야 정상적으로 처리했다고 생각이 들면 아래 코드처럼 Promise.all을 통하여 에러를 처리하는 것을 추천드립니다. Promise.all 인자에 Promise 객체가 담긴 배열을 넘겨주면 배열에 담긴 모든 Promise 객체가 Fulfilled 일 때 then 구문을 실행하게 됩니다. 이로 인하여 4s -> 2s로 비동기 처리 소요시간을 줄일 수 있게 됩니다.

Promise.all([
  ajaxWithPromise({url: `https://www.examples.com/post/${postId}`}),
  ajaxWithPromise({url: `https://www.examples.com/user/${userId}`})
]).then(([post, user]) => {
  console.log(post, user);
}).catch(error => {
  console.log(error);
})

 

Promise를 통하여 충분히 훌륭하게 비동기 처리가 가능합니다. 하지만 우리는 더 편하고 읽기 쉬운 코드를 원해 async & await 문법을 사용하게 됩니다.

async & await

async & await은 비동기 처리를 위한 새로운 객체는 아니고, Promise 객체를 보다 간단하고 편하게 처리할 수 있도록 도와주는 새로운 문법입니다.

async function initialize() {
  const post = await ajaxWithPromise({url: 'https://www.examples.com/post/1'})
  const user = await ajaxWithPromise({url: `https://www.examples.com/user/${post.userId}`})
  console.log(post, user)
}

async 예약어를 붙인 함수 본문에는 await을 통하여 보다 직관적이게 비동기 함수를 처리할 수 있게 됩니다. 하지만 한 가지 의문이 생기게 되는데요. "이렇게 되면 에러 처리는 어떻게 해야 되는 것인가?" 이 의문점은 try-catch 구문을 통하여 해결 가능합니다. (만약 두 요청이 독립적이라면 Promise.all으로 감싼 후 await을 통하여 효율적이게 사용 가능해집니다.)

async function initialize() {
  try {
    const post = await ajaxWithPromise({url: 'https://www.examples.com/post/1'})
    const user = await ajaxWithPromise({url: `https://www.examples.com/user/${post.userId}`})
    console.log(post, user)
  } catch (error) {
    console.log(error)
  }
}

마무리

Javascript의 비동기 처리방식의 변화에 대해서 정리해봤습니다. 비동기 처리는 API 서버와의 통신이 잦은 프런트엔드 개발자라면 반드시 알고 있어야 한다 생각합니다. 실제로는 async & await을 통하여 거의 대부분을 처리하고 있지만 과거 비동기 처리도 알아두면 좋을 거 같습니다. 위 글을 통해 비동기에 대한 이해에 도움이 됐으면 좋겠습니다.

 

참고

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

 

Promise - JavaScript | MDN

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

developer.mozilla.org

https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/

 

자바스크립트 비동기 처리와 콜백 함수

(중급) 중급 자바스크립트 개발자가 되기 위한 자바스크립트 비동기 처리와 콜백 함수 이해하기. 콜백 지옥과 해결 방법 등

joshua1988.github.io