본문 바로가기

자바스크립트 async / await 이해하기 본문

JavaScript

자바스크립트 async / await 이해하기

개발자로 거듭나기 2023. 5. 24. 16:24
반응형

async / await

1. async / await란?

  • async/await는 JavaScript에서 비동기적인 동작을 처리하기 위한 문법입니다.
  • 이를 사용하면 비동기 코드를 동기적으로 작성하고 관리할 수 있습니다.
  • async 함수는 항상 Promise를 반환하며, 이것은 async 함수의 반환값이 Promise.resolve()에 전달된 다음 호출자에게 반환된 것과 같습니다.
  • await 키워드는 Promise가 완료될 때까지 해당 코드의 실행을 일시 중지합니다.
  • await 키워드는 async 함수 내에서만 사용할 수 있습니다. 따라서, await를 사용하려는 코드가 async 함수 내에 위치하도록 주의해야 합니다.
  • **async / await** 구문을 사용하면 각 비동기 작업에서 다음 구문을 실행하기 전에 결과를 기다리며 차단되는 것처럼 보이는 함수를 작성할 수 있습니다.
  • async / await 구문을 이용한 비동기 코드는 전통적인 동기적 코드에 버금가는 가독성을 가지고 있습니다.
  • await 표현은 프라미스와 같은 비동기적인 표현뿐만 아니라, 어떠한 값으로도 작동합니다. 만약 프라미스가 아닌 값이 제공되면 그것의 동작은 Promise.resolve()에 전달된 값을 기다리는 것과 비슷합니다.

2. 사용 방법

async 함수 선언

  • 비동기 동작을 처리할 함수를 async 키워드로 선언합니다. 이 함수는 Promise를 반환합니다.
async function fetchData() {
  // 비동기 동작 처리
}

await 키워드 사용

  • await 키워드를 사용하여 Promise가 완료될 때까지 코드 실행을 일시 중지합니다.
async function fetchData() {
  const data = await fetch(url);
  // 데이터 요청이 완료될 때까지 대기하고, 데이터를 변수에 할당
  console.log(data);
}

3. Async / await 에서의 에러처리

  • async / await의 큰 장점 중 하나는 try…catch 블록의 동작을 정규화하여 동기적 throws와 비동기적 프라미스의 거부 두 상황 모두에서 잘 작동하도록 하는 것입니다.
  • 에러처리는 간단하고, 가독성이 좋으며, 무엇보다도 비동기적 / 동기적 에러를 모두 지원해야 합니다.
function delayError(milliseconds) {
  return new Promise((resolve, reject) => {
    // milliseconds 후에 거절되는 Promise
    setTimeout(() => {
      reject(new Error(`Error after ${milliseconds}ms`));
    }, milliseconds);
  });
}

async function playingWithErrors(throwSyncError) {
  try {
    // 동기적 오류상황
    if (throwSyncError) {
      throw new Error("This is a synchronous error");
    }

    // 비동기적 오류상황
    await delayError(1000);
  } catch (err) {
    // 동기적, 비동기적 오류상황에 맞게 메세지가 찍힌다.
    console.error(`We have an error: ${err.message}`);
  } finally {
    console.log("Done");
  }
}

// throws a synchronous error
playingWithErrors(true);
// awaited Promise will reject
playingWithErrors(false);

4. await와 return await의 차이

  • 흔한 안티패턴 중 하나는 async / await와 함께 에러를 다룰 때 호출자에 거부하는 프라미스를 반환하고, 프라미스를 반환하는 함수의 로컬 try…catch 블록에서 에러가 잡히는 것을 예상하는 것입니다.
  • 로컬 try…catch 블럭에서 에러를 잡고싶다면 await 키워드를 추가해야 합니다.
function delayError(milliseconds) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error(`Error after ${milliseconds}ms`));
    }, milliseconds);
  });
}

async function errorNotCaught() {
  try {
    // 거절되는 Promise 객체 return
    // 이렇게되면 1초후에 거절되는 프라미즈 객체를 기다리지 않고 곧바로 return 하게 된다.
    // 이렇게 되면 에러는 호출자에서 포착된다.
    return delayError(1000);

    // 만약 로컬에서 처리하고 프라미스의 거부를 포착하고 싶다면..
    return await delayError(1000); // awiat 키워드 추가
  } catch (err) {
    console.error("Error caught by the async function: " + err.message);
  }
}

errorNotCaught().catch((err) =>
  console.error("Error caught by the caller: " + err.message)
);
// 사실상 위의 catch 구문에서 에러핸들링이 가능합니다.

5. 순차 실행을 위한 forEach와 async / await의 사용의 안티패턴

  • 개발자들이 Array.forEach() 또는 Array.map()과 함께 async / await를 사용한 순차 비동기 반복을 구현하려고 시도하는 안티패턴이 존재합니다. 다음은 예상대로 동작하지 않습니다.
  • await 키워드를 붙였지만 forEach는 기다려주지 않습니다.
const doSomethingAsync = (item) => {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res(item + 1);
    }, (Math.random() + 1) * 1000);
  });
};

const items = [1, 2, 3];

items.forEach(async (item) => {
  const result = await doSomethingAsync(item);
  console.log(result);
});

// 뒤죽박죽인 순서
// 원래라면 첫번째 아이템의 초가 많이 걸리더라도 기다려주고 그다음걸로 넘어가야하는데 
// setTimemout을 포함한 프라미즈 구문을 기다리지 않음
  • setTimeout의 초를 랜덤하게 준 상태에서 forEach의 콜백으로 async함수를 선언하고 await 구문으로 doSomethingAsync함수를 기다리는게 의도였지만 forEach 함수에 의해 doSomethingAsync가 반환하는 프라미스는 무시됩니다.
  • 다음과 같이 for…of 나 Promise.all을 활용하여 해결할 수 있습니다.
for (const item of items) {
  const result = await doSomethingAsync(item);
  console.log(result);
}

// 또는

const results = await Promise.all(
  items.map(async (item) => {
    const result = await doSomethingAsync(item);
    return result;
  })
);

console.log(results);

안티패턴이란?

  • 안티패턴은 소프트웨어 공학 분야 용어이며, 실제 많이 사용되는 패턴이지만 비효율적이거나 비생산적인 패턴을 의미한다.

반응형

6. 콜백 패턴 ⇒ async / await 구문으로 바꿔보기 예제

콜백지옥 예제

class UserInfo {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(() => {
      if (
        (id === 'tom' && password === 'tom1234')
      ) {
        onSuccess(id);
      } else {
        onError(new Error('no user matched'));
      }
    }, 2000);
  }

  getRoles(user, onSuccess, onError) {
    setTimeout(() => {
      if (user === 'tom') {
        onSuccess({ name: 'tom', role: 'admin' });
      } else {
        onError(new Error('no roles'));
      }
    }, 1000);
  }
};

const userInfo = new UserInfo();
const id = "tom"
const password = "tom1234";

userInfo.loginUser(
  id,
  password,
  user => {
    userInfo.getRoles(
      user,
      userRole => {
        alert(`hello ${userRole.name}, you have a ${userRole.role} role`)
      }, // onSuccess
      error => {
        console.log(error)
      } // onError
    );
  }, // onSuccess
  error => { console.log(error) } // onError
);

async / await 적용

class UserInfo {
  // id, password를 입력받아서, 2초후에 로그인 id를 resolve 하는 프라미즈 객체 반환
  loginUser(id, password) {
    return new Promise((res, rej) => {
      setTimeout(() => {
        if (
          (id === 'tom' && password === 'tom1234')
        ) {
          res(id);
        } else {
          rej(new Error('no user matched'));
        }
      }, 2000);
    });
  }

  // id를 전달받아서, 1초후에 userRole 객체를 resolve 하는 프라미즈 객체 반환
  getRoles(user) {
    return new Promise((res, rej) => {
      setTimeout(() => {
        if (user === 'tom') {
          res({ name: 'tom', role: 'admin' });
        } else {
          rej(new Error('no roles'));
        }
      }, 1000);
    });
  }
};

// 기본 셋팅
const userInfo = new UserInfo();
const id = "tom"
const password = "tom1234";

// async 함수 선언
// 마치 동기코드처럼 위에서부터 순차대로 실행됩니다. (비동기 코드를 기다립니다.)
const process = async () => {
  console.log("login start");
  try {
    const userId = await userInfo.loginUser(id, password);
    const userRole = await userInfo.getRoles(userId);
    console.log(`hello ${userRole.name}, you have a ${userRole.role} role`);
  } catch (err) {
    console.log(err);
  } finally {
    console.log("login complete");
  }

  console.log("end of the function")
}
process();
반응형
Comments