비동기 처리와 Promise

고기호·2024년 9월 3일
1

자바스크립트

목록 보기
3/3

비동기 처리의 문제들

자바스크립트에서는 많은 작업이 비동기로 이루어진다. 과거에는 이러한 비동기 작업을 콜백함수로 처리했지만, 애플리케이션의 규모가 커지면서 코드의 복잡도가 증가하고, 관리가 어려워지는 문제가 발생했다.

콜백 지옥


step1(function (value1) {
    step2(function (value2) {
        step3(function (value3) {
            step4(function (value4) {
                step5(function (value5) {
                    step6(function (value6) {
                        // Do something with value6
                    });
                });
            });
        });
    });
});

stepN 함수에 콜백이 계속 중첩하며 콜백 지옥이 생성되고, 이로인해 코드의 가독성이 떨어지고, 유지보수가 어려워진다.

오류 처리의 문제


try {
	setTimeout(()=>{
    		throw new Error('Error');
        }, 1000}
} catch { 
	console.error('에러가 캐치 될까요?', e);
}


에러 캐치가 안된다!
이는 비동기의 특성으로 인해 콜백함수가 이벤트 루프에 의해 다시 콜스택으로 돌아왔을 때, 이전에 감싸졌던 try ... catch 문과 다른 실행 컨텍스트에서 실행되기 때문이다.

try {
    setTimeout(() => {
        try {
            throw new Error("Error!");
        } catch {
            console.error("에러 발생!");
        }
    }, 1000);
} catch (e) {
    console.error("캐치한 에러", e);
}

이렇게 콜백 함수 내부에서 감싸주면 에러 처리가 가능하지만, 벌써부터 코드의 가독성이 나빠져 눈살 찌뿌려지기 시작한다.

아무튼 이러한 문제를 해결하기 위해 나온 것이 Promise이다.

Promise

정의


Promise는 미래의 어떤 시점에 결과를 제공하겠다는 값의 대리자 역할을 한다. Promise는 비동기 작업의 성공 또는 실패를 표현하며, 다음 중 하나의 상태(state)를 가진다.

  • pending: 약속이 아직 수행 중인 상태
  • fulfilled (settled): 약속이 지켜진 상태
  • rejected (settled): 약속이 어떤 이유에서 지켜지지 않은 상태

Promise 선언 및 사용 예시


const promise = new Promise((resolve, reject) => {
    if (비동기 처리 성공) {
        resolve('result'); // fulfilled 상태로 result를 래핑해 반환
    } else { 
        reject('failure reason'); // rejected 상태로 failure reason을 래핑해 반환
    }
});

promise
  .then(v => console.log(v))  // fulfilled 상태일 때 실행
  .catch(e => console.error(e))  // rejected 상태일 때 실행
  .finally(() => console.log('finally'));  // 상태와 상관없이 항상 실행

프로미스 체이닝


Promise는 then, catch, finally 메서드를 이용하여 비동기 작업을 순차적으로 처리할 수 있으며, 이를 프로미스 체이닝이라고 한다.

Promise.resolve()
  .then(() => { /* 작업 1 */ })
  .then(() => { /* 작업 2 */ })
  .then(() => { /* 작업 3 */ })
  .catch(() => { /* 에러 처리 */ })
  .finally(() => { /* 마무리 작업 */ });

위에 있던 콜백 지옥과 비교해보면 가독성이 매우 좋아 보인다.

마이크로태스크 큐


Promise의 후속 처리 메서드(then, catch, finally)의 콜백 함수는 마이크로태스크 큐에 저장되며, 일반 태스크 큐 보다 우선적으로 실행된다.

https://velog.io/@kgh7427_/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84Event-loop

setTimeout(() => {
    console.log("1");
}, 0);

Promise.resolve()
    .then(() => {
        console.log("2");
    })
    .then(() => {
        console.log("3");
    });

Promise 유틸리티 메서드


Promise.all

  • 여러개의 Promise를 동시에 관리할 수 있다.
  • 모든 Promise가 성공해야 .then 블록이 실행된다.
  • 하나라도 실패하면 전체가 실패로 간주되어 .catch 블록으로 이동한다.
function makePayment(url, shouldSucceed) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (shouldSucceed) {
                resolve(`송금 성공: ${url}`);
            } else {
                reject(new Error(`송금 실패: ${url}`));
            }
        }, 1000); // 1초 후에 결과 반환
    });
}

const p1 = makePayment("url1", true); // 성공
const p2 = makePayment("url2", false); // 실패
const p3 = makePayment("url3", true); // 성공
const p4 = makePayment("url4", false); // 실패
const p5 = makePayment("url5", true); // 성공

Promise.all([p1, p2, p3, p4, p5])
    .then((results) => {
        console.log("all :", results);
        console.log("모든 송금이 성공적으로 완료되었습니다.");
    })
    .catch((error) => {
        console.error("송금 중 오류가 발생했습니다.", error);
    });

Promise.allSettled

  • 모든 Promse가 완료된 후 결과를 확인할 수 있다.
  • 실패한 Promise도 무시하지 않고 결과를 제공한다.
  • 모든 Promise가 완료된 후, 실패한 것만 별도로 처리가능하다.
function makePayment(url, shouldSucceed) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (shouldSucceed) {
                resolve(`송금 성공: ${url}`);
            } else {
                reject(new Error(`송금 실패: ${url}`));
            }
        }, 1000); // 1초 후에 결과 반환
    });
}

const p1 = makePayment("url1", true); // 성공
const p2 = makePayment("url2", false); // 실패
const p3 = makePayment("url3", true); // 성공
const p4 = makePayment("url4", false); // 실패
const p5 = makePayment("url5", true); // 성공

Promise.allSettled([p1, p2, p3, p4, p5]).then((results) => {
    results.forEach((result) => {
        if (result.status === "fulfilled") {
            console.log(result.value);
        } else {
            console.error(result.reason.message);
            // 실패한 송금에 대해 재시도 로직을 추가할 수 있습니다.
        }
    });
});

async/await와 Promise 중 무엇을 사용할까


async/await는 Promise를 더 쉽게 사용하기 위한 문법 설탕이다. 개인적으로 느끼기엔 병렬처리를 할 때는 Promise가 오히려 직관적이다.

const p1 = fetch('url1');
const p2 = fetch('url2');
const p3 = fetch('url3');

Promise.all([p1, p2, p3]).then((results) => {
  console.log(results); 
});

async/await를 사용해서 병렬처리를 할 때는 뭔가 빙 돌아가는 느낌이 든다.

async function fetchInParallel() {
  const p1 = fetch('url1');
  const p2 = fetch('url2');
  const p3 = fetch('url3');

  const [data1, data2, data3] = await Promise.all([p1, p2, p3]);
  console.log(data1, data2, data3);
}

async로 함수를 묶어주고 병렬처리할 때 Promise를 어차피 써야한다. 이럴꺼면 그냥 처음부터 Promise를 사용하는게 낫다고 생각된다.

Reference


https://www.youtube.com/watch?v=0f-jNhnN0Qc&list=PLcqDmjxt30Rt9wmSlw1u6sBYr-aZmpNB3&index=11
https://github.com/JaeYeopHan/Interview_Question_for_Beginner/tree/main/JavaScript#promise
https://velog.io/@seul06/JavaScript-콜백-지옥
https://programmingsummaries.tistory.com/325
모던 자바스크립트 Deep Dive
https://poiemaweb.com/es6-promise

profile
웹 개발자 고기호입니다.

0개의 댓글