[JavaScript] - async & await 개념 정리

dev_woo·2024년 12월 6일

JS 개념 완벽정리

목록 보기
3/3
post-thumbnail

1. 프로미스 지옥


Promise 는 비동기 처리에서 발생하는 콜백 지옥을 극복하기 위해 도입된 문법입니다.
하지만, Promise 역시 지나치게 많이 then() 체이닝을 사용하다 보면 코드가 복잡해지고, 가독성이 떨어지는 프로미스 지옥이 발생합니다.

아래 코드를 보시죠

fetch("url")
  .then((response) => {
  	if (!response.ok) { throw new Error("네트워크 에러"); }
  
    return response.json();
  })
  .then((data) => {
    return data.users;
  })
  .then((users) => {
    return users.filter((user) => user.isLogin)).join(", ");
  })
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });

위 코드를 보시면, then() 메서드가 반복적으로 체이닝 하고 있어, 무엇을 하는 코드인지 한눈에 파악하기 어렵습니다.

2. Async/Await


async/await 는 이런 Promise 를 더 쉽고 간결하게 사용할 수 있도록 도입된 문법입니다.
기본적으로 Promise를 기반으로 동작하지만, then()catch() 같은 메서드를 사용하지 않고도 비동기 작업을 처리할 수 있어 코드가 훨씬 간결하고 가독성이 좋아집니다. 즉, 비동기를 처리하는 방식 자체는 Promise 와 동일하지만, 이를 표현하는 문법이 더 직관적으로 바뀐 것이라고 볼 수 있습니다.

아래 코드를 보시죠

try{
  	const res = await fetch("url");
  
	if (!res.ok) { throw new Error("네트워크 에러");}
  
    const data = await res.json();
    const result = data.users.filter((user) => user.isLogin).join(", ");
    console.log(result);
}catch (error){
  	console.error(error);
}

위 코드를 보면 함수의 리턴값을 변수의 할당하는 형식으로 기존의 then() 메소드를 사용해 처리하는 것보다 더 직관적으로 변한 것을 보실 수 있습니다.

async / await 사용법

async / await 문법의 사용법은 매우 간단하고 이해가 쉽습니다.

async 키워드를 함수 선언 앞에 붙여주면 되고, 비동기를 처리하는 부분 앞에 await 키워드를 붙여주면 작업이 완료될 때까지 기다릴 수 있습니다. 마치 기존의 절차적으로 코드를 작성하듯이 비동기 작업을 자연스럽게 작성 할 수 있습니다.

아래 코드를 보시죠

function delay(time) {
  	return new Promise((resolve, reject) => {
    	setTimeout(() => {
          	console.log(`${time} 이 지났습니다.`);
          	resolve();
        }, time);
    })
}

// Promise 방식
function asyncTask() {
	delay(1000)
    	.then(() => {
        	return delay(500);
        })
        .then(() => {
        	return Promise.resolve("작업 완료");
        })
        .then((result) => {
        	console.log(result);
        });
}

// async/await 방식
async function asyncTask(){
  	await delay(1000);
  	await delay(500);
  	const result = await Promise.resolve("작업 완료");
  	console.log(result);
}

async 키워드

먼저 async 키워드에 대해서 살펴보겠습니다.

아래 코드를 보시죠

async function asyncFunc() {
  	return 10;
}

const result = asyncFunc();
console.log(result);

위 이미지를 보면 알 수 있듯이, 이행(fulfilled) 상태의 프로미스 객체 형태가 반환 되는 것을 볼 수 있습니다. 즉, async 함수는 어떤 값을 리턴하든 무조건 프로미스 객체로 감싸져 반환되는 것입니다.

이외에도 직접 정적 메서드를 통해 다음과 같은 프로미스 상태(state)를 다르게 지정하는 것도 가능합니다.

아래 코드를 보시죠

async function asyncResolve() {
  	return Promise.resolve(10);
}

async function asyncReject() {
  	return Promise.reject(10);
}

이렇게 정적메소드를 사용해서 반환할 수 도 있지만,

async function asyncError(){
  	throw new Error("에러 발생");
}

async function asyncVoid(){
}
예외처리로 인한 에러가 발생하거나, 일부러 `return`을 적용하지 않아도 프로미스 객체를 반환하게 됩니다.
async function asyncFunc() {
  return 10;
}

asyncFunc()
	.then((res) => console.log(res));

마지막으로 async 함수는 결국 Promise 이기 때문에 async 함수 자체에 then() 핸들러를 사용할 수 있습니다. 하지만 가독성 문제로 await 키워드를 통해 처리합니다.

await 키워드

다음으로 await 키워드에 대해서 살펴보겠습니다.

아래 코드를 보시죠

// Promise 방식
function asyncTask() {
	delay(1000)
    	.then(() => {
        	return delay(500);
        })
        .then(() => {
        	return Promise.resolve("작업 완료");
        })
        .then((result) => {
        	console.log(result);
        });
}

// async/await 방식
async function asyncTask(){
  	await delay(1000);
  	await delay(500);
  	const result = await Promise.resolve("작업 완료");
  	console.log(result);
}

위 코드에서 await 키워드는 Promise 객체가 처리(resolve) 될 때까지 실행을 중단시킵니다.
이를 통해 then() 체이닝 없이 비동기 결과를 직접 변수에 할당할 수 있습니다.

또한, async/await 를 사용하면 try/catch 블록으로 비동기 작업의 에러를 처리할 수 있습니다. 이를 통해 기존 catch() 메서드 체이닝보다 간단하게 에러를 관리할 수 있습니다

아래 코드를 보시죠

// async/await 방식
async function asyncFunc() {

    try {
        const res = await fetch(url);
        const data = await res.json(); 
        console.log(data); // 데이터 처리
    } catch (err) {
        console.error(err); // 에러 처리
    }

}

3. async/await 주의점

async/await 병렬처리

async/await 은 비동기 코드를 직관적으로 작성할 수 있는 강력한 도구이지만, 잘못 사용하면 성능 문제를 유발할 수 있습니다. 특히, 병렬로 처리할 수 있는 작업을 await로 순차적으로 처리하면 비효율적인 동작이 발생할 수 있습니다. 비동기 프로그래밍의 본질은 병렬로 처리 가능한 작업을 효율적으로 수행하는 데 있으며, await를 남용하면 이러한 장점을 상실하게 됩니다.

아래 코드를 보시죠

function getApple(){
  return new Promise( (resolve, reject) => {
    setTimeout(() => resolve("apple"), 1000);
  })
}

function getBanana(){
  return new Promise( (resolve, reject) => {
    setTimeout(() => resolve("banana"), 1000);
  })
}

async function getFruites(){
  let a = await getApple(); 
  let b = await getBanana(); 
  console.log(`${a} and ${b}`);
}

getFruites();

위 코드를 보면, 사과와 바나나의 정보를 가져오는 비동기 함수가 서로 독립적임을 알 수 있습니다. 하지만 await 키워드를 두 번 사용하면서, 사과의 정보를 가져온 뒤에야 바나나의 정보를 가져오는 방식으로 동작합니다. 즉, 비동기 함수임에도 await 키워드를 사용했기 때문에 동기 처리가 되어 순차적으로 처리된 것이죠.

그럼 어떻게 병렬 처리를 할 수 있을까요?

아래 코드를 보시죠

async function getFruites(){

  let getApplePromise = getApple(); // async함수를 미리 논블록킹으로 실행한다. 
  let getBananaPromise = getBanana(); // async함수를 미리 논블록킹으로 실행한다. 
  
  let a = await getApplePromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
  let b = await getBananaPromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
  
  console.log(`${a} and ${b}`); // 본래라면 1초+1초 를 기다려야 하는데, 위에서 1초기다리는 함수를 바로 연속으로 비동기로 불려왔기 때문에, 대충 1.01초만 기다리면 처리된다.
})

위 코드는 사과와 바나나 정보를 가져오는 비동기 작업을 병렬로 처리하는 방법을 보여줍니다.
getApple()getBanana() 호출될때 바로 실행되고, 그 결과를 Promise 객체로 받아옵니다. 이후 await 를 사용해 이 Promise 의 결과를 처리합니다.

이렇게하면 두 비동기 작업이 동시에 실행되기 때문에, 각각의 작업을 병렬로 처리 할 수 있습니다.

또 다른 방법으로는 Promise.all() 메서드를 사용하여 처리하는 방법이 있습니다.
위의 방식으로 처리하는 경우 완료 시점을 가늠하기 힘들기 때문에, 실무에선 Promise.all() 을 많이 사용합니다.

async function getFruites(){
  console.time();
  
  // 구조 분해로 각 프로미스 리턴값들을 변수에 담는다.
  let [ a, b ] = await Promise.all([getApple(), getBanana()]); 
  console.log(`${a} and ${b}`);  
  console.timeEnd();
}

getFruites();

참고자료

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC-async-await#async_/_await_%EA%B8%B0%EB%B3%B8_%EC%82%AC%EC%9A%A9%EB%B2%95
https://ko.javascript.info/async-await
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/await
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function

profile
꾸준히 한걸음씩

0개의 댓글