탄탄한 기본기!

Async/Await 잘 활용하는 법 본문

개인 공부/JS (자바스크립트)

Async/Await 잘 활용하는 법

두영두영 2021. 7. 28. 11:35
const getName = () =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve('Dooyeong');
    }, 2000);
  });

const sayHi = async () => {
  const name = await getName();
  console.log(`Hello ${name}!`);
};

sayHi();

자바스크립트에는 async와 await이라는 특별한 문법이 존재한다. 그리고 "비동기(async)"라는 어려울 것 같은 이름을 가지고 있지만 생각보다 이해하기 쉽고 사용하기도 쉽다. 

Asynchronus function

비동기 함수란, 동기적으로 작동하지 않는 함수이다. 이 말이 무슨 말이냐면, 두 개 이상의 이벤트들이 서로를 기다리지 않고 (blocking 하지 않고) 각자 동작한다는 것이다.

 

자바스크립트는 싱글 스레드로 동작하기 때문에 비동기 함수가 매우 중요하다. 이벤트 핸들링, Ajax 통신 등 모든 것이 비동기 방식으로 작동하기 때문에 비동기 프로그래밍에 대해서 잘 이해하고 있어야 한다.

Promise in JavaScript

비동기를 이해하려면, 자바스크립트의 Promise 객체를 반드시 이해하고 있어야한다. Promise 객체는 비동기 동작의 진행 상태와 결과 등을 가지고 있는 객체라고 할 수 있는데, Promise가 가질 수 있는 상태로는 "pending", "fulfilled", "rejected"가 있다.

  • pending: 초기 상태, 즉 fulfilled나 rejected가 되기 전의 상태
  • fulfilled: 비동기 작업이 성공적으로 완료된 상태
  • rejected: 비동기 작업이 실패한 상태

Promise 객체에 넘겨주는 함수를 보통 "executor"라고 칭한다. 그리고 이 executor는 Promise가 넘겨주는 resolve와 reject라는 콜백함수를 활용해 조건에 따라 resolve(성공)나 reject(실패)를 호출할 수 있다.

let state = false;

const myPromise = new Promise((res, rej) => {
  setTimeout(() => {
    if (state) {
      res('Success');
    } else {
      rej('Fail');
    }
  }, 2000);
});

myPromise
  .then((res) => {
    console.log(`${res} 성공!`);
  })
  .catch((err) => {
    console.log(`${err} 실패 ...`);
  });

매우 간단한 위 예제에서는 Promise 객체를 상황(state)에 따라 분기처리해 resolve 하거나 reject 하여 비동기 처리를 하여 콘솔에 결과를 찍고 있다.

 

그리고 then 그리고 catch 라는 후속 메서드를 통해서 프로미스의 결과를 처리해주고 있다. 이 부분이 기존의 콜백 헬을 해결해준다는 장점이 있긴 하지만 여전히 체이닝을 통해서 진행되는 모습을 보여주고 있다는 점을 극복하기 위한 것이 바로  async/await이다.

async/await

결론부터 설명하자면, async는 비동기 처리가 이루어지는 코드를 포함하고 있는 함수 앞에 붙이는 키워드이며, await은 그러한 비동기 처리가 이루어지는 코드 앞에 붙이는 키워드이다. 가장 흔한 예제인 fetch 함수를 사용하는 예제를 예시로 들면 아래와 같이 사용할 수 있을 것이다.

const request = async () => {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  const res = await fetch(url);

  return await res.json();
};

const getData = async () => {
  const data = await request();
  console.log(data);
};

getData();

물론 url에 해당하는 주소는 실제로 없기 때문에 에러가 난다. 에러 처리를 따로 해주기 위해서는 try/catch문으로 감싸주어서 에러 처리를 해주어야 하지만 예제를 좀 더 명확하게 볼 수 있게 하기 위해서 try/catch문은 제외하고 코드를 짰다.

 

그렇다면 try/catch를 넣어서 에러 처리를 함께 해주면 어떻게 될까? 아래 코드를 보자.

const request = async () => {
  try {
    const url = 'https://jsonplaceholder.typicode.com/todos/1';
    const res = await fetch(url);

    if (!res.ok) {
      throw new Error(`Error (status: ${res.status})`);
    }

    return await res.json();
  } catch (error) {
    console.error(`Something wrong, ${error.message}`);
  }
};

const logData = async () => {
  const data = await request();
  console.log(data);
};

logData();

하지만 async와 await을 사용해 반환 값을 사용하고 싶다면 IIFE(즉시 실행 함수)를 사용할 수 있다. 예를 들어서 request로 받아온 값을 아래와 같이 사용할 수 있을까?

const request = async () => {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  const res = await fetch(url);

  return await res.json();
};

const data = request();
console.log(data);

답은 "불가능하다"이다. 왜냐하면 async 함수는 항상 프로미스를 반환하기 때문에 아직 pending 상태인 프로미스를 반환하게 되고, 결국 data를 저렇게 받아서는 원하는 값을 출력할 수 없게 된다. 따라서 위에서 말한 즉시 실행 함수(IIFE)를 사용해 아래와 같이 코드를 수정해주어야 한다.

const request = async () => {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  const res = await fetch(url);

  return await res.json();
};

(async () => {
  const data = await request();
  console.log(data);
})();

하지만 Node 14.8 이상 버전에서 가능하기 때문에 주의해야 한다.

Promise.all(), 비동기를 함께

여러 비동기를 한꺼번에 처리하고 싶은 경우에는 어떻게 하면 될까? 다음처럼 await을 여러 번 사용해서 비동기를 호출하면 될까?

 

만약 그렇게 코드를 짠다면, 비동기 통신 횟수만큼 걸리는 시간을 모두 기다려야 할 것이다. 하지만 아래와 같이 Promise.all()을 통해 비동기 통신을 병렬적으로 수행해준다면 그럴 필요가 없다.

const loadData = async () => {
  try {
    const url1 = 'https://jsonplaceholder.typicode.com/todos/1';
    const url2 = 'https://jsonplaceholder.typicode.com/todos/2';
    const url3 = 'https://jsonplaceholder.typicode.com/todos/3';
    
    const results = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
    const dataPromises = await results.map(result => result.json());
    const finalData = Promise.all(dataPromises);
    
    return finalData;
  } catch (err) {
    console.log(err);
  }
};

(async () => {
  const data = await loadData();
  console.log(data);
})();

Conclusion

asyncawaitthencatch 같은 후속 메서드로 작업을 처리해줄 때 보다도 더 직관적이고 이해하기 이해하기 쉽게 코드를 짤 수 있기 때문에 thencatch보다는 asyncawait으로 코드를 작성하는 편이 더 좋을 것이라고 생각한다.

 

하지만 주의해야 할 점이 있는데, await 은 반드시 async 내부에서 사용되어야 하며, 바로 depth가 바로 한 단계 아래에서만 사용할 수 있다. 무슨 말이냐면, async function안에 async가 아닌 일반적인 내부 함수 function이 또 존재할 때, 이 내부 함수 안에서는 await을 사용할 수 없고 async function의 스코프 안에서만 사용될 수 있다는 점이다. (처음에 이 부분을 잘 몰라서 왜 에러가 나는지 디버깅하는데 어려움을 겪었다...)

 

Comments