비동기 프로그래밍
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이 핵심이다.
SEQUENCE
task와 Promise microtask 실행 순서
현재 작업과 Promise executor는 동기로 실행된다. 예약된 Promise 반응은 다음 task보다 먼저 microtask checkpoint에서 처리된다. 브라우저의 그리기는 별도의 기회다.
- 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로 실행된다.
- 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에서 다음 차례를 기다린다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 현재 동기 task | stack: 현재 동기 실행 / reaction: 실행 전 | 일반 코드와 Promise executor를 실행하고 timer를 등록한다 | executor는 생성 중 바로 실행된다 |
| microtask checkpoint | microtask: 현재 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이 핵심이다.
SEQUENCE
task와 Promise microtask 실행 순서
현재 작업과 Promise executor는 동기로 실행된다. 예약된 Promise 반응은 다음 task보다 먼저 microtask checkpoint에서 처리된다. 브라우저의 그리기는 별도의 기회다.
- 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로 실행된다.
- 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에서 다음 차례를 기다린다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 현재 동기 task | stack: 현재 동기 실행 / reaction: 실행 전 | 일반 코드와 Promise executor를 실행하고 timer를 등록한다 | executor는 생성 중 바로 실행된다 |
| microtask checkpoint | microtask: 현재 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 완료를 유지하고 나머지 작업도 계속 정착한다. 실제 타이머의 값/결과/미취소 상태를 보여 주고 전체 정책 표는 보존할 가치가 있다.
FLOWCHART · WEB PLATFORM CONTRACT
Promise 조합과 실패 경로
모든 작업을 기다릴지, 가장 이른 결과를 받을지 정한다. 실패를 다루는 규칙에 따라 조합 메서드가 달라지며, 작업 취소는 별도로 구현한다.
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 완료를 유지하고 나머지 작업도 계속 정착한다. 실제 타이머의 값/결과/미취소 상태를 보여 주고 전체 정책 표는 보존할 가치가 있다.
FLOWCHART · WEB PLATFORM CONTRACT
Promise 조합과 실패 경로
모든 작업을 기다릴지, 가장 이른 결과를 받을지 정한다. 실패를 다루는 규칙에 따라 조합 메서드가 달라지며, 작업 취소는 별도로 구현한다.
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 뒤에 외부 결과 처리가 실행됩니다. 취소 기능은 이 예시에 구현되어 있지 않습니다.
비동기 처리 정리
정리 포인트는 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의 취소 계약을 별도로 확인해야 합니다.
현재 코드와 결과 처리의 순서를 먼저 쓰고, 그다음에는 “성공 값인가, 오류인가”, “한 결과면 충분한가, 모두 필요한가”를 확인하세요. 비동기 문법의 모양만으로 계산이 병렬 실행되거나 취소된다고 판단하지 않습니다.