Skip to Content

45장 프로미스

자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 **콜백 헬(Callback Hell)**로 인해 가독성이 나쁘고, 비동기 처리 중 발생한 에러의 처리가 곤란하며, 여러 개의 비동기 처리를 한 번에 처리하는 데도 한계가 있다.

ES6에서는 비동기 처리를 위한 또 다른 패턴으로 **프로미스(Promise)**를 도입했다. 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다.

45.1 비동기 처리를 위한 콜백 패턴의 단점

45.1.1 콜백 헬

비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없다. 따라서 비동기 함수의 처리 결과(성공/실패)에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다.

이때 후속 처리를 위해 콜백 함수를 전달하는데, 콜백 함수 내에서 다시 비동기 호출을 하면서 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상을 콜백 헬이라 한다.

// 콜백 헬 예시 get('/step1', (a) => { get(`/step2/${a}`, (b) => { get(`/step3/${b}`, (c) => { console.log(c); }); }); });

45.1.2 에러 처리의 한계

가장 심각한 문제는 에러 처리가 곤란하다는 것이다.

try { setTimeout(() => { throw new Error('Error!'); }, 1000); } catch (e) { // 에러를 캐치하지 못한다. console.error('캐치한 에러', e); }

setTimeout의 콜백 함수가 실행될 때 setTimeout 함수는 이미 콜 스택에서 제거된 상태다. 에러는 호출자(caller) 방향으로 전파되는데, 콜백 함수의 호출자가 setTimeout이 아니기 때문에 catch 블록에서 에러를 잡을 수 없다.

45.2 프로미스의 생성

Promise 생성자 함수를 new 연산자와 함께 호출하면 프로미스 객체를 생성한다.

// 프로미스 생성 const promise = new Promise((resolve, reject) => { // 비동기 작업 수행 if (/* 비동기 처리 성공 */) { resolve('result'); } else { /* 비동기 처리 실패 */ reject('failure reason'); } });

프로미스의 상태 정보

상태 정보의미상태 변경 조건
pending비동기 처리가 아직 수행되지 않은 상태프로미스가 생성된 직후 기본 상태
fulfilled비동기 처리가 수행된 상태 (성공)resolve 함수 호출
rejected비동기 처리가 수행된 상태 (실패)reject 함수 호출

fulfilled 또는 rejected 상태를 settled 상태라고 한다. 일단 settled 상태가 되면 더 이상 다른 상태로 변화할 수 없다.

45.3 프로미스의 후속 처리 메서드

프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드(then, catch, finally)에 인수로 전달한 콜백 함수가 선택적으로 호출된다. 모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다.

45.3.1 Promise.prototype.then

두 개의 콜백 함수를 인수로 받는다.

  • 첫 번째: fulfilled 상태(성공) 시 호출
  • 두 번째: rejected 상태(실패) 시 호출
new Promise((resolve) => resolve('fulfilled')).then( (v) => console.log(v), (e) => console.error(e) );

45.3.2 Promise.prototype.catch

rejected 상태(실패) 시 호출될 콜백 함수를 인수로 받는다. then(undefined, onRejected)와 동일하게 동작한다.

45.3.3 Promise.prototype.finally

성공/실패와 상관없이 무조건 한 번 호출된다. 공통적인 뒷정리 작업에 유용하다.

45.4 프로미스의 에러 처리

에러 처리는 then 메서드의 두 번째 인수보다 catch 메서드를 사용하는 것을 권장한다. catch 메서드를 사용하면 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있고 가독성도 더 좋다.

promiseGet(url) .then((res) => console.log(res)) .catch((err) => console.error(err)); // 권장

45.5 프로미스 체이닝

then, catch, finally 후속 처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다. 이를 **프로미스 체이닝(Promise Chaining)**이라 한다.

promiseGet(`${url}/posts/1`) .then(({ userId }) => promiseGet(`${url}/users/${userId}`)) .then((userInfo) => console.log(userInfo)) .catch((err) => console.error(err));

45.6 프로미스의 정적 메서드

45.6.1 Promise.resolve / Promise.reject

이미 존재하는 값을 래핑하여 프로미스를 생성하기 위해 사용한다.

  • Promise.resolve는 인수로 전달받은 값을 resolve하는 프로미스를 생성한다.
  • Promise.reject는 인수로 전달받은 값을 reject하는 프로미스를 생성한다.

45.6.2 Promise.all

여러 개의 비동기 처리를 모두 병렬(parallel) 처리할 때 사용한다. 전달받은 모든 프로미스가 fulfilled 상태가 되면 모든 결과를 배열에 담아 반환한다. 하나라도 rejected 되면 즉시 에러를 반환한다.

45.6.3 Promise.race

가장 먼저 fulfilled 또는 rejected 상태가 된 프로미스의 결과를 반환한다.

45.6.4 Promise.allSettled

전달받은 프로미스가 모두 settled 상태(성공 또는 실패)가 되면 결과를 반환한다. 실패하더라도 중단되지 않고 모든 결과를 확인할 수 있다.

45.7 마이크로태스크 큐

프로미스의 후속 처리 메서드(then, catch, finally)의 콜백 함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장된다. 마이크로태스크 큐는 태스크 큐보다 우선순위가 높다.

setTimeout(() => console.log(1), 0); // 태스크 큐 Promise.resolve() .then(() => console.log(2)) // 마이크로태스크 큐 .then(() => console.log(3)); // 마이크로태스크 큐 // 출력 순서: 2 -> 3 -> 1

45.8 fetch

fetch 함수는 XMLHttpRequest 객체보다 사용법이 간단하고 프로미스를 지원하는 HTTP 요청 전송 기능의 Web API다.

fetch('https://jsonplaceholder.typicode.com/todos/1') .then((response) => response.json()) .then((json) => console.log(json));

주의할 점: fetch 함수가 반환하는 프로미스는 HTTP 에러(404, 500 등)가 발생해도 reject하지 않고 resolve한다. (단, ok 상태를 false로 설정). 오직 네트워크 장애나 요청이 완료되지 못한 경우에만 reject한다. 따라서 response.ok를 확인하여 에러 처리를 해야 한다.

fetch(wrongUrl) .then((response) => { if (!response.ok) throw new Error(response.statusText); return response.json(); }) .catch((err) => console.error(err));

과제: 퀴즈

퀴즈 1

프로미스의 3가지 상태(state)는 무엇인가?

정답 및 해설

정답: pending(대기), fulfilled(이행), rejected(거부)

해설:

  • pending: 비동기 처리가 아직 수행되지 않은 기본 상태.
  • fulfilled: 비동기 처리가 성공적으로 수행된 상태 (resolve 호출 시).
  • rejected: 비동기 처리가 실패한 상태 (reject 호출 시). fulfilledrejected를 합쳐 settled 상태라고 부르며, 한 번 settled 되면 상태는 변하지 않습니다.

👉 관련 내용으로 이동: 45.2 프로미스의 생성

퀴즈 2

프로미스 체이닝에서 에러 처리를 위해 then의 두 번째 인수를 사용하는 것보다 catch를 권장하는 이유는?

정답 및 해설

정답: then 내부의 에러까지 잡을 수 있고 가독성이 좋기 때문.

해설: then의 두 번째 콜백 함수는 같은 then의 첫 번째 콜백 함수에서 발생한 에러를 잡을 수 없습니다. 반면 catch는 그 앞의 모든 then 체인과 비동기 처리에서 발생한 에러를 모두 잡을 수 있으며 코드 가독성도 더 뛰어납니다.

👉 관련 내용으로 이동: 45.4 프로미스 에러 처리

퀴즈 3

Promise.allPromise.race의 차이점은?

정답 및 해설

정답: all은 모두 성공해야 완료, race는 가장 빠른 하나만 완료되면 종료.

해설:

  • Promise.all: 전달받은 모든 프로미스가 fulfilled 될 때까지 기다렸다가 결과 배열을 반환합니다. 하나라도 실패하면 즉시 실패 처리됩니다.
  • Promise.race: 전달받은 프로미스 중 가장 먼저 처리된(성공이든 실패든) 프로미스의 결과를 그대로 반환합니다.

👉 관련 내용으로 이동: 45.6 프로미스의 정적 메서드

퀴즈 4

fetch 함수 사용 시 HTTP 404 에러가 발생하면 catch 블록이 실행되는가?

정답 및 해설

정답: 실행되지 않는다. (오답 주의!)

해설: fetch는 네트워크 장애 등을 제외한 HTTP 에러(4xx, 5xx)에 대해서는 프로미스를 reject 하지 않습니다. 대신 resolve 상태의 응답 객체를 반환하며 ok 프로퍼티가 false가 됩니다. 따라서 response.ok를 체크하여 수동으로 에러를 던져야 합니다.

👉 관련 내용으로 이동: 45.8 fetch

퀴즈 5

마이크로태스크 큐에 들어가는 대표적인 작업은 무엇인가?

정답 및 해설

정답: 프로미스의 후속 처리 메서드(then, catch, finally)의 콜백 함수.

해설: setTimeout, setInterval 등의 콜백은 일반 태스크 큐에 들어가는 반면, 프로미스의 핸들러는 우선순위가 높은 마이크로태스크 큐에 들어갑니다. 따라서 이벤트 루프는 콜 스택이 비면 태스크 큐보다 마이크로태스크 큐를 먼저 비웁니다.

👉 관련 내용으로 이동: 45.7 마이크로태스크 큐

퀴즈 6: 실전 문제 - timeoutPromise 구현

fetchPromise가 500ms 이내에 완료되지 않으면 에러를 발생시키는 timeoutPromise 함수를 구현하여, Promise.race를 통해 타임아웃 처리를 완성하시오. (타임아웃 발생 시 결과는 null이 되어야 함)

요구사항:

  1. fetchPromise500ms 이내에 완료되면 정상적으로 결과를 반환
  2. 500ms를 초과하면 timeoutPromise가 먼저 완료되어 에러를 발생시키고, catch 블록에서 null을 반환
  3. Promise.race를 사용하여 두 Promise 중 먼저 완료되는 것을 처리
const fetchPromise = () => { return new Promise((resolve) => { const delay = Math.random() * 500 + 300; // 300ms ~ 800ms setTimeout(() => { resolve({ data: 'API 응답 데이터', timestamp: Date.now(), delay: delay }); }, delay); }); }; // TODO: 이 함수를 구현하세요 const timeoutPromise = (ms) => { // 여기에 코드를 작성하세요 }; const fetchWithTimeout = async () => { try { return await Promise.race([fetchPromise(), timeoutPromise(500)]); } catch (error) { return null; } }; fetchWithTimeout();

정답 및 해설

정답:

const timeoutPromise = (ms) => { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Request timeout after ${ms}ms`)); }, ms); }); };

해설: setTimeout은 프로미스를 반환하지 않으므로 new Promise로 감싸야 합니다. 지정된 시간(ms)이 지난 후 reject를 호출하여 에러를 발생시키면, Promise.race가 이를 감지하여 가장 먼저 완료된 상태(실패)로 처리합니다. 이후 try-catch 블록에서 에러를 잡아 null을 반환하게 됩니다.

👉 관련 내용으로 이동: 45.6.3 Promise.race

추천 자료

Last updated on