티스토리 뷰

반응형

[챗지피티와 공부를 해보자] 프로미스 체이닝 (Promise Chaining) 

프로미스 체이닝 (Promise Chaining)

 

 

프로미스(Promise)는 비동기 작업이 끝났을때 결과를 반환해주는 객체이다.

보통 비동기 코드(예: API 호출, 파일 읽기, 데이터베이스 작업 등) 처리 시에 사용한다.

 

프로미스는 3가지의 상태를 가질수 있다.

  • pending (대기):  아직 실행 중, 결과를 모르는 상태
  • fulfilled (성공): 작업이 완료되어 resolve() 호출 
  • rejected (실패): 작업이 실패하여 reject() 호출
const promise = new Promise((resolve, reject) => { 

     // 비동기 작업 실행 중... 
     setTimeout(() => { 
          const success = true; 
          if (success) { 
               resolve("작업 성공! 🎉");
          } else { 
               reject("작업 실패! ❌");
          }
     }, 2000); 

}); 

console.log(promise); // pending (대기 중)

 

 

프로미스는 3가지 메서드로 결과를 체이닝할 수 있다.

  • then(): 성공(resolve())하면 실행된다.
  • catch(): 실패(reject())하면 실행된다.
  • finally(): 성공/실패와 관계없이 실행된다. (무조건 실행)
promise .then((result) => { 
    console.log("✅ 성공:", result); 
}).catch((error) => { 
    console.error("❌ 실패:", error); 
}).finally(() => { 
    console.log("🎯 작업 완료!"); 
});

 

프로미스의 직접 resolve와 reject를 사용해서 결과를 반환할 수 있다.

비동기 작업을 처리 시, 프로미스의 resolve()는 성공을, reject()는 실패를 반환하는 역할을 한다.

const getData = (isSuccess) => { 
     return new Promise((resolve, reject) => { 
          setTimeout(() => { 
               if (isSuccess) {
                    resolve("🎉 데이터 가져오기 성공!");
               } else {
                    reject("❌ 데이터 가져오기 실패!");
               }
          }, 1500); 
     }); 
}; 

getData(true).then((data) => {
     // 🎉 데이터 가져오기 성공!
     console.log(data)
}).catch((error) => {
     // 실패 시 실행됨
     console.error(error)
});

 

 

 

프로미스 체이닝

위에서 프로미스를 '프로미스(Promise)는 비동기 작업이 끝났을때 결과를 반환해주는 객체이다.'라고 설명했다.

이때, 프로미스 체이닝은 이 반환된 객체를 통해 다음 작업을 이어하는 것을 말한다.

아래와 같이 순차적으로 실행할수 있다.

const step1 = () => { 
     return new Promise((resolve) => { 
          setTimeout(() => {
               resolve("1️⃣ Step 1 완료!")
          }, 1000); 
     }); 
}; 

const step2 = (prevResult) => { 
     return new Promise((resolve) => { 
          setTimeout(() => {
               resolve(`${prevResult} → 2️⃣ Step 2 완료!`)
          }, 1000);  
     }); 
}; 

step1().then((result) => {
     // Step 1이 끝난 후 Step 2 실행
     return step2(result);  
}).then((finalResult) => {
	// "1️⃣ Step 1 완료! → 2️⃣ Step 2 완료!"
     console.log(finalResult) 
});

 

비동기 작업을 순차적으로 실행할 때 체이닝을 사용하면 가독성이 좋아지는데, 체이닝했을 경우와 하지않았을 경우를 비교해보자.

 

 

체이닝했을때

then()을 사용해 순차적으로 실행하면서 결과를 전달한다.

이때 then을 통해 한 줄씩 읽어 내려가면서 실행 순서를 쉽게 파악할 수 있다.

(return Promise하지 않을 경우, 체이닝이 되지않는 것을 주의!)

function getUser() {
    return new Promise((resolve) => {
        setTimeout(() => resolve({ id: 1, name: "Alice" }), 1000);
    });
}

function getOrders(userId) {
    return new Promise((resolve) => {
        setTimeout(() => resolve([{ orderId: 101, status: "배송 중" }]), 1000);
    });
}

function getDeliveryStatus(orderId) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(`📦 주문 ${orderId} → 배송 상태: 도착 예정`), 1000);
    });
}

// ✅ 체이닝 방식
getUser()
    .then((user) => {
        console.log(`👤 사용자: ${user.name}`);
        return getOrders(user.id);
    })
    .then((orders) => {
        console.log(`🛒 주문 내역:`, orders);
        return getDeliveryStatus(orders[0].orderId);
    })
    .then((status) => {
        console.log(`🚚 배송 상태: ${status}`);
    })
    .catch((error) => console.error("❌ 오류 발생:", error));

 

 

체이닝 안했을때

각 Promise가 개별적으로 실행되며, 비동기 흐름이 명확하지 않고 이전 결과를 다음 단계로 넘기는 것이 불편하다.

이때 중첩 코드가 발생할 가능성이 높다.

체이닝 방식에서 마지막 catch에서 발생하는 에러를 한곳에서 처리했다면, 체이닝을 안했을 경우에는 catch로 인해 중복 코드가 발생할 수 있다. (체이닝 방식도 catch 사용에 따라서 여러개가 될수 있다.) 

const userPromise = getUser();
const ordersPromise = userPromise.then((user) => {
    console.log(`👤 사용자: ${user.name}`);
    return getOrders(user.id);
});
const deliveryPromise = ordersPromise.then((orders) => {
    console.log(`🛒 주문 내역:`, orders);
    return getDeliveryStatus(orders[0].orderId);
});

userPromise.catch((error) => console.error("❌ 사용자 가져오기 오류:", error));
ordersPromise.catch((error) => console.error("❌ 주문 가져오기 오류:", error));
deliveryPromise
    .then((status) => console.log(`🚚 배송 상태: ${status}`))
    .catch((error) => console.error("❌ 배송 상태 오류:", error));

 

보통 업무 시 프로미스 체이닝으로 작업하게되는데, 

주의할 점은... then()의 지옥에 빠질 수 있으니 (then 이 많아질 경우 가독성이 떨어질수도 있으니) 적당히 사용하는 것도 중요하다.

우리는 협업하는 사람들이니까..

 

 

 

프로미스 동시 실행

여러개의 프로미스를 병렬로 실행할 수도 있다.

기능 Promise.all() Promise.race()
실행 방식 모든 프로미스가 완료될 때까지 대기 가장 먼저 완료되는 프로미스 하나만 반환
성공 조건 모든 프로미스가 성공해야 then() 실행 가장 먼저 끝나는 프로미스가 성공하면 then() 실행
실패 조건 하나라도 실패하면 즉시 catch() 실행 가장 먼저 끝나는 프로미스가 실패하면 catch() 실행
사용 예시 여러 개의 비동기 작업이 모두 완료될 때까지 기다릴 때 여러 개 중에서 가장 빠른 응답을 받고 싶을 때

 

 

Promise.all() 

모든 프로미스가 완료될 때까지 대기

const p1 = new Promise((res) => setTimeout(() => res("🍎 사과"), 1000)); 
const p2 = new Promise((res) => setTimeout(() => res("🍌 바나나"), 2000)); 

Promise.all([p1, p2]).then((results) => { 
    console.log(results); // ["🍎 사과", "🍌 바나나"] (모든 작업 완료 후 실행) 
});

 

모든 프로미스가 성공하면 results 배열로 반환되며, 하나라도 실패하면 catch()가 실행된다.

 

 

Promise.race()

가장 먼저 완료되는 프로미스 하나만 반환

const p1 = new Promise((res) => setTimeout(() => res("🚀 빠른 응답"), 1000)); 
const p2 = new Promise((res) => setTimeout(() => res("🐢 느린 응답"), 3000)); 

Promise.race([p1, p2]).then((result) => { 
    console.log(result); // "🚀 빠른 응답" (가장 먼저 끝난 프로미스 반환) 
});

 

가장 빨리 끝나는 p1의 결과만 반환되며, 느린 p2는 무시된다.

 

 

'여러 개의 비동기 작업이 모두 끝난 후 결과를 한꺼번에 받고 싶을 때'에는 Promise.all()을 사용하면 되고

'여러 개의 비동기 작업 중 가장 빠른 결과를 먼저 받고 싶을 때'에는 Promise.race()를 사용하면 된다.

 

 

이때 궁금한 점이 있는데, 

Promise.all()의 실패 조건 (-하나라도 실패하면 즉시)

Promise.race()의 성공 조건 (-가장 먼저 완료되는 프로미스 하나만)을 보면..

Q. 하나의 Promise가 실패하거나 성공하면 다른 프로미스들은 어떻게 되는것일까?

즉, Promise.all()에서 하나라도 실패하면 다른 프로미스들은 실행이 즉시 취소되는가?

또는 Promise.race()에서 가장 먼저 완료되는 프로미스가 생길 경우 다른 프로미스들은 실행이 즉시 취소되는가?

Promise.all()에서 하나라도 실패가 발생하면 다른 프로미스들은 어떻게 될까? 

 

예시

const p1 = new Promise((res) => setTimeout(() => res("✅ 작업 1 완료"), 1000));
const p2 = new Promise((_, rej) => setTimeout(() => rej("❌ 작업 2 실패"), 2000));
const p3 = new Promise((res) => setTimeout(() => res("✅ 작업 3 완료"), 3000));

Promise.all([p1, p2, p3]).then((results) => {
    console.log("모든 작업 완료:", results);
}).catch((error) => {
    console.error("🚨 하나의 작업이 실패:", error);
});

 

p1은 1초 후 성공하고

p2는  2초 후 실패한다. p2가 실패하면서 Promise.all()은 즉시 catch()가 실행된다. (catch로 이동된다)
하지만, p2가 실패했다고해서 p3가 실행되지 않는 것은 아니다. 3초후 성공 예정이었지만, p2의 실패로 Promise.all()의 결과에 반영되지 않을 뿐이다.

A. 즉, 하나가 실패(reject())해도 다른 프로미스들은 여전히 실행된다.

 

 

Q. Promise.race()에서 하나라도 성공이 발생하면 다른 프로미스들은 어떻게 될까?

A. Promise.all()의 동작과 마찬가지로 하나가 성공(resolve())해도 다른 프로미스들은 여전히 실행된다.

 

예시

const p1 = new Promise((resolve) => setTimeout(() => resolve("🚀 가장 빠름!"), 1000)); 
const p2 = new Promise((resolve) => setTimeout(() => resolve("🐢 느린 응답"), 3000)); 
const p3 = new Promise((resolve) => setTimeout(() => resolve("🐌 더 느린 응답"), 5000)); 

Promise.race([p1, p2, p3]).then((result) => { 
    console.log("가장 먼저 완료된 프로미스:", result); 
}); 

setTimeout(() => { 
    console.log("나머지 프로미스가 계속 실행 중인지 확인"); 
}, 4000);

 

p1은 1초 후 완료하고, 이때 Promise.race()는 즉시 then() 실행한다.

p2가 3초 후 완료 예정이지만 이미 Promise.race()는 then()으로 이동했기에 결과에 반영되지 않는다. (=실행은 계속됨)

p3가 5초 후 완료 예정이지만, p2와 마찬가지로 결과에 반영되지 않는다. (=실행은 계속됨)

 

Q. 나머지 프로미스들을 취소하려면?

자바스크립트의 기본 Promise는 취소 기능이 따로 없다.

A. 하지만 AbortController 같은 기법을 활용하면 나머지 프로미스들을 중단할 수 있다.  

 

수동 취소 예제

const controller = new AbortController(); 
const { signal } = controller; 

// 1. createAbortablePromise()를 사용해 signal을 감지하도록 Promise를 감싸준다
const createAbortablePromise = (promise) => {
    return new Promise((resolve, reject) => {
    	// 2. signal에 'abort' 이벤트 바인딩 
        signal.addEventListener("abort", () => reject(new Error("❌ 중단됨!")));
        promise.then(resolve).catch(reject);
    });
};

const p1 = createAbortablePromise(new Promise((resolve) => 
    setTimeout(() => { 
        resolve("🚀 가장 빠름!");
        // 3. 다른 프로미스 취소
        controller.abort(); 
    }, 1000)
));

const p2 = createAbortablePromise(new Promise((resolve, reject) => 
    setTimeout(() => reject("🐢 느린 응답"), 3000)
));

const p3 = createAbortablePromise(new Promise((resolve, reject) => 
    setTimeout(() => reject("🐌 더 느린 응답"), 5000)
));

// Promise.race 실행
Promise.race([p1, p2, p3])
    .then((result) => console.log("가장 먼저 완료된 프로미스:", result))
    .catch((error) => console.error("❌ 오류 발생:", error));

// 3. AbortController로 다른 프로미스 취소
signal.addEventListener("abort", () => { 
    console.log("❌ 나머지 프로미스 취소됨!"); 
});

 

  1. createAbortablePromise() 를 사용해 signal을 감지하도록 Promise를 감싸준다.
  2. signal.addEventListener("abort", () => reject(new Error("❌ 중단됨!"))); 이벤트 바인딩을 통해 reject 되도록한다.
  3. p1이 먼저 실행 완료 후 controller.abort()를 호출하면, 모든 Promise를 reject()한다.
  4. p2, p3가 실행 중이었지만, abort()로 인해 강제 reject()가 된다.

 

Q. AbortController를 사용하면 모든 Promise를 종료 시키는가?

A. AbortController는 특정 Promise 또는 Fetch 요청을 취소하는데 사용할 수 있지만, 모든 Promise를 자동으로 종료하지는 않는다.
즉, AbortController를 사용하려면 개별 Promise가 signal을 감지하고 취소될 수 있도록 구현해야 한다.

 

예시

const controller = new AbortController(); 
const { signal } = controller; 

// fetch()의 signal 옵션을 통해 AbortController가 요청을 감지
fetch("https://jsonplaceholder.typicode.com/todos/1", { signal }).then((response) => {
    return response.json();
}).then((data) => {
    console.log("📌 데이터:", data)
}) .catch((error) => { 
    if (error.name === "AbortError") { 
        console.log("🚨 요청이 취소되었습니다."); 
    } else { 
        console.error("❌ 오류 발생:", error); 
    } 
}); 

// 1초 후 요청 취소 
setTimeout(() => { 
     // fetch 요청 중단 
    controller.abort();
}, 1000);

 

 

AbortController의 한계

기능 설명
fetch 요청 취소 가능 ✅ fetch()와 함께 사용하면 네트워크 요청을 중단할 수 있음
일반 Promise는 자동 취소 불가 ❌ 일반 Promise는 AbortController로 자동 취소되지 않음
수동 취소 필요 ✅ signal.addEventListener("abort", callback)을 사용하면 취소 가능

 

즉, AbortController는 모든 Promise를 자동으로 종료하지 않고, 특정 API(fetch)와 함께 사용할 때 유용하다.
일반적인 Promise를 취소하려면 수동으로 위 예시들 처럼 reject() 처리 작업을 해줘야한다.

 

 

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함