고양의 성장일기

[Javascript] 자바스크립트 비동기 3부작 <약속의 Promise> 본문

🖥️ Front-End/Javascript

[Javascript] 자바스크립트 비동기 3부작 <약속의 Promise>

고 양 2025. 12. 15. 22:51
반응형
자바스크립트 비동기 3부작 <약속의 Promise>

자바스크립트는 기본적으로 단일 스레드를 가지고 동작하기 때문에 시간이 오래 걸리는 작업을 동기적으로 처리하면 종종 화면이 멈추는 문제가 발생합니다.

이 문제를 해결하기 위해 자바스크립트는 비동기 처리 방식을 제공하는데,

비동기를 처리하는 여러 가지 방법 중 하나가 이번 글에서 알아볼 Promise입니다.

Promise가 뭐죵

지금 당장은 결과를 알 수 없지만 나중에 언젠가 성공 또는 실패의 결과를 반환할 작업을 표현하는 객체입니다.

Promise는 작업의 상태(State)와 결과(result)를 관리하며 항상 아래 3가지 상태 중 하나를 가지게 됩니다.

  • Pending : 아직 처리중이유
  • fulfilled : 성공했슈
  • rejected : 실패했슈

약속 잡기 Promise 생성하기

Promise 객체를 생성하고 변수에 할당하는 방식으로 새로운 Promise를 생성할 수 있습니다.

const promise = new Promise((resolve, reject) => {
    // 비동기 작업 수행
    if (성공조건) {
        resolve(결과값);
    } else {
        reject(에러);
    }
});

Promise가 작업을 완료하면 성공, 실패 결과에 따라 매개변수로 받은 콜백을 실행합니다.

Promise 결과 처리하기

여기까지 봤을 땐 예전에 사용하던 콜백 방식과 큰 차이가 없는 것 같습니다.

하지만 Promise의 진가는 여러 개의 콜백을 처리하거나 예외처리를 해야 할 때 빛이 납니다.

예외처리하기

Promise의 결과는 메서드 체이닝 방식으로 처리됩니다.

promise
    .then(result => {
        console.log('성공:', result);
    })
    .catch(error => {
        console.error('실패:', error);
    })
    .finally(() => {
        console.log('항상 실행');
    });
  • then()
    → resolve 되었을 때 실행
  • catch()
    → reject 되었을 때 실행
  • finally()
    → 성공/실패와 관계없이 항상 실행

여러 작업 체이닝 하기

또 then()은 새로운 Promise를 반환하기 때문에, 성공 여부에 따른 연속 처리가 가능합니다.

비동기 작업을 순차적으로 처리할 수 있게 해 주고 이른바 callback hell을 방지할 수 있는 것이죠.

fetch('/api/data')
    .then(res => res.json())
    .then(data => {
        return data.value;
    })
    .then(value => {
        console.log(value);
    })
    .catch(error => {
        console.error(error);
    });

fetch API 또한 Promise 객체를 반환하기 때문에 메서드 체이닝을 통해 여러 가지 후속 작업과 예외 처리가 가능합니다.

Promise 좀 더 깊게 알아보기

자바스크립트의 Promise는 단순히 예외처리를 편하게 하기 위한 개념이 아닙니다.

Promise 객체가 제공하는 메서드들을 응용하면 여러 비동기 객체를 조합, 제어, 생성, 정규화하여 여러 가지 상황에 대처할 수 있습니다.

1. 여러 Promise 조합하기

1-1. Promise.all()

매개변수로 받은 모든 Promise가 성공해야만 성공입니다.

Promise.all([p1, p2, p3])
    .then(results => {
        console.log(results); // [result1, result2, result3]
    })
    .catch(error => {
        console.error(error);
    });

하나라도 reject 되면 즉시 reject를 반환하며

결과는 입력 순서 그대로 배열로 반환됩니다.

병열 처리 후 전체적인 결과가 필요할 때 사용됩니다.

1-2. Promise.allSettled()

성공, 실패 여부와 관계없이 모두 완료되면 각각의 Promise에 대한 결과를 반환합니다.

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

결과는 아래와 같이 구분되어 반환됩니다.

[
  { status: 'fulfilled', value: ... },
  { status: 'rejected', reason: ... }
]

요청이 실패해도 전체 결과를 받을 수 있으며 catch가 아닌 then에서 처리됩니다.

일부 실패가 허용되는 작업에서 사용하거나 다량의 요청을 보낼 때 서버의 과부하를 방지할 수 있습니다.

 

만약 Promise.all()을 사용해 여러 개의 fetch요청을 보냈고 하나라도 실패할 경우 모든 요청을 다시 보내야 한다고 가정하면,

서버는 "아니 실패한 거만 따로 떼서 보내든가 왜 다 다시보내지"라고 생각할 수도 있겠죠.

반대로 클라이언트에서도 "아니 실패한거만 다시 처리를 하든가 왜 전부 다 결재를 올리래"라고 생각할 수도 있습니다.

 

그럴 때 rejected의 Promise만 추출해서 재요청하면 서버도 과부하를 덜고 클라이언트에서도 좀 더 안정적으로 요청을 관리할 수 있게 됩니다.

1-3. Promise.race()

가장 먼저 응답받은 Promise의 결과를 사용합니다.

Promise.race([p1, p2])
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error);
    });

성공/실패 여부는 중요하지 않으며 가장 빠른 Promise 하나만 반영됩니다.

타임아웃 처리를 하거나 여러 개의 요청 중 신속한 응답을 필요로 할 때 사용됩니다.

1-4. Promise.any()

가장 먼저 성공한 Promise 하나만 반환합니다.

reca()가 가장 먼저 성공, 실패 여부와 관계없이 "하지만 빨랐죠?" 드립을 날리는 메서드라면 any() 메서드는 성공을 보장합니다.

Promise.any([p1, p2])
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error); // 모든 Promise가 실패했을 때
    });

여러 대체 수단 중 하나만 성공하면 되는 경우 사용합니다.

2. Promise를 생성·정규화하는 메서드

2-1. Promise.resolve()

값을 즉시 성공한 Promise로 반환합니다.

Promise.resolve(10)
    .then(value => {
        console.log(value); // 10
    });

Promise가 아닌 값도 Promise로 통일할 수 있기 때문에 함수가 Promise를 반환하도록 인터페이스를 정리하거나

동기/비동기로 혼합된 코드를 정리할 때 유용합니다.

2-2. Promise.reject()

resolve() 메서드와 달리 즉시 실패한 Promise를 생성합니다.

Promise.reject(new Error('실패'))
    .catch(error => {
        console.error(error);
    });

테스트나 에러 흐름 강제 분기 시 사용하면 좋습니다.

3. Promise 결과를 처리하는 인스턴스 메서드

3-1. then()

Promise 성공 시 실행됩니다.

promise.then(result => {
    return result + 1;
});

새로운 Promise를 반환하기 때문에 then() 체이닝이 가능합니다.

이 경우 return 된 값은 다음번 then()으로 전달됩니다.

3-2. catch()

Promise가 실패했을 경우 실행됩니다.

promise.catch(error => {
    console.error("예외 발생: ", error);
});

catch() 블록은 체이닝 중 한 번만 포함되면 충분하며, 어느 단계의 에러든 처리가 가능합니다.

3-3. finally()

성공/실패와 무관하게 언제나 실행되는 블록입니다.

promise.finally(() => {
    console.log('정리 작업');
});

결과값을 변경하지 않기 때문에 로딩을 종료하거나 리소스를 해제하는 등의 작업에 적합합니다.

마치며

Promise는 단순한 하나의 비동기 객체가 아니라 비동기의 전체적인 흐름을 제어하기 위한 하나의 도구로 보는 것이 좋습니다.

위 정리된 메서드들을 정확히 이해하면

  • 복잡한 비동기 API 흐름 정리
  • 실패와 성공 전략을 명확히 구분하여 설계
  • 다음에 나올 async/await 구조에서도 내부 동작을 이해할 수 있는

능력치를 가지게 됩니다.

 

긴 글 읽어주셔서 감사합니다. 다음 번 글로는 async / await로 찾아오겠습니다.

반응형