모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

24강 / 전체 30강

Promise

12분 읽기 조회 0

Promise 3상태, new Promise 생성, .then/.catch/.finally 체이닝, 오류 전파, Promise.all/race/allSettled로 여러 비동기 작업 조합까지 완전 정복합니다.

🎯 학습 목표

이 강을 마치면 다음을 할 수 있습니다.

  • Promise가 무엇인지, 왜 콜백 지옥을 해결하는지 이해합니다.
  • new Promise()로 Promise를 직접 만들 수 있습니다.
  • .then()과 .catch(), .finally()로 비동기 흐름을 제어할 수 있습니다.
  • Promise.all(), Promise.race()로 여러 Promise를 조합할 수 있습니다.

💡 Promise란 무엇인가

이 섹션에서는 Promise의 개념과 세 가지 상태를 살펴보겠습니다.

Promise는 비동기 작업의 미래 결과를 나타내는 객체입니다. 음식 주문에 비유할 수 있습니다. 주문하면 즉시 음식이 나오지 않고 언젠가 나올 것이라는 약속(Promise)을 받습니다. 음식이 나오면(fulfilled) 먹을 수 있고, 품절이면(rejected) 환불을 받습니다.

Promise는 세 가지 상태를 가집니다. Pending(대기): 비동기 작업이 아직 완료되지 않은 초기 상태. Fulfilled(이행): 작업이 성공적으로 완료된 상태, resolve가 호출됨. Rejected(거부): 작업이 실패한 상태, reject가 호출됨. 상태가 한 번 결정되면 다시 변경되지 않습니다.

// Promise 생성
const promise = new Promise((resolve, reject) => {
  // executor 함수: Promise 생성 시 즉시 실행
  const success = true;

  if (success) {
    resolve('성공 데이터');  // fulfilled 상태로 전환
  } else {
    reject(new Error('실패 이유'));  // rejected 상태로 전환
  }
});

// 실전: 타이머를 Promise로 감싸기
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 사용
delay(1000).then(() => console.log('1초 후 실행'));

// fetch API는 이미 Promise를 반환 (26강에서 자세히)
// const responsePromise = fetch('/api/data');

Promise 생성자는 executor 함수를 인수로 받습니다. executor는 resolve와 reject를 인수로 받아, 작업 완료 시 resolve(결과값)를, 실패 시 reject(오류)를 호출합니다. resolve나 reject를 한 번 호출하면 그 이후의 호출은 무시됩니다. 위 delay 함수처럼 기존 콜백 기반 API를 Promise로 감싸는 패턴을 Promise 래핑(Promisification)이라고 합니다.

🔗 then · catch · finally — 체이닝

이 섹션에서는 Promise의 결과를 처리하는 세 가지 메서드와 체이닝 패턴을 살펴보겠습니다.

Promise는 .then(), .catch(), .finally() 메서드를 제공합니다. 이 메서드들 자체도 새로운 Promise를 반환하므로 체이닝(chaining)이 가능합니다. 이것이 콜백 지옥과의 결정적 차이입니다. 중첩 없이 수평으로 이어집니다.

// 기본 체이닝 패턴
fetch('/api/users/1')
  .then(response => response.json())  // Response → JSON 파싱 (Promise 반환)
  .then(user => {
    console.log(user.name);
    return user.id;          // 반환값이 다음 then의 인수로 전달
  })
  .then(userId => console.log('ID:', userId))
  .catch(error => {
    // 체인 중 어디서 오류가 발생해도 여기서 한 번에 처리
    console.error('오류:', error.message);
  })
  .finally(() => {
    // 성공/실패 관계없이 항상 실행
    console.log('로딩 완료');
    hideLoadingSpinner();
  });

// delay 함수로 순차 실행 — 콜백 지옥 없이!
delay(1000)
  .then(() => {
    console.log('1초 후');
    return delay(1000);  // Promise를 반환하면 다음 then이 기다림
  })
  .then(() => {
    console.log('2초 후');
    return delay(1000);
  })
  .then(() => console.log('3초 후'));

.then()이 반환하는 값이 다음 .then()의 인수가 됩니다. 만약 .then()에서 Promise를 반환하면, 다음 .then()은 그 Promise가 완료될 때까지 기다립니다. 이 메커니즘으로 비동기 작업을 순서대로 실행하는 체이닝이 가능합니다. .finally()는 로딩 스피너를 숨기거나, 파일을 닫거나, 연결을 해제하는 정리 작업에 활용합니다.

⚠️ 오류 처리

이 섹션에서는 Promise 체인에서 오류를 안전하게 처리하는 방법을 살펴보겠습니다.

Promise에서 오류는 체인을 따라 내려가 가장 가까운 .catch()에서 처리됩니다. .then() 안에서 throw를 사용해 오류를 발생시키거나, reject()를 호출한 Promise를 반환하면 체인이 .catch()로 건너뜁니다.

// 오류가 catch로 전파됨
Promise.resolve('시작')
  .then(val => {
    throw new Error('then에서 오류 발생');
  })
  .then(val => {
    console.log('이것은 실행되지 않음');  // 건너뜀
  })
  .catch(err => {
    console.error('잡음:', err.message);  // 잡음: then에서 오류 발생
    return '복구된 값';  // catch에서 반환하면 체인 계속 가능
  })
  .then(val => {
    console.log(val);  // '복구된 값'
  });

// Promise 직접 reject
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    if (!id) {
      reject(new Error('ID가 필요합니다'));
      return;  // 이후 코드 실행 방지
    }
    setTimeout(() => resolve({ id, name: '홍길동' }), 500);
  });
}

fetchUser(0)
  .then(user => console.log(user))
  .catch(err => console.error(err.message));  // 'ID가 필요합니다'

// 주의: .catch() 없이 reject되면 UnhandledPromiseRejection!
// 모든 Promise 체인 끝에 .catch()를 붙이세요

.catch()에서 값을 반환하면 체인을 계속 이어갈 수 있으므로 오류 상황의 기본값을 설정하거나 복구 로직을 구현할 수 있습니다. 오류를 처리하고 싶지 않으면 .catch()에서 다시 throw err하면 체인이 다시 rejected 상태가 됩니다.

⚡ Promise.all · race · allSettled

이 섹션에서는 여러 Promise를 동시에 처리하는 정적 메서드들을 살펴보겠습니다.

여러 비동기 작업을 순차가 아닌 병렬로 실행하면 전체 대기 시간을 크게 줄일 수 있습니다. 예를 들어 세 개의 API를 순차로 호출하면 3초 걸리지만, 병렬로 호출하면 가장 느린 것 하나의 시간만 걸립니다.

// Promise.all — 모두 성공해야 완료, 하나라도 실패하면 즉시 reject
Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/orders').then(r => r.json()),
  fetch('/api/products').then(r => r.json())
])
  .then(([user, orders, products]) => {
    // 세 가지 모두 성공했을 때만 실행, 구조 분해로 받음
    console.log(user, orders, products);
  })
  .catch(err => {
    // 어느 하나라도 실패하면 여기로
    console.error('하나 이상 실패:', err);
  });

// Promise.race — 가장 먼저 완료/실패한 것 반환
const fast = new Promise(resolve => setTimeout(() => resolve('빠름'), 500));
const slow = new Promise(resolve => setTimeout(() => resolve('느림'), 2000));

Promise.race([fast, slow]).then(result => {
  console.log(result);  // '빠름'
});

// 타임아웃 구현에 활용
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('타임아웃')), ms)
  );
  return Promise.race([promise, timeout]);
}

withTimeout(fetch('/api/slow'), 3000)
  .then(res => res.json())
  .catch(err => console.error(err.message));  // 3초 초과 시 '타임아웃'

// Promise.allSettled — 모두 완료될 때까지 기다림 (성공/실패 무관)
Promise.allSettled([
  Promise.resolve('성공1'),
  Promise.reject('실패'),
  Promise.resolve('성공2')
]).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('성공:', result.value);
    } else {
      console.log('실패:', result.reason);
    }
  });
});

Promise.all()은 모든 작업이 성공해야 하는 경우에 적합합니다. 페이지 초기화 시 여러 API를 병렬로 호출해 모두 받아야 렌더링하는 패턴이 대표적입니다. Promise.race()는 가장 빠른 결과만 필요하거나 타임아웃을 구현할 때 유용합니다. Promise.allSettled()는 일부가 실패해도 나머지 결과가 필요한 경우, 예를 들어 여러 파일 업로드의 개별 성공/실패 현황을 보고할 때 적합합니다.

🔄 콜백 지옥 vs Promise 비교

이 섹션에서는 23강의 콜백 지옥 예시를 Promise로 리팩터링해 차이를 직관적으로 확인해 보겠습니다.

// 콜백 지옥 (23강)
getUser(userId, (err1, user) => {
  if (err1) return handleError(err1);
  getOrders(user.id, (err2, orders) => {
    if (err2) return handleError(err2);
    getProduct(orders[0].productId, (err3, product) => {
      if (err3) return handleError(err3);
      console.log(user, orders, product);
    });
  });
});

// Promise 체이닝 — 훨씬 읽기 쉬움
getUser(userId)
  .then(user => {
    return getOrders(user.id)
      .then(orders => ({ user, orders }));
  })
  .then(({ user, orders }) => {
    return getProduct(orders[0].productId)
      .then(product => ({ user, orders, product }));
  })
  .then(({ user, orders, product }) => {
    console.log(user, orders, product);
  })
  .catch(err => handleError(err));  // 오류 처리 한 곳에서!

// 25강의 async/await로는 더 간결해짐
async function loadData(userId) {
  const user    = await getUser(userId);
  const orders  = await getOrders(user.id);
  const product = await getProduct(orders[0].productId);
  return { user, orders, product };
}

📝 핵심 요약

24강에서 배운 내용을 정리해 보겠습니다.

개념설명
Promise 3상태Pending → Fulfilled(resolve) / Rejected(reject)
.then(fn)성공 시 처리, 반환값이 다음 then의 인수
.catch(fn)체인 어디서든 오류를 잡아서 처리
.finally(fn)성공/실패 무관 항상 실행
Promise.all([])모두 성공해야 완료, 하나 실패 시 즉시 reject
Promise.race([])가장 먼저 완료/실패한 것 반환
Promise.allSettled([])전부 완료될 때까지 기다림

다음 강인 25강 — async·await에서는 Promise를 동기 코드처럼 작성하는 ES2017 문법을 배웁니다. async function과 await 키워드로 24강의 .then() 체이닝을 훨씬 읽기 쉬운 코드로 바꿀 수 있습니다. 내부적으로는 Promise를 사용하므로, 24강을 완벽히 이해한 지금이 최적의 타이밍입니다!

관련 주제

  • Promise 상태
  • resolve reject
  • then catch finally
  • Promise.all
  • Promise.race
  • Promise.allSettled
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 자바스크립트 기초 30강 — 웹을 움직이는 언어
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

자바스크립트 기초 30강 — 웹을 움직이는 언어

이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...