비동기 통신

지은·2023년 5월 1일
0

JavaScript

목록 보기
38/42

JavaScript는 싱글 스레드로 작동하기 때문에, 효율적인 프로그램을 만들기 위해서는 비동기 처리를 적절하게 사용하는 것이 중요하다.

비동기 통신이란?

비동기 처리 : 현재 처리중인 프로세스의 완료를 기다리지 않고 동시에 다른 작업을 처리하는 방식

컴퓨터는 동시에(비동기적으로) 여러 일을 수행하기 위해 점차 메모리 공간이 커졌고, 각 프로세스에 할당되는 메모리 공간이 분리되었다. 또한, CPU가 여러 프로세스와 스레드를 동시에 실행할 수 있도록 멀티 스레딩 방식이 생겨났다.

하지만 JavaScript는 싱글 스레드로 작동하는 프로그래밍 언어이기 때문에 동시에 두 개의 함수가 실행될 수 없다. 따라서 작업의 효율을 높이기 위해 비동기 처리가 필요하다.


모던 브라우저는 어떻게 비동기 로직을 처리하나요?

JavaScript는 싱글 스레드 언어이기 때문에 멀티 스레딩을 지원하지 않는다.
하지만 JavaScript 함수로 호출한 브라우저의 Web API'이벤트 루프'라는 메커니즘을 통해 JavaScript 코드를 비동기적으로 실행할 수 있게 한다.

브라우저의 콜 스택에는 호출된 함수들이 스택 구조로 쌓이는데, 이때 비동기 함수는 콜 스택에서 대기하는게 아니라, 콜백 큐로 들어간다.
이벤트 루프는 주기적으로 콜 스택을 확인하고 콜 스택이 완전히 비었을 때 콜백 큐에서 대기하던 태스크(비동기 함수)를 콜 스택으로 이동시키고 처리한다.
이러한 원리로 콜 스택은 비동기 함수가 처리되는 동안 정체되지 않고 다음 태스크를 수행할 수 있다.


JavaScript는 어떻게 비동기 로직을 지원해왔나요?

콜백 함수

전통적으로 비동기 처리는 콜백 함수를 통해 이루어졌다.
하지만 이 방식은 호출하는 비동기 함수의 깊이가 깊어질수록 에러 핸들링이 까다롭다.
콜 스택을 기준으로 안쪽 스택에서 발생한 에러는 바깥쪽의 호출자 방향으로 전파되는데, 함수의 깊이가 깊어지면 안쪽 콜 스택이 먼저 제거되어 바깥까지 전파되지 않을 수 있기 때문이다.

또한 콜백 함수를 중첩해서 호출하다 보면 콜백 함수의 깊이가 감당하기 힘들 정도로 깊어지는 '콜백 지옥(Callback Hell)' 현상이 발생한다.


Promise

그래서 JavaScript ES6에서는 비동기 함수의 성공과 실패 여부를 thencatch라는 별도의 블록으로 구분해서 받을 수 있는 Promise 패턴이 등장한다.
Promise 객체는 Promise 생성자를 사용해 만들 수 있으며, 인자로 콜백 함수를 받는다.

let promise = new Promise( // Promise 생성자
	function(resolve, reject) {
  		// 실행할 코드
	}
);

이 콜백 함수는 resolve, reject라는 인자를 순서대로 받고, 각각의 함수를 통해서 성공과 실패 케이스에 대해 처리할 수 있다.
그리고 resolve 시킨 결과는 then 블록으로 받을 수 있고, reject시킨 결과는 catch 블록으로 받을 수 있다.

let promise = new Promise((resolve, reject) => {
  const randomNumber = Math.random();
  if (randomNumber > 0.5) {
    resolve('Success!'); // 이행 상태
  } else {
    reject('Failed!'); // 거부 상태
  }
});

promise.then((result) => {
  console.log(result); // 'Success!' 출력
}).catch((error) => {
  console.log(error); // 'Failed!' 출력
});

이외에도 Promise 객체는 프로퍼티로 all(), allSettled() 등의 정적 메소드도 가지고 있어 활용도가 높다. 둘다 여러 개의 Promise 객체를 처리할 때 사용되는데, 차이점은

  • all()은 모든 Promise가 이행되었을 때만 결과를 반환하고 하나라도 거부된 경우 거부된 Promise를 반환한다.
  • allSettled()는 이행되지 않은 Promise들도 배열로 반환한다. 이 배열은 Promise의 결과를 담은 객체들을 가지고 있는데, statusvalue / reason 프로퍼티를 가지며, status 속성을 통해 Promise가 이행되었는지 거부되었는지 알 수 있다.

Promise.all()

  • 모든 Promise 객체가 이행(resolve) 상태인 경우에만 결과를 반환한다.
  • 만약 하나라도 거부(reject) 상태인 경우, catch() 메소드를 통해 처리한다.
const promise1 = Promise.resolve(1); // 즉시 이행됨
const promise2 = new Promise((resolve, reject) => { 
  setTimeout(resolve, 2000, 'foo'); // 2초 후 이행됨
});
const promise3 = 42; // 이미 값이 있음

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // [1, 'foo', 42] 출력
}).catch((error) => {
  console.log(error); // 호출되지 않음
});

Promise.allSettled()

  • 모든 Promise 객체가 이행(resolve) 또는 거부(reject) 상태일 때 결과를 반환한다.
const promise1 = Promise.resolve(1); // 즉시 이행됨
const promise2 = new Promise((resolve, reject) => {
  setTimeout(reject, 2000, 'bar'); // 2초 후 거부됨
});
const promise3 = 42; // 이미 값이 있음

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
  console.log(results); // [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 'bar'}, {status: 'fulfilled', value: 42}]
}).catch((error) => {
  console.log(error); // 호출되지 않음
});

async/await

만약, 반드시 여러 개의 비동기 함수 호출이 모두 완료된 후 수행해야 할 태스크가 있다면, 이 부분은 동기적으로 수행되어야 하는 상황으로 볼 수 있다. 이런 상황을 위해 JavaScript ES8에서는 async/await 구문이 도입되었다.

async 예약어를 붙여서 선언한 함수는 리턴 타입이 Promise 객체이다.
따라서 Promise 객체의 최종 결과값을 받기 위해서는 then-catch 블록으로 받거나, await 예약어를 붙여서 호출하면 된다.

async 함수에 then-catch 사용

const axios = require('axios');

async function fetchUserData(username) {
  return axios.get(`https://api.github.com/users/${username}`)
    .then(response => {
      const userData = response.data;
      console.log(`Name: ${userData.name}`);
      console.log(`Location: ${userData.location}`);
    })
    .catch(error => {
      console.log(error);
    });
}

fetchUserData('octocat');

async/await 사용

const axios = require('axios');

async function fetchUserData(username) {
  try {
    const response = await axios.get(`https://api.github.com/users/${username}`);
    const userData = response.data;
    console.log(`Name: ${userData.name}`);
    console.log(`Location: ${userData.location}`);
  } catch (error) {
    console.log(error);
  }
}

fetchUserData('octocat');

await으로 호출한 비동기 함수는 동기 함수처럼 동작하여, 해당 코드 라인이 완료되어 콜백 함수의 결과값이 받아진 이후에 다음 코드 라인이 실행된다.
즉, async/await은 비동기적인 처리를 보다 동기적으로 작성할 수 있도록 해주는 문법이다.

이 글은 주니어 웹 개발자가 알아야 할 ‘비동기 통신’ | 요즘IT 를 읽고 요약한 글입니다.

profile
개발 공부 기록 블로그

6개의 댓글

comment-user-thumbnail
2023년 5월 2일

기술면접 내용 준비 잘하셨네요 ! 잘읽힙니다.

답글 달기
comment-user-thumbnail
2023년 5월 6일

정리 너무 깔끔합니다 ! 참고해서 블로깅 해도 되나요 ~~?

1개의 답글
comment-user-thumbnail
2023년 5월 7일

복습하고 갑니당 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 5월 7일

비동기 오랜만에 보니 또 새롭군요. 덕분에 복습하고 갑니다!

답글 달기
comment-user-thumbnail
2023년 5월 7일

async, await가 진짜 신의 한수 인 거 같아요. 너무 편해

답글 달기