전체 목록
JavaScriptMedium#05

Promise와 async/await의 차이점 및 에러 처리 방법을 설명해주세요.

#JS#비동기#Promise#async/await
힌트

체이닝 방식과 동기 코드처럼 작성하는 방식의 차이를 생각해보세요.

정답 및 해설

Promise와 async/await의 차이점 및 에러 처리 방법을 설명해주세요.

JavaScript에서 비동기 작업을 처리하는 방법은 콜백(Callback)에서 시작해 Promise, 그리고 async/await로 진화해왔습니다. async/await는 Promise 위에 만들어진 **문법적 설탕(syntactic sugar)**으로, 비동기 코드를 동기 코드처럼 읽고 쓸 수 있게 해줍니다.

콜백 지옥에서 Promise로

콜백 지옥 (Callback Hell)

// 콜백 방식 — 중첩이 깊어질수록 읽기 어려워짐
getUser(userId, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      getAuthor(comments[0].authorId, (author) => {
        console.log(author); // 콜백 지옥
      }, handleError);
    }, handleError);
  }, handleError);
}, handleError);

Promise

Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 세 가지 상태를 가집니다.

  • Pending: 초기 상태, 아직 완료/실패 전
  • Fulfilled: 작업이 성공적으로 완료됨
  • Rejected: 작업이 실패함

기본 사용법

const promise = new Promise((resolve, reject) => {
  // 비동기 작업 수행
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('데이터 로드 성공'); // Fulfilled
    } else {
      reject(new Error('데이터 로드 실패')); // Rejected
    }
  }, 1000);
});

promise
  .then((data) => console.log(data))    // 'data 로드 성공'
  .catch((err) => console.error(err))   // 에러 처리
  .finally(() => console.log('완료'));  // 성공/실패 무관하게 실행

Promise 체이닝

function getUser(id) {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}

function getPosts(userId) {
  return fetch(`/api/posts?userId=${userId}`).then((res) => res.json());
}

getUser(1)
  .then((user) => getPosts(user.id))    // Promise를 반환하면 체인이 이어짐
  .then((posts) => console.log(posts))
  .catch((err) => console.error(err));  // 체인 어디서든 발생한 에러를 잡음

Promise.all / Promise.allSettled / Promise.race

// Promise.all — 모두 성공해야 resolve, 하나라도 실패하면 reject
const [user, posts] = await Promise.all([
  fetch('/api/user/1').then((r) => r.json()),
  fetch('/api/posts').then((r) => r.json()),
]);

// Promise.allSettled — 성공/실패 여부 무관하게 모든 결과 반환
const results = await Promise.allSettled([
  fetch('/api/a').then((r) => r.json()),
  Promise.reject(new Error('실패')),
]);
results.forEach((result) => {
  if (result.status === 'fulfilled') console.log(result.value);
  else console.error(result.reason);
});

// Promise.race — 가장 먼저 완료된 Promise의 결과 반환
const fastest = await Promise.race([
  fetch('/api/fast'),
  fetch('/api/slow'),
]);

async/await

async 함수는 항상 Promise를 반환합니다. await는 Promise가 settled될 때까지 해당 async 함수의 실행을 일시 중단합니다.

기본 사용법

async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`); // Promise가 resolve될 때까지 대기
  const user = await response.json();
  return user; // Promise<user>로 래핑되어 반환
}

// async 함수는 Promise를 반환하므로 .then()으로 받을 수 있음
fetchUser(1).then((user) => console.log(user));

// 또는 async 컨텍스트 내에서 await
async function main() {
  const user = await fetchUser(1);
  console.log(user);
}

Promise 체이닝 vs async/await 비교

같은 로직을 두 방식으로 작성한 예시입니다.

// Promise 체이닝
function getUserWithPosts(userId) {
  return getUser(userId)
    .then((user) => {
      return getPosts(user.id).then((posts) => ({ user, posts }));
    })
    .then(({ user, posts }) => {
      return getComments(posts[0].id).then((comments) => ({
        user,
        posts,
        comments,
      }));
    });
}

// async/await — 훨씬 읽기 쉬움
async function getUserWithPosts(userId) {
  const user = await getUser(userId);
  const posts = await getPosts(user.id);
  const comments = await getComments(posts[0].id);
  return { user, posts, comments };
}

에러 처리

Promise의 에러 처리

fetch('/api/data')
  .then((res) => {
    if (!res.ok) {
      throw new Error(`HTTP 에러: ${res.status}`);
    }
    return res.json();
  })
  .then((data) => console.log(data))
  .catch((err) => {
    // .then() 체인에서 발생한 모든 에러를 잡음
    console.error('에러 발생:', err.message);
  })
  .finally(() => {
    // 로딩 스피너 종료 등
    setLoading(false);
  });

async/await의 에러 처리

async function fetchData() {
  try {
    const response = await fetch('/api/data');

    if (!response.ok) {
      throw new Error(`HTTP 에러: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (err) {
    // await에서 발생한 에러를 잡음
    console.error('에러 발생:', err.message);
    throw err; // 필요하면 다시 throw
  } finally {
    setLoading(false);
  }
}

세밀한 에러 처리

async function processMultiple() {
  // 각 작업별로 다른 에러 처리가 필요한 경우
  let user;
  try {
    user = await getUser(1);
  } catch (err) {
    console.error('사용자 조회 실패:', err);
    return null;
  }

  let posts;
  try {
    posts = await getPosts(user.id);
  } catch (err) {
    console.error('게시글 조회 실패:', err);
    posts = []; // 기본값 사용
  }

  return { user, posts };
}

유틸리티 패턴 — 에러를 튜플로 반환

// Go 언어 스타일 에러 처리 패턴
async function safeAwait(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (err) {
    return [err, null];
  }
}

async function main() {
  const [err, user] = await safeAwait(getUser(1));
  if (err) {
    console.error('에러:', err);
    return;
  }
  console.log(user);
}

병렬 처리 주의사항

// 나쁜 예 — 순차 실행 (불필요하게 느림)
async function fetchSequential() {
  const user = await getUser(1);    // 1초 대기
  const posts = await getPosts(1);  // 1초 대기 (총 2초)
  return { user, posts };
}

// 좋은 예 — 병렬 실행
async function fetchParallel() {
  const [user, posts] = await Promise.all([
    getUser(1),   // 동시 시작
    getPosts(1),  // 동시 시작 (총 1초)
  ]);
  return { user, posts };
}

차이점 정리

구분Promiseasync/await
문법.then() / .catch() 체이닝동기 코드처럼 작성
에러 처리.catch()try/catch
가독성체이닝이 길어지면 복잡직관적, 읽기 쉬움
디버깅스택 트레이스 추적 어려움스택 트레이스 명확
내부 동작-Promise 기반
병렬 처리Promise.all()Promise.all() + await

실무에서는 async/await를 기본으로 사용하고, 병렬 처리가 필요하면 Promise.all()await와 함께 사용하는 것이 권장 패턴입니다.