🟨 2-21. 비동기 프로그래밍 완전정복 — Promise, async/await, 그리고 이벤트 루프의 협업 구조
1. 비동기란 무엇인가
먼저, 동기와 비동기의 차이를 명확히 보자.
console.log("A");
setTimeout(() => console.log("B"), 1000);
console.log("C");
출력 결과:
A
C
B
✅ setTimeout은 비동기 함수이기 때문에 1초 후 실행된다.
✅ JS 엔진은 B를 기다리지 않고 다음 줄(C)을 실행한다.
즉, “한 작업이 끝나길 기다리지 않고 다음으로 넘어간다.”
이게 바로 비동기다.
2. 콜백(callback) 방식의 한계
초기의 자바스크립트 비동기 코드는 콜백 함수로 처리했다.
function getData(callback) {
setTimeout(() => {
callback("서버에서 받은 데이터");
}, 1000);
}
getData((data) => {
console.log(data);
});
✅ 콜백은 간단하고 직관적이다.
하지만 여러 비동기 작업이 연결될 때 문제가 생긴다.
getData((data1) => {
getData((data2) => {
getData((data3) => {
console.log(data1, data2, data3);
});
});
});
이런 구조를 흔히 콜백 지옥(Callback Hell) 이라고 부른다.
3. Promise의 등장
콜백 지옥을 해결하기 위해 등장한 것이 Promise다.
Promise는 “비동기 연산의 미래값” 을 나타낸다.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("데이터 로딩 완료");
}, 1000);
});
promise.then((result) => console.log(result));
✅ resolve() → 성공 시
✅ reject() → 실패 시
4. Promise의 상태
Promise는 3가지 상태를 가진다 👇
상태 의미
| pending | 대기 중 |
| fulfilled | 성공적으로 완료 |
| rejected | 실패 |
const promise = new Promise((resolve, reject) => {
const success = true;
success ? resolve("성공!") : reject("실패!");
});
promise
.then((msg) => console.log("✅", msg))
.catch((err) => console.log("❌", err))
.finally(() => console.log("작업 종료"));
5. Promise 체이닝 (Chaining)
비동기 작업을 순차적으로 실행할 수 있다.
new Promise((resolve) => {
setTimeout(() => resolve(1), 500);
})
.then((result) => {
console.log(result);
return result * 2;
})
.then((result) => {
console.log(result);
return result * 2;
})
.then((result) => console.log(result));
출력:
1
2
4
✅ 각 then()은 이전 단계의 결과를 다음 단계로 전달한다.
✅ 덕분에 “콜백 지옥”이 “평평한 체인”으로 바뀐다.
6. Promise.all — 병렬 실행
여러 비동기 작업을 동시에 실행하고,
모두 끝나면 결과를 한 번에 받을 수도 있다.
const a = new Promise((res) => setTimeout(() => res("A 끝"), 1000));
const b = new Promise((res) => setTimeout(() => res("B 끝"), 500));
Promise.all([a, b]).then((results) => console.log(results));
출력:
["A 끝", "B 끝"]
✅ 모든 Promise가 완료될 때까지 기다린 뒤 결과 배열 반환
✅ 하나라도 실패하면 전체가 reject
7. Promise.race — 가장 먼저 끝난 결과
Promise.race([
new Promise((res) => setTimeout(() => res("1초 끝"), 1000)),
new Promise((res) => setTimeout(() => res("0.5초 끝"), 500)),
]).then(console.log);
출력:
0.5초 끝
✅ 여러 작업 중 가장 먼저 완료된 결과만 반환한다.
✅ API 응답 속도 비교나 타임아웃 구현에 자주 사용된다.
8. async / await — Promise의 진화형
async / await는 비동기 코드를 동기처럼 작성할 수 있는 문법이다.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function loadData() {
console.log("데이터 요청 중...");
await delay(1000);
console.log("데이터 로드 완료!");
}
loadData();
✅ await은 Promise가 완료될 때까지 “잠시 멈춤”
✅ 코드가 동기적으로 읽히기 때문에 가독성이 극대화됨
9. async 함수의 반환값
async 함수는 항상 Promise를 반환한다.
async function getValue() {
return 10;
}
getValue().then(console.log); // 10
✅ 내부에서 return → Promise.resolve() 자동 래핑
✅ throw → Promise.reject()로 처리됨
10. try / catch로 에러 처리
await를 사용할 때는 에러를 try/catch로 다룰 수 있다.
async function fetchData() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await res.json();
console.log(data);
} catch (err) {
console.error("데이터 요청 실패:", err);
}
}
fetchData();
✅ catch 블록에서 네트워크 에러, API 실패 등 처리 가능
✅ then().catch()보다 훨씬 직관적
11. 병렬 실행 — await 최적화
아래 두 코드는 결과는 같지만, 성능은 완전히 다르다.
❌ 느린 코드:
const a = await fetch(url1);
const b = await fetch(url2);
✅ 빠른 코드:
const [a, b] = await Promise.all([fetch(url1), fetch(url2)]);
👉 병렬로 실행하면 최대 2배 이상 빠름
12. async/await와 이벤트 루프
await은 실제로 이벤트 루프를 “멈추는” 게 아니라,
현재 함수의 실행을 잠시 중단하고 다음 루프로 넘긴다.
즉, 내부적으로는 다음과 같다 👇
await Promise.resolve()
→ then()으로 콜백 등록
→ 콜스택 비워짐
→ 다음 이벤트 루프에서 재개
이전 편의 “이벤트 루프”와 정확히 연결되는 구조다.
13. 실무 예제 — API 데이터 순차 로딩
async function loadUsers() {
const res1 = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await res1.json();
for (const user of users.slice(0, 3)) {
const res2 = await fetch(
`https://jsonplaceholder.typicode.com/users/${user.id}/posts`
);
const posts = await res2.json();
console.log(`${user.name}의 게시글 수: ${posts.length}`);
}
}
loadUsers();
✅ 사용자 목록을 가져온 뒤, 각 사용자별로 게시글 수를 순차적으로 출력
✅ for 루프 내에서도 자연스럽게 비동기 처리 가능
14. 비동기 함수에서의 예외 처리 전략
패턴 설명
| try/catch | 기본적인 예외 처리 |
| .catch() | Promise 체인용 에러 처리 |
| 전역 이벤트(window.onunhandledrejection) | 누락된 Promise 에러 감지 |
| custom error class | 에러 메시지 통일 및 로깅 가능 |
15. 마무리
이제 비동기 프로그래밍의 전체 구조를 이해했을 거야.
콜백 → Promise → async/await으로 이어지는 이 진화 과정이
자바스크립트의 근본적인 실행 모델을 만든다.
“비동기를 이해하지 못하면 자바스크립트를 안다고 할 수 없다.
하지만 이해하면, 코드가 시간의 흐름 속에서 어떻게 살아 움직이는지 보이게 된다.”