Frontend/Javascript

Javascript - async/await

취업하고싶다! 2024. 12. 22. 17:40

 

자바스크립트 비동기 처리 3가지 방식

자바스크립트는 싱글 스레드 프로그래밍 언어기 때문에 멀티 작업을 하기 위해선 비동기 처리 방식이 자주 쓰인다.

비동기 처리는 백그라운드로 동작되기 때문에 그 결과가 언제 반환될지 알수 없어, 완료되면 결과를 받아 처리하기 위해 사용되는 대표적인 방법으로 콜백 함수(Callback) 와 이를 개선한 프로미스 객체(Promise)가 있다.

하지만 서비스 규모가 커질 수록 코드가 복잡해짐에 따라 코드를 중첩해서 사용하다가 가독성이 떨어지고 유지보수가 어려워지는 상황이 발생되게 되는데, 이를 Callback Hell, Promise Hell 이라고 불리운다.

 

 

자바스크립트 async / await

async/await는 ES2017에 도입된 문법으로, Promise 로직을 더 쉽고 간결하게 사용할 수 있게 해준다.

유의해야 할 점이 async/await가 Promise를 대체하기 위한 기능이 아니라는 것이다. 내부적으로는 여전히 Promise를 사용해서 비동기를 처리하고, 단지 코드 작성 부분을 프로그래머가 유지보수하게 편하게 보이는 문법만 다르게 해줄 뿐이라는 것이다. 

마치 자바스크립트에서 prototype과 class 문법의 차이라고 봐도 무방하다. 자바스크립트를 자바와 같이 클래스 형식의 문법을 지원하더라고 내부적으로는 여전히 프로토타입 형식으로 객체 지향을 처리하듯이, async/await도 내부적으로는 프로미스 방식으로 비동기를 처리하지만, 문법 형태만 좀 더 간편한 스타일로 추가한 것으로 보면 된다.

 

 

async 키워드

async 키워드는 어렵게 생각할 필요없이 await를 사용하기 위한 선언문 정도로 이해하면 된다. 즉 function 앞에 async을 붙여줌으로써, 함수내에서 await 키워드를 사용할 수 있게 된다. 이는 반대로 말하면 await 키워드를 사용하기 위해선 반드시 async function 정의가 되어 있어야 한다는 말과 같다.

// 함수 선언식
async function func1() {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
}
func1();

// 함수 표현식
const func2 = async () => {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
}
func2();

 

 

async 리턴값은 Promise 객체

async function func1() {
  return 1;
}

const data = func1();
console.log(data); // 프로미스 객체가 반환된다

 

단순 정수 1을 리턴했음에도 위 결과에서 보듯이, 이행(fulfilled) 상태의 프로미스 객체 형태로 반환됨을 볼 수 있다.

이를 통해 async function에서 어떤 값을 리턴하든 무조건 프로미스 객체로 감싸져 반환 된다는 특징을 알 수 가 있다.

 

 

다른 Promise 상태를 반환하기

물론 직접 프로미스 정적 메서드를 통해 다음과 같이 프로미스 상태(state)를 다르게 지정하여 반환이 가능하다.

async function resolveP() {
  return Promise.resolve(2);
}

async function rejectP() {
  return Promise.reject(2);
}

 

reject 같은 경우 위와 같이 Promise.reject() 정적 메서드를 통해 반환되는 프로미스 상태를 실패(rejected) 상태로 지정해줄 수 있지만, async 함수 내부에서 예외 throw 를 해도 실패 상태의 프로미스 객체가 반환되게 된다.

async function errorFunc() {
    throw new Error("프로미스 reject 발생시킴");
}

 

만일 async function에서 일부러 return을 하지 않아도 자동으로 return undefiend 으로 처리 되기 때문에 어찌됫든 무조건 프로미스 객체를 반환하게 된다.

 

await 키워드

await 키워드는 promise.then() 보다 좀 더 세련되게 비동기 처리의 결과값을 얻을 수 있도록 해주는 문법이라고 보면 된다.

예로, 서버에 리소스를 요청하는 fetch() 비동기 함수를 다음과 같이 then 핸들러 방식으로 결과를 얻어 사용해왔을 것이다.

// then 핸들러 방식
fetch(url)
    .then(res => res.json()) // 응답을 JSON으로 파싱
    .then(data => {
      // data 처리
      console.log(data);
    })

 

await 키워드를 사용하면 then 핸들러를 복잡하게 처리할 필요 없이, 심플하게 비동기 함수 왼쪽에 await 만 명시해주고 결과값을 변수에 받도록 코드를 정의하면 끝이다. then과 콜백 함수를 남발하여 코드가 들여쓰기로 깊어지는 것을 방지하고, 한 줄 레벨에서 코드를 나열하여 가독성을 높일 수 있다.

// await 방식
async function func() {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
    // data 처리
    console.log(data);
}
func();

 

 

await는 Promise 처리가 끝날때까지 기다림

await는 키워드 이름에서 보듯이 Promise 비동기 처리가 완료될때 까지 코드 실행을 일시 중지하고 wait 한다라는 뜻으로 보면 된다.

예를 들어 fetch() 함수를 사용하여 서버에서 데이터를 가져오는 경우를 생각해보자. 이 함수는 Promise를 반환한다. 따라서 await 키워드를 사용하여 이 Promise가 처리될 때까지 코드 실행을 일시 중지하고, Promise가 처리되면 결과 값을 반환하여 변수에 할당하는 식이다.

async function getData() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
  const data = await response.json();
  console.log(data):
}

위 예제를 보면 getData() async 함수 내에서 fetch() 비동기 함수를 호출하고, 반환된 Promise를 await 키워드로 처리한다. 이로 인해 함수 내 코드 실행이 일시 중지되고, fetch() 함수가 완료될 때까지 기다리게 된다. 서버로부터 리소스를 성공적으로 가져와 fetch() 함수가 완료되면, 바로 다음 response.json() 함수를 호출하여 반환된 Promise를 다시 await로 처리한다. 다시 데이터를 가져오는 동안 코드 실행이 일시 중지되고, 데이터가 성공적으로 가져와지면 최종 결과 값을 반환한다.  따라서 await는 Promise를 처리하고 결과를 반환하는 데, 비동기적인 작업을 동기적으로 처리할 수 있게 되는 것이다.

 

 

async/await 에러 처리

기존의 Promise.then() 방식의 에러 처리는 catch() 핸들러를 중간 중간에 명시함으로써 에러를 받아야만 했다.

// then 핸들러 방식
function fetchResource(url) {
  fetch(url)
    .then(res => res.json()) // 응답을 JSON으로 파싱
    .then(data => {
      // data 처리
      console.log(data);
    })
    .catch(err => {
      // 에러 처리
      console.error(err);
    });
}

우리가 일반적으로 에러를 처리하기 위해선  try/catch 문을 사용하여 에러를 처리해왔다. async/await도 이와 같이 비동기 처리에 대한 에러를 처리할 필요가 생기면 고대로 try/catch문을 씌우면 되게 된다.

// async/await 방식
async function func() {
    try {
        const res = await fetch(url); // 요청을 기다림
        const data = await res.json(); // 응답을 JSON으로 파싱
        // data 처리
        console.log(data);
    } catch (err) {
        // 에러 처리
        console.error(err);
    }

}
func();

이처럼 async/await의 장점은 비동기 코드를 마치 동기 코드처럼 읽히게 해준다는 것이다. 우리가 일반적으로 코드를 쓰고 읽어 내리듯이 말이다.