본문 바로가기

비동기 프로그래밍

Promise의 상태와 체이닝, 병렬 조합을 익히고 async·await와 try·catch로 비동기 성공과 실패를 처리합니다.

우리는 6장 자바스크립트 심화 I에서 this, 실행 컨텍스트, 프로토타입처럼 내부 동작 원리를 학습했습니다.

또 객체 지향과 함수형 프로그래밍이라는 코드 구성 방식도 함께 다뤘습니다.

특히 마지막에서 비동기 처리의 출발점인 콜백 함수와 콜백 헬 문제를 확인했습니다.

이번 장은 바로 그 문제를 실무적으로 해결하는 단계입니다.

현대 웹 애플리케이션은 서버 통신, 파일 읽기, 타이머처럼 오래 걸리는 작업을 UI를 멈추지 않고 처리해야 합니다.

브라우저의 메인 실행 흐름에서는 한 번에 한 호출을 실행합니다. 그 흐름을 오래 점유하는 계산은 입력 처리와 화면 갱신을 지연시킵니다.

그래서 비동기 모델이 필요하고, 그 중심에 Promise와 async/await가 있습니다.

이 장에서 두 문법을 통해 비동기 코드를 읽기 쉽고 안전하게 작성하는 기준을 잡겠습니다.

앞 장의 타이머는 콜백을 등록한 호출이 반환한 뒤 실행되었습니다. 네트워크 대기도 브라우저 API가 진행하는 동안 현재 자바스크립트 코드가 계속 실행할 수 있습니다. 완료 결과를 다루는 콜백은 나중에 실행 기회를 얻습니다.

그러나 Promise로 감쌌다는 이유만으로 계산이 별도 스레드로 이동하지는 않습니다. new Promise의 executor와 각 결과 처리 함수도 실행 중에는 해당 실행 흐름을 점유합니다. 오래 걸리는 계산의 분할이나 Worker 사용은 별도 설계 문제입니다.

이전 장에서 콜백 함수를 통해 비동기 작업을 다루는 기초를 배웠지만, 복잡한 비동기 로직에서는 콜백 헬이라는 문제가 발생했습니다.

이제 이 문제를 깔끔하게 해결해주는 Promise와 async/await를 알아보겠습니다.

task와 Promise microtask 실행 순서 — 상태 추적

동기 코드/executor·then microtask·타이머 task에서 스택·대기 큐·출력 순서가 바뀐다. 타이머 준비/paint 조건을 명시한 큐 snapshot이 핵심이다.

task와 Promise microtask 실행 순서

SEQUENCE

task와 Promise microtask 실행 순서

현재 작업과 Promise executor는 동기로 실행된다. 예약된 Promise 반응은 다음 task보다 먼저 microtask checkpoint에서 처리된다. 브라우저의 그리기는 별도의 기회다.

현재 코드·Promise 반응·타이머 태스크현재 코드와 Promise executor는 동기로 실행한다. 타이머 등록은 API로, then 반응은 마이크로태스크 큐로 향한다. 현재 코드가 끝난 뒤 반응 큐를 처리한다. 타이머 준비 시점은 가변이며 준비된 작업은 태스크로 실행 기회를 얻는다. 모바일은 타이머 API와 태스크 예약 역할을 한 lifeline으로 묶어 같은 메시지 방향을 유지한다. 그리기는 반드시 실행되는 메시지가 아니라 별도 기회다. 현재 JS동기 호출타이머 API등록 · 대기MicrotaskPromise 반응Task queue타이머 작업executor 동기 실행타이머 등록then 반응 예약현재 코드 끝마이크로태스크 모두 처리타이머 준비 → 작업 예약실행 기회를 얻은 다음 태스크타이머 예약 시점은 가변 · 그리기는 별도 기회
현재 코드·Promise 반응·타이머 태스크현재 코드와 Promise executor는 동기로 실행한다. 타이머 등록은 API로, then 반응은 마이크로태스크 큐로 향한다. 현재 코드가 끝난 뒤 반응 큐를 처리한다. 타이머 준비 시점은 가변이며 준비된 작업은 태스크로 실행 기회를 얻는다. 모바일은 타이머 API와 태스크 예약 역할을 한 lifeline으로 묶어 같은 메시지 방향을 유지한다. 그리기는 반드시 실행되는 메시지가 아니라 별도 기회다.JS동기Promise반응 큐타이머API/taskexecutor는 동기타이머 등록then 반응 예약현재 코드 끝반응 큐 처리타이머 작업 준비다음 태스크 실행준비 시점은 가변그리기는 별도 기회API·태스크 역할 병합
  1. Current task

    script와 Promise executor가 call stack에서 동기로 실행된다.

  2. Host 등록

    setTimeout은 Host API에 timer를 등록하고 현재 task를 막지 않는다.

  3. Microtask 예약

    Promise가 fulfilled/rejected로 결정되면 해당 then 반응이 microtask queue에 예약된다. 이미 결정된 Promise에 then을 등록해도 동기 호출되지 않는다.

  4. Microtask checkpoint

    현재 task가 끝나면 queue가 비워질 때까지 microtask를 모두 실행한다.

  5. Render opportunity

    microtask drain 뒤 브라우저가 필요하면 layout·paint frame을 만든다.

  6. Next task

    Host completion은 task queue로 돌아오고 그 뒤 다음 task로 실행된다.

  • task 실행·전달
  • host·queue 예약
  • microtask checkpoint

Microtask는 다음 task 전에 처리

현재 작업이 끝난 뒤 microtask queue가 비워질 때까지 실행한다. 실행 중 추가된 microtask도 처리하므로, 이를 계속 추가하면 다음 task와 그리기가 지연될 수 있다.

타이머 완료의 큐 등록 시점과 그리기 여부는 이 예제의 순서로 항상 고정되지 않는다. timer 시간이 먼저 끝났더라도 현재 task와 microtask checkpoint를 끼어들어 실행하지 않는다. completion은 task queue에서 다음 차례를 기다린다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석Current task: script와 Promise executor가 call stack에서 동기로 실행된다.
원도표 조건·주석Host 등록: setTimeout은 Host API에 timer를 등록하고 현재 task를 막지 않는다.
원도표 조건·주석Microtask 예약: Promise가 fulfilled/rejected로 결정되면 해당 then 반응이 microtask queue에 예약된다. 이미 결정된 Promise에 then을 등록해도 동기 호출되지 않는다.
원도표 조건·주석Microtask checkpoint: 현재 task가 끝나면 queue가 비워질 때까지 microtask를 모두 실행한다.
원도표 조건·주석Render opportunity: microtask drain 뒤 브라우저가 필요하면 layout·paint frame을 만든다.
원도표 조건·주석Next task: Host completion은 task queue로 돌아오고 그 뒤 다음 task로 실행된다.
원도표 설명타이머 완료의 큐 등록 시점과 그리기 여부는 이 예제의 순서로 항상 고정되지 않는다. timer 시간이 먼저 끝났더라도 현재 task와 microtask checkpoint를 중단하고 실행하지 않는다. completion은 task queue에서 다음 차례를 기다린다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 동기 taskstack: 현재 동기 실행 / reaction: 실행 전일반 코드와 Promise executor를 실행하고 timer를 등록한다executor는 생성 중 바로 실행된다
microtask checkpointmicrotask: 현재 reaction / timer: 후속 task 준비 가능stack 뒤 Promise reaction을 실행한다반응은 executor와 별도 작업이다
timer task현재 task: timer callback / paint: 고정 순서 보장 아님호스트가 선택한 timer callback이 새 stack에서 실행된다0ms 요청은 즉시 동기 호출을 뜻하지 않는다

task와 Promise microtask 실행 순서 — 상태 추적

동기 코드/executor·then microtask·타이머 task에서 스택·대기 큐·출력 순서가 바뀐다. 타이머 준비/paint 조건을 명시한 큐 snapshot이 핵심이다.

task와 Promise microtask 실행 순서

SEQUENCE

task와 Promise microtask 실행 순서

현재 작업과 Promise executor는 동기로 실행된다. 예약된 Promise 반응은 다음 task보다 먼저 microtask checkpoint에서 처리된다. 브라우저의 그리기는 별도의 기회다.

현재 코드·Promise 반응·타이머 태스크현재 코드와 Promise executor는 동기로 실행한다. 타이머 등록은 API로, then 반응은 마이크로태스크 큐로 향한다. 현재 코드가 끝난 뒤 반응 큐를 처리한다. 타이머 준비 시점은 가변이며 준비된 작업은 태스크로 실행 기회를 얻는다. 모바일은 타이머 API와 태스크 예약 역할을 한 lifeline으로 묶어 같은 메시지 방향을 유지한다. 그리기는 반드시 실행되는 메시지가 아니라 별도 기회다. 현재 JS동기 호출타이머 API등록 · 대기MicrotaskPromise 반응Task queue타이머 작업executor 동기 실행타이머 등록then 반응 예약현재 코드 끝마이크로태스크 모두 처리타이머 준비 → 작업 예약실행 기회를 얻은 다음 태스크타이머 예약 시점은 가변 · 그리기는 별도 기회
현재 코드·Promise 반응·타이머 태스크현재 코드와 Promise executor는 동기로 실행한다. 타이머 등록은 API로, then 반응은 마이크로태스크 큐로 향한다. 현재 코드가 끝난 뒤 반응 큐를 처리한다. 타이머 준비 시점은 가변이며 준비된 작업은 태스크로 실행 기회를 얻는다. 모바일은 타이머 API와 태스크 예약 역할을 한 lifeline으로 묶어 같은 메시지 방향을 유지한다. 그리기는 반드시 실행되는 메시지가 아니라 별도 기회다.JS동기Promise반응 큐타이머API/taskexecutor는 동기타이머 등록then 반응 예약현재 코드 끝반응 큐 처리타이머 작업 준비다음 태스크 실행준비 시점은 가변그리기는 별도 기회API·태스크 역할 병합
  1. Current task

    script와 Promise executor가 call stack에서 동기로 실행된다.

  2. Host 등록

    setTimeout은 Host API에 timer를 등록하고 현재 task를 막지 않는다.

  3. Microtask 예약

    Promise가 fulfilled/rejected로 결정되면 해당 then 반응이 microtask queue에 예약된다. 이미 결정된 Promise에 then을 등록해도 동기 호출되지 않는다.

  4. Microtask checkpoint

    현재 task가 끝나면 queue가 비워질 때까지 microtask를 모두 실행한다.

  5. Render opportunity

    microtask drain 뒤 브라우저가 필요하면 layout·paint frame을 만든다.

  6. Next task

    Host completion은 task queue로 돌아오고 그 뒤 다음 task로 실행된다.

  • task 실행·전달
  • host·queue 예약
  • microtask checkpoint

Microtask는 다음 task 전에 처리

현재 작업이 끝난 뒤 microtask queue가 비워질 때까지 실행한다. 실행 중 추가된 microtask도 처리하므로, 이를 계속 추가하면 다음 task와 그리기가 지연될 수 있다.

타이머 완료의 큐 등록 시점과 그리기 여부는 이 예제의 순서로 항상 고정되지 않는다. timer 시간이 먼저 끝났더라도 현재 task와 microtask checkpoint를 끼어들어 실행하지 않는다. completion은 task queue에서 다음 차례를 기다린다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석Current task: script와 Promise executor가 call stack에서 동기로 실행된다.
원도표 조건·주석Host 등록: setTimeout은 Host API에 timer를 등록하고 현재 task를 막지 않는다.
원도표 조건·주석Microtask 예약: Promise가 fulfilled/rejected로 결정되면 해당 then 반응이 microtask queue에 예약된다. 이미 결정된 Promise에 then을 등록해도 동기 호출되지 않는다.
원도표 조건·주석Microtask checkpoint: 현재 task가 끝나면 queue가 비워질 때까지 microtask를 모두 실행한다.
원도표 조건·주석Render opportunity: microtask drain 뒤 브라우저가 필요하면 layout·paint frame을 만든다.
원도표 조건·주석Next task: Host completion은 task queue로 돌아오고 그 뒤 다음 task로 실행된다.
원도표 설명타이머 완료의 큐 등록 시점과 그리기 여부는 이 예제의 순서로 항상 고정되지 않는다. timer 시간이 먼저 끝났더라도 현재 task와 microtask checkpoint를 중단하고 실행하지 않는다. completion은 task queue에서 다음 차례를 기다린다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 동기 taskstack: 현재 동기 실행 / reaction: 실행 전일반 코드와 Promise executor를 실행하고 timer를 등록한다executor는 생성 중 바로 실행된다
microtask checkpointmicrotask: 현재 reaction / timer: 후속 task 준비 가능stack 뒤 Promise reaction을 실행한다반응은 executor와 별도 작업이다
timer task현재 task: timer callback / paint: 고정 순서 보장 아님호스트가 선택한 timer callback이 새 stack에서 실행된다0ms 요청은 즉시 동기 호출을 뜻하지 않는다

Promise (프로미스): 비동기 작업의 미래 값

Promise(프로미스)는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다.

비동기 작업이 "성공적으로 완료될 것인지", 아니면 "실패할 것인지"에 대한 약속 또는 미래의 값을 표현합니다.

콜백 헬을 해결하고 비동기 코드를 더 선형적이고 가독성 좋게 작성할 수 있도록 도입되었습니다.

Promise의 3가지 상태

Promise 객체는 다음 세 가지 상태 중 하나를 가집니다.

pending (대기): 비동기 작업이 아직 완료되지 않은 초기 상태.

fulfilled (이행): 비동기 작업이 성공적으로 완료되어 결과 값을 반환한 상태.

rejected (거부): 비동기 작업이 실패하여 오류 값을 반환한 상태.

pending 상태에서 fulfilled 또는 rejected 상태로 한 번만 전이(settled)되며, 일단 settled 상태가 되면 더 이상 상태가 변하지 않습니다.

Promise 생성하기

Promise 객체는 new Promise() 생성자를 통해 생성합니다.

생성자는 resolve와 reject 두 개의 인자를 받는 executor 함수를 받습니다.

  • resolve(value): 보통의 값을 전달하면 그 값으로 이행합니다. 다른 Promise나 thenable을 전달하면 그 결과를 따르므로, resolve 호출 직후에도 대기 중이거나 나중에 거부될 수 있습니다. “resolved”와 “fulfilled”를 항상 같은 상태로 보지는 않습니다.
  • reject(error): 비동기 작업이 실패했을 때 호출하여 Promise를 rejected 상태로 만들고 error를 전달합니다.
function fetchData() {
    return new Promise((resolve, reject) => {
        // 비동기 작업 시뮬레이션 (예: 서버에서 데이터 가져오기)
        const success = Math.random() > 0.5; // 50% 확률로 성공/실패

        setTimeout(() => {
            if (success) {
                resolve("데이터를 성공적으로 가져왔습니다!"); // 성공 시 resolve 호출
            } else {
                reject("데이터 가져오기 실패!"); // 실패 시 reject 호출
            }
        }, 1000); // 1초 뒤에 결과 결정
    });
}

// Promise 사용 예시
fetchData()
    .then(message => { // Promise가 fulfilled(이행) 상태가 되면 .then() 블록이 실행됩니다.
        console.log("성공:", message);
    })
    .catch(error => { // Promise가 rejected(거부) 상태가 되면 .catch() 블록이 실행됩니다.
        console.error("오류:", error);
    })
    .finally(() => { // Promise의 성공/실패 여부와 상관없이 항상 실행됩니다.
        console.log("작업 완료!");
    });

console.log("Promise 작업 시작 (비동기)"); // 이 메시지가 먼저 출력됩니다.

위 예시는 Math.random() 때문에 실행마다 성공·실패가 달라집니다. 먼저 Promise 작업 시작 (비동기)가 나오고, 성공이면 성공 메시지와 작업 완료!, 실패이면 오류 메시지와 작업 완료!가 이어집니다. 타이머의 1000은 최소 지연 요청입니다. 원래 주석의 “항상”은 Promise가 이행 또는 거부된 경우를 뜻합니다. 영원히 대기하면 finally도 실행되지 않으며, finally 함수가 오류를 던지면 이어지는 Promise는 그 오류로 거부될 수 있습니다.

예시

새 콘솔에서 아래 로그의 순서를 먼저 예상하세요. executor는 언제 호출되고, 이미 이행한 Promise의 then은 언제 실행될까요?

console.log("A 시작");
const orderForPromise = new Promise(resolve => {
    console.log("B executor");
    resolve(7);
});
orderForPromise.then(value => console.log("D then", value));
setTimeout(() => console.log("E 타이머"), 0);
console.log("C 현재 코드 끝");

출력은 A 시작 → B executor → C 현재 코드 끝 → D then 7 → E 타이머입니다. executor는 생성 중 동기적으로 호출됩니다. then의 반응은 현재 코드가 끝난 뒤 마이크로태스크로 실행되고, 이 예시의 타이머 태스크보다 먼저 처리됩니다. 이는 이 코드의 순서이며 여러 종류의 브라우저 태스크를 하나의 전역 FIFO 큐로 일반화하는 규칙은 아닙니다.

Promise 체이닝 (.then())

Promise의 가장 큰 장점 중 하나는 .then() 메서드를 통해 여러 비동기 작업을 순차적으로 연결(체이닝)할 수 있다는 것입니다.

각 .then()은 이전 Promise의 결과 값을 받아 다음 작업을 수행합니다.

function step1Promise() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("Step 1 완료");
            resolve("Step1 결과"); // 다음 .then()으로 전달될 값
        }, 1000);
    });
}

function step2Promise(prevResult) {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`Step 2 완료: 이전 결과는 ${prevResult}`);
            resolve("Step2 결과");
        }, 1000);
    });
}

function step3Promise(prevResult) {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`Step 3 완료: 이전 결과는 ${prevResult}`);
            resolve("모든 단계 완료!");
        }, 1000);
    });
}

// Promise 체이닝으로 콜백 헬 해결
step1Promise()
    .then(result1 => step2Promise(result1)) // Step1 결과 -> Step2로 전달
    .then(result2 => step3Promise(result2)) // Step2 결과 -> Step3로 전달
    .then(finalResult => {
        console.log(finalResult); // 최종 결과 출력
    })
    .catch(error => { // 체인 중 어느 한 곳에서라도 오류가 발생하면 catch로 이동
        console.error("오류 발생:", error);
    });

console.log("Promise 체인 시작!"); // 이 메시지가 가장 먼저 출력됩니다.

.then() 메서드는 항상 새로운 Promise를 반환하므로, 이를 통해 계속해서 .then()을 연결할 수 있습니다.

성공 처리 함수가 Promise를 반환하면 이어지는 Promise는 그 결과를 따릅니다. 이행하면 다음 성공 처리 함수로 값이 전달되고, 거부하거나 오류를 던지면 다음 거부 처리 함수로 전달됩니다. 위 result1 => step2Promise(result1)의 반환이 이 연결을 만듭니다. 중괄호를 붙인 뒤 return을 빠뜨리면 undefined를 반환하여 다음 단계가 그 작업을 기다리지 않게 될 수 있습니다.


Promise.all(), Promise.race()

Promise 조합과 실패 경로 — 상태 추적

본문 race 예제의 pB 1000ms 이행, pC 2000ms 거부, pA 3000ms 이행에서 조합은 B 완료를 유지하고 나머지 작업도 계속 정착한다. 실제 타이머의 값/결과/미취소 상태를 보여 주고 전체 정책 표는 보존할 가치가 있다.

Promise 조합과 실패 경로

FLOWCHART · WEB PLATFORM CONTRACT

Promise 조합과 실패 경로

모든 작업을 기다릴지, 가장 이른 결과를 받을지 정한다. 실패를 다루는 규칙에 따라 조합 메서드가 달라지며, 작업 취소는 별도로 구현한다.

Promise 조합의 분기와 취소 경계모든 결과 경로는 모든 성공이 필요하면 all, 실패도 기록하면 allSettled로 끝난다. 첫 결과 경로는 첫 결정이면 race, 첫 성공이면 any로 끝난다. all과 allSettled의 배열은 입력 순서이고, any에서 모두 거부되면 AggregateError다. 모바일은 두 경로를 세로로 배치하며 각 결과는 끝난다. 취소는 별도 계약이며 조합 메서드가 자동으로 제공하지 않는다.결과 조합의 목적모두 필요 / 첫 결과 필요모든 성공이 필요한가?아니면 성공·실패 모두 기록첫 결정인가, 첫 성공인가?실패를 첫 결과로 쓸지 확인모든 결과첫 결과Promise.all모두 이행 → 값 배열하나 거부 → 조합 거부allSettled모두 결정 → 상태 배열성공·실패 개별 기록Promise.race첫 이행 또는 거부첫 결정에 따름Promise.any첫 이행 값모두 거부 → AggregateError성공만모두 기록첫 결정첫 성공결과가 정해져도 나머지 작업은 자동 취소되지 않음작업 취소는 별도 API 계약해제 가능한 타이머 ID · signal을 지원하는 요청
Promise 조합의 분기와 취소 경계모든 결과 경로는 모든 성공이 필요하면 all, 실패도 기록하면 allSettled로 끝난다. 첫 결과 경로는 첫 결정이면 race, 첫 성공이면 any로 끝난다. all과 allSettled의 배열은 입력 순서이고, any에서 모두 거부되면 AggregateError다. 모바일은 두 경로를 세로로 배치하며 각 결과는 끝난다. 취소는 별도 계약이며 조합 메서드가 자동으로 제공하지 않는다.모든 결과 필요?예 / 아니오로 분기실패 즉시 거부?모두 성공 필요: 예Promise.all입력 순서의 값하나 거부: 조합 거부allSettled모든 상태 기록입력 순서 유지첫 성공 필요?첫 결과 경로Promise.any첫 성공의 값모두 거부: 오류Promise.race첫 이행 또는 거부첫 결정에 따름예예아니오 → 모두 기록모두 필요 없으면예아니오 → 첫 결정어느 경로도 자동 취소 없음취소는 작업별 계약타이머 ID 해제지원 API에 signal 전달

Promise.all

입력 순서의 모든 fulfilled 값이 필요하고 하나의 reject가 전체 실패일 때. 나머지 작업을 자동 취소하지는 않는다.

Promise.allSettled

입력 순서로 모든 작업의 fulfilled/rejected status와 value/reason을 모아 부분 실패를 각각 처리할 때

Promise.race

fulfilled 또는 rejected를 가리지 않고 첫 settled 결과가 필요할 때

Promise.any

첫 fulfilled가 필요할 때. 모두 reject면 AggregateError다.

조합 결과 결정 ≠ 나머지 작업 취소

all의 실패나 race·any의 결과가 정해져도 나머지 작업은 자동으로 중단되지 않는다. AbortController의 signal을 이를 지원하는 각 작업에 전달해 취소를 설계한다.

all·allSettled·race·any는 완료와 실패를 모으는 규칙이다. 실제 I/O 중단은 AbortSignal을 이해하는 작업이 협력해야 한다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
세 작업 시작A: 3000ms 대기 / B/C: 1000/2000ms 대기pA/pB/pC timer가 모두 등록되고 race는 pending이다race가 작업을 새로 시작하는 것은 아니다
B 먼저 이행race: B 완료 / A/C: 계속 대기B 완료로 race가 fulfilled된다첫 settlement 뒤 남은 작업은 자동 취소되지 않는다
나머지 정착race: B 완료 유지 / C/A: 거부/이행 완료C 실패와 A 완료 뒤에도 race 결과는 B다조합 결과와 원 작업들의 상태는 서로 다르다

Promise 조합과 실패 경로 — 상태 추적

본문 race 예제의 pB 1000ms 이행, pC 2000ms 거부, pA 3000ms 이행에서 조합은 B 완료를 유지하고 나머지 작업도 계속 정착한다. 실제 타이머의 값/결과/미취소 상태를 보여 주고 전체 정책 표는 보존할 가치가 있다.

Promise 조합과 실패 경로

FLOWCHART · WEB PLATFORM CONTRACT

Promise 조합과 실패 경로

모든 작업을 기다릴지, 가장 이른 결과를 받을지 정한다. 실패를 다루는 규칙에 따라 조합 메서드가 달라지며, 작업 취소는 별도로 구현한다.

Promise 조합의 분기와 취소 경계모든 결과 경로는 모든 성공이 필요하면 all, 실패도 기록하면 allSettled로 끝난다. 첫 결과 경로는 첫 결정이면 race, 첫 성공이면 any로 끝난다. all과 allSettled의 배열은 입력 순서이고, any에서 모두 거부되면 AggregateError다. 모바일은 두 경로를 세로로 배치하며 각 결과는 끝난다. 취소는 별도 계약이며 조합 메서드가 자동으로 제공하지 않는다.결과 조합의 목적모두 필요 / 첫 결과 필요모든 성공이 필요한가?아니면 성공·실패 모두 기록첫 결정인가, 첫 성공인가?실패를 첫 결과로 쓸지 확인모든 결과첫 결과Promise.all모두 이행 → 값 배열하나 거부 → 조합 거부allSettled모두 결정 → 상태 배열성공·실패 개별 기록Promise.race첫 이행 또는 거부첫 결정에 따름Promise.any첫 이행 값모두 거부 → AggregateError성공만모두 기록첫 결정첫 성공결과가 정해져도 나머지 작업은 자동 취소되지 않음작업 취소는 별도 API 계약해제 가능한 타이머 ID · signal을 지원하는 요청
Promise 조합의 분기와 취소 경계모든 결과 경로는 모든 성공이 필요하면 all, 실패도 기록하면 allSettled로 끝난다. 첫 결과 경로는 첫 결정이면 race, 첫 성공이면 any로 끝난다. all과 allSettled의 배열은 입력 순서이고, any에서 모두 거부되면 AggregateError다. 모바일은 두 경로를 세로로 배치하며 각 결과는 끝난다. 취소는 별도 계약이며 조합 메서드가 자동으로 제공하지 않는다.모든 결과 필요?예 / 아니오로 분기실패 즉시 거부?모두 성공 필요: 예Promise.all입력 순서의 값하나 거부: 조합 거부allSettled모든 상태 기록입력 순서 유지첫 성공 필요?첫 결과 경로Promise.any첫 성공의 값모두 거부: 오류Promise.race첫 이행 또는 거부첫 결정에 따름예예아니오 → 모두 기록모두 필요 없으면예아니오 → 첫 결정어느 경로도 자동 취소 없음취소는 작업별 계약타이머 ID 해제지원 API에 signal 전달

Promise.all

입력 순서의 모든 fulfilled 값이 필요하고 하나의 reject가 전체 실패일 때. 나머지 작업을 자동 취소하지는 않는다.

Promise.allSettled

입력 순서로 모든 작업의 fulfilled/rejected status와 value/reason을 모아 부분 실패를 각각 처리할 때

Promise.race

fulfilled 또는 rejected를 가리지 않고 첫 settled 결과가 필요할 때

Promise.any

첫 fulfilled가 필요할 때. 모두 reject면 AggregateError다.

조합 결과 결정 ≠ 나머지 작업 취소

all의 실패나 race·any의 결과가 정해져도 나머지 작업은 자동으로 중단되지 않는다. AbortController의 signal을 이를 지원하는 각 작업에 전달해 취소를 설계한다.

all·allSettled·race·any는 완료와 실패를 모으는 규칙이다. 실제 I/O 중단은 AbortSignal을 이해하는 작업이 협력해야 한다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
세 작업 시작A: 3000ms 대기 / B/C: 1000/2000ms 대기pA/pB/pC timer가 모두 등록되고 race는 pending이다race가 작업을 새로 시작하는 것은 아니다
B 먼저 이행race: B 완료 / A/C: 계속 대기B 완료로 race가 fulfilled된다첫 settlement 뒤 남은 작업은 자동 취소되지 않는다
나머지 정착race: B 완료 유지 / C/A: 거부/이행 완료C 실패와 A 완료 뒤에도 race 결과는 B다조합 결과와 원 작업들의 상태는 서로 다르다

여러 Promise의 결과 정책을 정하는 메서드들입니다. 앞에서 만든 Promise들의 작업을 새로 시작해 주는 메서드는 아닙니다. 아래 세 타이머는 각 new Promise가 실행될 때 이미 등록됩니다.

  • Promise.all([promise1, promise2, ...])
    • 모든 Promise가 성공적으로 완료될 때까지 기다립니다.
    • 모든 Promise가 fulfilled되면, 결과 배열은 입력 배열 순서를 유지합니다. 먼저 끝나는 p2가 첫 원소로 이동하지 않습니다.
    • 하나라도 rejected되면 조합 Promise도 해당 오류로 reject됩니다. 나머지 작업은 자동으로 취소되지 않습니다. (fail-fast)
    const p1 = new Promise(resolve => setTimeout(() => resolve("P1 완료"), 3000));
    const p2 = new Promise(resolve => setTimeout(() => resolve("P2 완료"), 1000));
    const p3 = new Promise(resolve => setTimeout(() => resolve("P3 완료"), 2000));
    Promise.all([p1, p2, p3])
        .then(results => {
            console.log("모두 완료:", results); // 결과: ["P1 완료", "P2 완료", "P3 완료"] (3초 후)
        })
        .catch(error => {
            console.error("하나라도 실패:", error);
        });
    // 실패 예시
    const p4 = Promise.reject("P4 실패");
    Promise.all([p1, p4, p3]) // p4가 즉시 실패하므로, "P4 실패"가 출력
        .then(results => console.log("모두 완료:", results))
        .catch(error => console.error("하나라도 실패:", error)); // 결과: 하나라도 실패: P4 실패 (즉시)
  • Promise.race([promise1, promise2, ...])
    • 가장 먼저 fulfilled 또는 rejected 되는 Promise의 결과(또는 오류)를 반환합니다.
    • 말 그대로 여러 경주마 중 가장 먼저 결승선에 도착하는 말을 따릅니다.
    const pA = new Promise(resolve => setTimeout(() => resolve("A 완료"), 3000));
    const pB = new Promise(resolve => setTimeout(() => resolve("B 완료"), 1000));
    const pC = new Promise((resolve, reject) => setTimeout(() => reject("C 실패"), 2000));
    Promise.race([pA, pB, pC])
        .then(result => {
            console.log("가장 빠른 결과:", result); // 이 타이머 예제에서는 B 완료 (지연은 최소 요청값)
        })
        .catch(error => {
            console.error("가장 빠른 오류:", error);
        });
    // 같은 Promise들의 배열 순서만 바꿔도, 아직 pending인 작업의 완료 순서는 바뀌지 않음
    Promise.race([pA, pC, pB])
        .then(result => console.log("가장 빠른 결과:", result))
        .catch(error => console.error("가장 빠른 오류:", error)); // 위와 같은 타이머 예제에서는 가장 빠른 결과: B 완료

조합이 먼저 거부되거나 결정되어도 나머지 작업이 자동 취소되지는 않습니다. 타이머를 중단하려면 타이머 ID로 해제하고, 취소를 지원하는 요청에는 해당 API의 취소 방법을 사용해야 합니다.

도식에 함께 있는 두 메서드는 요구가 다른 경우의 비교입니다.

요구조합 메서드결과
성공·실패를 모두 기록한 뒤 보고Promise.allSettled()입력 순서의 상태·값 또는 이유 배열
실패한 작업은 넘기고 첫 성공 사용Promise.any()첫 이행 값; 모두 거부되면 AggregateError

race는 첫 결정에 따르므로 첫 실패도 결과가 됩니다. any는 첫 성공을 기다린다는 차이를 설명할 수 있어야 합니다.

async/await: 비동기 코드를 동기 코드처럼

async/await는 ES2017(ES8)에서 도입된 문법으로, Promise 기반 비동기 코드를 동기 코드처럼 읽고 쓸 수 있게 해주는 기능입니다.

콜백 헬뿐만 아니라 Promise 체이닝의 복잡성마저도 크게 줄여줍니다.

async 함수

  • 함수 앞에 async 키워드를 붙이면, 해당 함수는 항상 Promise를 반환합니다.
  • async 함수 내부에서 일반 값을 return하면, 그 값은 resolve된 Promise로 감싸져 반환됩니다.
  • async 함수 내부에서 오류를 throw하면, 그 오류는 reject된 Promise로 감싸져 반환됩니다.
async function getGreeting() {
    return "안녕하세요!"; // Promise.resolve("안녕하세요!")와 동일
}

getGreeting().then(message => console.log(message)); // 결과: 안녕하세요!

async function getError() {
    throw new Error("오류 발생!"); // Promise.reject(new Error("오류 발생!"))와 동일
}

getError().catch(error => console.error(error.message)); // 결과: 오류 발생!

await 키워드

  • await는 async 함수 내부에서 사용할 수 있고, ES module의 최상위에서도 사용할 수 있습니다.
  • await는 Promise·thenable·일반 값에 사용할 수 있습니다. 결과를 기다리는 동안 해당 함수의 남은 부분을 미루며, 일반 값이나 이미 이행한 Promise여도 이어지는 부분은 현재 동기 코드 사이에 즉시 끼어들지 않습니다.
  • Promise가 fulfilled되면, await 표현식은 Promise의 결과 값을 반환합니다.
  • Promise가 rejected되면, await는 해당 오류를 던지며, 이는 try...catch 블록으로 잡을 수 있습니다.
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function processOrder() {
    console.log("주문 처리 시작...");

    try {
        // 1. 첫 번째 비동기 작업 대기
        await delay(1000); // 1초 대기
        console.log("Step 1: 주문 접수 완료");

        // 2. 두 번째 비동기 작업 대기
        await delay(1500); // 1.5초 대기
        console.log("Step 2: 상품 준비 완료");

        // 3. 세 번째 비동기 작업 대기 (오류 발생 가능성)
        const randomNumber = Math.random();
        if (randomNumber < 0.3) {
            throw new Error("배송 중 문제 발생!");
        }
        await delay(1000); // 1초 대기
        console.log("Step 3: 배송 시작");

        // 최종 결과 반환
        return "주문이 성공적으로 완료되었습니다!";

    } catch (error) {
        console.error("주문 처리 중 오류 발생:", error.message);
        throw error; // 오류를 외부로 다시 던져서 .catch()로 처리되도록 함
    } finally {
        console.log("주문 처리 과정 종료.");
    }
}

// async 함수 호출 및 결과 처리
processOrder()
    .then(message => console.log("최종 결과:", message))
    .catch(err => console.error("최종 실패:", err.message));

console.log("프로세스 시작 명령 (비동기)"); // "주문 처리 시작..." 다음, 첫 await로 중단된 뒤 출력됩니다.

async/await를 사용하면 비동기 흐름을 마치 위에서 아래로 읽는 동기 코드처럼 작성할 수 있어, 코드의 가독성과 유지보수성이 크게 향상됩니다.

try...catch 블록을 사용하여 비동기 작업에서 발생하는 오류를 쉽게 처리할 수 있다는 장점도 있습니다.

await는 전체 자바스크립트 실행을 멈추는 것이 아니라, 해당 async 함수의 남은 부분만 잠시 미룹니다.

호출자는 Promise를 받은 뒤 다음 코드를 계속 실행하고, 기다리던 Promise가 settled되면 async 함수가 이어서 재개됩니다.


실패에서 회복할지 결정하기

아래 코드는 네트워크나 난수 없이 실패 경로를 확인합니다. 새 콘솔에서 실행하기 전에 검사, 회복, 최종이 어떤 순서로 출력되는지 적어 보세요.

Promise.resolve(4)
    .then(value => {
        console.log("검사", value);
        if (value < 5) throw new Error("최솟값 미달");
        return value * 2;
    })
    .catch(error => {
        console.log("회복", error.message);
        return 0;
    })
    .then(value => console.log("최종", value))
    .catch(error => console.log("마지막 오류", error.message));

출력은 검사 4, 회복 최솟값 미달, 최종 0입니다. 첫 성공 처리 함수의 오류가 catch로 전달됩니다. 그 catch는 일반 값 0을 반환해 체인을 성공 상태로 이어 줍니다. “오류를 출력했다”는 사실만으로 실패가 계속 전파되는 것은 아닙니다.

독립 연습

위 첫 catch의 return 0;만 throw error;로 바꾸세요. 나머지 줄은 그대로 둡니다. 최종은 출력되는지, 어느 함수가 같은 오류를 받는지 예측하고 실행으로 확인하세요.

회복을 제거한 결과

검사 4, 회복 최솟값 미달, 마지막 오류 최솟값 미달이 출력됩니다. 다시 던진 오류가 이어지는 Promise를 거부하므로 성공 처리 함수 최종은 건너뜁니다. 마지막 catch가 받는 오류는 첫 검사에서 만든 오류입니다.

processOrder도 내부 catch에서 다시 던지므로 외부의 최종 실패 처리로 이어집니다. 성공·실패 두 경우 모두 내부 finally 뒤에 외부 결과 처리가 실행됩니다. 취소 기능은 이 예시에 구현되어 있지 않습니다.

근거: MDN Promise의 상태·체이닝·조합.

비동기 처리 정리

정리 포인트는 Promise 체이닝과 async/await를 상황별로 구분해 오류 전파를 일관되게 다루는 것입니다.

이번 장에서는 자바스크립트의 비동기 프로그래밍을 위한 핵심 개념인 Promise와 async/await에 대해 깊이 있게 학습했습니다.

Promise는 비동기 작업의 결과를 pending, fulfilled, rejected 상태로 표현합니다.

.then(), .catch(), .finally()와 Promise.all(), Promise.race()를 함께 보면 순차 처리와 병렬 처리의 차이를 구분할 수 있습니다.

나아가, async/await 문법을 통해 Promise 기반의 비동기 코드를 마치 동기 코드처럼 직관적으로 작성할 수 있게 되었고, try...catch를 이용한 오류 처리 방법도 살펴보았습니다.

이러한 비동기 프로그래밍 기법은 네트워크 요청, 타이머, 사용자 이벤트처럼 완료 시점이 늦게 정해지는 작업을 다룰 때 필요합니다.

Promise와 async/await를 함께 연습하면서 성공 경로, 오류 회복, 다시 던지기를 구분하세요. 취소가 필요하면 사용하는 API의 취소 계약을 별도로 확인해야 합니다.

현재 코드와 결과 처리의 순서를 먼저 쓰고, 그다음에는 “성공 값인가, 오류인가”, “한 결과면 충분한가, 모두 필요한가”를 확인하세요. 비동기 문법의 모양만으로 계산이 병렬 실행되거나 취소된다고 판단하지 않습니다.