[TIL / DAY 18] async / await

miseullang·2024년 11월 6일
post-thumbnail

✅ 강의 내용 정리

📍 Promise 복습


Promise

// Promise
// 생산자(producer)
const promise = new Promise();

// promise 객체를 추상화하면 다음과 같다
const promiseInterface = {
  state: "pending",
  value: undefined,
  thenHandler: () => {
    // then절을 처리하는 부분
  },
  catchHandler: () => {
    // catch절을 처리하는 부분
  },
  finallyHandler: () => {
    // finally절을 처리하는 부분
  },
};
// 소비자(consumer)

동작 방식

콜스택 → 웹 API
직역 실행 컨텍스트가 생성되어 setTimeout과 같은 비동기 작업이 웹 API로 이동

웹 API → 마이크로 태스크 큐
setTimeout의 콜백이 끝나면 then/catch 콜백이 마이크로 태스크 큐로 이동

마이크로 태스크 큐 → 태스크 큐
Promise의 then과 catch 콜백들이 태스크 큐에 있는 콜백들보다 우선순위가 높아 먼저 실행됨

태스크 큐 → 콜스택
모든 마이크로 태스크가 실행된 후에야 태스크 큐의 콜백들이 콜스택으로 이동하여 실행

* 정리 콜스택 → 웹 API → 마이크로 태스크 큐 → 태스크 큐 → 콜스택의 순서로 진행되며, 이벤트 루프가 관리한다.



📍 API


✅ 사용 툴

  1. Thunder Client

    • VS CODE 확장 프로그램. 포스트맨처럼 API 테스트 가능
  2. JSONPlaceholder


프론트에서 API를 주고 받는 방법

  1. fetch API (웹 API)
  2. axios 라이브러리
    Next.js에서는 fetch 사용을 권장 => 따라서 fetch API로 학습할 예정
const getApi = () => {
  fetch("https://jsonplaceholder.typicode.com/posts", { method: "GET" }); // fetch('주소', 메소드) 형태여야 하지만, get인 경우 메소드 생략 가능
  // fetch는 응답으로 promise 객체를 반환
  promise //
    .then((res) => res.json())
    .then((data) => {
      console.log(data);
    });
};

getApi();



리소스 에러 핸들링하기

fetch는 promise를 반환하기 때문에
fetch 에러는 웹 엔진의 에러만 잡을 수 있다
따라서 호출 주소 자체를 잘못 적으면 에러 캐치가 되지만 리소스(주소가 올바르고, 메소드가 잘못된 경우) 에러로 처리하지 않음
한 마디로 API 명세되지 않은 접근을 하면 오류도 반환하지 않고, 응답도 반환하지 않는다

그렇다면 리소스 에러는 어떻게 처리해야 할까?

⇒ ok를 사용해서 에러를 던지면 된다.

if (!response.ok) throw new Error('에러 처리')

function getSortedPostTitles() {
  // 여기에 코드를 작성하세요
  return fetch("https://jsonplaceholder.typicode.com/posts")
    .then((response) => {
      if (!response.ok) {
        throw new Error("오류");
      }
      return response.json();
    })
    .then((posts) => {
      const sortedTitles = posts.map((post) => post.title).sort();

      sortedTitles.forEach((title) => {
        console.log(title);
      });

      return sortedTitles;
    })
    .catch((error) => {
      console.error("Error:", error.message);
    });
}

getSortedPostTitles();



📍 async / await


async 비동기 처리에 대한 예제

// async ES8
// async (sugar syntax), Promise
// await 비동기 함수를 기다리는 역할을 해요
// 함수 안에 값이 반환되면 해결된다라고 치는겁니다.
const getSunIcon = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("☀️");
        }, 1000);
    });
};

const getWeatherIcon = async () => {
    const sun = await getSunIcon();
    console.log(sun);
};

setTimeout(() => {
    console.log("settimeout");
}, 0);

Promise.resolve().then(() => {
    console.log("promise 1");
});

getWeatherIcon();
console.log("end");



실행 결과

end
promise 1
settimeout
☀️

실행 순서가 코드의 작성 순서와 다른 이유는 자바스크립트의 이벤트 루프와 태스크 큐의 동작 방식 때문이다.


async에서 에러 핸들링하기

async/await에서의 에러 핸들링은 일반적인 동기 코드처럼 try-catch 구문을 사용할 수 있다.

finally 사용도 가능하다.

const getApis = async () => {
  try {
    // API 호출 및 응답 처리
    const response = await fetch("https://jsonplaceholder.typicode.com");
    if (!response.ok) throw new Error("API 요청 실패");
    const data = await response.json();
    console.log(data);
    
  } catch (err) {
    // 에러 발생 시 처리
    console.error(err);
    
  } finally {
    // 성공/실패 상관없이 마지막에 실행
    console.log("API 호출 완료");
  }
};



🔍 async 동작 방식 들여다보기

asyncpromise를 더 쉽게 다루기 위한 문법적 설탕(sugar syntax)이다. 함수 앞에 async 키워드를 붙이면 그 함수는 자동으로 promise를 반환하게 된다.

async 함수 안에서는 await이라는 키워드를 사용할 수 있는데, 이는 비동기 작업이 완료될 때까지 함수의 실행을 일시적으로 멈추는 역할을 한다. 예를 들어 서버에서 데이터를 가져오는 작업이 끝날 때까지, 또는 promiseresolve될 때까지 기다리게 된다.

여기서 재미있는 점은 await의 동작 방식이다. await을 만나면 해당 함수의 실행 컨텍스트에 잠금을 걸고, 이를 마이크로태스크 큐로 보낸다. 하지만 이때 함수는 아직 마이크로태스크 큐에 들어간 것이 아니라 대기 상태에 있다. 그래서 다른 마이크로태스크가 있다면 그것이 먼저 실행되는 것이다.

이런 방식으로 async/await은 비동기 코드를 마치 동기 코드처럼 쉽게 작성할 수 있게 해주면서, 동시에 자바스크립트의 이벤트 루프와 태스크 큐 시스템을 효과적으로 활용할 수 있게 해준다.

⇒ async는 개발자가 비동기 함수를 동기 방식처럼 읽을 수 있게 해주는 것


async/awaitPromise 가독성 비교

// === async/await 방식 ===
const getWeatherIcon = async () => {
    const sun = await getSunIcon();
    const wave = await getWaveIcon();
    const cloud = await getCloundIcon();
    console.log(sun, wave, cloud);
};

// === Promise 체이닝 방식 ===
const getWeatherIconPromise = () => {
    getSunIcon()
        .then((sun) => {
            return getWaveIcon().then((wave) => {
                return getCloundIcon().then((cloud) => {
                    console.log(sun, wave, cloud);
                });
            });
        });
};



async를 조금 더 빠르게 동작하도록 하는 방법론

  1. 첫번째 방법
const getWeatherIcon = async () => {
    console.time(); // 실행 시간 측정 시작

    // 1. Promise 객체들을 먼저 생성하여 비동기 작업 시작
    const sunPromise = getSunIcon();
    const wavePromise = getWaveIcon();
    const cloudPromise = getCloundIcon();

    // 2. 생성된 Promise들을 나중에 await으로 처리
    const sun = await sunPromise;
    const wave = await wavePromise;
    const cloud = await cloudPromise;

    console.log(sun, wave, cloud);
    console.timeEnd(); // 실행 시간 측정 종료
};

최적화 포인트

  1. 비동기 작업 먼저 시작

    • Promise 객체들을 먼저 생성해 비동기 작업들이 동시에 시작되게 한다.
    • 각 함수 호출로 Promise가 생성되는 즉시 비동기 작업이 시작된다.
  2. await 후처리

    • await을 나중에 모아서 처리해 불필요한 동결 시간을 줄인다.
    • 이렇게 하면 비동기 작업들이 병렬로 처리되는 효과를 얻을 수 있다.

이전 방식처럼 각각의 await을 순차적으로 처리하면 한 작업이 완료될 때까지 다음 작업을 시작하지 못한다. 하지만 이 방식을 사용하면 모든 비동기 작업이 거의 동시에 시작되어 전체적인 실행 시간을 단축할 수 있다.

⇒ 각 Promise 결과를 개별적으로 처리해야 할 때

  1. 두 번째 방법(Promise.all().then())
const getAllWeatherIconAsync = async () => {
  console.time();
  Promise.all([getSunIcon(), getWaveIcon(), getCloundIcon()]).then((icons) =>
    console.log(icons)
  );
  console.timeEnd();
};

getAllWeatherIconAsync();

최적화 포인트

  1. 병렬 실행 보장

    • Promise.all()을 사용하여 모든 Promise들을 동시에 실행
    • 배열로 전달된 모든 Promise가 동시에 시작되어 병렬 처리됨
  2. then() 활용

    • async/await 대신 then()을 사용하여 콜백 방식으로 처리
    • Promise가 모두 완료된 후 결과를 한 번에 처리할 수 있음
  3. 동기화 오버헤드 감소

    • await 키워드를 사용하지 않아 추가적인 동기화 오버헤드가 없음
    • 비동기 작업의 결과만 필요할 때 효율적

⇒ 콜백 기반의 처리가 필요하거나 최소한의 오버헤드를 원할 때

  1. 세 번째 방법(await Promise.all())
const getAllWeatherIconAsync = async () => {
  console.time();
  const [sun, wave, cloud] = await Promise.all([
    getSunIcon(),
    getWaveIcon(),
    getCloundIcon(),
  ]);
  console.log(sun, wave, cloud);
  console.timeEnd();
};

getAllWeatherIconAsync();

최적화 포인트

  1. 구조 분해 할당과 병렬 처리

    • Promise.all()로 병렬 실행을 보장하면서
    • 구조 분해 할당으로 각 결과값을 명확하게 분리하여 가독성 향상
  2. 코드 간결성

    • async/await를 사용하여 더 직관적인 코드 작성 가능
    • 비동기 코드를 동기 코드처럼 읽기 쉽게 작성 가능
  3. 에러 처리 용이성

    • try/catch 구문을 사용하여 에러 처리가 용이
    • 동기 코드와 비슷한 방식으로 에러 핸들링 가능

⇒ 코드 가독성과 에러 처리가 중요할 때



💡 새롭게 알게 된 것

🆕 함수의 표기 생략


생략 가능한 조건

  1. 콜백 함수가 받는 인자를 그대로 다른 함수에 전달할 때
  2. 인자의 개수와 순서가 동일할 때

예시 코드

// 배열 메서드에서도 가능
['1', '2', '3'].map(Number);  // [1, 2, 3]
// 위는 아래와 동일
['1', '2', '3'].map(str => Number(str));

// 이벤트 리스너에서도 가능
button.addEventListener('click', console.log);
// 위는 아래와 동일
button.addEventListener('click', (event) => console.log(event));

// Promise에서도 가능
promise.then(console.log);
// 위는 아래와 동일
promise.then(result => console.log(result));

// 하지만 인자 처리가 다르면 생략 불가
promise.then(result => console.log('결과:', result));  // 이건 생략 불가



🆕 finally도 여러 개 사용할 수 있다


그 동안은 switch의 defalt문처럼 이해하고 있어서 finally문도 하나만 사용이 가능한 줄 알았는데, 여러 개 사용이 가능하는 걸 오늘 새롭게 알게 됐다.

⇒ 하지만 코드의 가독성 등을 고려해서 하나만 사용하는 것을 권장!

✅ 여러 개의 finally를 사용했을 때

promise
  .then(() => console.log('첫번째 작업'))
  .finally(() => console.log('첫번째 finally'))
  .then(() => console.log('두번째 작업'))
  .finally(() => console.log('두번째 finally'))
  .catch(error => console.error(error));
  1. 코드가 복잡해지고 읽기 어려워진다
  2. 실행 순서를 예측하기 어려울 수 있다
  3. 유지보수가 어려워질 수 있다

✅ 위 코드를 하나의 finally로 작성했을 때

promise
  .then(() => console.log('작업 실행'))
  .catch(error => console.error(error))
  .finally(() => {
    // 모든 정리 작업을 여기서 수행
    console.log('모든 작업 완료');
  });

이렇게 하면 코드의 의도가 더 명확해지고 관리하기도 쉬워진다



💭 느낀 점

확실히 비동기 개념은 실행 흐름을 예측하는 데 많은 시간이 걸린다.
오늘 강사님이 생각해보라고 주신 번외 문제를 가지고 팀 미팅을 했는데, 실행 순서를 예측해 보면서 내가 어떤 점을 잘못 이해하고 있는지 확인 할 수 있었다.

await이 함수의 값을 반환 받을 때까지 함수를 일시 중단시킨다는 부분이 async로 선언된 함수 부분이라고 생각했는데, await으로 함수를 호출한 컨텍스트를 일시 중단시키는 거였다.

async function z() {
  console.log("z1");
  setTimeout(() => console.log("setTimeout"), 0);
  await w(); // w()만 일시 중단 된다고 생각했음
  console.log("z2");
} // 실제로 일시 중단 되는 것 => z()

팀 미팅에서 안 다뤘으면 아직도 착각하고 있었겠지... ㅠ_ㅠ 우리 팀 최고!!

할 일

  • 오늘 번외 문제를 주말에 토토로에게 설명하기

이해를 했다면 설명할 수 있어야 함.
충분히 설명하지 못했다면 다시 공부할 것

추가적인 궁금증

async/await은 promise의 문법적 설탕이니까, 결국 동작은 promise와 같다는 건데 await만 함수를 일시 중단 할 수 있는걸까? promise는 그렇게 동작할 수 없는 건가?

profile
괴발개발 💻

0개의 댓글