JavaScript는 싱글 스레드 언어입니다. 하나의 작업이 끝나야 다음 작업을 시작할 수 있습니다. 그런데 네트워크 요청처럼 시간이 오래 걸리는 작업을 동기적으로 처리하면 브라우저가 완전히 멈춰버립니다. 이 문제를 해결하기 위해 비동기 프로그래밍이 존재합니다.

1단계: 콜백(Callback)

가장 초기 방식은 콜백 함수입니다. 작업이 완료됐을 때 호출할 함수를 미리 넘기는 방식입니다.

function fetchUser(id, callback) {
  setTimeout(() => {
    callback(null, { id, name: '홍길동' });
  }, 1000);
}

fetchUser(1, (err, user) => {
  if (err) return console.error(err);
  console.log(user.name);
});

문제는 중첩이 깊어질수록 "콜백 지옥"이 발생한다는 점입니다.

// 콜백 지옥 — 읽기도, 관리하기도 어렵습니다
getUser(1, (err, user) => {
  getPosts(user.id, (err, posts) => {
    getComments(posts[0].id, (err, comments) => {
      getLikes(comments[0].id, (err, likes) => {
        console.log(likes); // 4단계 중첩...
      });
    });
  });
});

2단계: Promise

Promise는 비동기 작업의 미래 결과를 나타내는 객체입니다. pending(대기), fulfilled(이행), rejected(거부) 세 가지 상태를 가집니다.

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: '홍길동' });
      } else {
        reject(new Error('잘못된 ID'));
      }
    }, 1000);
  });
}

fetchUser(1)
  .then(user => console.log(user.name)) // 성공
  .catch(err => console.error(err))     // 실패
  .finally(() => console.log('완료'));  // 항상 실행

Promise를 체이닝하면 중첩 없이 순차 처리가 가능합니다.

getUser(1)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => getLikes(comments[0].id))
  .then(likes => console.log(likes))
  .catch(err => console.error(err));

병렬 처리: Promise.all & Promise.allSettled

// 모두 성공해야 함 — 하나라도 실패하면 catch
const [user, posts] = await Promise.all([
  fetchUser(1),
  fetchPosts(1)
]);

// 각각의 성공/실패를 모두 받음
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(999) // 실패해도 OK
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log(result.value);
  } else {
    console.error(result.reason);
  }
});

3단계: async/await

ES2017에서 도입된 async/await는 Promise를 동기 코드처럼 읽기 좋게 작성할 수 있게 해줍니다.

async function loadUserDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);

    return { user, posts, comments };
  } catch (err) {
    console.error('데이터 로드 실패:', err);
    throw err;
  }
}

// 최상위에서 async 함수 호출
loadUserDashboard(1).then(dashboard => {
  renderDashboard(dashboard);
});

실전 패턴: API 요청 유틸리티

async function api(url, options = {}) {
  const response = await fetch(url, {
    headers: { 'Content-Type': 'application/json' },
    ...options
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message || `HTTP ${response.status}`);
  }

  return response.json();
}

// 사용
const user = await api('/api/users/1');
const newPost = await api('/api/posts', {
  method: 'POST',
  body: JSON.stringify({ title: '새 글', content: '...' })
});
핵심 정리
콜백 → Promise → async/await 순서로 발전했습니다. 현대 JavaScript에서는 async/await를 기본으로 사용하고, 병렬 처리가 필요할 땐 Promise.all을 활용하세요. 에러 처리는 반드시 try/catch로 감싸세요.