TIL-02: JavaScript 비동기 처리와 Promise

왜 '비동기'가 필요한가?
JavaScript는 싱글 스레드 언어입니다. 즉, 한 번에 하나의 일만 할 수 있습니다.
그런데 현실 세계에서는 이런 일들이 생깁니다:
사용자가 버튼 클릭
→ 서버에 데이터 요청 (3초 걸림)
→ 그 3초 동안 화면이 완전히 멈춤?? ❌
식당에 비유하자면, 주방장이 한 명인데 손님 A의 스테이크가 구워지는 30분 동안 손님 B,C,D의 주문을 아예 받지 않는 것과 같습니다. 말이 안 되죠.
그래서 현실의 주방장은 스테이크를 오븐에 넣어두고, 그 사이에 다른 주문을 처리합니다. 오븐 타이머가 울리면 그때 꺼내면 되니까요. JavaScript도 마찬가지입니다. **이벤트 루프(Event Loop)**라는 메커니즘이 오래 걸리는 작업(네트워크 요청, 타이머 등)을 브라우저나 Node.js에 위임하고, 작업이 끝나면 콜백 큐를 통해 결과를 돌려받습니다.
비동기 처리 = "오래 걸리는 일은 맡겨두고, 그 사이에 다른 일을 한다"
비동기 처리 진화의 역사 (왜 Promise가 나왔는가)
시대1. 콜백 함수 (Callback) - 태초의 방법
// 데이터 가져오고 → 가공하고 → 저장하고 → 알림 보내기
fetchUser(userId, function(user) {
fetchOrders(user.id, function(orders) {
processOrders(orders, function(result) {
saveResult(result, function(saved) {
sendNotification(saved, function(notif) {
// 콜백 지옥! (Callback Hell)
// 여기까지 오는데 들여쓰기가 총 5단계
})
})
})
})
})이것이 바로 악명 높은 "콜백 지옥 (Callback Hell) 또는 Pyramid of Doom" 입니다.
콜백의 문제점:
- 가독성이 극도로 낮음 (오른쪽으로 계속 들여쓰기를 해야 됨)
- 에러 처리가 각 콜백마다 따로따로
- 코드 흐름을 추적하기 매우 어려움
- 순서 보장이 어려움
시대2. Promise - ES6(2015) 등장
Promise는 "미래에 값을 줄게" 라는 약속 객체입니다.
// Promise로 같은 로직 작성
fetchUser(userId)
.then(user => fetchOrders(user.id))
.then(orders => processOrders(orders))
.then(result => saveResult(result))
.then(saved => sendNotification(saved))
.catch(error => console.error("어디서든 에러나면 여기로"));훨씬 읽기 좋아졌습니다. 위에서 아래로 흐르는 것처럼 보입니다.
Promise의 3가지 상태:
┌────────────────────────────────────────────────────────┐
│ │
│ ⏳ Pending (대기) │
│ → 초기 상태. 아직 결과가 없음 │
│ ↙ ↘ │
│ ✅ Fulfilled (이행) ❌ Rejected (거부) │
│ → 성공! 값이 생김 → 실패. 에러가 생김 │
│ │
│ * 한 번 Fulfilled 또는 Rejected되면 상태가 확정(Settled)됨 │
│ * Settled된 Promise는 다시 변하지 않음 │
│ │
└────────────────────────────────────────────────────────┘
Promise 기본 생성:
const myPromise = new Promise((resolve, reject) => {
// 비동기 작업 수행
const success = true;
if (success) {
resolve("성공 값!"); // Fulfilled 상태로 전환
} else {
reject(new Error("실패")); // Rejected 상태로 전환
}
});
// 사용
myPromise
.then(value => console.log(value)) // "성공 값!"
.catch(error => console.error(error)); // 에러 처리실전 예제 - fetch API로 데이터 가져오기:
// 브라우저 콘솔에 복붙해서 바로 실행해볼 수 있습니다
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(user => console.log(user.name)) // "Leanne Graham"
.catch(error => console.error("요청 실패:", error));
fetch()는 Promise를 반환하는 대표적인 Web API입니다. 위 코드에서.then()이 두 번 체이닝되는 것을 볼 수 있습니다 — 첫 번째는 응답을 JSON으로 변환하고, 두 번째는 그 데이터를 사용합니다.
시대3. async/await - ES2017
Promise의 단점을 가장 근본적으로 해결한 문법적 설탕(Syntax Sugar)입니다. Promise를 마치 동기 코드처럼 쓸 수 있게 해줍니다.
// Promise 버전 (단점: 변수 공유 어려움, 읽기 어려움)
function getOrdersForUser(userId) {
return fetchUser(userId)
.then(user => {
return fetchOrders(user.id)
.then(orders => ({ user, orders })); // user를 넘기는 꼼수
})
.then(({ user, orders}) => {
return processOrders(user, orders);
});
}
// async/await 버전
async function getOrdersForUser(userId) {
const user = await fetchUser(userId); // user 변수 자유롭게 사용
const orders = await fetchOrders(user.id); // orders 변수 자유롭게 사용
return processOrders(user, orders); // 둘 다 그냥 쓰면 됨!
}에러 처리도 자연스러워집니다:
async function getOrdersForUser(userId) {
try {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
return processOrders(user, orders);
} catch (error) {
// 어디서 에러가 발생했는지 스택 트레이스가 명확함
console.error("에러 발생:", error);
}
}중요:
async/await은 Promise를 대체하는 것이 아니라 Promise 위에서 동작하는 더 편한 문법입니다.
Promise의 장단점 분석
✅ 장점:
| 장점 | 설명 | 콜백 대비 |
|---|---|---|
| 체이닝 | .then().then()으로 순차적 표현 | 콜백 지옥 탈출 |
| 통합 에러 처리 | .catch() 하나로 전체 에러 처리 | 각 콜백마다 에러 처리 불필요 |
| 상태 확정(Settled) | 한 번 결정된 상태는 변하지 않음 | 예측 가능한 동작 |
| 병렬 처리 | Promise.all()로 여러 작업 동시 처리 | 순차 처리보다 빠름 |
❌ 단점:
단점1. 여전히 읽기 어려운 체이닝
// 각 then 안에서 변수를 공유하기 어려움
fetchUser()
.then(user => {
return fetchOrders(user.id); // user를 다음 then에서도 쓰고 싶다면?
})
.then(orders => {
// 여기서 user에 접근하려면?
// user를 쓰려면 외부 변수에 저장하는 꼼수를 써야 함
});단점2. 디버깅의 어려움
// 에러가 어느 then에서 발생한건지 스택 트레이스가 불명확
fetchUser()
.then(processData) // 여기서 났나?
.then(saveData) // 아니면 여기?
.then(notify) // 아니면 여기?
.catch(e => console.log(e)); // 에러 위치 추적이 어려움단점3. 취소(Cancel)가 불가능함
const promise = fetchHugeData(); // 시작
// 사용자가 페이지를 떠남.. 근데 멈출 수가 없음
// promise.cancel() ← 이런 메서드가 없음단점4. then 체이닝의 복잡한 분기 처리
// 조건에 따라 다른 처리가 필요할 때 가독성이 나빠짐
fetchData()
.then(data => {
if (data.type === 'A') {
return processA(data).then(result => ({result, type: 'A'}));
} else {
return processB(data).then(result => ({result, type: 'B'}));
}
})
.then(({result, type}) => {
// type을 억지로 넘겨줘야 함. 어색한 코드
})실전에서 유용한 도구들
지금까지 콜백 → Promise → async/await으로 이어지는 비동기 처리의 진화를 살펴보았습니다. 이제 Promise 생태계 안에서 실전에 유용한 보조 도구들을 알아보겠습니다.
Promise.all/Promise.allSettled/Promise.race - 병렬 처리
Promise.all - "모두 성공해야 진행"
// 3개를 순차적으로 하면 3초씩 = 9초
// Promise.all로 동시에 하면 3초 (가장 오래 걸리는 것만큼)
const [user, posts, comments] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchComments(userId)
]);
// 단, 하나라도 실패하면 전체 실패Promise.allSettled - "결과에 상관없이 모두 기다림" (ES2020)
// Promise.all의 특성: 하나라도 실패하면 나머지도 무시됨
// Promise.allSettled: 성공이든 실패든 모두의 결과를 돌려줌
const results = await Promise.allSettled([
fetchUser(userId),
fetchPosts(userId), // ← 이게 실패해도
fetchComments(userId) // 이건 결과를 돌려줌
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value);
}
if (result.status === 'rejected') {
console.log(result.reason);
}
})Promise.race - "가장 빠른 녀석만"
// 타임아웃 구현에 유용
const result = await Promise.race([
fetchData(), // 실제 요청
new Promise((_, reject) => { // 타임아웃
setTimeout(() => reject(new Error("Timeout")), 5000);
})
]);AbortController - Promise 취소 문제 해결
const controller = new AbortController();
// 요청 시작
fetch('/api/huge-data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('요청이 취소되었습니다'); // 취소 감지
}
});
// 사용자가 페이지를 떠날 때
controller.abort(); // 요청 취소참고: Promise는 단발성(one-shot)으로 하나의 값만 반환합니다. 실시간 데이터 스트림(WebSocket, 연속 이벤트 등)을 다뤄야 한다면 RxJS의 Observable 같은 별도의 스트림 처리 라이브러리를 살펴보세요.
전체 비교 요약
| 콜백 | Promise | async/await | Observable | |
|---|---|---|---|---|
| 가독성 | ❌ 낮음 | ⚠️ 중간 | ✅ 높음 | ⚠️ 중간 |
| 에러 처리 | ❌ 분산 | ⚠️ .catch | ✅ try/catch | ✅ .catch |
| 취소 가능 | ❌ | ⚠️ (AbortController) | ⚠️ (동일) | ✅ |
| 다중 값 | ❌ | ❌ | ❌ | ✅ |
| 디버깅 | ❌ 어려움 | ⚠️ 중간 | ✅ 쉬움 | ⚠️ 중간 |
| 학습 난이도 | ✅ 쉬움 | ⚠️ 중간 | ✅ 쉬움 | ❌ 어려움 |
Comments (0)
Checking login status...
No comments yet. Be the first to comment!
