[스터디] 비동기 프로그래밍 in JavaScript

Moen·2023년 2월 3일
1

비동기 프로그래밍

비동기 프로그래밍은 현재 실행중인 작업이 완료되지 않아도 다음 작업을 실행할 수 있는 방식입니다.

즉, 동식에 여러 작업이 진행할 수 있는 장점이 있습니다. 하지만 코드가 실제로 실행되는 순서가 항상 동일하지 않으므로, 코드의 가독성을 해치고 디버깅을 어렵게 만든다는 비판을 받아왔습니다. 이러한 문제점을 해결하기 위해 JavaScript에서는 다양한 비동기 프로그래밍 기법이 존재합니다.

Callback 함수

프로그래밍에서 콜백 또는 콜백 함수는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드입니다. 콜백 함수로 비동기 로직을 작성할 수 있지만, 모든 콜백 함수가 비동기 로직에 사용되는 것은 아닙니다. 배열 고차 함수 map, filter, etc… 에서 인수로 콜백 함수를 받아서 동기적으로 사용 될 수 있습니다.

// [Deep Dive] 도서에 코드를 가져왔습니다.

const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts';

const getPosts = (url) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      console.log(JSON.parse(xhr.response));
    } else {
      console.error(`${xhr.status} ${xhr.statusText}`);
    }
  };
};

const posts = getPosts(POSTS_URL);

console.log(posts) // ??

console.log(posts)의 결과는 undefined 입니다. undefined 값이 나온 이유는 getPostsxhr 내장 함수가 비동기로 동작하기 때문에 모든 동기 로직이 끝난 후에 비동기 로직이 실행되어서 실행 순서를 보장받지 못해서입니다. 이렇듯 비동기 로직을 통해 결괏값을 받아 오기 위해서는 콜백 함수 내부에서 그 처리를 진행해야 합니다.

const getPosts = (url, successCallback, failCallback) => {
  const xhr = new XMLHttpRequest();

  xhr.open('GET', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      successCallback(JSON.parse(xhr.response));
    } else {
      failCallback(xhr.status, xhr, statusText);
    }
  };
};

const posts = getPosts(
  POSTS_URL,
  result => {
    // 성공 로직
  },
  error => {
    // 실패 로직
  }
);

이제는 비동기 로직의 통한 결괏값을 받아 오기 위해서는 콜백 함수 내부에서 모든 작업을 처리하는 방법으로 비동기 로직을 동기적으로 수행할 수 있게 되었습니다. 그런데 만약 해당 콜백 함수의 결괏값을 받아서 또 다른 네트워크 통신을 여러 번 진행해야 하거나, 여러 에러 처리를 진행해야 하는 경우는 어떻게 해야 할까요?

getPosts(POSTS_URL, result => {
  getPosts(POSTS_URL, result => {
    getPosts(POSTS_URL, result => {
      getPosts(POSTS_URL, result => {
        getPosts(POSTS_URL, result => {
          getPosts(POSTS_URL, result => {
            getPosts(POSTS_URL, result => {
              getPosts(POSTS_URL, result => {
                getPosts(POSTS_URL, result => {});
              });
            });
          });
        });
      });
    });
  });
});

극단적인 예시이지만 데이터 로직이 조금만 복잡해져도 가독성이 떨어지면서 쉽게 접근할 수 없는 로직이 만들어집니다. 이런 상황이 콜백 헬(callback hell) 이라는 단어로 붙었습니다.

Promise

콜백 함수의 문제점을 해결하기 위해 ES6에서 Promise 생성자 함수가 등장했습니다. Promise 생성자 함수의 인수 resolvereject 는 JavaScript에서 자체 제공하는 콜백 함수입니다.

  • resolve(value): 성공적으로 로직이 끝난 경우 value 인자와 함께 호출됩니다.
  • reject(error): 에러가 발생 시 에러 객체를 나타내는 error 인자와 함께 호출됩니다.
const getPostsWithPromise = (url) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open('GET', url);
    xhr.send();

    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.response));
      } else {
        reject(xhr.status, xhr.statusText);
      }
    };
  });
};

const posts = getPostsWithPromise(POSTS_URL);
console.log('posts: ', posts);

그렇다면 Promise 생성자 함수에서 어떻게 성공과 실패를 판단하고 후속 처리를 할 수 있을까요? 

Promise 생성자 함수 결괏값으로는 Promise 객체를 반환합니다. 반환 결과로 상태를 가지고 있으며 JavaScript에서 직접 접근할 수 없는 값으로 이루어져 있습니다.

  • pendding: 비동기 처리가 아직 처리되지 않은 상태
  • fulfilled: 비동기 처리가 성공적으로 수행된 상태 → resolve(value) 콜백 함수를 실행
  • rejected: 비동기 처리가 실패적으로 수행된 상태 → reject(error) 콜백 함수를 실행
🔑 settled: pendding 상태에서 fulfilled 또는 rejected 상태로 변화하는 모든 과정

pendding, fulfilled, rejected 의 결과를 통해 Promise 함수가 판단해서 콜백 함수를 실행해줍니다.

또한, Promise 생성자 함수의 인수 resolvereject 콜백 함수에서 비동기 처리 상태가 변화하면 이에 따른 후속처리를 해야 합니다. Promise 생성자 함수가 반환하는 Promise 객체에는 then, catch, fallnay 후속 처리 메서드를 제공합니다.

Promise.propotype.then

Promise.propotype.then 메서드는 Promise 후처리에서 가장 기본입니다.

  • 첫 번째 콜백 함수는 Promise가 fullfilled 상태가 되면 호출됩니다. 이때 콜백 함수의 인수로 비동기 처리 결과를 받습니다.
  • 두 번째 콜백 함수는 Promise가 rejected 상태가 되면 호출됩니다. 이때 콜백 함수의 인수로 에러 객체를 받습니다.
const posts = getPostsWithPromise(POSTS_URL);

posts.then(
  result => console.log(result),
  error => console.error(error)
);

then 메서드는 언제나 Promise 객체를 반환합니다. 만약 이전 비동기 결과를 바탕으로 새로운 비동기 결과를 반환하고 싶다면 then 메서드를 체이닝 할 수 있습니다.

const posts = getPostsWithPromise(POSTS_URL);

posts
  .then(
    result => getPostsWithPromise(result.POSTS_URL),
    error => console.error(error)
  )
  .then(
    result => getPostsWithPromise(result.POSTS_URL),
    error => console.error(error)
  )
  .then(
    result => getPostsWithPromise(result.POSTS_URL),
    error => console.error(error)
  )
  .then(
    result => console.log(result),
    error => console.error(error)
  );

Promise.prototype.catch

Promise.prototype.catch 메서드는 한 개의 콜백 함수를 인수로 전달 받습니다. catch 메서드는 rejected 상태인 경우에만 호출됩니다. 즉, Error 처리를 위한 후처리 메서드입니다.

const posts = getPostsWithPromise(POSTS_URL);

posts.catch(
  error => console.error(error)
);

또한 catch 메서드 항상 Promise 객체를 반환받으며 then(null, onErrorCallback) 과 동일하게 동작합니다.

const posts = getPostsWithPromise(POSTS_URL);

posts.then(
	null,
  error => console.error(error)
);

Promise.prototype.finally

  • Promise.prototype.finally 메서드는 한 개의 콜백 함수를 순수로 전달받습니다.
  • Promise.prototype.finally 메서드의 콜백 함수는 성공 또는 실패와 무관하게 무조건 한 번 실행(호출)합니다.
  • Promise.prototype.finally 메서드는 Promise 객체를 반환합니다.
  • Promise.prototype.finally 메서드는 프로미스 상태와 상관없이 공통적으로 수행해야 하는 처리 과정이 있을 때 유용합니다.
posts.finally(() => console.log('프로미스 공통 처리'));

async-await

그러나 Promise는 여전히 콜백 함수를 사용하기 때문에, 콜백 헬의 문제를 해결할 수 없습니다. ES6에 제너레이터가 도입되어 비동기를 동기처럼 구현했지만, 코드가 장황해지고 가독성이 나빠졌다. 이에 뒤따라 ES8에서 보다 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 구현하는 async-await가 도입되었습니다.

async/await는 Promise를 기반으로 동작하며, then/catch/finally와 같은 후속 처리 메서드 없이 마치 동기 처리처럼 사용할 수 있습니다.

const getPostWithAsync = async (url) => {
  try {
    const response = await fetch(url);
    return await response.json();
  } catch (err) {
    console.err(err);
  } finally {
    console.log('공통 처리');
  }
};

const posts = getPostWithAsync(POSTS_URL);
console.log('posts: ', posts);

posts.then(console.log);

콜백 함수나 Promise는 무조건 api를 호출한 후, 또 다른 콜백 함수를 실행하여 데이터의 처리가 가능했지만, async/await는 해당 함수 내부에서 바로 동기 처리처럼 데이터를 수정할 수 있다. 또한 try-catch 문으로 에러 처리를 진행할 수 있습니다.

profile
게시글에 잘못된 부분이 있으면 댓글로 알려주시면 빠르게 수정 및 수용도 하겠습니다. 🥲

0개의 댓글